diff --git a/common/src/main/scala/explore/components/ui/ExploreStyles.scala b/common/src/main/scala/explore/components/ui/ExploreStyles.scala index 200a0539b0..34763faa65 100644 --- a/common/src/main/scala/explore/components/ui/ExploreStyles.scala +++ b/common/src/main/scala/explore/components/ui/ExploreStyles.scala @@ -508,6 +508,7 @@ object ExploreStyles: val ProgramDetailsTile: Css = Css("program-details-tile") val ProgramDetailsInfoArea: Css = Css("program-details-info-area") val ProgramDetailsLeft: Css = Css("program-details-left") + val ProgramDetailsRight: Css = Css("program-details-right") val ProgramTabTable: Css = Css("program-tab-table") val ProgramDetailsUsers: Css = Css("program-details-users") diff --git a/common/src/main/scala/explore/model/ExploreGridLayouts.scala b/common/src/main/scala/explore/model/ExploreGridLayouts.scala index 52724b2917..6bd8c77a13 100644 --- a/common/src/main/scala/explore/model/ExploreGridLayouts.scala +++ b/common/src/main/scala/explore/model/ExploreGridLayouts.scala @@ -362,7 +362,7 @@ object ExploreGridLayouts: end observationList object programs: - private lazy val DetailsHeight: NonNegInt = 6.refined + private lazy val DetailsHeight: NonNegInt = 10.refined private lazy val NotesHeight: NonNegInt = 6.refined private lazy val ChangeRequestsHeight: NonNegInt = 6.refined private lazy val UnrequestedConfigsHeight: NonNegInt = 6.refined diff --git a/common/src/main/webapp/sass/explore.scss b/common/src/main/webapp/sass/explore.scss index ee2c7f315b..80e099c48f 100644 --- a/common/src/main/webapp/sass/explore.scss +++ b/common/src/main/webapp/sass/explore.scss @@ -3384,13 +3384,18 @@ a.explore-upgrade-link { flex: 1 1 0; padding: 1em; - &.program-details-left { + &.program-details-left, + .program-details-right { display: grid; grid-template-columns: [label] auto [value] 1fr; - gap: 0.25em 1em; + gap: 0.25em 3em; grid-auto-rows: max-content; } + .program-details-right { + margin-top: 3rem; + } + label { text-align: right; font-weight: bold; diff --git a/explore/src/clue/scala/queries/common/ProgramDetailsSubquery.scala b/explore/src/clue/scala/queries/common/ProgramDetailsSubquery.scala index 6c1be17f01..065d9f266f 100644 --- a/explore/src/clue/scala/queries/common/ProgramDetailsSubquery.scala +++ b/explore/src/clue/scala/queries/common/ProgramDetailsSubquery.scala @@ -24,6 +24,7 @@ object ProgramDetailsSubquery allocations $AllocationSubquery goa { proprietaryMonths + shouldNotify } } """ diff --git a/explore/src/main/scala/explore/common/ProgramQueries.scala b/explore/src/main/scala/explore/common/ProgramQueries.scala index 137127c8e1..8c711d2a12 100644 --- a/explore/src/main/scala/explore/common/ProgramQueries.scala +++ b/explore/src/main/scala/explore/common/ProgramQueries.scala @@ -71,6 +71,17 @@ object ProgramQueries: SET = ProgramPropertiesInput(name = name.orUnassign) ) + def updateGoaShouldNotify[F[_]: Async](id: Program.Id, shouldNotify: Boolean)(using + FetchClient[F, ObservationDB] + ): F[Unit] = + updateProgram: + UpdateProgramsInput( + WHERE = id.toWhereProgram.assign, + SET = ProgramPropertiesInput( + goa = GoaPropertiesInput(shouldNotify = shouldNotify.assign).assign + ) + ) + def updateAttachmentDescription[F[_]: Async]( oid: Attachment.Id, desc: Option[NonEmptyString] diff --git a/explore/src/main/scala/explore/programs/ProgramDetailsTile.scala b/explore/src/main/scala/explore/programs/ProgramDetailsTile.scala index 3d858b8081..924e5c1adf 100644 --- a/explore/src/main/scala/explore/programs/ProgramDetailsTile.scala +++ b/explore/src/main/scala/explore/programs/ProgramDetailsTile.scala @@ -3,10 +3,14 @@ package explore.programs +import cats.effect.IO import cats.syntax.all.* import crystal.Pot import crystal.react.View +import crystal.react.syntax.effect.* +import explore.common.ProgramQueries import explore.components.ui.ExploreStyles +import explore.model.AppContext import explore.model.Constants import explore.model.ProgramDetails import explore.model.ProgramTimes @@ -17,52 +21,94 @@ import lucuma.core.model.Program import lucuma.core.model.Semester import lucuma.core.syntax.display.* import lucuma.react.common.ReactFnProps +import lucuma.refined.* +import lucuma.ui.primereact.CheckboxView import lucuma.ui.primereact.FormInfo +import lucuma.ui.primereact.given case class ProgramDetailsTile( - programId: Program.Id, - programDetails: View[ProgramDetails], - programTimes: Pot[ProgramTimes], - semester: Semester + programId: Program.Id, + programDetails: View[ProgramDetails], + programTimes: Pot[ProgramTimes], + semester: Semester, + userIsReadonlyCoi: Boolean ) extends ReactFnProps(ProgramDetailsTile.component) object ProgramDetailsTile: private type Props = ProgramDetailsTile private val component = ScalaFnComponent[Props]: props => - val details: ProgramDetails = props.programDetails.get - val thesis: Boolean = details.allUsers.exists(_.thesis.exists(_ === true)) - val users: View[List[ProgramUser]] = props.programDetails.zoom(ProgramDetails.allUsers) + useContext(AppContext.ctx).map: ctx => + import ctx.given - <.div(ExploreStyles.ProgramDetailsTile)( - <.div(ExploreStyles.ProgramDetailsInfoArea, ExploreStyles.ProgramDetailsLeft)( - FormInfo(details.reference.map(_.label).getOrElse("---"), "Reference"), - FormInfo(Constants.GppDateFormatter.format(props.semester.start.localDate), "Start"), - FormInfo(Constants.GppDateFormatter.format(props.semester.end.localDate), "End"), - // Thesis should be set True if any of the investigators will use the proposal as part of their thesis (3390) - FormInfo(if (thesis) "Yes" else "No", "Thesis"), - FormInfo(s"${details.proprietaryMonths} months", "Proprietary") - ), - <.div( - TimeAwardTable(details.allocations), - TimeAccountingTable(props.programTimes) - ), - <.div(ExploreStyles.ProgramDetailsInfoArea)( - SupportUsers( - props.programId, - users, - "Principal Support", - SupportUsers.SupportRole.Primary + val details: ProgramDetails = props.programDetails.get + val thesis: Boolean = details.allUsers.exists(_.thesis.exists(_ === true)) + val users: View[List[ProgramUser]] = props.programDetails.zoom(ProgramDetails.allUsers) + val newDataNotificationView = + props.programDetails + .zoom(ProgramDetails.shouldNotify) + .withOnMod(b => ProgramQueries.updateGoaShouldNotify[IO](props.programId, b).runAsync) + + <.div(ExploreStyles.ProgramDetailsTile)( + <.div(ExploreStyles.ProgramDetailsInfoArea, ExploreStyles.ProgramDetailsLeft)( + FormInfo(details.reference.map(_.label).getOrElse("---"), "Reference"), + FormInfo(Constants.GppDateFormatter.format(props.semester.start.localDate), "Start"), + FormInfo(Constants.GppDateFormatter.format(props.semester.end.localDate), "End"), + // Thesis should be set True if any of the investigators will use the proposal as part of their thesis (3390) + FormInfo(if (thesis) "Yes" else "No", "Thesis"), + FormInfo(s"${details.proprietaryMonths} months", "Proprietary") + ), + <.div( + TimeAwardTable(details.allocations), + TimeAccountingTable(props.programTimes) ), - SupportUsers( - props.programId, - users, - "Additional Support", - SupportUsers.SupportRole.Secondary + <.div(ExploreStyles.ProgramDetailsInfoArea)( + SupportUsers( + props.programId, + users, + "Principal Support", + SupportUsers.SupportRole.Primary + ), + SupportUsers( + props.programId, + users, + "Additional Support", + SupportUsers.SupportRole.Secondary + ), + + // The two Notifications flags are user-settable and determine whether the archive sends email notifications for new data and whether the ODB sends notifications for expired timing windows (3388, 3389) + <.div( + ExploreStyles.ProgramDetailsRight, + FormInfo( + CheckboxView( + id = "shouldNotify".refined, + value = newDataNotificationView, + label = "New Science Data", + disabled = props.userIsReadonlyCoi + ), + "Notifications" + ) + // FormInfo( + // CheckboxView( + // id = "expiredTimingWindows".refined, + // value = ???, + // label = "Expired Timing Windows" + // ), + // "" + // ) + ) + + // The Eavesdropping` UI will allow PIs of accepted programs to select dates when they are available for eavesdropping. This is not needed for XT. (NEED TICKET) + // <.div( + // ExploreStyles.ProgramDetailsRight, + // FormInfo( + // CheckboxView( + // id = "eavesdropping".refined, + // value = ???, + // label = ??? // instead of a label there will be a date picker or something? + // ), + // "Eavesdropping" + // ) + // ) ) - // The two Notifications flags are user-settable and determine whether the archive sends email notifications for new data and whether the ODB sends notifications for expired timing windows (3388, 3389) - // FormInfo("", "Notifications") - // The Eavesdropping` UI will allow PIs of accepted programs to select dates when they are available for eavesdropping. This is not needed for XT. (NEED TICKET) - // FormInfo("", "Eavesdropping") ) - ) diff --git a/explore/src/main/scala/explore/tabs/ProgramTabContents.scala b/explore/src/main/scala/explore/tabs/ProgramTabContents.scala index f078480d1c..d2e55045fb 100644 --- a/explore/src/main/scala/explore/tabs/ProgramTabContents.scala +++ b/explore/src/main/scala/explore/tabs/ProgramTabContents.scala @@ -82,7 +82,8 @@ object ProgramTabContents: props.programId, props.programDetails, props.programTimes, - props.semester + props.semester, + props.userIsReadonlyCoi ) ) diff --git a/model/shared/src/main/scala/explore/model/ProgramDetails.scala b/model/shared/src/main/scala/explore/model/ProgramDetails.scala index e2531b485e..530836a89b 100644 --- a/model/shared/src/main/scala/explore/model/ProgramDetails.scala +++ b/model/shared/src/main/scala/explore/model/ProgramDetails.scala @@ -29,7 +29,8 @@ case class ProgramDetails( users: List[ProgramUser], reference: Option[ProgramReference], allocations: CategoryAllocationList, - proprietaryMonths: NonNegInt + proprietaryMonths: NonNegInt, + shouldNotify: Boolean ) derives Eq: val allUsers: List[ProgramUser] = pi.fold(users)(_ :: users) @@ -47,6 +48,7 @@ object ProgramDetails: val pi: Lens[ProgramDetails, Option[ProgramUser]] = Focus[ProgramDetails](_.pi) val piPartner: Optional[ProgramDetails, Option[PartnerLink]] = pi.some.andThen(ProgramUser.partnerLink) + val shouldNotify: Lens[ProgramDetails, Boolean] = Focus[ProgramDetails](_.shouldNotify) given Decoder[ProgramDetails] = Decoder.instance(c => for { @@ -61,5 +63,6 @@ object ProgramDetails: c.downField("reference").downField("label").success.traverse(_.as[Option[ProgramReference]]) as <- c.downField("allocations").as[CategoryAllocationList] pm <- c.downField("goa").downField("proprietaryMonths").as[NonNegInt] - } yield ProgramDetails(n, d, t, p, ps, pi, us, r.flatten, as, pm) + sn <- c.downField("goa").downField("shouldNotify").as[Boolean] + } yield ProgramDetails(n, d, t, p, ps, pi, us, r.flatten, as, pm, sn) )