<commit_before>package drt.client.components

import diode.data.Pot
import drt.client.actions.Actions._
import drt.client.logger.{Logger, LoggerFactory}
import drt.client.services.FixedPoints._
import drt.client.services.JSDateConversions._
import drt.client.services._
import drt.shared.CrunchApi.MillisSinceEpoch
import drt.shared.FlightsApi.TerminalName
import drt.shared.{AirportConfig, MilliDate, SDateLike, StaffMovement}
import japgolly.scalajs.react._
import japgolly.scalajs.react.component.Scala.Unmounted
import japgolly.scalajs.react.vdom.html_<^._
import org.scalajs.dom.html.{Div, Table}

import scala.collection.immutable.NumericRange
import scala.scalajs.js.Date
import scala.util.{Success, Try}


object TerminalStaffing {
  val log: Logger = LoggerFactory.getLogger(getClass.getName)

  val oneMinute = 60000L

  case class Props(
                    terminalName: TerminalName,
                    potShifts: Pot[String],
                    potFixedPoints: Pot[String],
                    potStaffMovements: Pot[Seq[StaffMovement]],
                    airportConfig: AirportConfig,
                    roles: Pot[List[String]],
                    viewMode: ViewMode
                  )

  def movementsForDay(movements: Seq[StaffMovement], day: SDateLike): Seq[StaffMovement] = {
    val startOfDayMillis = startOfDay(day).millisSinceEpoch
    val endOfDayMillis = endOfDay(day).millisSinceEpoch

    movements
      .groupBy(_.uUID)
      .filter {
        case (_, movementsPair) => areInWindow(startOfDayMillis, endOfDayMillis, movementsPair)
      }
      .values
      .flatten
      .toSeq
  }

  def areInWindow(startOfDayMillis: MillisSinceEpoch, endOfDayMillis: MillisSinceEpoch, movementsPair: Seq[StaffMovement]): Boolean = {
    val chronologicalMovementsPair = movementsPair.sortBy(_.time.millisSinceEpoch).toList

    chronologicalMovementsPair match {
      case singleMovement :: Nil =>
        val movementMillis = singleMovement.time.millisSinceEpoch
        isInWindow(startOfDayMillis, endOfDayMillis, movementMillis)

      case start :: end :: Nil =>
        val firstInWindow = isInWindow(startOfDayMillis, endOfDayMillis, start.time.millisSinceEpoch)
        val lastInWindow = isInWindow(startOfDayMillis, endOfDayMillis, end.time.millisSinceEpoch)
        firstInWindow || lastInWindow

      case _ => false
    }
  }

  private def isInWindow(startOfDayMillis: MillisSinceEpoch, endOfDayMillis: MillisSinceEpoch, movementMillis: MillisSinceEpoch) = {
    startOfDayMillis <= movementMillis && movementMillis <= endOfDayMillis
  }

  class Backend($: BackendScope[Props, Unit]) {

    def render(props: Props): VdomTagOf[Div] = {

      <.div(
        props.potShifts.render((rawShifts: String) => {
          props.potFixedPoints.render((rawFixedPoints: String) => {
            props.potStaffMovements.render((movements: Seq[StaffMovement]) => {
              val shifts: List[Try[StaffAssignment]] = StaffAssignmentParser(rawShifts).parsedAssignments.toList
              val fixedPoints: List[Try[StaffAssignment]] = StaffAssignmentParser(rawFixedPoints).parsedAssignments.toList
              <.div(
                <.div(^.className := "container",
                  <.div(^.className := "col-md-3", FixedPointsEditor(FixedPointsProps(rawFixedPoints, props.airportConfig, props.terminalName, props.roles))),
                  <.div(^.className := "col-md-3", movementsEditor(movementsForDay(movements, props.viewMode.time), props.terminalName))
                ),
                <.div(^.className := "container",
                  <.div(^.className := "col-md-10", staffOverTheDay(movementsForDay(movements, props.viewMode.time), shifts, fixedPoints, props.terminalName)))
              )
            })
          })
        })
      )
    }

    def filterByTerminal(fixedPoints: String, terminalName: String): String = {
      fixedPoints.split("\n").filter(line => {
        val cells = line.split(",").map(cell => cell.trim())
        cells(1) == terminalName
      }).mkString("\n")
    }

