Skip to content

Commit

Permalink
Use Awarded Time in Time Allocation calculations instead of planned time
Browse files Browse the repository at this point in the history
  • Loading branch information
toddburnside committed Feb 14, 2025
1 parent 75aadee commit 6b468be
Show file tree
Hide file tree
Showing 4 changed files with 53 additions and 36 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ object ProgramDetailsTile
),
<.div(
TimeAwardTable(details.allocations),
props.programTimes.renderPot(TimeAccountingTable(_))
props.programTimes.renderPot(TimeAccountingTable(_, details.allocations))
),
<.div(ExploreStyles.ProgramDetailsInfoArea)(
SupportUsers(
Expand Down
58 changes: 37 additions & 21 deletions explore/src/main/scala/explore/programs/TimeAccountingTable.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
package explore.programs

import cats.syntax.all.*
import explore.components.HelpIcon
import explore.components.ui.ExploreStyles
import explore.model.BandedProgramTime
import explore.model.CategoryAllocationList
import explore.model.ProgramTimes
import explore.model.display.given
import explore.model.reusability.given
Expand All @@ -16,24 +18,31 @@ import lucuma.core.syntax.display.*
import lucuma.core.util.Enumerated
import lucuma.core.util.TimeSpan
import lucuma.react.common.ReactFnProps
import lucuma.react.floatingui.syntax.*
import lucuma.react.syntax.*
import lucuma.react.table.*
import lucuma.refined.*
import lucuma.ui.components.TimeSpanView
import lucuma.ui.format.TimeSpanFormatter
import lucuma.ui.reusability.given
import lucuma.ui.syntax.all.*
import lucuma.ui.table.*

case class TimeAccountingTable(programTimes: ProgramTimes)
case class TimeAccountingTable(programTimes: ProgramTimes, allocations: CategoryAllocationList)
extends ReactFnProps(TimeAccountingTable.component)

object TimeAccountingTable:
given Reusability[Map[ScienceBand, TimeSpan]] = Reusability.map

private type TimeSpanMap = Map[Option[ScienceBand], TimeSpan]
private type DataMap = Map[Option[ScienceBand], Either[TimeSpan, BigDecimal]]

extension (e: Either[TimeSpan, BigDecimal])
private def toCell: VdomNode = e match
case Left(ts) => TimeSpanView(ts, TimeSpanFormatter.DecimalHours)
case Right(bd) => f"${bd * 100}%.1f%%"
private def toCell(isRemain: Boolean): VdomNode = e match
case Left(ts) =>
<.span(TimeSpanView(ts, TimeSpanFormatter.DecimalHours))
.withTooltipWhen(isRemain, "Time Awarded - Time Used")
case Right(bd) => <.span(f"${bd * 100}%.1f%%").withTooltip("Time Used / Time Awarded")

extension (l: List[BandedProgramTime])
private def toTimeSpanMap: TimeSpanMap =
Expand Down Expand Up @@ -64,30 +73,34 @@ object TimeAccountingTable:
Row(label, data, total.asLeft)

private def completionRow(
plannedMap: TimeSpanMap,
awardedMap: TimeSpanMap,
usedMap: TimeSpanMap
): Row =
val data: DataMap =
DataColumnKeys
.map(osb =>
(osb, calcPercent(plannedMap.get(osb).orEmpty, usedMap.get(osb).orEmpty).asRight)
(osb, calcPercent(awardedMap.get(osb).orEmpty, usedMap.get(osb).orEmpty).asRight)
)
.toMap
val total: BigDecimal =
calcPercent(calcTotal(plannedMap), calcTotal(usedMap))
calcPercent(calcTotal(awardedMap), calcTotal(usedMap))
Row("Completion", data, total.asRight)

def remainRow(plannedMap: TimeSpanMap, usedMap: TimeSpanMap): Row =
def remainRow(awardedMap: TimeSpanMap, usedMap: TimeSpanMap): Row =
val remainMap: Map[Option[ScienceBand], TimeSpan] =
DataColumnKeys
.map(osb => (osb, plannedMap.get(osb).orEmpty -| usedMap.get(osb).orEmpty))
.map(osb => (osb, awardedMap.get(osb).orEmpty -| usedMap.get(osb).orEmpty))
.toMap
fromTimeSpanMap(remainMap, "Remain")

def fromProgramTimes(plannedMap: TimeSpanMap, usedMap: TimeSpanMap): List[Row] =
val planned: Row = fromTimeSpanMap(plannedMap, "Planned")
def fromProgramTimes(
awardedMap: TimeSpanMap,
plannedMap: TimeSpanMap,
usedMap: TimeSpanMap
): List[Row] =
val planned: Row = fromTimeSpanMap(plannedMap, "Prepared")
val used: Row = fromTimeSpanMap(usedMap, "Used")
val completion: Row = completionRow(plannedMap, usedMap)
val completion: Row = completionRow(awardedMap, usedMap)
List(planned, used, completion)

// A Row is also used for the table metadata for creating the footer
Expand All @@ -103,7 +116,7 @@ object TimeAccountingTable:
ColDef(
LabelColId,
_.label,
"Time Accounting",
header = _ => <.span("Time Accounting", HelpIcon("program/time-accounting.md".refined)),
footer = _ => "Remain"
).setSize(200.toPx)

Expand All @@ -112,17 +125,17 @@ object TimeAccountingTable:
BandColId(osb),
_.data(osb),
header = osb.fold("No Band")(_.shortName),
cell = _.value.toCell,
footer = _.table.options.meta.fold(EmptyVdom)(_.data(osb).toCell)
cell = _.value.toCell(false),
footer = _.table.options.meta.fold(EmptyVdom)(_.data(osb).toCell(true))
).setSize(90.toPx)

private val TotalColDef =
ColDef(
TotalColId,
_.total,
"Total",
cell = _.value.toCell,
footer = _.table.options.meta.fold(EmptyVdom)(_.total.toCell)
cell = _.value.toCell(false),
footer = _.table.options.meta.fold(EmptyVdom)(_.total.toCell(true))
).setSize(90.toPx)

private val Columns: Reusable[List[ColumnDef.WithTableMeta[Row, ?, Row]]] =
Expand All @@ -133,10 +146,13 @@ object TimeAccountingTable:
for {
plannedMap <- useMemo(props.programTimes.timeEstimateBanded)(_.toTimeSpanMap)
usedMap <- useMemo(props.programTimes.timeCharge)(_.toTimeSpanMap)
rows <- useMemo((plannedMap, usedMap)): (planned, used) =>
Row.fromProgramTimes(planned, used)
remainRow <- useMemo((plannedMap, usedMap)): (planned, used) =>
Row.remainRow(planned, used)
awardedMap <- useMemo(props.allocations.totalByBand.value.unsorted)(
_.map((sb, ts) => sb.some -> ts).toMap
)
rows <- useMemo((awardedMap, plannedMap, usedMap)): (awarded, planned, used) =>
Row.fromProgramTimes(awarded, planned, used)
remainRow <- useMemo((awardedMap, usedMap)): (awarded, used) =>
Row.remainRow(awarded, used)
table <- useReactTable:
TableOptions(
Columns,
Expand Down
17 changes: 4 additions & 13 deletions explore/src/main/scala/explore/programs/TimeAwardTable.scala
Original file line number Diff line number Diff line change
Expand Up @@ -35,17 +35,8 @@ object TimeAwardTable:
def fromCategoryAllocationList(allocations: CategoryAllocationList): List[Row] =
allocations.value.toList.map(Row(_, _))

private case class TableMeta(totalByBand: Map[ScienceBand, TimeSpan]):
lazy val grandTotal: TimeSpan = totalByBand.values.toList.combineAll

private object TableMeta:
def fromCategoryAllocationList(allocations: CategoryAllocationList): TableMeta =
TableMeta(
totalByBand = Enumerated[ScienceBand].all
.map: band =>
band -> allocations.value.values.flatMap(_.value.get(band)).toList.combineAll
.toMap
)
private case class TableMeta(totalByBand: BandAllocations):
lazy val grandTotal: TimeSpan = totalByBand.total

private val ColDef = ColumnDef.WithTableMeta[Row, TableMeta]

Expand Down Expand Up @@ -74,7 +65,7 @@ object TimeAwardTable:
cell = cell => TimeSpanView(cell.value, TimeSpanFormatter.DecimalHours),
footer = footer =>
TimeSpanView(
footer.table.options.meta.foldMap(_.totalByBand(band)),
footer.table.options.meta.foldMap(_.totalByBand.value.get(band).orEmpty),
TimeSpanFormatter.DecimalHours
)
).setSize(90.toPx)
Expand Down Expand Up @@ -105,7 +96,7 @@ object TimeAwardTable:
columns,
rows,
getRowId = (row, _, _) => RowId(row._1.tag),
meta = TableMeta.fromCategoryAllocationList(props.allocations),
meta = TableMeta(props.allocations.totalByBand),
enableSorting = false,
enableColumnResizing = false
)
Expand Down
12 changes: 11 additions & 1 deletion model/shared/src/main/scala/explore/model/allocations.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ package explore.model
import cats.Eq
import cats.Order.given
import cats.derived.*
import cats.syntax.all.*
import io.circe.Decoder
import io.circe.generic.semiauto.*
import lucuma.core.enums.ScienceBand
import lucuma.core.enums.TimeAccountingCategory
import lucuma.core.util.Enumerated
import lucuma.core.util.NewType
import lucuma.core.util.TimeSpan
import lucuma.odb.json.time.decoder.given
Expand All @@ -32,7 +34,9 @@ object Allocation:

given Decoder[Allocation] = deriveDecoder

object BandAllocations extends NewType[SortedMap[ScienceBand, TimeSpan]]
object BandAllocations extends NewType[SortedMap[ScienceBand, TimeSpan]]:
extension (self: BandAllocations) def total: TimeSpan = self.value.values.toList.combineAll

type BandAllocations = BandAllocations.Type

object CategoryAllocationList extends NewType[SortedMap[TimeAccountingCategory, BandAllocations]]:
Expand All @@ -52,5 +56,11 @@ object CategoryAllocationList extends NewType[SortedMap[TimeAccountingCategory,
extension (self: CategoryAllocationList)
def scienceBands: SortedSet[ScienceBand] =
SortedSet.from(self.value.values.flatMap(_.value.keySet))
def totalByBand: BandAllocations =
BandAllocations:
SortedMap.from:
Enumerated[ScienceBand].all
.map: band =>
band -> self.value.values.flatMap(_.value.get(band)).toList.combineAll

type CategoryAllocationList = CategoryAllocationList.Type

0 comments on commit 6b468be

Please sign in to comment.