From b02350a6dbc0d9ab9d43d3d122b6ebb5fcfea97c Mon Sep 17 00:00:00 2001 From: Carlos Quiroz Date: Sun, 11 Aug 2024 17:03:41 -0400 Subject: [PATCH 1/2] Show the deadline if a proposal can be retracted --- .../main/scala/explore/model/RootModel.scala | 5 ++ .../main/scala/explore/ExploreLayout.scala | 18 ++++- explore/src/main/scala/explore/Routing.scala | 1 + .../main/scala/explore/cache/CfpCache.scala | 42 ++++++++++++ .../explore/proposal/ProposalEditor.scala | 66 +++++-------------- .../proposal/ProposalTabContents.scala | 48 ++++++-------- .../main/scala/explore/CallForProposal.scala | 10 ++- .../src/main/scala/explore/Proposal.scala | 34 +++++++++- .../explore/model/ProgramSummaries.scala | 3 + 9 files changed, 148 insertions(+), 79 deletions(-) create mode 100644 explore/src/main/scala/explore/cache/CfpCache.scala diff --git a/common/src/main/scala/explore/model/RootModel.scala b/common/src/main/scala/explore/model/RootModel.scala index b70bc0291b..71bbf66a85 100644 --- a/common/src/main/scala/explore/model/RootModel.scala +++ b/common/src/main/scala/explore/model/RootModel.scala @@ -35,6 +35,7 @@ case class RootModel( programSummaries: Option[ProgramSummaries] = none, userPreferences: Option[UserPreferences] = none, spectroscopyModes: Option[SpectroscopyModesMatrix] = none, + cfps: Option[List[CallForProposal]] = none, undoStacks: UndoStacks[IO, ProgramSummaries] = UndoStacks.empty[IO, ProgramSummaries], otherUndoStacks: ModelUndoStacks[IO] = ModelUndoStacks[IO]() ) derives Eq { @@ -54,6 +55,7 @@ object RootModel: val programSummaries = Focus[RootModel](_.programSummaries) val userPreferences = Focus[RootModel](_.userPreferences) val spectroscopyModes = Focus[RootModel](_.spectroscopyModes) + val cfps = Focus[RootModel](_.cfps) val undoStacks = Focus[RootModel](_.undoStacks) val otherUndoStacks = Focus[RootModel](_.otherUndoStacks) @@ -83,3 +85,6 @@ object RootModel: ProgramDetails.proposal.some.andThen(Proposal.reference.some) ) ) + + val proposal: Optional[RootModel, Option[Proposal]] = + programSummaries.some.andThen(ProgramSummaries.proposal) diff --git a/explore/src/main/scala/explore/ExploreLayout.scala b/explore/src/main/scala/explore/ExploreLayout.scala index 3a34a58546..46b64eef67 100644 --- a/explore/src/main/scala/explore/ExploreLayout.scala +++ b/explore/src/main/scala/explore/ExploreLayout.scala @@ -9,6 +9,7 @@ import clue.data.syntax.* import crystal.react.* import crystal.react.hooks.* import eu.timepit.refined.types.string.NonEmptyString +import explore.cache.CfpCache import explore.cache.ModesCache import explore.cache.PreferencesCache import explore.cache.ProgramCache @@ -29,6 +30,7 @@ import japgolly.scalajs.react.extra.router.SetRouteVia import japgolly.scalajs.react.vdom.html_<^.* import lucuma.core.enums.ProgramType import lucuma.core.util.Display +import lucuma.core.util.Timestamp import lucuma.react.common.* import lucuma.react.hotkeys.* import lucuma.react.hotkeys.hooks.* @@ -198,6 +200,17 @@ object ExploreLayout: } } + val deadline: Option[Timestamp] = + props.view + .zoom(RootModel.proposal) + .get + .flatten + .flatMap(_.deadline(props.view.get.cfps.orEmpty)) + + val deadlineStr = deadline + .map(Proposal.deadlineString) + .orEmpty + React.Fragment( ConfirmDialog(), Toast(Toast.Position.BottomRight, baseZIndex = 2000).withRef(toastRef.ref), @@ -216,11 +229,12 @@ object ExploreLayout: ) ), <.div(LayoutStyles.MainGrid)( + ModesCache(props.view.zoom(RootModel.spectroscopyModes).async.set), + CfpCache(props.view.zoom(RootModel.cfps).async.set), // This might use the `RoutingInfo.dummyProgramId` if the URL had no // no program id in it. But, that's OK, because the list of user // programs will still load and they will be redirected to the program // selection popup. - ModesCache(props.view.zoom(RootModel.spectroscopyModes).async.set), ProgramCache( routingInfo.programId, props.view.zoom(RootModel.user).get.map(_.role.name), @@ -275,7 +289,7 @@ object ExploreLayout: props.resolution.renderP(props.view), if (isSubmitted) Message(text = - s"The proposal has been submitted as ${proposalReference.foldMap(_.label)} and may be retracted to allow modifications until the proposal deadline." + s"The proposal has been submitted as ${proposalReference.foldMap(_.label)} and may be retracted until the proposal deadline at ${deadlineStr}." ) else EmptyVdom ) diff --git a/explore/src/main/scala/explore/Routing.scala b/explore/src/main/scala/explore/Routing.scala index 93c279b6c4..995853696e 100644 --- a/explore/src/main/scala/explore/Routing.scala +++ b/explore/src/main/scala/explore/Routing.scala @@ -147,6 +147,7 @@ object Routing: routingInfo.programId, model.zoom(RootModel.vault).get, detailsView, + model.zoom(RootModel.cfps).get.orEmpty, programSummaries.model.get.programTimesPot.map(_.timeEstimateRange), programSummaries.model.zoom(ProgramSummaries.proposalAttachments), model.zoom(RootModel.otherUndoStacks).zoom(ModelUndoStacks.forProposal), diff --git a/explore/src/main/scala/explore/cache/CfpCache.scala b/explore/src/main/scala/explore/cache/CfpCache.scala new file mode 100644 index 0000000000..84fb614314 --- /dev/null +++ b/explore/src/main/scala/explore/cache/CfpCache.scala @@ -0,0 +1,42 @@ +// Copyright (c) 2016-2023 Association of Universities for Research in Astronomy, Inc. (AURA) +// For license information see LICENSE or https://opensource.org/licenses/BSD-3-Clause + +package explore.cache + +import cats.effect.IO +import cats.effect.kernel.Resource +import cats.syntax.all.* +import clue.StreamingClient +import explore.DefaultErrorPolicy +import explore.model.CallForProposal +import fs2.Stream +import japgolly.scalajs.react.* +import lucuma.react.common.ReactFnProps +import lucuma.schemas.ObservationDB +import queries.common.CallsQueriesGQL.ReadOpenCFPs + +case class CfpCache( + setCalls: Option[List[CallForProposal]] => IO[Unit] +)(using client: StreamingClient[IO, ObservationDB]) + extends ReactFnProps[CfpCache](CfpCache.component) + with CacheComponent.Props[List[CallForProposal]]: + val setState = setCalls + given StreamingClient[IO, ObservationDB] = client + +object CfpCache extends CacheComponent[List[CallForProposal], CfpCache]: + + override protected val updateStream + : CfpCache => Resource[IO, Stream[IO, List[CallForProposal] => List[CallForProposal]]] = + _ => Resource.pure(Stream.empty) + + given Reusability[CfpCache] = Reusability.always + + override protected val initial: CfpCache => IO[ + (List[CallForProposal], fs2.Stream[IO, List[CallForProposal] => List[CallForProposal]]) + ] = props => + import props.given + + ReadOpenCFPs[IO] + .query() + .map(_.map(_.callsForProposals.matches)) + .tupleRight(Stream.empty) diff --git a/explore/src/main/scala/explore/proposal/ProposalEditor.scala b/explore/src/main/scala/explore/proposal/ProposalEditor.scala index 44311455f9..ce9b6951c7 100644 --- a/explore/src/main/scala/explore/proposal/ProposalEditor.scala +++ b/explore/src/main/scala/explore/proposal/ProposalEditor.scala @@ -57,7 +57,6 @@ import lucuma.core.model.ZeroTo100 import lucuma.core.syntax.all.* import lucuma.core.util.Enumerated import lucuma.core.util.TimeSpan -import lucuma.core.util.Timestamp import lucuma.core.validation.* import lucuma.react.common.Css import lucuma.react.common.ReactFnProps @@ -79,7 +78,6 @@ import lucuma.ui.syntax.all.given import lucuma.ui.syntax.pot.* import monocle.Iso import org.typelevel.log4cats.Logger -import queries.common.CallsQueriesGQL.* import queries.common.ProposalQueriesGQL import spire.std.any.* @@ -93,7 +91,7 @@ case class ProposalEditor( invitations: View[List[CoIInvitation]], attachments: View[List[ProposalAttachment]], authToken: Option[NonEmptyString], - deadline: View[Option[Timestamp]], + cfps: List[CallForProposal], layout: LayoutsMap, readonly: Boolean ) extends ReactFnProps(ProposalEditor.component) @@ -571,15 +569,9 @@ object ProposalEditor: ScalaFnComponent .withHooks[Props] .useContext(AppContext.ctx) - // cfps - .useEffectResultOnMountBy: (_, ctx) => - import ctx.given - ReadOpenCFPs[IO] - .query() - .map(_.map(_.callsForProposals.matches)) // total time - we need `Hours` for editing and also to preserve if // the user switches between classes with and without total time. - .useStateViewBy: (props, _, _) => + .useStateViewBy: (props, _) => props.proposal .zoom(Proposal.proposalType.some.andThen(ProposalType.totalTime)) .get @@ -595,44 +587,23 @@ object ProposalEditor: .useStateView(Visible.Hidden) // show partner splits modal .useStateView(List.empty[PartnerSplit]) // partner splits modal // Update the partner splits when a new callId is set - .useEffectWithDepsBy((props, _, cfps, _, _, _) => - (props.proposal.get.callId, cfps.toOption.orEmpty) - ): (props, _, _, _, _, ps) => - (callId, cfps) => - callId.foldMap(cid => - val currentSplits = Proposal.proposalType.some - .andThen(ProposalType.partnerSplits) - .getOption(props.proposal.get) - val cfpPartners = cfps - .find(_.id === cid) - .foldMap(_.partners.map(_.partner)) - val proposalPartners = currentSplits.orEmpty.filter(_._2 > 0).map(_.partner) - - if (proposalPartners.nonEmpty && proposalPartners.forall(cfpPartners.contains)) - ps.set(currentSplits.orEmpty) - else - ps.set(cfpPartners.map(p => PartnerSplit(p, 0.refined))) - ) - // Update the deadline when the call or partner splits change - .useEffectWithDepsBy((props, _, cfps, _, _, splits) => - (props.proposal.get.callId, cfps.toOption.orEmpty, splits.get) - ): (props, _, _, _, _, ps) => - (callId, cfps, splits) => - callId.fold(Callback.empty)(cid => - val proposalPartners = splits.filter(_._2 > 0).map(_.partner) - props.deadline.set( - cfps + .useEffectWithDepsBy((props, _, _, _, _) => (props.proposal.get.callId, props.cfps)): + (props, _, _, _, ps) => + (callId, cfps) => + callId.foldMap(cid => + val currentSplits = Proposal.proposalType.some + .andThen(ProposalType.partnerSplits) + .getOption(props.proposal.get) + val cfpPartners = cfps .find(_.id === cid) - .flatMap { c => - val callPartners = c.partners - proposalPartners - .map(p => callPartners.find(_.partner === p).flatMap(_.submissionDeadline)) - .minimumOption - .flatten - .orElse(c.submissionDeadlineDefault) - } + .foldMap(_.partners.map(_.partner)) + val proposalPartners = currentSplits.orEmpty.filter(_._2 > 0).map(_.partner) + + if (proposalPartners.nonEmpty && proposalPartners.forall(cfpPartners.contains)) + ps.set(currentSplits.orEmpty) + else + ps.set(cfpPartners.map(p => PartnerSplit(p, 0.refined))) ) - ) // .useEffectWithDepsBy((props, _, _, _, _, _, _, _, _) => props.proposal.get.proposalClass)( // // Deal with changes to the ProposalClass. // (props, _, _, totalHours, minPct2, classType, _, _, oldClass) => @@ -659,7 +630,6 @@ object ProposalEditor: ( props, ctx, - cfps, totalHours, // minPct2, showDialog, @@ -682,7 +652,7 @@ object ProposalEditor: props.users, props.invitations, props.attachments, - cfps.toOption.orEmpty, + props.cfps, props.authToken, props.layout, props.readonly, diff --git a/explore/src/main/scala/explore/proposal/ProposalTabContents.scala b/explore/src/main/scala/explore/proposal/ProposalTabContents.scala index ac851c9d5a..f6af9b02c3 100644 --- a/explore/src/main/scala/explore/proposal/ProposalTabContents.scala +++ b/explore/src/main/scala/explore/proposal/ProposalTabContents.scala @@ -17,7 +17,7 @@ import explore.Icons import explore.common.ProposalQueries.* import explore.components.ui.ExploreStyles import explore.model.AppContext -import explore.model.Constants +import explore.model.CallForProposal import explore.model.ProgramDetails import explore.model.ProgramTimeRange import explore.model.Proposal @@ -55,16 +55,9 @@ import org.typelevel.log4cats.Logger import queries.common.ProposalQueriesGQL.* import java.time.Instant -import java.time.LocalDateTime -import java.time.ZoneOffset import scala.concurrent.duration.* -case class CallDeadline( - deadline: Timestamp -) extends ReactFnProps(CallDeadline.component): - val deadlineLDT: LocalDateTime = deadline.toLocalDateTime - val deadlineStr: String = - s"${Constants.GppDateFormatter.format(deadlineLDT)} ${Constants.GppTimeTZFormatterWithZone.format(deadlineLDT)}" +case class CallDeadline(deadline: Timestamp) extends ReactFnProps(CallDeadline.component) object CallDeadline: private type Props = CallDeadline @@ -73,21 +66,19 @@ object CallDeadline: ScalaFnComponent .withHooks[Props] .useStreamOnMount( - Stream.awakeDelay[IO](1.seconds).flatMap(_ => Stream.eval(IO(Instant.now()))) + Stream.eval(IO(Instant.now())) ++ + Stream + .awakeDelay[IO](1.seconds) + .flatMap(_ => Stream.eval(IO(Instant.now()))) ) .render { (p, n) => n.toOption.map(n => - val now = LocalDateTime.ofInstant(n, ZoneOffset.UTC) - val diff = java.time.Duration.between(now, p.deadlineLDT) - val dateFmt = - if (diff.isNegative) p.deadlineStr - else - val left = Constants.DurationLongWithSecondsFormatter(diff) - s"${p.deadlineStr} [$left]" + val (deadlineStr, left) = Proposal.deadlineStrings(n, p.deadline) + val text = left.fold(deadlineStr)(l => s"$deadlineStr [$l]") <.span( ExploreStyles.ProposalDeadline, Message( - text = s"Deadline: $dateFmt", + text = s"Deadline: $text", severity = Message.Severity.Info ) ) @@ -98,6 +89,7 @@ case class ProposalTabContents( programId: Program.Id, userVault: Option[UserVault], programDetails: View[ProgramDetails], + cfps: List[CallForProposal], timeEstimateRange: Pot[Option[ProgramTimeRange]], attachments: View[List[ProposalAttachment]], undoStacks: View[UndoStacks[IO, Proposal]], @@ -129,6 +121,7 @@ object ProposalTabContents: programId: Program.Id, userVault: Option[UserVault], programDetails: View[ProgramDetails], + cfps: List[CallForProposal], timeEstimateRange: Pot[Option[ProgramTimeRange]], attachments: View[List[ProposalAttachment]], undoStacks: View[UndoStacks[IO, Proposal]], @@ -136,8 +129,7 @@ object ProposalTabContents: layout: LayoutsMap, isUpdatingStatus: View[IsUpdatingStatus], readonly: Boolean, - errorMessage: UseState[Option[String]], - deadline: View[Option[Timestamp]] + errorMessage: UseState[Option[String]] ): VdomNode = { import ctx.given @@ -171,6 +163,9 @@ object ProposalTabContents: programDetails .zoom(ProgramDetails.proposal) .mapValue((proposalView: View[Proposal]) => + val deadline: Option[Timestamp] = + proposalView.get.deadline(cfps) + <.div( ExploreStyles.ProposalTab, ProposalEditor( @@ -183,7 +178,7 @@ object ProposalTabContents: invitations, attachments, userVault.map(_.token), - deadline, + cfps, layout, readonly ), @@ -204,7 +199,7 @@ object ProposalTabContents: onClick = updateStatus(ProposalStatus.Submitted), disabled = isUpdatingStatus.get.value || proposalView.get.callId.isEmpty ).compact.tiny, - deadline.get.map(CallDeadline.apply) + deadline.map(CallDeadline.apply) ) .when( isStdUser && proposalStatus === ProposalStatus.NotSubmitted @@ -260,16 +255,16 @@ object ProposalTabContents: .useMemoBy((props, _, _) => props.programDetails.get.proposalStatus)((_, _, _) => p => p === ProposalStatus.Submitted || p === ProposalStatus.Accepted ) - .useState(none[String]) // Submission error message + .useState(none[String]) // Submission error message .useLayoutEffectWithDepsBy((props, _, _, _, _) => props.programDetails.get.proposal.flatMap(_.callId) )((_, _, _, _, e) => _ => e.setState(none)) - .useStateView(none[Timestamp]) // CFP/Proposal Deadline - .render { (props, ctx, isUpdatingStatus, readonly, errorMsg, deadline) => + .render { (props, ctx, isUpdatingStatus, readonly, errorMsg) => renderFn( props.programId, props.userVault, props.programDetails, + props.cfps, props.timeEstimateRange, props.attachments, props.undoStacks, @@ -277,7 +272,6 @@ object ProposalTabContents: props.layout, isUpdatingStatus, readonly, - errorMsg, - deadline + errorMsg ) } diff --git a/model/shared/src/main/scala/explore/CallForProposal.scala b/model/shared/src/main/scala/explore/CallForProposal.scala index 270452022d..d7a4b7e2db 100644 --- a/model/shared/src/main/scala/explore/CallForProposal.scala +++ b/model/shared/src/main/scala/explore/CallForProposal.scala @@ -34,7 +34,15 @@ case class CallForProposal( partners: List[CallPartner], submissionDeadlineDefault: Option[Timestamp] ) derives Eq, - Decoder + Decoder: + + def deadline(proposalPartners: List[Partner]): Option[Timestamp] = + val callPartners = partners + proposalPartners + .map(p => callPartners.find(_.partner === p).flatMap(_.submissionDeadline)) + .minimumOption + .flatten + .orElse(submissionDeadlineDefault) object CallForProposal: val id: Lens[CallForProposal, CallForProposals.Id] = diff --git a/model/shared/src/main/scala/explore/Proposal.scala b/model/shared/src/main/scala/explore/Proposal.scala index 2b7331af13..39bf69675c 100644 --- a/model/shared/src/main/scala/explore/Proposal.scala +++ b/model/shared/src/main/scala/explore/Proposal.scala @@ -10,13 +10,19 @@ import eu.timepit.refined.cats.given import eu.timepit.refined.types.string.NonEmptyString import io.circe.Decoder import io.circe.refined.* +import lucuma.core.enums.Partner import lucuma.core.enums.TacCategory import lucuma.core.model.CallForProposals import lucuma.core.model.ProposalReference +import lucuma.core.util.Timestamp import monocle.Focus import monocle.Iso import monocle.Lens +import java.time.Instant +import java.time.LocalDateTime +import java.time.ZoneOffset + case class Proposal( callId: Option[CallForProposals.Id], title: Option[NonEmptyString], @@ -24,7 +30,17 @@ case class Proposal( abstrakt: Option[NonEmptyString], proposalType: Option[ProposalType], reference: Option[ProposalReference] -) derives Eq +) derives Eq { + val partners: List[Partner] = + proposalType + .flatMap(ProposalType.partnerSplits.getOption) + .orEmpty + .map(_.partner) + + def deadline(cfps: List[CallForProposal]): Option[Timestamp] = + cfps.find(u => callId.exists(_ === u.id)).flatMap(_.deadline(partners)) + +} object Proposal: val callId: Lens[Proposal, Option[CallForProposals.Id]] = @@ -58,3 +74,19 @@ object Proposal: } yield Proposal(callId.flatten, title, category, abstrakt, pte, r.flatten) val Default = Proposal(None, None, None, None, None, None) + + def deadlineStrings(n: Instant, deadline: Timestamp): (String, Option[String]) = { + val deadlineLDT = deadline.toLocalDateTime + val now = LocalDateTime.ofInstant(n, ZoneOffset.UTC) + val diff = java.time.Duration.between(now, deadlineLDT) + val deadlineStr: String = deadlineString(deadline) + if (diff.isNegative) (deadlineStr, None) + else + val left = Constants.DurationLongWithSecondsFormatter(diff) + (deadlineStr, left.some) + } + + def deadlineString(deadline: Timestamp): String = { + val deadlineLDT = deadline.toLocalDateTime + s"${Constants.GppDateFormatter.format(deadlineLDT)} ${Constants.GppTimeTZFormatterWithZone.format(deadlineLDT)}" + } diff --git a/model/shared/src/main/scala/explore/model/ProgramSummaries.scala b/model/shared/src/main/scala/explore/model/ProgramSummaries.scala index 7951c9a701..6ad4797465 100644 --- a/model/shared/src/main/scala/explore/model/ProgramSummaries.scala +++ b/model/shared/src/main/scala/explore/model/ProgramSummaries.scala @@ -18,6 +18,7 @@ import lucuma.schemas.enums.ProposalStatus import lucuma.schemas.model.TargetWithId import monocle.Focus import monocle.Lens +import monocle.Optional import scala.collection.immutable.SortedMap import scala.collection.immutable.SortedSet @@ -131,6 +132,8 @@ case class ProgramSummaries( object ProgramSummaries: val optProgramDetails: Lens[ProgramSummaries, Option[ProgramDetails]] = Focus[ProgramSummaries](_.optProgramDetails) + val proposal: Optional[ProgramSummaries, Option[Proposal]] = + optProgramDetails.some.andThen(ProgramDetails.proposal) val targets: Lens[ProgramSummaries, TargetList] = Focus[ProgramSummaries](_.targets) val observations: Lens[ProgramSummaries, ObservationList] = Focus[ProgramSummaries](_.observations) From f556da17a5dde5bf7caadb076bcf95b7a2792788 Mon Sep 17 00:00:00 2001 From: Carlos Quiroz Date: Sun, 11 Aug 2024 19:14:21 -0400 Subject: [PATCH 2/2] PR comments --- .../src/main/scala/explore/model/RootModel.scala | 7 +++++++ explore/src/main/scala/explore/ExploreLayout.scala | 14 ++++---------- .../explore/proposal/ProposalTabContents.scala | 2 +- model/shared/src/main/scala/explore/Proposal.scala | 2 +- 4 files changed, 13 insertions(+), 12 deletions(-) diff --git a/common/src/main/scala/explore/model/RootModel.scala b/common/src/main/scala/explore/model/RootModel.scala index 71bbf66a85..8ee34adfd3 100644 --- a/common/src/main/scala/explore/model/RootModel.scala +++ b/common/src/main/scala/explore/model/RootModel.scala @@ -19,6 +19,7 @@ import lucuma.core.model.ServiceUser import lucuma.core.model.StandardUser import lucuma.core.model.Target import lucuma.core.model.User +import lucuma.core.util.Timestamp import lucuma.ui.sso.UserVault import monocle.Focus import monocle.Lens @@ -44,6 +45,12 @@ case class RootModel( .getOption(this) .map(_.label) .orElse(RootModel.proposalReference.getOption(this).map(_.label)) + + val deadline: Option[Timestamp] = + (RootModel.proposal.getOption(this).flatten, RootModel.cfps.get(this)) + .mapN(_.deadline(_)) + .flatten + } object RootModel: diff --git a/explore/src/main/scala/explore/ExploreLayout.scala b/explore/src/main/scala/explore/ExploreLayout.scala index 46b64eef67..44829f0e3e 100644 --- a/explore/src/main/scala/explore/ExploreLayout.scala +++ b/explore/src/main/scala/explore/ExploreLayout.scala @@ -200,16 +200,10 @@ object ExploreLayout: } } - val deadline: Option[Timestamp] = - props.view - .zoom(RootModel.proposal) - .get - .flatten - .flatMap(_.deadline(props.view.get.cfps.orEmpty)) - - val deadlineStr = deadline - .map(Proposal.deadlineString) - .orEmpty + val deadlineStr = + props.view.get.deadline + .map(Proposal.deadlineString) + .orEmpty React.Fragment( ConfirmDialog(), diff --git a/explore/src/main/scala/explore/proposal/ProposalTabContents.scala b/explore/src/main/scala/explore/proposal/ProposalTabContents.scala index f6af9b02c3..c51de9a098 100644 --- a/explore/src/main/scala/explore/proposal/ProposalTabContents.scala +++ b/explore/src/main/scala/explore/proposal/ProposalTabContents.scala @@ -73,7 +73,7 @@ object CallDeadline: ) .render { (p, n) => n.toOption.map(n => - val (deadlineStr, left) = Proposal.deadlineStrings(n, p.deadline) + val (deadlineStr, left) = Proposal.deadlineAndTimeLeft(n, p.deadline) val text = left.fold(deadlineStr)(l => s"$deadlineStr [$l]") <.span( ExploreStyles.ProposalDeadline, diff --git a/model/shared/src/main/scala/explore/Proposal.scala b/model/shared/src/main/scala/explore/Proposal.scala index 39bf69675c..8fdd018c8c 100644 --- a/model/shared/src/main/scala/explore/Proposal.scala +++ b/model/shared/src/main/scala/explore/Proposal.scala @@ -75,7 +75,7 @@ object Proposal: val Default = Proposal(None, None, None, None, None, None) - def deadlineStrings(n: Instant, deadline: Timestamp): (String, Option[String]) = { + def deadlineAndTimeLeft(n: Instant, deadline: Timestamp): (String, Option[String]) = { val deadlineLDT = deadline.toLocalDateTime val now = LocalDateTime.ofInstant(n, ZoneOffset.UTC) val diff = java.time.Duration.between(now, deadlineLDT)