    def staffOverTheDay(movements: Seq[StaffMovement], shifts: List[Try[StaffAssignment]], fixedPoints: List[Try[StaffAssignment]], terminalName: TerminalName): VdomTagOf[Div] = {
      val didParseFixedPointsFail = fixedPoints exists (s => s.isFailure)
      val didParseShiftsFail = shifts exists (s => s.isFailure)
      <.div(
        <.h2("Staff over the day"), if (didParseShiftsFail || didParseFixedPointsFail) {
          if (didParseShiftsFail)
            <.div(^.className := "error", "Error in Shifts")
          else ""
          if (fixedPoints exists (s => s.isFailure))
            <.div(^.className := "error", "Error in Fixed Points")
          else ""
        }
        else {
          val successfulShifts: List[StaffAssignment] = shifts.collect { case Success(s) => s }
          val successfulTerminalShifts = successfulShifts.filter(_.terminalName == terminalName)
          val ss = StaffAssignmentServiceWithDates(successfulTerminalShifts)
          val staffWithShiftsAndMovementsAt = StaffMovements.terminalStaffAt(ss)(movements) _
          staffingTableHourPerColumn(terminalName, daysWorthOf15Minutes(SDate.midnightThisMorning()), staffWithShiftsAndMovementsAt)
        }
      )
    }

    def movementsEditor(movements: Seq[StaffMovement], terminalName: TerminalName): VdomTagOf[Div] = {
      val terminalMovements = movements.filter(_.terminalName == terminalName)
      <.div(
        <.h2("Movements"),

        if (terminalMovements.nonEmpty) {
          val iterable: Iterable[TagMod] = terminalMovements
            .groupBy(_.uUID)
            .toSeq
            .sortBy {
              case (_, head :: _) => head.time
            }
            .map {
              case (_, movementPair) =>
                movementPair.toList.sortBy(_.time) match {
                  case first :: second :: Nil =>
                    val remove = <.a(Icon.remove, ^.key := first.uUID.toString, ^.onClick ==> ((_: ReactEventFromInput) => Callback(SPACircuit.dispatch(RemoveStaffMovement(0, first.uUID)))))
                    <.li(remove, " ", MovementDisplay.displayPair(first, second))
                  case mm :: Nil =>
                    val remove = <.a(Icon.remove, ^.key := mm.uUID.toString, ^.onClick ==> ((_: ReactEventFromInput) => Callback(SPACircuit.dispatch(RemoveStaffMovement(0, mm.uUID)))))
                    <.li(remove, " ", MovementDisplay.displaySingle(mm))
                  case x =>
                    log.info(s"didn't get a pair: $x")
                    TagMod()
                }
            }
          <.ul(^.className := "list-unstyled", iterable.toTagMod)
        } else {
          <.p("No movements recorded")
        }
      )
    }

    case class FixedPointsProps(rawFixedPoints: String,
                                airportConfig: AirportConfig,
                                terminalName: TerminalName,
                                roles: Pot[List[String]])

    case class FixedPointsState(rawFixedPoints: String)

