cityssm/attendance-tracking

View on GitHub
public-typescript/admin.employees.ts

Summary

Maintainability
B
5 hrs
Test Coverage
// eslint-disable-next-line eslint-comments/disable-enable-pair
/* eslint-disable @typescript-eslint/indent */

// eslint-disable-next-line eslint-comments/disable-enable-pair
/* eslint-disable unicorn/prefer-module */

// 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 { DoAddEmployeeResponse } from '../handlers/admin-post/doAddEmployee.js'
import type { DoDeleteEmployeeResponse } from '../handlers/admin-post/doDeleteEmployee.js'
import type { DoGetEmployeePropertiesResponse } from '../handlers/admin-post/doGetEmployeeProperties.js'
import type { DoModifyEmployeePropertyResponse } from '../handlers/admin-post/doModifyEmployeeProperty.js'
import type { DoUpdateEmployeeResponse } from '../handlers/admin-post/doUpdateEmployee.js'
import type { Attend as AttendGlobal } from '../types/globalTypes.js'
import type { Employee, EmployeeProperty } from '../types/recordTypes.js'
declare const bulmaJS: BulmaJS

declare const cityssm: cityssmGlobal
;(() => {
  const Attend = exports.Attend as AttendGlobal

  let unfilteredEmployees = exports.employees as Employee[]
  delete exports.employees

  let filteredEmployees = unfilteredEmployees

  const employeeNumberRegularExpression =
    exports.employeeNumberRegularExpression as RegExp | undefined

  // Employee Modal

  const selfServiceEnabled = exports.selfService as boolean

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

    const inputElements = (clickEvent.currentTarget as HTMLElement)
      .closest('.columns')
      ?.querySelectorAll('input') as NodeListOf<HTMLInputElement>

    if (inputElements.length !== 2) {
      return
    }

    const value = inputElements.item(0).value
    inputElements.item(0).value = inputElements.item(1).value
    inputElements.item(1).value = value
  }

  function openEmployeeModal(employeeNumber: string): void {
    let employeeModalElement: HTMLElement
    let closeEmployeeModalFunction: () => void

    const employee = unfilteredEmployees.find((possibleEmployee) => {
      return possibleEmployee.employeeNumber === employeeNumber
    }) as Employee

    let employeeProperties: EmployeeProperty[] = []

    function updateEmployeeProperty(clickEvent: Event): void {
      const rowElement = (clickEvent.currentTarget as HTMLElement).closest('tr')

      const propertyName = rowElement?.dataset.propertyName
      const propertyValue = rowElement?.querySelector('input')?.value
      const isSynced = rowElement?.querySelector('select')?.value

      cityssm.postJSON(
        `${Attend.urlPrefix}/admin/doUpdateEmployeeProperty`,
        {
          employeeNumber,
          propertyName,
          propertyValue,
          isSynced
        },
        (rawResponseJSON) => {
          const responseJSON =
            rawResponseJSON as unknown as DoModifyEmployeePropertyResponse

          if (responseJSON.success) {
            bulmaJS.alert({
              message: 'Property updated successfully.',
              contextualColorName: 'success'
            })

            employeeProperties = responseJSON.employeeProperties
          }
        }
      )
    }

    function deleteEmployeeProperty(clickEvent: Event): void {
      const rowElement = (clickEvent.currentTarget as HTMLElement).closest('tr')

      function doDelete(): void {
        const propertyName = rowElement?.dataset.propertyName
        const propertyValue = rowElement?.querySelector('input')?.value
        const isSynced = rowElement?.querySelector('select')?.value

        cityssm.postJSON(
          `${Attend.urlPrefix}/admin/doDeleteEmployeeProperty`,
          {
            employeeNumber,
            propertyName,
            propertyValue,
            isSynced
          },
          (rawResponseJSON) => {
            const responseJSON =
              rawResponseJSON as unknown as DoModifyEmployeePropertyResponse

            if (responseJSON.success) {
              bulmaJS.alert({
                message: 'Property deleted successfully.',
                contextualColorName: 'success'
              })

              employeeProperties = responseJSON.employeeProperties
              rowElement?.remove()
            }
          }
        )
      }

      bulmaJS.confirm({
        title: 'Delete Employee Property',
        message: 'Are you sure you want to remove this employee property?',
        contextualColorName: 'warning',
        okButton: {
          text: 'Delete Property',
          callbackFunction: doDelete
        }
      })
    }

    function renderEmployeeProperties(): void {
      const tbodyElement = employeeModalElement.querySelector(
        '#employeePropertyTab--current tbody'
      ) as HTMLTableSectionElement

      tbodyElement.innerHTML = ''

      for (const employeeProperty of employeeProperties) {
        const rowElement = document.createElement('tr')
        rowElement.dataset.propertyName = employeeProperty.propertyName

        rowElement.innerHTML = `<td class="is-size-7 is-vcentered">${employeeProperty.propertyName}</td>
          <td>
            <div class="control">
              <input class="input is-small" name="propertyValue" type="text" maxlength="500" />
            </div>
          </td>
          <td>
            <div class="control">
            <div class="select is-small is-fullwidth">
              <select name="isSynced">
                <option value="1">Synced</option>
                <option value="0">Not Synced</option>
              </select>
            </div>
            </div>
          </td>
          <td class="has-text-right">
          <div class="field is-grouped is-justify-content-flex-end">
            <div class="control">
            <button class="button is-update-button is-success is-small" data-tooltip="Save" type="button" aria-label="Save">
              <i class="fas fa-save" aria-hidden="true"></i>
            </button>
            </div>
            <div class="control">
            <button class="button is-delete-button is-danger is-small" data-tooltip="Delete" type="button" aria-label="Delete">
              <i class="fas fa-trash" aria-hidden="true"></i>
            </button>
            </div>
          </div>
          </td>`
        ;(rowElement.querySelector('input') as HTMLInputElement).value =
          employeeProperty.propertyValue
        ;(rowElement.querySelector('select') as HTMLSelectElement).value =
          (employeeProperty.isSynced as boolean) ? '1' : '0'

        rowElement
          .querySelector('.is-update-button')
          ?.addEventListener('click', updateEmployeeProperty)

        rowElement
          .querySelector('.is-delete-button')
          ?.addEventListener('click', deleteEmployeeProperty)

        tbodyElement.append(rowElement)
      }
    }

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

      const addPropertyFormElement = formEvent.currentTarget as HTMLFormElement

      cityssm.postJSON(
        `${Attend.urlPrefix}/admin/doAddEmployeeProperty`,
        addPropertyFormElement,
        (rawResponseJSON) => {
          const responseJSON =
            rawResponseJSON as unknown as DoModifyEmployeePropertyResponse

          if (responseJSON.success) {
            bulmaJS.alert({
              message: 'Property added successfully.',
              contextualColorName: 'success',
              okButton: {
                callbackFunction() {
                  // eslint-disable-next-line no-extra-semi
                  ;(
                    employeeModalElement.querySelector(
                      '#employeePropertyAdd--propertyName'
                    ) as HTMLInputElement
                  ).focus()
                }
              }
            })

            addPropertyFormElement.reset()

            employeeProperties = responseJSON.employeeProperties ?? []
            renderEmployeeProperties()
          } else {
            bulmaJS.alert({
              title: 'Error Adding Property',
              message:
                'The property may already be set. Please check, then try again.',
              contextualColorName: 'danger'
            })
          }
        }
      )
    }

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

      cityssm.postJSON(
        `${Attend.urlPrefix}/admin/doUpdateEmployee`,
        formEvent.currentTarget,
        (rawResponseJSON) => {
          const responseJSON =
            rawResponseJSON as unknown as DoUpdateEmployeeResponse

          if (responseJSON.success) {
            bulmaJS.alert({
              message: 'Employee updated successfully.',
              contextualColorName: 'success'
            })

            unfilteredEmployees = responseJSON.employees
            refreshFilteredEmployees()
          }
        }
      )
    }

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

      function doDelete(): void {
        cityssm.postJSON(
          `${Attend.urlPrefix}/admin/doDeleteEmployee`,
          {
            employeeNumber
          },
          (rawResponseJSON) => {
            const responseJSON =
              rawResponseJSON as unknown as DoDeleteEmployeeResponse
            if (responseJSON.success) {
              closeEmployeeModalFunction()

              bulmaJS.alert({
                message: 'Employee deleted successfully',
                contextualColorName: 'info'
              })

              unfilteredEmployees = responseJSON.employees
              refreshFilteredEmployees()
            }
          }
        )
      }

      bulmaJS.confirm({
        title: 'Delete Employee',
        message: `Are you sure you want to delete this employee?<br />
          Note that if the employee is found in a subsequent syncing process, they may be restored.`,
        messageIsHtml: true,
        contextualColorName: 'warning',
        okButton: {
          text: 'Yes, Delete Employee',
          callbackFunction: doDelete
        }
      })
    }

    cityssm.openHtmlModal('employeeAdmin-employee', {
      onshow(modalElement) {
        employeeModalElement = modalElement
        ;(
          modalElement.querySelector('.modal-card-title') as HTMLElement
        ).textContent = `${employee.employeeSurname}, ${employee.employeeGivenName}`
        ;(
          modalElement.querySelector(
            '#employeeEdit--employeeNumber'
          ) as HTMLInputElement
        ).value = employee.employeeNumber
        ;(
          modalElement.querySelector(
            '#employeeEdit--employeeNumberSpan'
          ) as HTMLElement
        ).textContent = employee.employeeNumber
        ;(
          modalElement.querySelector(
            '#employeeEdit--isActive'
          ) as HTMLSelectElement
        ).value = employee.isActive ? '1' : '0'

        // Main Details
        ;(
          modalElement.querySelector(
            '#employeeEdit--isSynced'
          ) as HTMLSelectElement
        ).value = employee.isSynced ? '1' : '0'
        ;(
          modalElement.querySelector(
            '#employeeEdit--employeeSurname'
          ) as HTMLInputElement
        ).value = employee.employeeSurname
        ;(
          modalElement.querySelector(
            '#employeeEdit--employeeGivenName'
          ) as HTMLInputElement
        ).value = employee.employeeGivenName
        ;(
          modalElement.querySelector(
            '#employeeEdit--jobTitle'
          ) as HTMLInputElement
        ).value = employee.jobTitle ?? ''
        ;(
          modalElement.querySelector(
            '#employeeEdit--userName'
          ) as HTMLInputElement
        ).value = employee.userName ?? ''
        ;(
          modalElement.querySelector(
            '#employeeEdit--department'
          ) as HTMLInputElement
        ).value = employee.department ?? ''

        if ((employee.seniorityDateTime ?? '') !== '') {
          // eslint-disable-next-line no-extra-semi
          ;(
            modalElement.querySelector(
              '#employeeEdit--seniorityDateTime'
            ) as HTMLInputElement
          ).valueAsDate = new Date(employee.seniorityDateTime as string)
        }

        // Contact Information
        // eslint-disable-next-line no-extra-semi
        ;(
          modalElement.querySelector(
            '#employeeEdit--syncContacts'
          ) as HTMLSelectElement
        ).value = employee.syncContacts ? '1' : '0'
        ;(
          modalElement.querySelector(
            '#employeeEdit--workContact1'
          ) as HTMLInputElement
        ).value = employee.workContact1 ?? ''
        ;(
          modalElement.querySelector(
            '#employeeEdit--workContact2'
          ) as HTMLInputElement
        ).value = employee.workContact2 ?? ''

        if (selfServiceEnabled) {
          modalElement
            .querySelector('.is-self-service-message')
            ?.classList.remove('is-hidden')
        }

        ;(
          modalElement.querySelector(
            '#employeeEdit--homeContact1'
          ) as HTMLInputElement
        ).value = employee.homeContact1 ?? ''
        ;(
          modalElement.querySelector(
            '#employeeEdit--homeContact2'
          ) as HTMLInputElement
        ).value = employee.homeContact2 ?? ''

        // Properties
        ;(
          modalElement.querySelector(
            '#employeePropertyAdd--employeeNumber'
          ) as HTMLInputElement
        ).value = employee.employeeNumber

        cityssm.postJSON(
          `${Attend.urlPrefix}/admin/doGetEmployeeProperties`,
          {
            employeeNumber
          },
          (rawResponseJSON) => {
            employeeProperties = (
              rawResponseJSON as unknown as DoGetEmployeePropertiesResponse
            ).employeeProperties

            renderEmployeeProperties()
          }
        )
      },
      onshown(modalElement, closeModalFunction) {
        closeEmployeeModalFunction = closeModalFunction

        bulmaJS.toggleHtmlClipped()

        bulmaJS.init(modalElement)

        Attend.initializeMenuTabs(
          modalElement.querySelectorAll('.menu a'),
          modalElement.querySelectorAll('.tabs-container > article')
        )

        modalElement
          .querySelector('#form--employeeEdit')
          ?.addEventListener('submit', updateEmployee)

        const swapButtonElements =
          modalElement.querySelectorAll('.is-swap-button')

        for (const buttonElement of swapButtonElements) {
          buttonElement.addEventListener('click', swapFields)
        }

        modalElement
          .querySelector('#form--employeePropertyAdd')
          ?.addEventListener('submit', addEmployeeProperty)

        modalElement
          .querySelector('.is-delete-employee')
          ?.addEventListener('click', deleteEmployee)
      },
      onremoved() {
        bulmaJS.toggleHtmlClipped()
      }
    })
  }

  function openEmployeeModalByClick(clickEvent: MouseEvent): void {
    clickEvent.preventDefault()
    const employeeNumber = (clickEvent.currentTarget as HTMLElement).dataset
      .employeeNumber as string
    openEmployeeModal(employeeNumber)
  }

  // Add

  document
    .querySelector('#is-add-employee-button')
    ?.addEventListener('click', () => {
      let addCloseModalFunction: () => void

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

        cityssm.postJSON(
          `${Attend.urlPrefix}/admin/doAddEmployee`,
          formEvent.currentTarget,
          (rawResponseJSON) => {
            const responseJSON =
              rawResponseJSON as unknown as DoAddEmployeeResponse

            if (responseJSON.success) {
              addCloseModalFunction()

              bulmaJS.alert({
                message: 'Employee added successfully.',
                okButton: {
                  callbackFunction() {
                    openEmployeeModal(responseJSON.employeeNumber)
                  }
                }
              })

              unfilteredEmployees = responseJSON.employees
              refreshFilteredEmployees()
            } else {
              bulmaJS.alert({
                title: 'Error Adding Employee',
                message:
                  'Please check to make sure that an employee does not already exist with the same employee number.',
                contextualColorName: 'danger'
              })
            }
          }
        )
      }

      cityssm.openHtmlModal('employeeAdmin-addEmployee', {
        onshown(modalElement, closeModalFunction) {
          addCloseModalFunction = closeModalFunction

          bulmaJS.toggleHtmlClipped()

          const employeeNumberElement = modalElement.querySelector(
            '#employeeAdd--employeeNumber'
          ) as HTMLInputElement

          if (employeeNumberRegularExpression !== undefined) {
            employeeNumberElement.pattern =
              employeeNumberRegularExpression.source
          }

          employeeNumberElement.focus()

          modalElement
            .querySelector('form')
            ?.addEventListener('submit', addEmployee)
        },
        onremoved() {
          bulmaJS.toggleHtmlClipped()
        }
      })
    })

  // Search

  const employeeNameNumberSearchElement = document.querySelector(
    '#employeeSearch--employeeNameNumber'
  ) as HTMLInputElement
  const isActiveSearchElement = document.querySelector(
    '#employeeSearch--isActive'
  ) as HTMLSelectElement

  const employeesContainerElement = document.querySelector(
    '#container--employees'
  ) as HTMLElement

  const limit = 50
  let offset = 0

  function renderEmployees(): void {
    if (filteredEmployees.length === 0) {
      employeesContainerElement.innerHTML = `<div class="message is-info">
        <p class="message-body">There are no employees that meet your search criteria.</p>
        </div>`

      return
    }

    const panelElement = document.createElement('div')
    panelElement.className = 'panel'

    for (
      let index = offset;
      index < Math.min(limit + offset, filteredEmployees.length);
      index += 1
    ) {
      const employee = filteredEmployees[index]

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

      if (!employee.isActive) {
        panelBlockElement.classList.add(
          'is-italic',
          'has-background-warning-light'
        )
      }

      panelBlockElement.href = '#'
      panelBlockElement.dataset.employeeNumber = employee.employeeNumber ?? ''

      panelBlockElement.innerHTML = `<div class="columns is-mobile">
        <div class="column is-narrow">
          <i class="fas fa-hard-hat" aria-hidden="true"></i>
        </div>
        <div class="column is-4">${employee.employeeNumber}</div>
        <div class="column">
          ${employee.employeeSurname}, ${employee.employeeGivenName}<br />
          <span class="is-size-7">
            ${employee.jobTitle ?? ''}
          </span>
        </div>
        </div>`

      panelBlockElement.addEventListener('click', openEmployeeModalByClick)

      panelElement.append(panelBlockElement)
    }

    employeesContainerElement.innerHTML = ''
    employeesContainerElement.append(panelElement)

    // Pager

    const pagerElement = document.createElement('div')
    pagerElement.className = 'field is-grouped is-justify-content-center'

    pagerElement.innerHTML = `<div class="control">
      <button class="button is-previous-button" data-tooltip="Previous Employees" type="button" aria-label="Previous">
        <i class="fas fa-arrow-left" aria-hidden="true"></i>
      </button>
      <button class="button is-next-button" data-tooltip="Next Employees" type="button" aria-label="Next">
        <span>Next</span>  
        <span class="icon is-small"><i class="fas fa-arrow-right" aria-hidden="true"></i></span>
      </button>
      </div>`

    if (offset === 0) {
      // eslint-disable-next-line no-extra-semi
      ;(
        pagerElement.querySelector('.is-previous-button') as HTMLButtonElement
      ).disabled = true
    } else {
      pagerElement
        .querySelector('.is-previous-button')
        ?.addEventListener('click', goToPrevious)
    }

    if (limit + offset >= filteredEmployees.length) {
      // eslint-disable-next-line no-extra-semi
      ;(
        pagerElement.querySelector('.is-next-button') as HTMLButtonElement
      ).disabled = true
    } else {
      pagerElement
        .querySelector('.is-next-button')
        ?.addEventListener('click', goToNext)
    }

    employeesContainerElement.append(pagerElement)
  }

  function goToPrevious(): void {
    offset = Math.max(offset - limit, 0)

    employeesContainerElement.scrollIntoView(true)
    window.scrollTo({ top: window.scrollY - 60 })

    renderEmployees()
  }

  function goToNext(): void {
    offset += limit
    if (offset >= filteredEmployees.length) {
      offset = 0
    }
    employeesContainerElement.scrollIntoView(true)
    window.scrollTo({ top: window.scrollY - 60 })

    renderEmployees()
  }

  function refreshFilteredEmployees(): void {
    filteredEmployees = unfilteredEmployees.filter((possibleEmployee) => {
      if (
        (isActiveSearchElement.value === '1' && !possibleEmployee.isActive) ||
        (isActiveSearchElement.value === '0' && possibleEmployee.isActive)
      ) {
        return false
      }

      const employeeSearchString =
        `${possibleEmployee.employeeGivenName} ${possibleEmployee.employeeSurname} ${possibleEmployee.employeeNumber}`.toLowerCase()

      const searchStringPieces = employeeNameNumberSearchElement.value
        .trim()
        .toLowerCase()
        .split(' ')

      for (const searchStringPiece of searchStringPieces) {
        if (!employeeSearchString.includes(searchStringPiece)) {
          return false
        }
      }

      return true
    })

    renderEmployees()
  }

  function resetOffsetAndFilterEmployees(): void {
    offset = 0
    refreshFilteredEmployees()
  }

  // Initialize page

  resetOffsetAndFilterEmployees()

  employeeNameNumberSearchElement.addEventListener(
    'keyup',
    resetOffsetAndFilterEmployees
  )
  isActiveSearchElement.addEventListener(
    'change',
    resetOffsetAndFilterEmployees
  )
})()