    object FixedPointsEditor {
      val component = ScalaComponent.builder[FixedPointsProps]("FixedPointsEditor")
        .initialStateFromProps(props => {
          val onlyOurTerminal = filterTerminal(props.terminalName, props.rawFixedPoints)
          val withoutTerminalName = removeTerminalNameAndDate(onlyOurTerminal)
          FixedPointsState(withoutTerminalName)
        })
        .renderPS((scope, props, state) => {

          val defaultExamples = Seq("Roving Officer, 00:00, 23:59, 1")
          val examples = if (props.airportConfig.fixedPointExamples.nonEmpty)
            props.airportConfig.fixedPointExamples
          else
            defaultExamples

          <.div(
            <.h2("Fixed Points"),
            props.roles.render(r => {
              if (r.contains("staff:edit")) {
                <.div(
                  <.p("One entry per line with values separated by commas, e.g.:"),
                  <.pre(<.div(examples.map(line => <.div(line)).toTagMod)),
                  <.textarea(^.value := state.rawFixedPoints, ^.className := "staffing-editor"),
                  ^.onChange ==> ((e: ReactEventFromInput) => {
                    val newRawFixedPoints = e.target.value
                    scope.modState(_.copy(rawFixedPoints = newRawFixedPoints))
                  }),
                  <.button("Save", ^.onClick ==> ((e: ReactEventFromInput) => {<commit_msg>DRT-5120 rename fixed points to miscelaneous staff<commit_after>package drt.client.components

import diode.data.Pot
import drt.client.actions.Actions._
import drt.client.logger.{Logger, LoggerFactory}
import drt.client.services.FixedPoints._
import drt.client.services.JSDateConversions._
import drt.client.services._
import drt.shared.CrunchApi.MillisSinceEpoch
import drt.shared.FlightsApi.TerminalName
import drt.shared.{AirportConfig, MilliDate, SDateLike, StaffMovement}
import japgolly.scalajs.react._
import japgolly.scalajs.react.component.Scala.Unmounted
import japgolly.scalajs.react.vdom.html_<^._
import org.scalajs.dom.html.{Div, Table}

import scala.collection.immutable.NumericRange
import scala.scalajs.js.Date
import scala.util.{Success, Try}


object TerminalStaffing {
  val log: Logger = LoggerFactory.getLogger(getClass.getName)

  val oneMinute = 60000L

  case class Props(
                    terminalName: TerminalName,
                    potShifts: Pot[String],
                    potFixedPoints: Pot[String],
                    potStaffMovements: Pot[Seq[StaffMovement]],
                    airportConfig: AirportConfig,
                    roles: Pot[List[String]],
                    viewMode: ViewMode
                  )

  def movementsForDay(movements: Seq[StaffMovement], day: SDateLike): Seq[StaffMovement] = {
    val startOfDayMillis = startOfDay(day).millisSinceEpoch
    val endOfDayMillis = endOfDay(day).millisSinceEpoch

    movements
      .groupBy(_.uUID)
      .filter {
        case (_, movementsPair) => areInWindow(startOfDayMillis, endOfDayMillis, movementsPair)
      }
      .values
      .flatten
      .toSeq
  }

  def areInWindow(startOfDayMillis: MillisSinceEpoch, endOfDayMillis: MillisSinceEpoch, movementsPair: Seq[StaffMovement]): Boolean = {
    val chronologicalMovementsPair = movementsPair.sortBy(_.time.millisSinceEpoch).toList

    chronologicalMovementsPair match {
      case singleMovement :: Nil =>
        val movementMillis = singleMovement.time.millisSinceEpoch
        isInWindow(startOfDayMillis, endOfDayMillis, movementMillis)

      case start :: end :: Nil =>
        val firstInWindow = isInWindow(startOfDayMillis, endOfDayMillis, start.time.millisSinceEpoch)
        val lastInWindow = isInWindow(startOfDayMillis, endOfDayMillis, end.time.millisSinceEpoch)
        firstInWindow || lastInWindow

      case _ => false
    }
  }

  private def isInWindow(startOfDayMillis: MillisSinceEpoch, endOfDayMillis: MillisSinceEpoch, movementMillis: MillisSinceEpoch) = {
    startOfDayMillis <= movementMillis && movementMillis <= endOfDayMillis
  }

  class Backend($: BackendScope[Props, Unit]) {

    def render(props: Props): VdomTagOf[Div] = {

      <.div(
        props.potShifts.render((rawShifts: String) => {
          props.potFixedPoints.render((rawFixedPoints: String) => {
            props.potStaffMovements.render((movements: Seq[StaffMovement]) => {
              val shifts: List[Try[StaffAssignment]] = StaffAssignmentParser(rawShifts).parsedAssignments.toList
              val fixedPoints: List[Try[StaffAssignment]] = StaffAssignmentParser(rawFixedPoints).parsedAssignments.toList
              <.div(
                <.div(^.className := "container",
                  <.div(^.className := "col-md-3", FixedPointsEditor(FixedPointsProps(rawFixedPoints, props.airportConfig, props.terminalName, props.roles))),
                  <.div(^.className := "col-md-3", movementsEditor(movementsForDay(movements, props.viewMode.time), props.terminalName))
                ),
                <.div(^.className := "container",
                  <.div(^.className := "col-md-10", staffOverTheDay(movementsForDay(movements, props.viewMode.time), shifts, fixedPoints, props.terminalName)))
              )
            })
          })
        })
      )
    }

    def filterByTerminal(fixedPoints: String, terminalName: String): String = {
      fixedPoints.split("\n").filter(line => {
        val cells = line.split(",").map(cell => cell.trim())
        cells(1) == terminalName
      }).mkString("\n")
    }

    def staffOverTheDay(movements: Seq[StaffMovement], shifts: List[Try[StaffAssignment]], fixedPoints: List[Try[StaffAssignment]], terminalName: TerminalName): VdomTagOf[Div] = {
      val didParseFixedPointsFail = fixedPoints exists (s => s.isFailure)
      val didParseShiftsFail = shifts exists (s => s.isFailure)
      <.div(
        <.h2("Staff over the day"), if (didParseShiftsFail || didParseFixedPointsFail) {
          if (didParseShiftsFail)
            <.div(^.className := "error", "Error in Shifts")
          else ""
          if (fixedPoints exists (s => s.isFailure))
            <.div(^.className := "error", "Error in Fixed Points")
          else ""
        }
        else {
          val successfulShifts: List[StaffAssignment] = shifts.collect { case Success(s) => s }
          val successfulTerminalShifts = successfulShifts.filter(_.terminalName == terminalName)
          val ss = StaffAssignmentServiceWithDates(successfulTerminalShifts)
          val staffWithShiftsAndMovementsAt = StaffMovements.terminalStaffAt(ss)(movements) _
          staffingTableHourPerColumn(terminalName, daysWorthOf15Minutes(SDate.midnightThisMorning()), staffWithShiftsAndMovementsAt)
        }
      )
    }

    def movementsEditor(movements: Seq[StaffMovement], terminalName: TerminalName): VdomTagOf[Div] = {
      val terminalMovements = movements.filter(_.terminalName == terminalName)
      <.div(
        <.h2("Movements"),

        if (terminalMovements.nonEmpty) {
          val iterable: Iterable[TagMod] = terminalMovements
            .groupBy(_.uUID)
            .toSeq
            .sortBy {
              case (_, head :: _) => head.time
            }
            .map {
              case (_, movementPair) =>
                movementPair.toList.sortBy(_.time) match {
                  case first :: second :: Nil =>
                    val remove = <.a(Icon.remove, ^.key := first.uUID.toString, ^.onClick ==> ((_: ReactEventFromInput) => Callback(SPACircuit.dispatch(RemoveStaffMovement(0, first.uUID)))))
                    <.li(remove, " ", MovementDisplay.displayPair(first, second))
                  case mm :: Nil =>
                    val remove = <.a(Icon.remove, ^.key := mm.uUID.toString, ^.onClick ==> ((_: ReactEventFromInput) => Callback(SPACircuit.dispatch(RemoveStaffMovement(0, mm.uUID)))))
                    <.li(remove, " ", MovementDisplay.displaySingle(mm))
                  case x =>
                    log.info(s"didn't get a pair: $x")
                    TagMod()
                }
            }
          <.ul(^.className := "list-unstyled", iterable.toTagMod)
        } else {
          <.p("No movements recorded")
        }
      )
    }

    case class FixedPointsProps(rawFixedPoints: String,
                                airportConfig: AirportConfig,
                                terminalName: TerminalName,
                                roles: Pot[List[String]])

    case class FixedPointsState(rawFixedPoints: String)

    object FixedPointsEditor {
      val component = ScalaComponent.builder[FixedPointsProps]("FixedPointsEditor")
        .initialStateFromProps(props => {
          val onlyOurTerminal = filterTerminal(props.terminalName, props.rawFixedPoints)
          val withoutTerminalName = removeTerminalNameAndDate(onlyOurTerminal)
          FixedPointsState(withoutTerminalName)
        })
        .renderPS((scope, props, state) => {

          val defaultExamples = Seq("Roving Officer, 00:00, 23:59, 1")
          val examples = if (props.airportConfig.fixedPointExamples.nonEmpty)
            props.airportConfig.fixedPointExamples
          else
            defaultExamples

          <.div(
            <.h2("Miscelaneous Staff"),
            props.roles.render(r => {
              if (r.contains("staff:edit")) {
                <.div(
                  <.p("One entry per line with values separated by commas, e.g.:"),
                  <.pre(<.div(examples.map(line => <.div(line)).toTagMod)),
                  <.textarea(^.value := state.rawFixedPoints, ^.className := "staffing-editor"),
                  ^.onChange ==> ((e: ReactEventFromInput) => {
                    val newRawFixedPoints = e.target.value
                    scope.modState(_.copy(rawFixedPoints = newRawFixedPoints))
                  }),
                  <.button("Save", ^.onClick ==> ((e: ReactEventFromInput) => {
{'fraction_non_alphanumeric': 0.1144425426429794, 'fraction_numerical': 0.0033051997875228707, 'mean_word_length': 2.506622516556291, 'pattern_counts': {'":': 0, '<': 65, '<?xml version=': 0, '>': 63, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1088365', 'n_tokens_mistral': 4997, 'n_tokens_neox': 4811, 'n_words': 858}