diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala deleted file mode 100644 index a3b00af..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala +++ /dev/null @@ -1,103 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import scala.scalajs.js.annotation.JSExportTopLevel -import scala.scalajs.js.annotation.JSExport -import scala.scalajs.js.annotation.JSImport -import scala.scalajs.js -import org.scalajs.dom -import com.raquo.laminar.api.L.{*, given} -import scala.scalajs.js.Date -import com.raquo.waypoint.Router -import com.raquo.waypoint.SplitRender -import cz.e_bs.cmi.mdr.pdb.app.services.DataFetcher -import scala.scalajs.js.JSON -import zio.json._ -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.state.AppState - -@js.native -@JSImport("stylesheets/main.css", JSImport.Namespace) -object Css extends js.Any - -@js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native -@JSImport("params/pdb-params.json", JSImport.Default) -object pdbParams extends js.Object - -@JSExportTopLevel("app") -object Main: - - @JSExport - def main(args: Array[String]): Unit = - import Routes.given - onLoad { - setupAirstream() - val appContainer = dom.document.querySelector("#app") - val _ = - render( - appContainer, - renderPage(state.MockAppState(using unsafeWindowOwner, router)) - ) - } - - private def onLoad(f: => Unit): Unit = - documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) - - private def setupAirstream()(using router: Router[Page]): Unit = - AirstreamError.registerUnhandledErrorCallback(err => - router.forcePage( - Page.UnhandledError( - Some(err.getClass.getName), // TODO: Fill only in dev mode - Some(err.getMessage) - ) - ) - ) - - def renderPage(state: AppState)(using - router: Router[Page] - ): HtmlElement = - val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) - .collectSignal[Page.Detail]( - connectors - .DetailPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailParametru]( - connectors - .DetailParametruPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailKriteria]( - connectors - .DetailKriteriaPageConnector(state)(_) - .apply - ) - .collectSignal[Page.UpravDukazKriteria]( - pages.detail.UpravDukaz.Connector(state)(_).apply - ) - .collectStatic(Page.Dashboard)( - connectors.DashboardPageConnector(state.actionBus).apply - ) - .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) - ) - .collect[Page.UnhandledError](pg => - pages.errors - .UnhandledErrorPage( - pages.errors.UnhandledErrorPage - .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), - state.actionBus - ) - ) - .collectStatic(Page.Directory)( - connectors - .DirectoryPageConnector(state.users, state.actionBus) - .apply - ) - div(cls := "h-full", child <-- pageSplitter.$view) - - // Pull in the stylesheet - val css: Css.type = Css diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala deleted file mode 100644 index a3b00af..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala +++ /dev/null @@ -1,103 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import scala.scalajs.js.annotation.JSExportTopLevel -import scala.scalajs.js.annotation.JSExport -import scala.scalajs.js.annotation.JSImport -import scala.scalajs.js -import org.scalajs.dom -import com.raquo.laminar.api.L.{*, given} -import scala.scalajs.js.Date -import com.raquo.waypoint.Router -import com.raquo.waypoint.SplitRender -import cz.e_bs.cmi.mdr.pdb.app.services.DataFetcher -import scala.scalajs.js.JSON -import zio.json._ -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.state.AppState - -@js.native -@JSImport("stylesheets/main.css", JSImport.Namespace) -object Css extends js.Any - -@js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native -@JSImport("params/pdb-params.json", JSImport.Default) -object pdbParams extends js.Object - -@JSExportTopLevel("app") -object Main: - - @JSExport - def main(args: Array[String]): Unit = - import Routes.given - onLoad { - setupAirstream() - val appContainer = dom.document.querySelector("#app") - val _ = - render( - appContainer, - renderPage(state.MockAppState(using unsafeWindowOwner, router)) - ) - } - - private def onLoad(f: => Unit): Unit = - documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) - - private def setupAirstream()(using router: Router[Page]): Unit = - AirstreamError.registerUnhandledErrorCallback(err => - router.forcePage( - Page.UnhandledError( - Some(err.getClass.getName), // TODO: Fill only in dev mode - Some(err.getMessage) - ) - ) - ) - - def renderPage(state: AppState)(using - router: Router[Page] - ): HtmlElement = - val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) - .collectSignal[Page.Detail]( - connectors - .DetailPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailParametru]( - connectors - .DetailParametruPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailKriteria]( - connectors - .DetailKriteriaPageConnector(state)(_) - .apply - ) - .collectSignal[Page.UpravDukazKriteria]( - pages.detail.UpravDukaz.Connector(state)(_).apply - ) - .collectStatic(Page.Dashboard)( - connectors.DashboardPageConnector(state.actionBus).apply - ) - .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) - ) - .collect[Page.UnhandledError](pg => - pages.errors - .UnhandledErrorPage( - pages.errors.UnhandledErrorPage - .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), - state.actionBus - ) - ) - .collectStatic(Page.Directory)( - connectors - .DirectoryPageConnector(state.users, state.actionBus) - .apply - ) - div(cls := "h-full", child <-- pageSplitter.$view) - - // Pull in the stylesheet - val css: Css.type = Css diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala deleted file mode 100644 index 3bbc8c0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ /dev/null @@ -1,198 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.* -import org.scalajs.dom -import zio.json.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import scala.scalajs.js -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.Page.Titled - -// enum is not working with Waypoints' SplitRender collectStatic -sealed abstract class Page( - val id: String, - val title: String, - val parent: Option[Page] -) { - val path: Vector[Page] = - parent match - case None => Vector(this) - case Some(p) => p.path :+ this - - val isRoot: Boolean = parent.isEmpty -} - -object Page: - - case class Titled[V](value: V, title: Option[String] = None): - val show: String = title.getOrElse(value.toString) - - case object Directory extends Page("directory", "Adresář", None) - - case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) - - case class Detail(osobniCislo: Titled[OsobniCislo]) - extends Page("user", osobniCislo.show, Some(Directory)) - - object Detail { - def apply(o: UserInfo): Detail = Detail( - Titled(o.personalNumber, Some(o.name)) - ) - } - - case class DetailParametru( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String] - ) extends Page( - "parameter", - parametr.show, - Some(Detail(osobniCislo)) - ) - - object DetailParametru { - def apply(o: UserInfo, p: Parameter): DetailParametru = - DetailParametru( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)) - ) - } - - case class DetailKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "criteria", - kriterium.show, - Some(DetailParametru(osobniCislo, parametr)) - ) - - object DetailKriteria { - def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = - DetailKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class UpravDukazKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "addProof", - "Důkaz", - Some(DetailKriteria(osobniCislo, parametr, kriterium)) - ) - - object UpravDukazKriteria { - def apply( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): UpravDukazKriteria = - UpravDukazKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class NotFound(url: String) extends Page("404", "404", Some(Directory)) - - case class UnhandledError( - errorName: Option[String], - errorMessage: Option[String] - ) extends Page("500", "Unexpected error", Some(Directory)) - -object Routes: - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) - given [V: JsonEncoder]: JsonEncoder[Titled[V]] = - DeriveJsonEncoder.gen[Titled[V]] - given [V: JsonDecoder]: JsonDecoder[Titled[V]] = - DeriveJsonDecoder.gen[Titled[V]] - given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] - given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] - - val base = - js.`import`.meta.env.BASE_URL - .asInstanceOf[String] + "app" - - val homePage: Page = Page.Directory - - given router: Router[Page] = Router[Page]( - routes = List( - Route.static(homePage, root / endOfSegments, basePath = base), - Route.static( - Page.Dashboard, - root / "dashboard" / endOfSegments, - basePath = base - ), - Route[Page.Detail, String]( - encode = _.osobniCislo.value.toString, - decode = osc => Page.Detail(Titled(OsobniCislo(osc))), - root / "osoba" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.DetailParametru, (String, String)]( - encode = p => (p.osobniCislo.value.toString, p.parametr.value), - decode = - p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / endOfSegments, - basePath = base - ), - Route[Page.DetailKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.DetailKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.UpravDukazKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.UpravDukazKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / "edit" / endOfSegments, - basePath = base - ) - ), - serializePage = _.toJson, - deserializePage = _.fromJson[Page] - .fold(s => throw IllegalStateException(s), identity), - getPageTitle = _.title, - routeFallback = url => Page.NotFound(url), - deserializeFallback = _ => Page.Dashboard - )( - $popStateEvent = windowEvents.onPopState, - owner = unsafeWindowOwner - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala deleted file mode 100644 index a3b00af..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala +++ /dev/null @@ -1,103 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import scala.scalajs.js.annotation.JSExportTopLevel -import scala.scalajs.js.annotation.JSExport -import scala.scalajs.js.annotation.JSImport -import scala.scalajs.js -import org.scalajs.dom -import com.raquo.laminar.api.L.{*, given} -import scala.scalajs.js.Date -import com.raquo.waypoint.Router -import com.raquo.waypoint.SplitRender -import cz.e_bs.cmi.mdr.pdb.app.services.DataFetcher -import scala.scalajs.js.JSON -import zio.json._ -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.state.AppState - -@js.native -@JSImport("stylesheets/main.css", JSImport.Namespace) -object Css extends js.Any - -@js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native -@JSImport("params/pdb-params.json", JSImport.Default) -object pdbParams extends js.Object - -@JSExportTopLevel("app") -object Main: - - @JSExport - def main(args: Array[String]): Unit = - import Routes.given - onLoad { - setupAirstream() - val appContainer = dom.document.querySelector("#app") - val _ = - render( - appContainer, - renderPage(state.MockAppState(using unsafeWindowOwner, router)) - ) - } - - private def onLoad(f: => Unit): Unit = - documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) - - private def setupAirstream()(using router: Router[Page]): Unit = - AirstreamError.registerUnhandledErrorCallback(err => - router.forcePage( - Page.UnhandledError( - Some(err.getClass.getName), // TODO: Fill only in dev mode - Some(err.getMessage) - ) - ) - ) - - def renderPage(state: AppState)(using - router: Router[Page] - ): HtmlElement = - val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) - .collectSignal[Page.Detail]( - connectors - .DetailPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailParametru]( - connectors - .DetailParametruPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailKriteria]( - connectors - .DetailKriteriaPageConnector(state)(_) - .apply - ) - .collectSignal[Page.UpravDukazKriteria]( - pages.detail.UpravDukaz.Connector(state)(_).apply - ) - .collectStatic(Page.Dashboard)( - connectors.DashboardPageConnector(state.actionBus).apply - ) - .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) - ) - .collect[Page.UnhandledError](pg => - pages.errors - .UnhandledErrorPage( - pages.errors.UnhandledErrorPage - .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), - state.actionBus - ) - ) - .collectStatic(Page.Directory)( - connectors - .DirectoryPageConnector(state.users, state.actionBus) - .apply - ) - div(cls := "h-full", child <-- pageSplitter.$view) - - // Pull in the stylesheet - val css: Css.type = Css diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala deleted file mode 100644 index 3bbc8c0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ /dev/null @@ -1,198 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.* -import org.scalajs.dom -import zio.json.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import scala.scalajs.js -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.Page.Titled - -// enum is not working with Waypoints' SplitRender collectStatic -sealed abstract class Page( - val id: String, - val title: String, - val parent: Option[Page] -) { - val path: Vector[Page] = - parent match - case None => Vector(this) - case Some(p) => p.path :+ this - - val isRoot: Boolean = parent.isEmpty -} - -object Page: - - case class Titled[V](value: V, title: Option[String] = None): - val show: String = title.getOrElse(value.toString) - - case object Directory extends Page("directory", "Adresář", None) - - case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) - - case class Detail(osobniCislo: Titled[OsobniCislo]) - extends Page("user", osobniCislo.show, Some(Directory)) - - object Detail { - def apply(o: UserInfo): Detail = Detail( - Titled(o.personalNumber, Some(o.name)) - ) - } - - case class DetailParametru( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String] - ) extends Page( - "parameter", - parametr.show, - Some(Detail(osobniCislo)) - ) - - object DetailParametru { - def apply(o: UserInfo, p: Parameter): DetailParametru = - DetailParametru( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)) - ) - } - - case class DetailKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "criteria", - kriterium.show, - Some(DetailParametru(osobniCislo, parametr)) - ) - - object DetailKriteria { - def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = - DetailKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class UpravDukazKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "addProof", - "Důkaz", - Some(DetailKriteria(osobniCislo, parametr, kriterium)) - ) - - object UpravDukazKriteria { - def apply( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): UpravDukazKriteria = - UpravDukazKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class NotFound(url: String) extends Page("404", "404", Some(Directory)) - - case class UnhandledError( - errorName: Option[String], - errorMessage: Option[String] - ) extends Page("500", "Unexpected error", Some(Directory)) - -object Routes: - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) - given [V: JsonEncoder]: JsonEncoder[Titled[V]] = - DeriveJsonEncoder.gen[Titled[V]] - given [V: JsonDecoder]: JsonDecoder[Titled[V]] = - DeriveJsonDecoder.gen[Titled[V]] - given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] - given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] - - val base = - js.`import`.meta.env.BASE_URL - .asInstanceOf[String] + "app" - - val homePage: Page = Page.Directory - - given router: Router[Page] = Router[Page]( - routes = List( - Route.static(homePage, root / endOfSegments, basePath = base), - Route.static( - Page.Dashboard, - root / "dashboard" / endOfSegments, - basePath = base - ), - Route[Page.Detail, String]( - encode = _.osobniCislo.value.toString, - decode = osc => Page.Detail(Titled(OsobniCislo(osc))), - root / "osoba" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.DetailParametru, (String, String)]( - encode = p => (p.osobniCislo.value.toString, p.parametr.value), - decode = - p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / endOfSegments, - basePath = base - ), - Route[Page.DetailKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.DetailKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.UpravDukazKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.UpravDukazKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / "edit" / endOfSegments, - basePath = base - ) - ), - serializePage = _.toJson, - deserializePage = _.fromJson[Page] - .fold(s => throw IllegalStateException(s), identity), - getPageTitle = _.title, - routeFallback = url => Page.NotFound(url), - deserializeFallback = _ => Page.Dashboard - )( - $popStateEvent = windowEvents.onPopState, - owner = unsafeWindowOwner - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala deleted file mode 100644 index b24b82b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -sealed trait Action - -case object FetchDirectory extends Action -case class FetchUserDetails(osc: OsobniCislo) extends Action -case class FetchParameters(osc: OsobniCislo) extends Action -case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action -case class FetchParameterCriteria( - osc: OsobniCislo, - paramId: String, - critId: String, - page: (UserInfo, Parameter, ParameterCriteria) => Page -) extends Action -case class FetchAvailableFiles(osc: OsobniCislo) extends Action -case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala deleted file mode 100644 index a3b00af..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala +++ /dev/null @@ -1,103 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import scala.scalajs.js.annotation.JSExportTopLevel -import scala.scalajs.js.annotation.JSExport -import scala.scalajs.js.annotation.JSImport -import scala.scalajs.js -import org.scalajs.dom -import com.raquo.laminar.api.L.{*, given} -import scala.scalajs.js.Date -import com.raquo.waypoint.Router -import com.raquo.waypoint.SplitRender -import cz.e_bs.cmi.mdr.pdb.app.services.DataFetcher -import scala.scalajs.js.JSON -import zio.json._ -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.state.AppState - -@js.native -@JSImport("stylesheets/main.css", JSImport.Namespace) -object Css extends js.Any - -@js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native -@JSImport("params/pdb-params.json", JSImport.Default) -object pdbParams extends js.Object - -@JSExportTopLevel("app") -object Main: - - @JSExport - def main(args: Array[String]): Unit = - import Routes.given - onLoad { - setupAirstream() - val appContainer = dom.document.querySelector("#app") - val _ = - render( - appContainer, - renderPage(state.MockAppState(using unsafeWindowOwner, router)) - ) - } - - private def onLoad(f: => Unit): Unit = - documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) - - private def setupAirstream()(using router: Router[Page]): Unit = - AirstreamError.registerUnhandledErrorCallback(err => - router.forcePage( - Page.UnhandledError( - Some(err.getClass.getName), // TODO: Fill only in dev mode - Some(err.getMessage) - ) - ) - ) - - def renderPage(state: AppState)(using - router: Router[Page] - ): HtmlElement = - val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) - .collectSignal[Page.Detail]( - connectors - .DetailPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailParametru]( - connectors - .DetailParametruPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailKriteria]( - connectors - .DetailKriteriaPageConnector(state)(_) - .apply - ) - .collectSignal[Page.UpravDukazKriteria]( - pages.detail.UpravDukaz.Connector(state)(_).apply - ) - .collectStatic(Page.Dashboard)( - connectors.DashboardPageConnector(state.actionBus).apply - ) - .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) - ) - .collect[Page.UnhandledError](pg => - pages.errors - .UnhandledErrorPage( - pages.errors.UnhandledErrorPage - .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), - state.actionBus - ) - ) - .collectStatic(Page.Directory)( - connectors - .DirectoryPageConnector(state.users, state.actionBus) - .apply - ) - div(cls := "h-full", child <-- pageSplitter.$view) - - // Pull in the stylesheet - val css: Css.type = Css diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala deleted file mode 100644 index 3bbc8c0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ /dev/null @@ -1,198 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.* -import org.scalajs.dom -import zio.json.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import scala.scalajs.js -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.Page.Titled - -// enum is not working with Waypoints' SplitRender collectStatic -sealed abstract class Page( - val id: String, - val title: String, - val parent: Option[Page] -) { - val path: Vector[Page] = - parent match - case None => Vector(this) - case Some(p) => p.path :+ this - - val isRoot: Boolean = parent.isEmpty -} - -object Page: - - case class Titled[V](value: V, title: Option[String] = None): - val show: String = title.getOrElse(value.toString) - - case object Directory extends Page("directory", "Adresář", None) - - case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) - - case class Detail(osobniCislo: Titled[OsobniCislo]) - extends Page("user", osobniCislo.show, Some(Directory)) - - object Detail { - def apply(o: UserInfo): Detail = Detail( - Titled(o.personalNumber, Some(o.name)) - ) - } - - case class DetailParametru( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String] - ) extends Page( - "parameter", - parametr.show, - Some(Detail(osobniCislo)) - ) - - object DetailParametru { - def apply(o: UserInfo, p: Parameter): DetailParametru = - DetailParametru( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)) - ) - } - - case class DetailKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "criteria", - kriterium.show, - Some(DetailParametru(osobniCislo, parametr)) - ) - - object DetailKriteria { - def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = - DetailKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class UpravDukazKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "addProof", - "Důkaz", - Some(DetailKriteria(osobniCislo, parametr, kriterium)) - ) - - object UpravDukazKriteria { - def apply( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): UpravDukazKriteria = - UpravDukazKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class NotFound(url: String) extends Page("404", "404", Some(Directory)) - - case class UnhandledError( - errorName: Option[String], - errorMessage: Option[String] - ) extends Page("500", "Unexpected error", Some(Directory)) - -object Routes: - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) - given [V: JsonEncoder]: JsonEncoder[Titled[V]] = - DeriveJsonEncoder.gen[Titled[V]] - given [V: JsonDecoder]: JsonDecoder[Titled[V]] = - DeriveJsonDecoder.gen[Titled[V]] - given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] - given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] - - val base = - js.`import`.meta.env.BASE_URL - .asInstanceOf[String] + "app" - - val homePage: Page = Page.Directory - - given router: Router[Page] = Router[Page]( - routes = List( - Route.static(homePage, root / endOfSegments, basePath = base), - Route.static( - Page.Dashboard, - root / "dashboard" / endOfSegments, - basePath = base - ), - Route[Page.Detail, String]( - encode = _.osobniCislo.value.toString, - decode = osc => Page.Detail(Titled(OsobniCislo(osc))), - root / "osoba" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.DetailParametru, (String, String)]( - encode = p => (p.osobniCislo.value.toString, p.parametr.value), - decode = - p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / endOfSegments, - basePath = base - ), - Route[Page.DetailKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.DetailKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.UpravDukazKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.UpravDukazKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / "edit" / endOfSegments, - basePath = base - ) - ), - serializePage = _.toJson, - deserializePage = _.fromJson[Page] - .fold(s => throw IllegalStateException(s), identity), - getPageTitle = _.title, - routeFallback = url => Page.NotFound(url), - deserializeFallback = _ => Page.Dashboard - )( - $popStateEvent = windowEvents.onPopState, - owner = unsafeWindowOwner - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala deleted file mode 100644 index b24b82b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -sealed trait Action - -case object FetchDirectory extends Action -case class FetchUserDetails(osc: OsobniCislo) extends Action -case class FetchParameters(osc: OsobniCislo) extends Action -case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action -case class FetchParameterCriteria( - osc: OsobniCislo, - paramId: String, - critId: String, - page: (UserInfo, Parameter, ParameterCriteria) => Page -) extends Action -case class FetchAvailableFiles(osc: OsobniCislo) extends Action -case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala deleted file mode 100644 index c637d35..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala +++ /dev/null @@ -1,82 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.{UserProfile, UserInfo, OsobniCislo} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo -import cz.e_bs.cmi.mdr.pdb.UserFunction - -object AppPage: - // TODO: pages by logged in user - val pages: List[Page] = List(Page.Directory, Page.Dashboard) - - import NavigationBar.{Logo, MenuItem} - - val logo = Logo( - "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", - "Workflow" - ) - - // TODO: menu items by user profile - val userMenu = - List( - MenuItem("Your Profile"), - MenuItem("Settings"), - MenuItem("Sign out") - ) - - // TODO: load user profile - val $userProfile = Var( - UserProfile( - "tom", - UserInfo( - OsobniCislo("1031"), - "tom", - "Tom", - "Cook", - None, - None, - Some("tom@example.com"), - Some("+420 222 866 180"), - Some( - UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") - ), - Nil, - None - ) - ) - ) - - val $userInfo = $userProfile.signal.map(_.userInfo) - - type ViewModel = Option[HtmlElement] - def apply( - actionBus: Observer[Action] - )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using - router: Router[Page] - ): HtmlElement = - PageLayout(actionBus)( - $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => - PageLayout.ViewModel( - NavigationBar.ViewModel( - u, - pages.map(p => - NavigationBar.Link( - () => - PageLink( - p, - actionBus - ), - p == cp - ) - ), - userMenu, - logo - ), - c - ) - ), - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala deleted file mode 100644 index a3b00af..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala +++ /dev/null @@ -1,103 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import scala.scalajs.js.annotation.JSExportTopLevel -import scala.scalajs.js.annotation.JSExport -import scala.scalajs.js.annotation.JSImport -import scala.scalajs.js -import org.scalajs.dom -import com.raquo.laminar.api.L.{*, given} -import scala.scalajs.js.Date -import com.raquo.waypoint.Router -import com.raquo.waypoint.SplitRender -import cz.e_bs.cmi.mdr.pdb.app.services.DataFetcher -import scala.scalajs.js.JSON -import zio.json._ -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.state.AppState - -@js.native -@JSImport("stylesheets/main.css", JSImport.Namespace) -object Css extends js.Any - -@js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native -@JSImport("params/pdb-params.json", JSImport.Default) -object pdbParams extends js.Object - -@JSExportTopLevel("app") -object Main: - - @JSExport - def main(args: Array[String]): Unit = - import Routes.given - onLoad { - setupAirstream() - val appContainer = dom.document.querySelector("#app") - val _ = - render( - appContainer, - renderPage(state.MockAppState(using unsafeWindowOwner, router)) - ) - } - - private def onLoad(f: => Unit): Unit = - documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) - - private def setupAirstream()(using router: Router[Page]): Unit = - AirstreamError.registerUnhandledErrorCallback(err => - router.forcePage( - Page.UnhandledError( - Some(err.getClass.getName), // TODO: Fill only in dev mode - Some(err.getMessage) - ) - ) - ) - - def renderPage(state: AppState)(using - router: Router[Page] - ): HtmlElement = - val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) - .collectSignal[Page.Detail]( - connectors - .DetailPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailParametru]( - connectors - .DetailParametruPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailKriteria]( - connectors - .DetailKriteriaPageConnector(state)(_) - .apply - ) - .collectSignal[Page.UpravDukazKriteria]( - pages.detail.UpravDukaz.Connector(state)(_).apply - ) - .collectStatic(Page.Dashboard)( - connectors.DashboardPageConnector(state.actionBus).apply - ) - .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) - ) - .collect[Page.UnhandledError](pg => - pages.errors - .UnhandledErrorPage( - pages.errors.UnhandledErrorPage - .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), - state.actionBus - ) - ) - .collectStatic(Page.Directory)( - connectors - .DirectoryPageConnector(state.users, state.actionBus) - .apply - ) - div(cls := "h-full", child <-- pageSplitter.$view) - - // Pull in the stylesheet - val css: Css.type = Css diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala deleted file mode 100644 index 3bbc8c0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ /dev/null @@ -1,198 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.* -import org.scalajs.dom -import zio.json.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import scala.scalajs.js -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.Page.Titled - -// enum is not working with Waypoints' SplitRender collectStatic -sealed abstract class Page( - val id: String, - val title: String, - val parent: Option[Page] -) { - val path: Vector[Page] = - parent match - case None => Vector(this) - case Some(p) => p.path :+ this - - val isRoot: Boolean = parent.isEmpty -} - -object Page: - - case class Titled[V](value: V, title: Option[String] = None): - val show: String = title.getOrElse(value.toString) - - case object Directory extends Page("directory", "Adresář", None) - - case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) - - case class Detail(osobniCislo: Titled[OsobniCislo]) - extends Page("user", osobniCislo.show, Some(Directory)) - - object Detail { - def apply(o: UserInfo): Detail = Detail( - Titled(o.personalNumber, Some(o.name)) - ) - } - - case class DetailParametru( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String] - ) extends Page( - "parameter", - parametr.show, - Some(Detail(osobniCislo)) - ) - - object DetailParametru { - def apply(o: UserInfo, p: Parameter): DetailParametru = - DetailParametru( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)) - ) - } - - case class DetailKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "criteria", - kriterium.show, - Some(DetailParametru(osobniCislo, parametr)) - ) - - object DetailKriteria { - def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = - DetailKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class UpravDukazKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "addProof", - "Důkaz", - Some(DetailKriteria(osobniCislo, parametr, kriterium)) - ) - - object UpravDukazKriteria { - def apply( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): UpravDukazKriteria = - UpravDukazKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class NotFound(url: String) extends Page("404", "404", Some(Directory)) - - case class UnhandledError( - errorName: Option[String], - errorMessage: Option[String] - ) extends Page("500", "Unexpected error", Some(Directory)) - -object Routes: - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) - given [V: JsonEncoder]: JsonEncoder[Titled[V]] = - DeriveJsonEncoder.gen[Titled[V]] - given [V: JsonDecoder]: JsonDecoder[Titled[V]] = - DeriveJsonDecoder.gen[Titled[V]] - given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] - given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] - - val base = - js.`import`.meta.env.BASE_URL - .asInstanceOf[String] + "app" - - val homePage: Page = Page.Directory - - given router: Router[Page] = Router[Page]( - routes = List( - Route.static(homePage, root / endOfSegments, basePath = base), - Route.static( - Page.Dashboard, - root / "dashboard" / endOfSegments, - basePath = base - ), - Route[Page.Detail, String]( - encode = _.osobniCislo.value.toString, - decode = osc => Page.Detail(Titled(OsobniCislo(osc))), - root / "osoba" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.DetailParametru, (String, String)]( - encode = p => (p.osobniCislo.value.toString, p.parametr.value), - decode = - p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / endOfSegments, - basePath = base - ), - Route[Page.DetailKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.DetailKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.UpravDukazKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.UpravDukazKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / "edit" / endOfSegments, - basePath = base - ) - ), - serializePage = _.toJson, - deserializePage = _.fromJson[Page] - .fold(s => throw IllegalStateException(s), identity), - getPageTitle = _.title, - routeFallback = url => Page.NotFound(url), - deserializeFallback = _ => Page.Dashboard - )( - $popStateEvent = windowEvents.onPopState, - owner = unsafeWindowOwner - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala deleted file mode 100644 index b24b82b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -sealed trait Action - -case object FetchDirectory extends Action -case class FetchUserDetails(osc: OsobniCislo) extends Action -case class FetchParameters(osc: OsobniCislo) extends Action -case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action -case class FetchParameterCriteria( - osc: OsobniCislo, - paramId: String, - critId: String, - page: (UserInfo, Parameter, ParameterCriteria) => Page -) extends Action -case class FetchAvailableFiles(osc: OsobniCislo) extends Action -case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala deleted file mode 100644 index c637d35..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala +++ /dev/null @@ -1,82 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.{UserProfile, UserInfo, OsobniCislo} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo -import cz.e_bs.cmi.mdr.pdb.UserFunction - -object AppPage: - // TODO: pages by logged in user - val pages: List[Page] = List(Page.Directory, Page.Dashboard) - - import NavigationBar.{Logo, MenuItem} - - val logo = Logo( - "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", - "Workflow" - ) - - // TODO: menu items by user profile - val userMenu = - List( - MenuItem("Your Profile"), - MenuItem("Settings"), - MenuItem("Sign out") - ) - - // TODO: load user profile - val $userProfile = Var( - UserProfile( - "tom", - UserInfo( - OsobniCislo("1031"), - "tom", - "Tom", - "Cook", - None, - None, - Some("tom@example.com"), - Some("+420 222 866 180"), - Some( - UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") - ), - Nil, - None - ) - ) - ) - - val $userInfo = $userProfile.signal.map(_.userInfo) - - type ViewModel = Option[HtmlElement] - def apply( - actionBus: Observer[Action] - )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using - router: Router[Page] - ): HtmlElement = - PageLayout(actionBus)( - $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => - PageLayout.ViewModel( - NavigationBar.ViewModel( - u, - pages.map(p => - NavigationBar.Link( - () => - PageLink( - p, - actionBus - ), - p == cp - ) - ), - userMenu, - logo - ), - c - ) - ), - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala deleted file mode 100644 index bf473c6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ /dev/null @@ -1,143 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden -import fiftyforms.ui.components.tailwind.list.IconText.ViewModel -import fiftyforms.ui.components.tailwind.Icons -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Action - -object Breadcrumbs: - - private def slash = - import svg.{*, given} - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-300", - xmlns := "http://www.w3.org/2000/svg", - fill := "currentColor", - viewBox := "0 0 20 20", - ariaHidden := true, - path( - d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" - ) - ) - - def backIcon = - Icons.solid - .`arrow-narrow-left`() - .amend( - svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" - ) - - object Link: - case class ViewModel( - page: Page, - icon: Option[SvgElement], - extraClasses: String, - text: String, - textClasses: Option[String] = None - ) - - val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" - - def shortHome(p: Page) = ViewModel( - p, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - "Zpět na úvodní stránku", - None - ) - - def fullHome(p: Page) = ViewModel( - p, - Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), - "text-gray-400 hover:text-gray-500", - "Domů", - Some("sr-only") - ) - - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - PageLink - .container($m.map(_.page), actionBus) - .amend( - cls <-- $m.map(_.extraClasses), - child.maybe <-- $m.map(_.icon), - span( - cls <-- $m.map(_.textClasses.getOrElse("")), - child.text <-- $m.map(_.text) - ) - ) - - object FullBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - ol( - role := "list", - cls := "flex items-center space-x-4", - children <-- $m.map(_.path).split(_.id)((_, _, $p) => - li( - div( - cls := "flex items-center", - child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), - Link( - $p.map(p => - if (p.isRoot) then Link.fullHome(p) - else - Link.ViewModel( - p, - None, - s"ml-4 max-w-xs truncate ${Link.baseClasses}", - p.title - ) - ), - actionBus - ) - ) - ) - ) - ) - - object ShortBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - Link( - $m.map { p => - val target = p.parent.getOrElse(p) - if target.isRoot then Link.shortHome(target) - else - Link.ViewModel( - target, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - s"Zpět na ${target.title}" - ) - }, - actionBus - ) - - def apply(actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - nav( - cls := "flex", - aria.label := "Breadcrumb", - div( - cls := "flex sm:hidden", - ShortBreadcrumbs(router.$currentPage, actionBus) - ), - div( - cls := "hidden sm:block", - FullBreadcrumbs(router.$currentPage, actionBus) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala deleted file mode 100644 index a3b00af..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala +++ /dev/null @@ -1,103 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import scala.scalajs.js.annotation.JSExportTopLevel -import scala.scalajs.js.annotation.JSExport -import scala.scalajs.js.annotation.JSImport -import scala.scalajs.js -import org.scalajs.dom -import com.raquo.laminar.api.L.{*, given} -import scala.scalajs.js.Date -import com.raquo.waypoint.Router -import com.raquo.waypoint.SplitRender -import cz.e_bs.cmi.mdr.pdb.app.services.DataFetcher -import scala.scalajs.js.JSON -import zio.json._ -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.state.AppState - -@js.native -@JSImport("stylesheets/main.css", JSImport.Namespace) -object Css extends js.Any - -@js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native -@JSImport("params/pdb-params.json", JSImport.Default) -object pdbParams extends js.Object - -@JSExportTopLevel("app") -object Main: - - @JSExport - def main(args: Array[String]): Unit = - import Routes.given - onLoad { - setupAirstream() - val appContainer = dom.document.querySelector("#app") - val _ = - render( - appContainer, - renderPage(state.MockAppState(using unsafeWindowOwner, router)) - ) - } - - private def onLoad(f: => Unit): Unit = - documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) - - private def setupAirstream()(using router: Router[Page]): Unit = - AirstreamError.registerUnhandledErrorCallback(err => - router.forcePage( - Page.UnhandledError( - Some(err.getClass.getName), // TODO: Fill only in dev mode - Some(err.getMessage) - ) - ) - ) - - def renderPage(state: AppState)(using - router: Router[Page] - ): HtmlElement = - val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) - .collectSignal[Page.Detail]( - connectors - .DetailPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailParametru]( - connectors - .DetailParametruPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailKriteria]( - connectors - .DetailKriteriaPageConnector(state)(_) - .apply - ) - .collectSignal[Page.UpravDukazKriteria]( - pages.detail.UpravDukaz.Connector(state)(_).apply - ) - .collectStatic(Page.Dashboard)( - connectors.DashboardPageConnector(state.actionBus).apply - ) - .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) - ) - .collect[Page.UnhandledError](pg => - pages.errors - .UnhandledErrorPage( - pages.errors.UnhandledErrorPage - .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), - state.actionBus - ) - ) - .collectStatic(Page.Directory)( - connectors - .DirectoryPageConnector(state.users, state.actionBus) - .apply - ) - div(cls := "h-full", child <-- pageSplitter.$view) - - // Pull in the stylesheet - val css: Css.type = Css diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala deleted file mode 100644 index 3bbc8c0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ /dev/null @@ -1,198 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.* -import org.scalajs.dom -import zio.json.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import scala.scalajs.js -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.Page.Titled - -// enum is not working with Waypoints' SplitRender collectStatic -sealed abstract class Page( - val id: String, - val title: String, - val parent: Option[Page] -) { - val path: Vector[Page] = - parent match - case None => Vector(this) - case Some(p) => p.path :+ this - - val isRoot: Boolean = parent.isEmpty -} - -object Page: - - case class Titled[V](value: V, title: Option[String] = None): - val show: String = title.getOrElse(value.toString) - - case object Directory extends Page("directory", "Adresář", None) - - case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) - - case class Detail(osobniCislo: Titled[OsobniCislo]) - extends Page("user", osobniCislo.show, Some(Directory)) - - object Detail { - def apply(o: UserInfo): Detail = Detail( - Titled(o.personalNumber, Some(o.name)) - ) - } - - case class DetailParametru( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String] - ) extends Page( - "parameter", - parametr.show, - Some(Detail(osobniCislo)) - ) - - object DetailParametru { - def apply(o: UserInfo, p: Parameter): DetailParametru = - DetailParametru( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)) - ) - } - - case class DetailKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "criteria", - kriterium.show, - Some(DetailParametru(osobniCislo, parametr)) - ) - - object DetailKriteria { - def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = - DetailKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class UpravDukazKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "addProof", - "Důkaz", - Some(DetailKriteria(osobniCislo, parametr, kriterium)) - ) - - object UpravDukazKriteria { - def apply( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): UpravDukazKriteria = - UpravDukazKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class NotFound(url: String) extends Page("404", "404", Some(Directory)) - - case class UnhandledError( - errorName: Option[String], - errorMessage: Option[String] - ) extends Page("500", "Unexpected error", Some(Directory)) - -object Routes: - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) - given [V: JsonEncoder]: JsonEncoder[Titled[V]] = - DeriveJsonEncoder.gen[Titled[V]] - given [V: JsonDecoder]: JsonDecoder[Titled[V]] = - DeriveJsonDecoder.gen[Titled[V]] - given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] - given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] - - val base = - js.`import`.meta.env.BASE_URL - .asInstanceOf[String] + "app" - - val homePage: Page = Page.Directory - - given router: Router[Page] = Router[Page]( - routes = List( - Route.static(homePage, root / endOfSegments, basePath = base), - Route.static( - Page.Dashboard, - root / "dashboard" / endOfSegments, - basePath = base - ), - Route[Page.Detail, String]( - encode = _.osobniCislo.value.toString, - decode = osc => Page.Detail(Titled(OsobniCislo(osc))), - root / "osoba" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.DetailParametru, (String, String)]( - encode = p => (p.osobniCislo.value.toString, p.parametr.value), - decode = - p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / endOfSegments, - basePath = base - ), - Route[Page.DetailKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.DetailKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.UpravDukazKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.UpravDukazKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / "edit" / endOfSegments, - basePath = base - ) - ), - serializePage = _.toJson, - deserializePage = _.fromJson[Page] - .fold(s => throw IllegalStateException(s), identity), - getPageTitle = _.title, - routeFallback = url => Page.NotFound(url), - deserializeFallback = _ => Page.Dashboard - )( - $popStateEvent = windowEvents.onPopState, - owner = unsafeWindowOwner - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala deleted file mode 100644 index b24b82b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -sealed trait Action - -case object FetchDirectory extends Action -case class FetchUserDetails(osc: OsobniCislo) extends Action -case class FetchParameters(osc: OsobniCislo) extends Action -case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action -case class FetchParameterCriteria( - osc: OsobniCislo, - paramId: String, - critId: String, - page: (UserInfo, Parameter, ParameterCriteria) => Page -) extends Action -case class FetchAvailableFiles(osc: OsobniCislo) extends Action -case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala deleted file mode 100644 index c637d35..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala +++ /dev/null @@ -1,82 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.{UserProfile, UserInfo, OsobniCislo} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo -import cz.e_bs.cmi.mdr.pdb.UserFunction - -object AppPage: - // TODO: pages by logged in user - val pages: List[Page] = List(Page.Directory, Page.Dashboard) - - import NavigationBar.{Logo, MenuItem} - - val logo = Logo( - "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", - "Workflow" - ) - - // TODO: menu items by user profile - val userMenu = - List( - MenuItem("Your Profile"), - MenuItem("Settings"), - MenuItem("Sign out") - ) - - // TODO: load user profile - val $userProfile = Var( - UserProfile( - "tom", - UserInfo( - OsobniCislo("1031"), - "tom", - "Tom", - "Cook", - None, - None, - Some("tom@example.com"), - Some("+420 222 866 180"), - Some( - UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") - ), - Nil, - None - ) - ) - ) - - val $userInfo = $userProfile.signal.map(_.userInfo) - - type ViewModel = Option[HtmlElement] - def apply( - actionBus: Observer[Action] - )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using - router: Router[Page] - ): HtmlElement = - PageLayout(actionBus)( - $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => - PageLayout.ViewModel( - NavigationBar.ViewModel( - u, - pages.map(p => - NavigationBar.Link( - () => - PageLink( - p, - actionBus - ), - p == cp - ) - ), - userMenu, - logo - ), - c - ) - ), - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala deleted file mode 100644 index bf473c6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ /dev/null @@ -1,143 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden -import fiftyforms.ui.components.tailwind.list.IconText.ViewModel -import fiftyforms.ui.components.tailwind.Icons -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Action - -object Breadcrumbs: - - private def slash = - import svg.{*, given} - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-300", - xmlns := "http://www.w3.org/2000/svg", - fill := "currentColor", - viewBox := "0 0 20 20", - ariaHidden := true, - path( - d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" - ) - ) - - def backIcon = - Icons.solid - .`arrow-narrow-left`() - .amend( - svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" - ) - - object Link: - case class ViewModel( - page: Page, - icon: Option[SvgElement], - extraClasses: String, - text: String, - textClasses: Option[String] = None - ) - - val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" - - def shortHome(p: Page) = ViewModel( - p, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - "Zpět na úvodní stránku", - None - ) - - def fullHome(p: Page) = ViewModel( - p, - Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), - "text-gray-400 hover:text-gray-500", - "Domů", - Some("sr-only") - ) - - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - PageLink - .container($m.map(_.page), actionBus) - .amend( - cls <-- $m.map(_.extraClasses), - child.maybe <-- $m.map(_.icon), - span( - cls <-- $m.map(_.textClasses.getOrElse("")), - child.text <-- $m.map(_.text) - ) - ) - - object FullBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - ol( - role := "list", - cls := "flex items-center space-x-4", - children <-- $m.map(_.path).split(_.id)((_, _, $p) => - li( - div( - cls := "flex items-center", - child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), - Link( - $p.map(p => - if (p.isRoot) then Link.fullHome(p) - else - Link.ViewModel( - p, - None, - s"ml-4 max-w-xs truncate ${Link.baseClasses}", - p.title - ) - ), - actionBus - ) - ) - ) - ) - ) - - object ShortBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - Link( - $m.map { p => - val target = p.parent.getOrElse(p) - if target.isRoot then Link.shortHome(target) - else - Link.ViewModel( - target, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - s"Zpět na ${target.title}" - ) - }, - actionBus - ) - - def apply(actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - nav( - cls := "flex", - aria.label := "Breadcrumb", - div( - cls := "flex sm:hidden", - ShortBreadcrumbs(router.$currentPage, actionBus) - ), - div( - cls := "hidden sm:block", - FullBreadcrumbs(router.$currentPage, actionBus) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala deleted file mode 100644 index 0db2047..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ /dev/null @@ -1,242 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent -import fiftyforms.ui.components.tailwind.Icons -import fiftyforms.ui.components.tailwind.Avatar -import cz.e_bs.cmi.mdr.pdb.UserInfo -import io.laminext.syntax.core.* - -object NavigationBar: - - case class Logo(img: String, name: String) - case class Link(a: () => Anchor, active: Boolean) - case class MenuItem(title: String) - - case class ViewModel( - userInfo: UserInfo, - pages: List[Link], - userMenu: List[MenuItem], - logo: Logo - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - val $userInfo = $m.map(_.userInfo) - val mobileMenuOpen = Var(false) - - val desktopOnly = cls("hidden md:block") - val mobileOnly = cls("md:hidden") - - inline def avatarImage(size: Int = 8) = - Avatar($userInfo.map(_.img)).avatarImage(size) - - def notificationButton = button( - tpe := "button", - cls := List( - "bg-indigo-600", - "focus:outline-none", - "focus:ring-2", - "focus:ring-offset-2", - "focus:ring-offset-indigo-600", - "focus:ring-white", - "hover:text-white", - "p-1", - "rounded-full", - "text-indigo-200" - ), - span(cls := "sr-only", "View notifications"), - Icons.outline.bell() - ) - - def userProfile: HtmlElement = - val menuOpen = Var(false) - - def menuItem(item: MenuItem, idx: Int): HtmlElement = - a( - href := "#", - cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", - role := "menuitem", - tabIndex := -1, - idAttr := s"user-menu-item-$idx", - item.title - ) - - div( - cls := "ml-3 relative", - div( - button( - tpe := "button", - cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - idAttr := "user-menu-button", - aria.expanded <-- menuOpen.signal, - aria.hasPopup := true, - span(cls := "sr-only", "Open user menu"), - child <-- avatarImage(), - onClick.preventDefault.mapTo( - !menuOpen.now() - ) --> menuOpen.writer - ) - ), - /* - * */ - div( - cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", - cls <-- menuOpen.signal.map { o => - if (o) "md:block" else "md:hidden" - }, - role := "menu", - aria.orientation := "vertical", - aria.labelledBy := "user-menu-button", - tabIndex := -1, - // : keyboard navigation - children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) - ) - ) - - def mobileProfile = - def menuItem(item: MenuItem): HtmlElement = - a( - href := "#", - cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", - item.title - ) - - div( - cls := "pt-4 pb-3 border-t border-indigo-700", - div( - cls := "flex items-center px-5", - div( - cls := "flex-shrink-0", - child <-- avatarImage(10) - ), - div( - cls := "ml-3", - div( - cls := "text-base font-medium text-white", - child.text <-- $userInfo.map(_.name) - ), - child.maybe <-- $userInfo.map( - _.email.map(e => - div( - cls := "text-sm font-medium text-indigo-300", - e - ) - ) - ) - ), - notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) - ), - div( - cls := "mt-3 px-2 space-y-1", - children <-- $m.map(_.userMenu.map(menuItem)) - ) - ) - - def pageLink(page: Link): Anchor = - page - .a() - .amend( - cls := "text-white px-3 py-2 rounded-md text-sm font-medium", - cls := Seq( - "bg-indigo-700" -> page.active, - "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active - ), - ariaCurrent := (if page.active then "page" else "false") - ) - - def logoImg: Image = - img( - cls := "h-8 w-8", - src <-- $m.map(_.logo.img), - alt <-- $m.map(_.logo.name) - ) - - def mobileMenuButton = button( - tpe := "button", - cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - aria.controls := "mobile-menu", - aria.expanded <-- mobileMenuOpen.signal, - span(cls := "sr-only", "Open main menu"), - Icons.outline - .menu() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") - ), - Icons.outline - .x() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") - ), - onClick.preventDefault.mapTo( - !mobileMenuOpen.now() - ) --> mobileMenuOpen.writer - ) - - def navBarLeft = - div( - cls := "flex items-center", - div(cls := "flex-shrink-0", logoImg), - div( - desktopOnly, - div( - cls := "ml-10 flex items-baseline space-x-4", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ) - ) - ) - - def navBarRight = - div( - desktopOnly, - div( - cls := "ml-4 flex items-center md:ml-6", - notificationButton, - userProfile - ) - ) - - def navBarMobile = - div( - cls := "-mr-2 flex", - mobileOnly, - mobileMenuButton - ) - - def navBar = - div( - cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex items-center justify-between h-16", - navBarLeft, - navBarRight, - navBarMobile - ) - ) - - def mobileMenu = - div( - mobileOnly, - cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }, - idAttr := "mobile-menu", - div( - cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ), - mobileProfile - ) - - nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala deleted file mode 100644 index a3b00af..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala +++ /dev/null @@ -1,103 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import scala.scalajs.js.annotation.JSExportTopLevel -import scala.scalajs.js.annotation.JSExport -import scala.scalajs.js.annotation.JSImport -import scala.scalajs.js -import org.scalajs.dom -import com.raquo.laminar.api.L.{*, given} -import scala.scalajs.js.Date -import com.raquo.waypoint.Router -import com.raquo.waypoint.SplitRender -import cz.e_bs.cmi.mdr.pdb.app.services.DataFetcher -import scala.scalajs.js.JSON -import zio.json._ -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.state.AppState - -@js.native -@JSImport("stylesheets/main.css", JSImport.Namespace) -object Css extends js.Any - -@js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native -@JSImport("params/pdb-params.json", JSImport.Default) -object pdbParams extends js.Object - -@JSExportTopLevel("app") -object Main: - - @JSExport - def main(args: Array[String]): Unit = - import Routes.given - onLoad { - setupAirstream() - val appContainer = dom.document.querySelector("#app") - val _ = - render( - appContainer, - renderPage(state.MockAppState(using unsafeWindowOwner, router)) - ) - } - - private def onLoad(f: => Unit): Unit = - documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) - - private def setupAirstream()(using router: Router[Page]): Unit = - AirstreamError.registerUnhandledErrorCallback(err => - router.forcePage( - Page.UnhandledError( - Some(err.getClass.getName), // TODO: Fill only in dev mode - Some(err.getMessage) - ) - ) - ) - - def renderPage(state: AppState)(using - router: Router[Page] - ): HtmlElement = - val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) - .collectSignal[Page.Detail]( - connectors - .DetailPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailParametru]( - connectors - .DetailParametruPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailKriteria]( - connectors - .DetailKriteriaPageConnector(state)(_) - .apply - ) - .collectSignal[Page.UpravDukazKriteria]( - pages.detail.UpravDukaz.Connector(state)(_).apply - ) - .collectStatic(Page.Dashboard)( - connectors.DashboardPageConnector(state.actionBus).apply - ) - .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) - ) - .collect[Page.UnhandledError](pg => - pages.errors - .UnhandledErrorPage( - pages.errors.UnhandledErrorPage - .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), - state.actionBus - ) - ) - .collectStatic(Page.Directory)( - connectors - .DirectoryPageConnector(state.users, state.actionBus) - .apply - ) - div(cls := "h-full", child <-- pageSplitter.$view) - - // Pull in the stylesheet - val css: Css.type = Css diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala deleted file mode 100644 index 3bbc8c0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ /dev/null @@ -1,198 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.* -import org.scalajs.dom -import zio.json.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import scala.scalajs.js -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.Page.Titled - -// enum is not working with Waypoints' SplitRender collectStatic -sealed abstract class Page( - val id: String, - val title: String, - val parent: Option[Page] -) { - val path: Vector[Page] = - parent match - case None => Vector(this) - case Some(p) => p.path :+ this - - val isRoot: Boolean = parent.isEmpty -} - -object Page: - - case class Titled[V](value: V, title: Option[String] = None): - val show: String = title.getOrElse(value.toString) - - case object Directory extends Page("directory", "Adresář", None) - - case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) - - case class Detail(osobniCislo: Titled[OsobniCislo]) - extends Page("user", osobniCislo.show, Some(Directory)) - - object Detail { - def apply(o: UserInfo): Detail = Detail( - Titled(o.personalNumber, Some(o.name)) - ) - } - - case class DetailParametru( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String] - ) extends Page( - "parameter", - parametr.show, - Some(Detail(osobniCislo)) - ) - - object DetailParametru { - def apply(o: UserInfo, p: Parameter): DetailParametru = - DetailParametru( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)) - ) - } - - case class DetailKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "criteria", - kriterium.show, - Some(DetailParametru(osobniCislo, parametr)) - ) - - object DetailKriteria { - def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = - DetailKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class UpravDukazKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "addProof", - "Důkaz", - Some(DetailKriteria(osobniCislo, parametr, kriterium)) - ) - - object UpravDukazKriteria { - def apply( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): UpravDukazKriteria = - UpravDukazKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class NotFound(url: String) extends Page("404", "404", Some(Directory)) - - case class UnhandledError( - errorName: Option[String], - errorMessage: Option[String] - ) extends Page("500", "Unexpected error", Some(Directory)) - -object Routes: - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) - given [V: JsonEncoder]: JsonEncoder[Titled[V]] = - DeriveJsonEncoder.gen[Titled[V]] - given [V: JsonDecoder]: JsonDecoder[Titled[V]] = - DeriveJsonDecoder.gen[Titled[V]] - given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] - given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] - - val base = - js.`import`.meta.env.BASE_URL - .asInstanceOf[String] + "app" - - val homePage: Page = Page.Directory - - given router: Router[Page] = Router[Page]( - routes = List( - Route.static(homePage, root / endOfSegments, basePath = base), - Route.static( - Page.Dashboard, - root / "dashboard" / endOfSegments, - basePath = base - ), - Route[Page.Detail, String]( - encode = _.osobniCislo.value.toString, - decode = osc => Page.Detail(Titled(OsobniCislo(osc))), - root / "osoba" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.DetailParametru, (String, String)]( - encode = p => (p.osobniCislo.value.toString, p.parametr.value), - decode = - p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / endOfSegments, - basePath = base - ), - Route[Page.DetailKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.DetailKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.UpravDukazKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.UpravDukazKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / "edit" / endOfSegments, - basePath = base - ) - ), - serializePage = _.toJson, - deserializePage = _.fromJson[Page] - .fold(s => throw IllegalStateException(s), identity), - getPageTitle = _.title, - routeFallback = url => Page.NotFound(url), - deserializeFallback = _ => Page.Dashboard - )( - $popStateEvent = windowEvents.onPopState, - owner = unsafeWindowOwner - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala deleted file mode 100644 index b24b82b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -sealed trait Action - -case object FetchDirectory extends Action -case class FetchUserDetails(osc: OsobniCislo) extends Action -case class FetchParameters(osc: OsobniCislo) extends Action -case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action -case class FetchParameterCriteria( - osc: OsobniCislo, - paramId: String, - critId: String, - page: (UserInfo, Parameter, ParameterCriteria) => Page -) extends Action -case class FetchAvailableFiles(osc: OsobniCislo) extends Action -case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala deleted file mode 100644 index c637d35..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala +++ /dev/null @@ -1,82 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.{UserProfile, UserInfo, OsobniCislo} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo -import cz.e_bs.cmi.mdr.pdb.UserFunction - -object AppPage: - // TODO: pages by logged in user - val pages: List[Page] = List(Page.Directory, Page.Dashboard) - - import NavigationBar.{Logo, MenuItem} - - val logo = Logo( - "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", - "Workflow" - ) - - // TODO: menu items by user profile - val userMenu = - List( - MenuItem("Your Profile"), - MenuItem("Settings"), - MenuItem("Sign out") - ) - - // TODO: load user profile - val $userProfile = Var( - UserProfile( - "tom", - UserInfo( - OsobniCislo("1031"), - "tom", - "Tom", - "Cook", - None, - None, - Some("tom@example.com"), - Some("+420 222 866 180"), - Some( - UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") - ), - Nil, - None - ) - ) - ) - - val $userInfo = $userProfile.signal.map(_.userInfo) - - type ViewModel = Option[HtmlElement] - def apply( - actionBus: Observer[Action] - )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using - router: Router[Page] - ): HtmlElement = - PageLayout(actionBus)( - $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => - PageLayout.ViewModel( - NavigationBar.ViewModel( - u, - pages.map(p => - NavigationBar.Link( - () => - PageLink( - p, - actionBus - ), - p == cp - ) - ), - userMenu, - logo - ), - c - ) - ), - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala deleted file mode 100644 index bf473c6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ /dev/null @@ -1,143 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden -import fiftyforms.ui.components.tailwind.list.IconText.ViewModel -import fiftyforms.ui.components.tailwind.Icons -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Action - -object Breadcrumbs: - - private def slash = - import svg.{*, given} - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-300", - xmlns := "http://www.w3.org/2000/svg", - fill := "currentColor", - viewBox := "0 0 20 20", - ariaHidden := true, - path( - d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" - ) - ) - - def backIcon = - Icons.solid - .`arrow-narrow-left`() - .amend( - svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" - ) - - object Link: - case class ViewModel( - page: Page, - icon: Option[SvgElement], - extraClasses: String, - text: String, - textClasses: Option[String] = None - ) - - val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" - - def shortHome(p: Page) = ViewModel( - p, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - "Zpět na úvodní stránku", - None - ) - - def fullHome(p: Page) = ViewModel( - p, - Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), - "text-gray-400 hover:text-gray-500", - "Domů", - Some("sr-only") - ) - - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - PageLink - .container($m.map(_.page), actionBus) - .amend( - cls <-- $m.map(_.extraClasses), - child.maybe <-- $m.map(_.icon), - span( - cls <-- $m.map(_.textClasses.getOrElse("")), - child.text <-- $m.map(_.text) - ) - ) - - object FullBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - ol( - role := "list", - cls := "flex items-center space-x-4", - children <-- $m.map(_.path).split(_.id)((_, _, $p) => - li( - div( - cls := "flex items-center", - child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), - Link( - $p.map(p => - if (p.isRoot) then Link.fullHome(p) - else - Link.ViewModel( - p, - None, - s"ml-4 max-w-xs truncate ${Link.baseClasses}", - p.title - ) - ), - actionBus - ) - ) - ) - ) - ) - - object ShortBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - Link( - $m.map { p => - val target = p.parent.getOrElse(p) - if target.isRoot then Link.shortHome(target) - else - Link.ViewModel( - target, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - s"Zpět na ${target.title}" - ) - }, - actionBus - ) - - def apply(actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - nav( - cls := "flex", - aria.label := "Breadcrumb", - div( - cls := "flex sm:hidden", - ShortBreadcrumbs(router.$currentPage, actionBus) - ), - div( - cls := "hidden sm:block", - FullBreadcrumbs(router.$currentPage, actionBus) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala deleted file mode 100644 index 0db2047..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ /dev/null @@ -1,242 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent -import fiftyforms.ui.components.tailwind.Icons -import fiftyforms.ui.components.tailwind.Avatar -import cz.e_bs.cmi.mdr.pdb.UserInfo -import io.laminext.syntax.core.* - -object NavigationBar: - - case class Logo(img: String, name: String) - case class Link(a: () => Anchor, active: Boolean) - case class MenuItem(title: String) - - case class ViewModel( - userInfo: UserInfo, - pages: List[Link], - userMenu: List[MenuItem], - logo: Logo - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - val $userInfo = $m.map(_.userInfo) - val mobileMenuOpen = Var(false) - - val desktopOnly = cls("hidden md:block") - val mobileOnly = cls("md:hidden") - - inline def avatarImage(size: Int = 8) = - Avatar($userInfo.map(_.img)).avatarImage(size) - - def notificationButton = button( - tpe := "button", - cls := List( - "bg-indigo-600", - "focus:outline-none", - "focus:ring-2", - "focus:ring-offset-2", - "focus:ring-offset-indigo-600", - "focus:ring-white", - "hover:text-white", - "p-1", - "rounded-full", - "text-indigo-200" - ), - span(cls := "sr-only", "View notifications"), - Icons.outline.bell() - ) - - def userProfile: HtmlElement = - val menuOpen = Var(false) - - def menuItem(item: MenuItem, idx: Int): HtmlElement = - a( - href := "#", - cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", - role := "menuitem", - tabIndex := -1, - idAttr := s"user-menu-item-$idx", - item.title - ) - - div( - cls := "ml-3 relative", - div( - button( - tpe := "button", - cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - idAttr := "user-menu-button", - aria.expanded <-- menuOpen.signal, - aria.hasPopup := true, - span(cls := "sr-only", "Open user menu"), - child <-- avatarImage(), - onClick.preventDefault.mapTo( - !menuOpen.now() - ) --> menuOpen.writer - ) - ), - /* - * */ - div( - cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", - cls <-- menuOpen.signal.map { o => - if (o) "md:block" else "md:hidden" - }, - role := "menu", - aria.orientation := "vertical", - aria.labelledBy := "user-menu-button", - tabIndex := -1, - // : keyboard navigation - children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) - ) - ) - - def mobileProfile = - def menuItem(item: MenuItem): HtmlElement = - a( - href := "#", - cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", - item.title - ) - - div( - cls := "pt-4 pb-3 border-t border-indigo-700", - div( - cls := "flex items-center px-5", - div( - cls := "flex-shrink-0", - child <-- avatarImage(10) - ), - div( - cls := "ml-3", - div( - cls := "text-base font-medium text-white", - child.text <-- $userInfo.map(_.name) - ), - child.maybe <-- $userInfo.map( - _.email.map(e => - div( - cls := "text-sm font-medium text-indigo-300", - e - ) - ) - ) - ), - notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) - ), - div( - cls := "mt-3 px-2 space-y-1", - children <-- $m.map(_.userMenu.map(menuItem)) - ) - ) - - def pageLink(page: Link): Anchor = - page - .a() - .amend( - cls := "text-white px-3 py-2 rounded-md text-sm font-medium", - cls := Seq( - "bg-indigo-700" -> page.active, - "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active - ), - ariaCurrent := (if page.active then "page" else "false") - ) - - def logoImg: Image = - img( - cls := "h-8 w-8", - src <-- $m.map(_.logo.img), - alt <-- $m.map(_.logo.name) - ) - - def mobileMenuButton = button( - tpe := "button", - cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - aria.controls := "mobile-menu", - aria.expanded <-- mobileMenuOpen.signal, - span(cls := "sr-only", "Open main menu"), - Icons.outline - .menu() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") - ), - Icons.outline - .x() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") - ), - onClick.preventDefault.mapTo( - !mobileMenuOpen.now() - ) --> mobileMenuOpen.writer - ) - - def navBarLeft = - div( - cls := "flex items-center", - div(cls := "flex-shrink-0", logoImg), - div( - desktopOnly, - div( - cls := "ml-10 flex items-baseline space-x-4", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ) - ) - ) - - def navBarRight = - div( - desktopOnly, - div( - cls := "ml-4 flex items-center md:ml-6", - notificationButton, - userProfile - ) - ) - - def navBarMobile = - div( - cls := "-mr-2 flex", - mobileOnly, - mobileMenuButton - ) - - def navBar = - div( - cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex items-center justify-between h-16", - navBarLeft, - navBarRight, - navBarMobile - ) - ) - - def mobileMenu = - div( - mobileOnly, - cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }, - idAttr := "mobile-menu", - div( - cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ), - mobileProfile - ) - - nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala deleted file mode 100644 index 75339df..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object PageHeader: - def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = - header( - cls := "bg-white shadow-sm", - div( - cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", - h1( - cls := "text-lg leading-6 font-semibold text-gray-900", - Breadcrumbs(actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala deleted file mode 100644 index a3b00af..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala +++ /dev/null @@ -1,103 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import scala.scalajs.js.annotation.JSExportTopLevel -import scala.scalajs.js.annotation.JSExport -import scala.scalajs.js.annotation.JSImport -import scala.scalajs.js -import org.scalajs.dom -import com.raquo.laminar.api.L.{*, given} -import scala.scalajs.js.Date -import com.raquo.waypoint.Router -import com.raquo.waypoint.SplitRender -import cz.e_bs.cmi.mdr.pdb.app.services.DataFetcher -import scala.scalajs.js.JSON -import zio.json._ -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.state.AppState - -@js.native -@JSImport("stylesheets/main.css", JSImport.Namespace) -object Css extends js.Any - -@js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native -@JSImport("params/pdb-params.json", JSImport.Default) -object pdbParams extends js.Object - -@JSExportTopLevel("app") -object Main: - - @JSExport - def main(args: Array[String]): Unit = - import Routes.given - onLoad { - setupAirstream() - val appContainer = dom.document.querySelector("#app") - val _ = - render( - appContainer, - renderPage(state.MockAppState(using unsafeWindowOwner, router)) - ) - } - - private def onLoad(f: => Unit): Unit = - documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) - - private def setupAirstream()(using router: Router[Page]): Unit = - AirstreamError.registerUnhandledErrorCallback(err => - router.forcePage( - Page.UnhandledError( - Some(err.getClass.getName), // TODO: Fill only in dev mode - Some(err.getMessage) - ) - ) - ) - - def renderPage(state: AppState)(using - router: Router[Page] - ): HtmlElement = - val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) - .collectSignal[Page.Detail]( - connectors - .DetailPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailParametru]( - connectors - .DetailParametruPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailKriteria]( - connectors - .DetailKriteriaPageConnector(state)(_) - .apply - ) - .collectSignal[Page.UpravDukazKriteria]( - pages.detail.UpravDukaz.Connector(state)(_).apply - ) - .collectStatic(Page.Dashboard)( - connectors.DashboardPageConnector(state.actionBus).apply - ) - .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) - ) - .collect[Page.UnhandledError](pg => - pages.errors - .UnhandledErrorPage( - pages.errors.UnhandledErrorPage - .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), - state.actionBus - ) - ) - .collectStatic(Page.Directory)( - connectors - .DirectoryPageConnector(state.users, state.actionBus) - .apply - ) - div(cls := "h-full", child <-- pageSplitter.$view) - - // Pull in the stylesheet - val css: Css.type = Css diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala deleted file mode 100644 index 3bbc8c0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ /dev/null @@ -1,198 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.* -import org.scalajs.dom -import zio.json.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import scala.scalajs.js -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.Page.Titled - -// enum is not working with Waypoints' SplitRender collectStatic -sealed abstract class Page( - val id: String, - val title: String, - val parent: Option[Page] -) { - val path: Vector[Page] = - parent match - case None => Vector(this) - case Some(p) => p.path :+ this - - val isRoot: Boolean = parent.isEmpty -} - -object Page: - - case class Titled[V](value: V, title: Option[String] = None): - val show: String = title.getOrElse(value.toString) - - case object Directory extends Page("directory", "Adresář", None) - - case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) - - case class Detail(osobniCislo: Titled[OsobniCislo]) - extends Page("user", osobniCislo.show, Some(Directory)) - - object Detail { - def apply(o: UserInfo): Detail = Detail( - Titled(o.personalNumber, Some(o.name)) - ) - } - - case class DetailParametru( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String] - ) extends Page( - "parameter", - parametr.show, - Some(Detail(osobniCislo)) - ) - - object DetailParametru { - def apply(o: UserInfo, p: Parameter): DetailParametru = - DetailParametru( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)) - ) - } - - case class DetailKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "criteria", - kriterium.show, - Some(DetailParametru(osobniCislo, parametr)) - ) - - object DetailKriteria { - def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = - DetailKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class UpravDukazKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "addProof", - "Důkaz", - Some(DetailKriteria(osobniCislo, parametr, kriterium)) - ) - - object UpravDukazKriteria { - def apply( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): UpravDukazKriteria = - UpravDukazKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class NotFound(url: String) extends Page("404", "404", Some(Directory)) - - case class UnhandledError( - errorName: Option[String], - errorMessage: Option[String] - ) extends Page("500", "Unexpected error", Some(Directory)) - -object Routes: - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) - given [V: JsonEncoder]: JsonEncoder[Titled[V]] = - DeriveJsonEncoder.gen[Titled[V]] - given [V: JsonDecoder]: JsonDecoder[Titled[V]] = - DeriveJsonDecoder.gen[Titled[V]] - given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] - given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] - - val base = - js.`import`.meta.env.BASE_URL - .asInstanceOf[String] + "app" - - val homePage: Page = Page.Directory - - given router: Router[Page] = Router[Page]( - routes = List( - Route.static(homePage, root / endOfSegments, basePath = base), - Route.static( - Page.Dashboard, - root / "dashboard" / endOfSegments, - basePath = base - ), - Route[Page.Detail, String]( - encode = _.osobniCislo.value.toString, - decode = osc => Page.Detail(Titled(OsobniCislo(osc))), - root / "osoba" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.DetailParametru, (String, String)]( - encode = p => (p.osobniCislo.value.toString, p.parametr.value), - decode = - p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / endOfSegments, - basePath = base - ), - Route[Page.DetailKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.DetailKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.UpravDukazKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.UpravDukazKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / "edit" / endOfSegments, - basePath = base - ) - ), - serializePage = _.toJson, - deserializePage = _.fromJson[Page] - .fold(s => throw IllegalStateException(s), identity), - getPageTitle = _.title, - routeFallback = url => Page.NotFound(url), - deserializeFallback = _ => Page.Dashboard - )( - $popStateEvent = windowEvents.onPopState, - owner = unsafeWindowOwner - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala deleted file mode 100644 index b24b82b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -sealed trait Action - -case object FetchDirectory extends Action -case class FetchUserDetails(osc: OsobniCislo) extends Action -case class FetchParameters(osc: OsobniCislo) extends Action -case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action -case class FetchParameterCriteria( - osc: OsobniCislo, - paramId: String, - critId: String, - page: (UserInfo, Parameter, ParameterCriteria) => Page -) extends Action -case class FetchAvailableFiles(osc: OsobniCislo) extends Action -case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala deleted file mode 100644 index c637d35..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala +++ /dev/null @@ -1,82 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.{UserProfile, UserInfo, OsobniCislo} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo -import cz.e_bs.cmi.mdr.pdb.UserFunction - -object AppPage: - // TODO: pages by logged in user - val pages: List[Page] = List(Page.Directory, Page.Dashboard) - - import NavigationBar.{Logo, MenuItem} - - val logo = Logo( - "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", - "Workflow" - ) - - // TODO: menu items by user profile - val userMenu = - List( - MenuItem("Your Profile"), - MenuItem("Settings"), - MenuItem("Sign out") - ) - - // TODO: load user profile - val $userProfile = Var( - UserProfile( - "tom", - UserInfo( - OsobniCislo("1031"), - "tom", - "Tom", - "Cook", - None, - None, - Some("tom@example.com"), - Some("+420 222 866 180"), - Some( - UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") - ), - Nil, - None - ) - ) - ) - - val $userInfo = $userProfile.signal.map(_.userInfo) - - type ViewModel = Option[HtmlElement] - def apply( - actionBus: Observer[Action] - )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using - router: Router[Page] - ): HtmlElement = - PageLayout(actionBus)( - $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => - PageLayout.ViewModel( - NavigationBar.ViewModel( - u, - pages.map(p => - NavigationBar.Link( - () => - PageLink( - p, - actionBus - ), - p == cp - ) - ), - userMenu, - logo - ), - c - ) - ), - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala deleted file mode 100644 index bf473c6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ /dev/null @@ -1,143 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden -import fiftyforms.ui.components.tailwind.list.IconText.ViewModel -import fiftyforms.ui.components.tailwind.Icons -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Action - -object Breadcrumbs: - - private def slash = - import svg.{*, given} - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-300", - xmlns := "http://www.w3.org/2000/svg", - fill := "currentColor", - viewBox := "0 0 20 20", - ariaHidden := true, - path( - d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" - ) - ) - - def backIcon = - Icons.solid - .`arrow-narrow-left`() - .amend( - svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" - ) - - object Link: - case class ViewModel( - page: Page, - icon: Option[SvgElement], - extraClasses: String, - text: String, - textClasses: Option[String] = None - ) - - val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" - - def shortHome(p: Page) = ViewModel( - p, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - "Zpět na úvodní stránku", - None - ) - - def fullHome(p: Page) = ViewModel( - p, - Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), - "text-gray-400 hover:text-gray-500", - "Domů", - Some("sr-only") - ) - - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - PageLink - .container($m.map(_.page), actionBus) - .amend( - cls <-- $m.map(_.extraClasses), - child.maybe <-- $m.map(_.icon), - span( - cls <-- $m.map(_.textClasses.getOrElse("")), - child.text <-- $m.map(_.text) - ) - ) - - object FullBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - ol( - role := "list", - cls := "flex items-center space-x-4", - children <-- $m.map(_.path).split(_.id)((_, _, $p) => - li( - div( - cls := "flex items-center", - child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), - Link( - $p.map(p => - if (p.isRoot) then Link.fullHome(p) - else - Link.ViewModel( - p, - None, - s"ml-4 max-w-xs truncate ${Link.baseClasses}", - p.title - ) - ), - actionBus - ) - ) - ) - ) - ) - - object ShortBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - Link( - $m.map { p => - val target = p.parent.getOrElse(p) - if target.isRoot then Link.shortHome(target) - else - Link.ViewModel( - target, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - s"Zpět na ${target.title}" - ) - }, - actionBus - ) - - def apply(actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - nav( - cls := "flex", - aria.label := "Breadcrumb", - div( - cls := "flex sm:hidden", - ShortBreadcrumbs(router.$currentPage, actionBus) - ), - div( - cls := "hidden sm:block", - FullBreadcrumbs(router.$currentPage, actionBus) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala deleted file mode 100644 index 0db2047..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ /dev/null @@ -1,242 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent -import fiftyforms.ui.components.tailwind.Icons -import fiftyforms.ui.components.tailwind.Avatar -import cz.e_bs.cmi.mdr.pdb.UserInfo -import io.laminext.syntax.core.* - -object NavigationBar: - - case class Logo(img: String, name: String) - case class Link(a: () => Anchor, active: Boolean) - case class MenuItem(title: String) - - case class ViewModel( - userInfo: UserInfo, - pages: List[Link], - userMenu: List[MenuItem], - logo: Logo - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - val $userInfo = $m.map(_.userInfo) - val mobileMenuOpen = Var(false) - - val desktopOnly = cls("hidden md:block") - val mobileOnly = cls("md:hidden") - - inline def avatarImage(size: Int = 8) = - Avatar($userInfo.map(_.img)).avatarImage(size) - - def notificationButton = button( - tpe := "button", - cls := List( - "bg-indigo-600", - "focus:outline-none", - "focus:ring-2", - "focus:ring-offset-2", - "focus:ring-offset-indigo-600", - "focus:ring-white", - "hover:text-white", - "p-1", - "rounded-full", - "text-indigo-200" - ), - span(cls := "sr-only", "View notifications"), - Icons.outline.bell() - ) - - def userProfile: HtmlElement = - val menuOpen = Var(false) - - def menuItem(item: MenuItem, idx: Int): HtmlElement = - a( - href := "#", - cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", - role := "menuitem", - tabIndex := -1, - idAttr := s"user-menu-item-$idx", - item.title - ) - - div( - cls := "ml-3 relative", - div( - button( - tpe := "button", - cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - idAttr := "user-menu-button", - aria.expanded <-- menuOpen.signal, - aria.hasPopup := true, - span(cls := "sr-only", "Open user menu"), - child <-- avatarImage(), - onClick.preventDefault.mapTo( - !menuOpen.now() - ) --> menuOpen.writer - ) - ), - /* - * */ - div( - cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", - cls <-- menuOpen.signal.map { o => - if (o) "md:block" else "md:hidden" - }, - role := "menu", - aria.orientation := "vertical", - aria.labelledBy := "user-menu-button", - tabIndex := -1, - // : keyboard navigation - children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) - ) - ) - - def mobileProfile = - def menuItem(item: MenuItem): HtmlElement = - a( - href := "#", - cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", - item.title - ) - - div( - cls := "pt-4 pb-3 border-t border-indigo-700", - div( - cls := "flex items-center px-5", - div( - cls := "flex-shrink-0", - child <-- avatarImage(10) - ), - div( - cls := "ml-3", - div( - cls := "text-base font-medium text-white", - child.text <-- $userInfo.map(_.name) - ), - child.maybe <-- $userInfo.map( - _.email.map(e => - div( - cls := "text-sm font-medium text-indigo-300", - e - ) - ) - ) - ), - notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) - ), - div( - cls := "mt-3 px-2 space-y-1", - children <-- $m.map(_.userMenu.map(menuItem)) - ) - ) - - def pageLink(page: Link): Anchor = - page - .a() - .amend( - cls := "text-white px-3 py-2 rounded-md text-sm font-medium", - cls := Seq( - "bg-indigo-700" -> page.active, - "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active - ), - ariaCurrent := (if page.active then "page" else "false") - ) - - def logoImg: Image = - img( - cls := "h-8 w-8", - src <-- $m.map(_.logo.img), - alt <-- $m.map(_.logo.name) - ) - - def mobileMenuButton = button( - tpe := "button", - cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - aria.controls := "mobile-menu", - aria.expanded <-- mobileMenuOpen.signal, - span(cls := "sr-only", "Open main menu"), - Icons.outline - .menu() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") - ), - Icons.outline - .x() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") - ), - onClick.preventDefault.mapTo( - !mobileMenuOpen.now() - ) --> mobileMenuOpen.writer - ) - - def navBarLeft = - div( - cls := "flex items-center", - div(cls := "flex-shrink-0", logoImg), - div( - desktopOnly, - div( - cls := "ml-10 flex items-baseline space-x-4", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ) - ) - ) - - def navBarRight = - div( - desktopOnly, - div( - cls := "ml-4 flex items-center md:ml-6", - notificationButton, - userProfile - ) - ) - - def navBarMobile = - div( - cls := "-mr-2 flex", - mobileOnly, - mobileMenuButton - ) - - def navBar = - div( - cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex items-center justify-between h-16", - navBarLeft, - navBarRight, - navBarMobile - ) - ) - - def mobileMenu = - div( - mobileOnly, - cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }, - idAttr := "mobile-menu", - div( - cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ), - mobileProfile - ) - - nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala deleted file mode 100644 index 75339df..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object PageHeader: - def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = - header( - cls := "bg-white shadow-sm", - div( - cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", - h1( - cls := "text-lg leading-6 font-semibold text-gray-900", - Breadcrumbs(actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala deleted file mode 100644 index 5096463..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ /dev/null @@ -1,30 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import fiftyforms.ui.components.tailwind.Loading -import io.laminext.syntax.core.* - -object PageLayout: - case class ViewModel( - navigation: NavigationBar.ViewModel, - content: Option[HtmlElement] - ) - def apply(actionBus: Observer[Action])( - $m: Signal[ViewModel], - mods: Modifier[HtmlElement]* - )(using router: Router[Page]): HtmlElement = - val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) - div( - cls := "h-full flex flex-col", - NavigationBar($m.map(_.navigation)), - child.maybe <-- router.$currentPage.map(_.isRoot) - .switch(None, Some(PageHeader(actionBus))), - main( - cls := "flex-grow-1 overflow-y-auto", - mods, - child <-- $maybeContent.map(_.getOrElse(Loading)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala deleted file mode 100644 index a3b00af..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala +++ /dev/null @@ -1,103 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import scala.scalajs.js.annotation.JSExportTopLevel -import scala.scalajs.js.annotation.JSExport -import scala.scalajs.js.annotation.JSImport -import scala.scalajs.js -import org.scalajs.dom -import com.raquo.laminar.api.L.{*, given} -import scala.scalajs.js.Date -import com.raquo.waypoint.Router -import com.raquo.waypoint.SplitRender -import cz.e_bs.cmi.mdr.pdb.app.services.DataFetcher -import scala.scalajs.js.JSON -import zio.json._ -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.state.AppState - -@js.native -@JSImport("stylesheets/main.css", JSImport.Namespace) -object Css extends js.Any - -@js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native -@JSImport("params/pdb-params.json", JSImport.Default) -object pdbParams extends js.Object - -@JSExportTopLevel("app") -object Main: - - @JSExport - def main(args: Array[String]): Unit = - import Routes.given - onLoad { - setupAirstream() - val appContainer = dom.document.querySelector("#app") - val _ = - render( - appContainer, - renderPage(state.MockAppState(using unsafeWindowOwner, router)) - ) - } - - private def onLoad(f: => Unit): Unit = - documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) - - private def setupAirstream()(using router: Router[Page]): Unit = - AirstreamError.registerUnhandledErrorCallback(err => - router.forcePage( - Page.UnhandledError( - Some(err.getClass.getName), // TODO: Fill only in dev mode - Some(err.getMessage) - ) - ) - ) - - def renderPage(state: AppState)(using - router: Router[Page] - ): HtmlElement = - val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) - .collectSignal[Page.Detail]( - connectors - .DetailPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailParametru]( - connectors - .DetailParametruPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailKriteria]( - connectors - .DetailKriteriaPageConnector(state)(_) - .apply - ) - .collectSignal[Page.UpravDukazKriteria]( - pages.detail.UpravDukaz.Connector(state)(_).apply - ) - .collectStatic(Page.Dashboard)( - connectors.DashboardPageConnector(state.actionBus).apply - ) - .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) - ) - .collect[Page.UnhandledError](pg => - pages.errors - .UnhandledErrorPage( - pages.errors.UnhandledErrorPage - .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), - state.actionBus - ) - ) - .collectStatic(Page.Directory)( - connectors - .DirectoryPageConnector(state.users, state.actionBus) - .apply - ) - div(cls := "h-full", child <-- pageSplitter.$view) - - // Pull in the stylesheet - val css: Css.type = Css diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala deleted file mode 100644 index 3bbc8c0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ /dev/null @@ -1,198 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.* -import org.scalajs.dom -import zio.json.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import scala.scalajs.js -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.Page.Titled - -// enum is not working with Waypoints' SplitRender collectStatic -sealed abstract class Page( - val id: String, - val title: String, - val parent: Option[Page] -) { - val path: Vector[Page] = - parent match - case None => Vector(this) - case Some(p) => p.path :+ this - - val isRoot: Boolean = parent.isEmpty -} - -object Page: - - case class Titled[V](value: V, title: Option[String] = None): - val show: String = title.getOrElse(value.toString) - - case object Directory extends Page("directory", "Adresář", None) - - case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) - - case class Detail(osobniCislo: Titled[OsobniCislo]) - extends Page("user", osobniCislo.show, Some(Directory)) - - object Detail { - def apply(o: UserInfo): Detail = Detail( - Titled(o.personalNumber, Some(o.name)) - ) - } - - case class DetailParametru( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String] - ) extends Page( - "parameter", - parametr.show, - Some(Detail(osobniCislo)) - ) - - object DetailParametru { - def apply(o: UserInfo, p: Parameter): DetailParametru = - DetailParametru( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)) - ) - } - - case class DetailKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "criteria", - kriterium.show, - Some(DetailParametru(osobniCislo, parametr)) - ) - - object DetailKriteria { - def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = - DetailKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class UpravDukazKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "addProof", - "Důkaz", - Some(DetailKriteria(osobniCislo, parametr, kriterium)) - ) - - object UpravDukazKriteria { - def apply( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): UpravDukazKriteria = - UpravDukazKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class NotFound(url: String) extends Page("404", "404", Some(Directory)) - - case class UnhandledError( - errorName: Option[String], - errorMessage: Option[String] - ) extends Page("500", "Unexpected error", Some(Directory)) - -object Routes: - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) - given [V: JsonEncoder]: JsonEncoder[Titled[V]] = - DeriveJsonEncoder.gen[Titled[V]] - given [V: JsonDecoder]: JsonDecoder[Titled[V]] = - DeriveJsonDecoder.gen[Titled[V]] - given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] - given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] - - val base = - js.`import`.meta.env.BASE_URL - .asInstanceOf[String] + "app" - - val homePage: Page = Page.Directory - - given router: Router[Page] = Router[Page]( - routes = List( - Route.static(homePage, root / endOfSegments, basePath = base), - Route.static( - Page.Dashboard, - root / "dashboard" / endOfSegments, - basePath = base - ), - Route[Page.Detail, String]( - encode = _.osobniCislo.value.toString, - decode = osc => Page.Detail(Titled(OsobniCislo(osc))), - root / "osoba" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.DetailParametru, (String, String)]( - encode = p => (p.osobniCislo.value.toString, p.parametr.value), - decode = - p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / endOfSegments, - basePath = base - ), - Route[Page.DetailKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.DetailKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.UpravDukazKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.UpravDukazKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / "edit" / endOfSegments, - basePath = base - ) - ), - serializePage = _.toJson, - deserializePage = _.fromJson[Page] - .fold(s => throw IllegalStateException(s), identity), - getPageTitle = _.title, - routeFallback = url => Page.NotFound(url), - deserializeFallback = _ => Page.Dashboard - )( - $popStateEvent = windowEvents.onPopState, - owner = unsafeWindowOwner - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala deleted file mode 100644 index b24b82b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -sealed trait Action - -case object FetchDirectory extends Action -case class FetchUserDetails(osc: OsobniCislo) extends Action -case class FetchParameters(osc: OsobniCislo) extends Action -case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action -case class FetchParameterCriteria( - osc: OsobniCislo, - paramId: String, - critId: String, - page: (UserInfo, Parameter, ParameterCriteria) => Page -) extends Action -case class FetchAvailableFiles(osc: OsobniCislo) extends Action -case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala deleted file mode 100644 index c637d35..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala +++ /dev/null @@ -1,82 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.{UserProfile, UserInfo, OsobniCislo} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo -import cz.e_bs.cmi.mdr.pdb.UserFunction - -object AppPage: - // TODO: pages by logged in user - val pages: List[Page] = List(Page.Directory, Page.Dashboard) - - import NavigationBar.{Logo, MenuItem} - - val logo = Logo( - "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", - "Workflow" - ) - - // TODO: menu items by user profile - val userMenu = - List( - MenuItem("Your Profile"), - MenuItem("Settings"), - MenuItem("Sign out") - ) - - // TODO: load user profile - val $userProfile = Var( - UserProfile( - "tom", - UserInfo( - OsobniCislo("1031"), - "tom", - "Tom", - "Cook", - None, - None, - Some("tom@example.com"), - Some("+420 222 866 180"), - Some( - UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") - ), - Nil, - None - ) - ) - ) - - val $userInfo = $userProfile.signal.map(_.userInfo) - - type ViewModel = Option[HtmlElement] - def apply( - actionBus: Observer[Action] - )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using - router: Router[Page] - ): HtmlElement = - PageLayout(actionBus)( - $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => - PageLayout.ViewModel( - NavigationBar.ViewModel( - u, - pages.map(p => - NavigationBar.Link( - () => - PageLink( - p, - actionBus - ), - p == cp - ) - ), - userMenu, - logo - ), - c - ) - ), - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala deleted file mode 100644 index bf473c6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ /dev/null @@ -1,143 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden -import fiftyforms.ui.components.tailwind.list.IconText.ViewModel -import fiftyforms.ui.components.tailwind.Icons -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Action - -object Breadcrumbs: - - private def slash = - import svg.{*, given} - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-300", - xmlns := "http://www.w3.org/2000/svg", - fill := "currentColor", - viewBox := "0 0 20 20", - ariaHidden := true, - path( - d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" - ) - ) - - def backIcon = - Icons.solid - .`arrow-narrow-left`() - .amend( - svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" - ) - - object Link: - case class ViewModel( - page: Page, - icon: Option[SvgElement], - extraClasses: String, - text: String, - textClasses: Option[String] = None - ) - - val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" - - def shortHome(p: Page) = ViewModel( - p, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - "Zpět na úvodní stránku", - None - ) - - def fullHome(p: Page) = ViewModel( - p, - Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), - "text-gray-400 hover:text-gray-500", - "Domů", - Some("sr-only") - ) - - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - PageLink - .container($m.map(_.page), actionBus) - .amend( - cls <-- $m.map(_.extraClasses), - child.maybe <-- $m.map(_.icon), - span( - cls <-- $m.map(_.textClasses.getOrElse("")), - child.text <-- $m.map(_.text) - ) - ) - - object FullBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - ol( - role := "list", - cls := "flex items-center space-x-4", - children <-- $m.map(_.path).split(_.id)((_, _, $p) => - li( - div( - cls := "flex items-center", - child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), - Link( - $p.map(p => - if (p.isRoot) then Link.fullHome(p) - else - Link.ViewModel( - p, - None, - s"ml-4 max-w-xs truncate ${Link.baseClasses}", - p.title - ) - ), - actionBus - ) - ) - ) - ) - ) - - object ShortBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - Link( - $m.map { p => - val target = p.parent.getOrElse(p) - if target.isRoot then Link.shortHome(target) - else - Link.ViewModel( - target, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - s"Zpět na ${target.title}" - ) - }, - actionBus - ) - - def apply(actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - nav( - cls := "flex", - aria.label := "Breadcrumb", - div( - cls := "flex sm:hidden", - ShortBreadcrumbs(router.$currentPage, actionBus) - ), - div( - cls := "hidden sm:block", - FullBreadcrumbs(router.$currentPage, actionBus) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala deleted file mode 100644 index 0db2047..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ /dev/null @@ -1,242 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent -import fiftyforms.ui.components.tailwind.Icons -import fiftyforms.ui.components.tailwind.Avatar -import cz.e_bs.cmi.mdr.pdb.UserInfo -import io.laminext.syntax.core.* - -object NavigationBar: - - case class Logo(img: String, name: String) - case class Link(a: () => Anchor, active: Boolean) - case class MenuItem(title: String) - - case class ViewModel( - userInfo: UserInfo, - pages: List[Link], - userMenu: List[MenuItem], - logo: Logo - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - val $userInfo = $m.map(_.userInfo) - val mobileMenuOpen = Var(false) - - val desktopOnly = cls("hidden md:block") - val mobileOnly = cls("md:hidden") - - inline def avatarImage(size: Int = 8) = - Avatar($userInfo.map(_.img)).avatarImage(size) - - def notificationButton = button( - tpe := "button", - cls := List( - "bg-indigo-600", - "focus:outline-none", - "focus:ring-2", - "focus:ring-offset-2", - "focus:ring-offset-indigo-600", - "focus:ring-white", - "hover:text-white", - "p-1", - "rounded-full", - "text-indigo-200" - ), - span(cls := "sr-only", "View notifications"), - Icons.outline.bell() - ) - - def userProfile: HtmlElement = - val menuOpen = Var(false) - - def menuItem(item: MenuItem, idx: Int): HtmlElement = - a( - href := "#", - cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", - role := "menuitem", - tabIndex := -1, - idAttr := s"user-menu-item-$idx", - item.title - ) - - div( - cls := "ml-3 relative", - div( - button( - tpe := "button", - cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - idAttr := "user-menu-button", - aria.expanded <-- menuOpen.signal, - aria.hasPopup := true, - span(cls := "sr-only", "Open user menu"), - child <-- avatarImage(), - onClick.preventDefault.mapTo( - !menuOpen.now() - ) --> menuOpen.writer - ) - ), - /* - * */ - div( - cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", - cls <-- menuOpen.signal.map { o => - if (o) "md:block" else "md:hidden" - }, - role := "menu", - aria.orientation := "vertical", - aria.labelledBy := "user-menu-button", - tabIndex := -1, - // : keyboard navigation - children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) - ) - ) - - def mobileProfile = - def menuItem(item: MenuItem): HtmlElement = - a( - href := "#", - cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", - item.title - ) - - div( - cls := "pt-4 pb-3 border-t border-indigo-700", - div( - cls := "flex items-center px-5", - div( - cls := "flex-shrink-0", - child <-- avatarImage(10) - ), - div( - cls := "ml-3", - div( - cls := "text-base font-medium text-white", - child.text <-- $userInfo.map(_.name) - ), - child.maybe <-- $userInfo.map( - _.email.map(e => - div( - cls := "text-sm font-medium text-indigo-300", - e - ) - ) - ) - ), - notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) - ), - div( - cls := "mt-3 px-2 space-y-1", - children <-- $m.map(_.userMenu.map(menuItem)) - ) - ) - - def pageLink(page: Link): Anchor = - page - .a() - .amend( - cls := "text-white px-3 py-2 rounded-md text-sm font-medium", - cls := Seq( - "bg-indigo-700" -> page.active, - "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active - ), - ariaCurrent := (if page.active then "page" else "false") - ) - - def logoImg: Image = - img( - cls := "h-8 w-8", - src <-- $m.map(_.logo.img), - alt <-- $m.map(_.logo.name) - ) - - def mobileMenuButton = button( - tpe := "button", - cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - aria.controls := "mobile-menu", - aria.expanded <-- mobileMenuOpen.signal, - span(cls := "sr-only", "Open main menu"), - Icons.outline - .menu() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") - ), - Icons.outline - .x() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") - ), - onClick.preventDefault.mapTo( - !mobileMenuOpen.now() - ) --> mobileMenuOpen.writer - ) - - def navBarLeft = - div( - cls := "flex items-center", - div(cls := "flex-shrink-0", logoImg), - div( - desktopOnly, - div( - cls := "ml-10 flex items-baseline space-x-4", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ) - ) - ) - - def navBarRight = - div( - desktopOnly, - div( - cls := "ml-4 flex items-center md:ml-6", - notificationButton, - userProfile - ) - ) - - def navBarMobile = - div( - cls := "-mr-2 flex", - mobileOnly, - mobileMenuButton - ) - - def navBar = - div( - cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex items-center justify-between h-16", - navBarLeft, - navBarRight, - navBarMobile - ) - ) - - def mobileMenu = - div( - mobileOnly, - cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }, - idAttr := "mobile-menu", - div( - cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ), - mobileProfile - ) - - nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala deleted file mode 100644 index 75339df..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object PageHeader: - def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = - header( - cls := "bg-white shadow-sm", - div( - cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", - h1( - cls := "text-lg leading-6 font-semibold text-gray-900", - Breadcrumbs(actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala deleted file mode 100644 index 5096463..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ /dev/null @@ -1,30 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import fiftyforms.ui.components.tailwind.Loading -import io.laminext.syntax.core.* - -object PageLayout: - case class ViewModel( - navigation: NavigationBar.ViewModel, - content: Option[HtmlElement] - ) - def apply(actionBus: Observer[Action])( - $m: Signal[ViewModel], - mods: Modifier[HtmlElement]* - )(using router: Router[Page]): HtmlElement = - val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) - div( - cls := "h-full flex flex-col", - NavigationBar($m.map(_.navigation)), - child.maybe <-- router.$currentPage.map(_.isRoot) - .switch(None, Some(PageHeader(actionBus))), - main( - cls := "flex-grow-1 overflow-y-auto", - mods, - child <-- $maybeContent.map(_.getOrElse(Loading)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala deleted file mode 100644 index 62d820f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.LinkSupport.* -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo - -object PageLink: - type ViewModel = Page - def apply($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = - a(mods($m, actions), child.text <-- $m.map(_.title)) - - def apply(m: ViewModel, actions: Observer[Action])(using - router: Router[Page] - ): Anchor = apply(Val(m), actions) - - def container($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = a(mods($m, actions)) - - def container(m: ViewModel, actions: Observer[Action])(using - Router[Page] - ): Anchor = container(Val(m), actions) - - private def mods($m: Signal[Page], actions: Observer[Action])(using - router: Router[Page] - ): Modifier[Anchor] = - Seq( - href <-- $m.map(router.absoluteUrlForPage).recover { case _ => - Some("invalid url") - }, - composeEvents(onClick.noKeyMod.preventDefault)( - _.sample($m) - .map(NavigateTo.apply) - ) --> actions - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala deleted file mode 100644 index a3b00af..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala +++ /dev/null @@ -1,103 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import scala.scalajs.js.annotation.JSExportTopLevel -import scala.scalajs.js.annotation.JSExport -import scala.scalajs.js.annotation.JSImport -import scala.scalajs.js -import org.scalajs.dom -import com.raquo.laminar.api.L.{*, given} -import scala.scalajs.js.Date -import com.raquo.waypoint.Router -import com.raquo.waypoint.SplitRender -import cz.e_bs.cmi.mdr.pdb.app.services.DataFetcher -import scala.scalajs.js.JSON -import zio.json._ -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.state.AppState - -@js.native -@JSImport("stylesheets/main.css", JSImport.Namespace) -object Css extends js.Any - -@js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native -@JSImport("params/pdb-params.json", JSImport.Default) -object pdbParams extends js.Object - -@JSExportTopLevel("app") -object Main: - - @JSExport - def main(args: Array[String]): Unit = - import Routes.given - onLoad { - setupAirstream() - val appContainer = dom.document.querySelector("#app") - val _ = - render( - appContainer, - renderPage(state.MockAppState(using unsafeWindowOwner, router)) - ) - } - - private def onLoad(f: => Unit): Unit = - documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) - - private def setupAirstream()(using router: Router[Page]): Unit = - AirstreamError.registerUnhandledErrorCallback(err => - router.forcePage( - Page.UnhandledError( - Some(err.getClass.getName), // TODO: Fill only in dev mode - Some(err.getMessage) - ) - ) - ) - - def renderPage(state: AppState)(using - router: Router[Page] - ): HtmlElement = - val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) - .collectSignal[Page.Detail]( - connectors - .DetailPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailParametru]( - connectors - .DetailParametruPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailKriteria]( - connectors - .DetailKriteriaPageConnector(state)(_) - .apply - ) - .collectSignal[Page.UpravDukazKriteria]( - pages.detail.UpravDukaz.Connector(state)(_).apply - ) - .collectStatic(Page.Dashboard)( - connectors.DashboardPageConnector(state.actionBus).apply - ) - .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) - ) - .collect[Page.UnhandledError](pg => - pages.errors - .UnhandledErrorPage( - pages.errors.UnhandledErrorPage - .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), - state.actionBus - ) - ) - .collectStatic(Page.Directory)( - connectors - .DirectoryPageConnector(state.users, state.actionBus) - .apply - ) - div(cls := "h-full", child <-- pageSplitter.$view) - - // Pull in the stylesheet - val css: Css.type = Css diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala deleted file mode 100644 index 3bbc8c0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ /dev/null @@ -1,198 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.* -import org.scalajs.dom -import zio.json.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import scala.scalajs.js -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.Page.Titled - -// enum is not working with Waypoints' SplitRender collectStatic -sealed abstract class Page( - val id: String, - val title: String, - val parent: Option[Page] -) { - val path: Vector[Page] = - parent match - case None => Vector(this) - case Some(p) => p.path :+ this - - val isRoot: Boolean = parent.isEmpty -} - -object Page: - - case class Titled[V](value: V, title: Option[String] = None): - val show: String = title.getOrElse(value.toString) - - case object Directory extends Page("directory", "Adresář", None) - - case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) - - case class Detail(osobniCislo: Titled[OsobniCislo]) - extends Page("user", osobniCislo.show, Some(Directory)) - - object Detail { - def apply(o: UserInfo): Detail = Detail( - Titled(o.personalNumber, Some(o.name)) - ) - } - - case class DetailParametru( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String] - ) extends Page( - "parameter", - parametr.show, - Some(Detail(osobniCislo)) - ) - - object DetailParametru { - def apply(o: UserInfo, p: Parameter): DetailParametru = - DetailParametru( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)) - ) - } - - case class DetailKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "criteria", - kriterium.show, - Some(DetailParametru(osobniCislo, parametr)) - ) - - object DetailKriteria { - def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = - DetailKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class UpravDukazKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "addProof", - "Důkaz", - Some(DetailKriteria(osobniCislo, parametr, kriterium)) - ) - - object UpravDukazKriteria { - def apply( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): UpravDukazKriteria = - UpravDukazKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class NotFound(url: String) extends Page("404", "404", Some(Directory)) - - case class UnhandledError( - errorName: Option[String], - errorMessage: Option[String] - ) extends Page("500", "Unexpected error", Some(Directory)) - -object Routes: - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) - given [V: JsonEncoder]: JsonEncoder[Titled[V]] = - DeriveJsonEncoder.gen[Titled[V]] - given [V: JsonDecoder]: JsonDecoder[Titled[V]] = - DeriveJsonDecoder.gen[Titled[V]] - given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] - given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] - - val base = - js.`import`.meta.env.BASE_URL - .asInstanceOf[String] + "app" - - val homePage: Page = Page.Directory - - given router: Router[Page] = Router[Page]( - routes = List( - Route.static(homePage, root / endOfSegments, basePath = base), - Route.static( - Page.Dashboard, - root / "dashboard" / endOfSegments, - basePath = base - ), - Route[Page.Detail, String]( - encode = _.osobniCislo.value.toString, - decode = osc => Page.Detail(Titled(OsobniCislo(osc))), - root / "osoba" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.DetailParametru, (String, String)]( - encode = p => (p.osobniCislo.value.toString, p.parametr.value), - decode = - p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / endOfSegments, - basePath = base - ), - Route[Page.DetailKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.DetailKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.UpravDukazKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.UpravDukazKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / "edit" / endOfSegments, - basePath = base - ) - ), - serializePage = _.toJson, - deserializePage = _.fromJson[Page] - .fold(s => throw IllegalStateException(s), identity), - getPageTitle = _.title, - routeFallback = url => Page.NotFound(url), - deserializeFallback = _ => Page.Dashboard - )( - $popStateEvent = windowEvents.onPopState, - owner = unsafeWindowOwner - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala deleted file mode 100644 index b24b82b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -sealed trait Action - -case object FetchDirectory extends Action -case class FetchUserDetails(osc: OsobniCislo) extends Action -case class FetchParameters(osc: OsobniCislo) extends Action -case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action -case class FetchParameterCriteria( - osc: OsobniCislo, - paramId: String, - critId: String, - page: (UserInfo, Parameter, ParameterCriteria) => Page -) extends Action -case class FetchAvailableFiles(osc: OsobniCislo) extends Action -case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala deleted file mode 100644 index c637d35..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala +++ /dev/null @@ -1,82 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.{UserProfile, UserInfo, OsobniCislo} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo -import cz.e_bs.cmi.mdr.pdb.UserFunction - -object AppPage: - // TODO: pages by logged in user - val pages: List[Page] = List(Page.Directory, Page.Dashboard) - - import NavigationBar.{Logo, MenuItem} - - val logo = Logo( - "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", - "Workflow" - ) - - // TODO: menu items by user profile - val userMenu = - List( - MenuItem("Your Profile"), - MenuItem("Settings"), - MenuItem("Sign out") - ) - - // TODO: load user profile - val $userProfile = Var( - UserProfile( - "tom", - UserInfo( - OsobniCislo("1031"), - "tom", - "Tom", - "Cook", - None, - None, - Some("tom@example.com"), - Some("+420 222 866 180"), - Some( - UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") - ), - Nil, - None - ) - ) - ) - - val $userInfo = $userProfile.signal.map(_.userInfo) - - type ViewModel = Option[HtmlElement] - def apply( - actionBus: Observer[Action] - )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using - router: Router[Page] - ): HtmlElement = - PageLayout(actionBus)( - $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => - PageLayout.ViewModel( - NavigationBar.ViewModel( - u, - pages.map(p => - NavigationBar.Link( - () => - PageLink( - p, - actionBus - ), - p == cp - ) - ), - userMenu, - logo - ), - c - ) - ), - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala deleted file mode 100644 index bf473c6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ /dev/null @@ -1,143 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden -import fiftyforms.ui.components.tailwind.list.IconText.ViewModel -import fiftyforms.ui.components.tailwind.Icons -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Action - -object Breadcrumbs: - - private def slash = - import svg.{*, given} - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-300", - xmlns := "http://www.w3.org/2000/svg", - fill := "currentColor", - viewBox := "0 0 20 20", - ariaHidden := true, - path( - d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" - ) - ) - - def backIcon = - Icons.solid - .`arrow-narrow-left`() - .amend( - svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" - ) - - object Link: - case class ViewModel( - page: Page, - icon: Option[SvgElement], - extraClasses: String, - text: String, - textClasses: Option[String] = None - ) - - val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" - - def shortHome(p: Page) = ViewModel( - p, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - "Zpět na úvodní stránku", - None - ) - - def fullHome(p: Page) = ViewModel( - p, - Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), - "text-gray-400 hover:text-gray-500", - "Domů", - Some("sr-only") - ) - - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - PageLink - .container($m.map(_.page), actionBus) - .amend( - cls <-- $m.map(_.extraClasses), - child.maybe <-- $m.map(_.icon), - span( - cls <-- $m.map(_.textClasses.getOrElse("")), - child.text <-- $m.map(_.text) - ) - ) - - object FullBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - ol( - role := "list", - cls := "flex items-center space-x-4", - children <-- $m.map(_.path).split(_.id)((_, _, $p) => - li( - div( - cls := "flex items-center", - child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), - Link( - $p.map(p => - if (p.isRoot) then Link.fullHome(p) - else - Link.ViewModel( - p, - None, - s"ml-4 max-w-xs truncate ${Link.baseClasses}", - p.title - ) - ), - actionBus - ) - ) - ) - ) - ) - - object ShortBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - Link( - $m.map { p => - val target = p.parent.getOrElse(p) - if target.isRoot then Link.shortHome(target) - else - Link.ViewModel( - target, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - s"Zpět na ${target.title}" - ) - }, - actionBus - ) - - def apply(actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - nav( - cls := "flex", - aria.label := "Breadcrumb", - div( - cls := "flex sm:hidden", - ShortBreadcrumbs(router.$currentPage, actionBus) - ), - div( - cls := "hidden sm:block", - FullBreadcrumbs(router.$currentPage, actionBus) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala deleted file mode 100644 index 0db2047..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ /dev/null @@ -1,242 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent -import fiftyforms.ui.components.tailwind.Icons -import fiftyforms.ui.components.tailwind.Avatar -import cz.e_bs.cmi.mdr.pdb.UserInfo -import io.laminext.syntax.core.* - -object NavigationBar: - - case class Logo(img: String, name: String) - case class Link(a: () => Anchor, active: Boolean) - case class MenuItem(title: String) - - case class ViewModel( - userInfo: UserInfo, - pages: List[Link], - userMenu: List[MenuItem], - logo: Logo - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - val $userInfo = $m.map(_.userInfo) - val mobileMenuOpen = Var(false) - - val desktopOnly = cls("hidden md:block") - val mobileOnly = cls("md:hidden") - - inline def avatarImage(size: Int = 8) = - Avatar($userInfo.map(_.img)).avatarImage(size) - - def notificationButton = button( - tpe := "button", - cls := List( - "bg-indigo-600", - "focus:outline-none", - "focus:ring-2", - "focus:ring-offset-2", - "focus:ring-offset-indigo-600", - "focus:ring-white", - "hover:text-white", - "p-1", - "rounded-full", - "text-indigo-200" - ), - span(cls := "sr-only", "View notifications"), - Icons.outline.bell() - ) - - def userProfile: HtmlElement = - val menuOpen = Var(false) - - def menuItem(item: MenuItem, idx: Int): HtmlElement = - a( - href := "#", - cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", - role := "menuitem", - tabIndex := -1, - idAttr := s"user-menu-item-$idx", - item.title - ) - - div( - cls := "ml-3 relative", - div( - button( - tpe := "button", - cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - idAttr := "user-menu-button", - aria.expanded <-- menuOpen.signal, - aria.hasPopup := true, - span(cls := "sr-only", "Open user menu"), - child <-- avatarImage(), - onClick.preventDefault.mapTo( - !menuOpen.now() - ) --> menuOpen.writer - ) - ), - /* - * */ - div( - cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", - cls <-- menuOpen.signal.map { o => - if (o) "md:block" else "md:hidden" - }, - role := "menu", - aria.orientation := "vertical", - aria.labelledBy := "user-menu-button", - tabIndex := -1, - // : keyboard navigation - children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) - ) - ) - - def mobileProfile = - def menuItem(item: MenuItem): HtmlElement = - a( - href := "#", - cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", - item.title - ) - - div( - cls := "pt-4 pb-3 border-t border-indigo-700", - div( - cls := "flex items-center px-5", - div( - cls := "flex-shrink-0", - child <-- avatarImage(10) - ), - div( - cls := "ml-3", - div( - cls := "text-base font-medium text-white", - child.text <-- $userInfo.map(_.name) - ), - child.maybe <-- $userInfo.map( - _.email.map(e => - div( - cls := "text-sm font-medium text-indigo-300", - e - ) - ) - ) - ), - notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) - ), - div( - cls := "mt-3 px-2 space-y-1", - children <-- $m.map(_.userMenu.map(menuItem)) - ) - ) - - def pageLink(page: Link): Anchor = - page - .a() - .amend( - cls := "text-white px-3 py-2 rounded-md text-sm font-medium", - cls := Seq( - "bg-indigo-700" -> page.active, - "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active - ), - ariaCurrent := (if page.active then "page" else "false") - ) - - def logoImg: Image = - img( - cls := "h-8 w-8", - src <-- $m.map(_.logo.img), - alt <-- $m.map(_.logo.name) - ) - - def mobileMenuButton = button( - tpe := "button", - cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - aria.controls := "mobile-menu", - aria.expanded <-- mobileMenuOpen.signal, - span(cls := "sr-only", "Open main menu"), - Icons.outline - .menu() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") - ), - Icons.outline - .x() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") - ), - onClick.preventDefault.mapTo( - !mobileMenuOpen.now() - ) --> mobileMenuOpen.writer - ) - - def navBarLeft = - div( - cls := "flex items-center", - div(cls := "flex-shrink-0", logoImg), - div( - desktopOnly, - div( - cls := "ml-10 flex items-baseline space-x-4", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ) - ) - ) - - def navBarRight = - div( - desktopOnly, - div( - cls := "ml-4 flex items-center md:ml-6", - notificationButton, - userProfile - ) - ) - - def navBarMobile = - div( - cls := "-mr-2 flex", - mobileOnly, - mobileMenuButton - ) - - def navBar = - div( - cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex items-center justify-between h-16", - navBarLeft, - navBarRight, - navBarMobile - ) - ) - - def mobileMenu = - div( - mobileOnly, - cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }, - idAttr := "mobile-menu", - div( - cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ), - mobileProfile - ) - - nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala deleted file mode 100644 index 75339df..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object PageHeader: - def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = - header( - cls := "bg-white shadow-sm", - div( - cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", - h1( - cls := "text-lg leading-6 font-semibold text-gray-900", - Breadcrumbs(actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala deleted file mode 100644 index 5096463..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ /dev/null @@ -1,30 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import fiftyforms.ui.components.tailwind.Loading -import io.laminext.syntax.core.* - -object PageLayout: - case class ViewModel( - navigation: NavigationBar.ViewModel, - content: Option[HtmlElement] - ) - def apply(actionBus: Observer[Action])( - $m: Signal[ViewModel], - mods: Modifier[HtmlElement]* - )(using router: Router[Page]): HtmlElement = - val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) - div( - cls := "h-full flex flex-col", - NavigationBar($m.map(_.navigation)), - child.maybe <-- router.$currentPage.map(_.isRoot) - .switch(None, Some(PageHeader(actionBus))), - main( - cls := "flex-grow-1 overflow-y-auto", - mods, - child <-- $maybeContent.map(_.getOrElse(Loading)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala deleted file mode 100644 index 62d820f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.LinkSupport.* -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo - -object PageLink: - type ViewModel = Page - def apply($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = - a(mods($m, actions), child.text <-- $m.map(_.title)) - - def apply(m: ViewModel, actions: Observer[Action])(using - router: Router[Page] - ): Anchor = apply(Val(m), actions) - - def container($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = a(mods($m, actions)) - - def container(m: ViewModel, actions: Observer[Action])(using - Router[Page] - ): Anchor = container(Val(m), actions) - - private def mods($m: Signal[Page], actions: Observer[Action])(using - router: Router[Page] - ): Modifier[Anchor] = - Seq( - href <-- $m.map(router.absoluteUrlForPage).recover { case _ => - Some("invalid url") - }, - composeEvents(onClick.noKeyMod.preventDefault)( - _.sample($m) - .map(NavigateTo.apply) - ) --> actions - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala deleted file mode 100644 index 87f825a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.pages.dashboard.DashboardPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -class DashboardPageConnector(actionBus: Observer[Action])(using - router: Router[Page] -): - def apply: HtmlElement = - AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala deleted file mode 100644 index a3b00af..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala +++ /dev/null @@ -1,103 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import scala.scalajs.js.annotation.JSExportTopLevel -import scala.scalajs.js.annotation.JSExport -import scala.scalajs.js.annotation.JSImport -import scala.scalajs.js -import org.scalajs.dom -import com.raquo.laminar.api.L.{*, given} -import scala.scalajs.js.Date -import com.raquo.waypoint.Router -import com.raquo.waypoint.SplitRender -import cz.e_bs.cmi.mdr.pdb.app.services.DataFetcher -import scala.scalajs.js.JSON -import zio.json._ -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.state.AppState - -@js.native -@JSImport("stylesheets/main.css", JSImport.Namespace) -object Css extends js.Any - -@js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native -@JSImport("params/pdb-params.json", JSImport.Default) -object pdbParams extends js.Object - -@JSExportTopLevel("app") -object Main: - - @JSExport - def main(args: Array[String]): Unit = - import Routes.given - onLoad { - setupAirstream() - val appContainer = dom.document.querySelector("#app") - val _ = - render( - appContainer, - renderPage(state.MockAppState(using unsafeWindowOwner, router)) - ) - } - - private def onLoad(f: => Unit): Unit = - documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) - - private def setupAirstream()(using router: Router[Page]): Unit = - AirstreamError.registerUnhandledErrorCallback(err => - router.forcePage( - Page.UnhandledError( - Some(err.getClass.getName), // TODO: Fill only in dev mode - Some(err.getMessage) - ) - ) - ) - - def renderPage(state: AppState)(using - router: Router[Page] - ): HtmlElement = - val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) - .collectSignal[Page.Detail]( - connectors - .DetailPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailParametru]( - connectors - .DetailParametruPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailKriteria]( - connectors - .DetailKriteriaPageConnector(state)(_) - .apply - ) - .collectSignal[Page.UpravDukazKriteria]( - pages.detail.UpravDukaz.Connector(state)(_).apply - ) - .collectStatic(Page.Dashboard)( - connectors.DashboardPageConnector(state.actionBus).apply - ) - .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) - ) - .collect[Page.UnhandledError](pg => - pages.errors - .UnhandledErrorPage( - pages.errors.UnhandledErrorPage - .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), - state.actionBus - ) - ) - .collectStatic(Page.Directory)( - connectors - .DirectoryPageConnector(state.users, state.actionBus) - .apply - ) - div(cls := "h-full", child <-- pageSplitter.$view) - - // Pull in the stylesheet - val css: Css.type = Css diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala deleted file mode 100644 index 3bbc8c0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ /dev/null @@ -1,198 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.* -import org.scalajs.dom -import zio.json.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import scala.scalajs.js -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.Page.Titled - -// enum is not working with Waypoints' SplitRender collectStatic -sealed abstract class Page( - val id: String, - val title: String, - val parent: Option[Page] -) { - val path: Vector[Page] = - parent match - case None => Vector(this) - case Some(p) => p.path :+ this - - val isRoot: Boolean = parent.isEmpty -} - -object Page: - - case class Titled[V](value: V, title: Option[String] = None): - val show: String = title.getOrElse(value.toString) - - case object Directory extends Page("directory", "Adresář", None) - - case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) - - case class Detail(osobniCislo: Titled[OsobniCislo]) - extends Page("user", osobniCislo.show, Some(Directory)) - - object Detail { - def apply(o: UserInfo): Detail = Detail( - Titled(o.personalNumber, Some(o.name)) - ) - } - - case class DetailParametru( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String] - ) extends Page( - "parameter", - parametr.show, - Some(Detail(osobniCislo)) - ) - - object DetailParametru { - def apply(o: UserInfo, p: Parameter): DetailParametru = - DetailParametru( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)) - ) - } - - case class DetailKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "criteria", - kriterium.show, - Some(DetailParametru(osobniCislo, parametr)) - ) - - object DetailKriteria { - def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = - DetailKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class UpravDukazKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "addProof", - "Důkaz", - Some(DetailKriteria(osobniCislo, parametr, kriterium)) - ) - - object UpravDukazKriteria { - def apply( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): UpravDukazKriteria = - UpravDukazKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class NotFound(url: String) extends Page("404", "404", Some(Directory)) - - case class UnhandledError( - errorName: Option[String], - errorMessage: Option[String] - ) extends Page("500", "Unexpected error", Some(Directory)) - -object Routes: - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) - given [V: JsonEncoder]: JsonEncoder[Titled[V]] = - DeriveJsonEncoder.gen[Titled[V]] - given [V: JsonDecoder]: JsonDecoder[Titled[V]] = - DeriveJsonDecoder.gen[Titled[V]] - given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] - given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] - - val base = - js.`import`.meta.env.BASE_URL - .asInstanceOf[String] + "app" - - val homePage: Page = Page.Directory - - given router: Router[Page] = Router[Page]( - routes = List( - Route.static(homePage, root / endOfSegments, basePath = base), - Route.static( - Page.Dashboard, - root / "dashboard" / endOfSegments, - basePath = base - ), - Route[Page.Detail, String]( - encode = _.osobniCislo.value.toString, - decode = osc => Page.Detail(Titled(OsobniCislo(osc))), - root / "osoba" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.DetailParametru, (String, String)]( - encode = p => (p.osobniCislo.value.toString, p.parametr.value), - decode = - p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / endOfSegments, - basePath = base - ), - Route[Page.DetailKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.DetailKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.UpravDukazKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.UpravDukazKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / "edit" / endOfSegments, - basePath = base - ) - ), - serializePage = _.toJson, - deserializePage = _.fromJson[Page] - .fold(s => throw IllegalStateException(s), identity), - getPageTitle = _.title, - routeFallback = url => Page.NotFound(url), - deserializeFallback = _ => Page.Dashboard - )( - $popStateEvent = windowEvents.onPopState, - owner = unsafeWindowOwner - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala deleted file mode 100644 index b24b82b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -sealed trait Action - -case object FetchDirectory extends Action -case class FetchUserDetails(osc: OsobniCislo) extends Action -case class FetchParameters(osc: OsobniCislo) extends Action -case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action -case class FetchParameterCriteria( - osc: OsobniCislo, - paramId: String, - critId: String, - page: (UserInfo, Parameter, ParameterCriteria) => Page -) extends Action -case class FetchAvailableFiles(osc: OsobniCislo) extends Action -case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala deleted file mode 100644 index c637d35..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala +++ /dev/null @@ -1,82 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.{UserProfile, UserInfo, OsobniCislo} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo -import cz.e_bs.cmi.mdr.pdb.UserFunction - -object AppPage: - // TODO: pages by logged in user - val pages: List[Page] = List(Page.Directory, Page.Dashboard) - - import NavigationBar.{Logo, MenuItem} - - val logo = Logo( - "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", - "Workflow" - ) - - // TODO: menu items by user profile - val userMenu = - List( - MenuItem("Your Profile"), - MenuItem("Settings"), - MenuItem("Sign out") - ) - - // TODO: load user profile - val $userProfile = Var( - UserProfile( - "tom", - UserInfo( - OsobniCislo("1031"), - "tom", - "Tom", - "Cook", - None, - None, - Some("tom@example.com"), - Some("+420 222 866 180"), - Some( - UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") - ), - Nil, - None - ) - ) - ) - - val $userInfo = $userProfile.signal.map(_.userInfo) - - type ViewModel = Option[HtmlElement] - def apply( - actionBus: Observer[Action] - )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using - router: Router[Page] - ): HtmlElement = - PageLayout(actionBus)( - $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => - PageLayout.ViewModel( - NavigationBar.ViewModel( - u, - pages.map(p => - NavigationBar.Link( - () => - PageLink( - p, - actionBus - ), - p == cp - ) - ), - userMenu, - logo - ), - c - ) - ), - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala deleted file mode 100644 index bf473c6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ /dev/null @@ -1,143 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden -import fiftyforms.ui.components.tailwind.list.IconText.ViewModel -import fiftyforms.ui.components.tailwind.Icons -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Action - -object Breadcrumbs: - - private def slash = - import svg.{*, given} - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-300", - xmlns := "http://www.w3.org/2000/svg", - fill := "currentColor", - viewBox := "0 0 20 20", - ariaHidden := true, - path( - d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" - ) - ) - - def backIcon = - Icons.solid - .`arrow-narrow-left`() - .amend( - svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" - ) - - object Link: - case class ViewModel( - page: Page, - icon: Option[SvgElement], - extraClasses: String, - text: String, - textClasses: Option[String] = None - ) - - val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" - - def shortHome(p: Page) = ViewModel( - p, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - "Zpět na úvodní stránku", - None - ) - - def fullHome(p: Page) = ViewModel( - p, - Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), - "text-gray-400 hover:text-gray-500", - "Domů", - Some("sr-only") - ) - - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - PageLink - .container($m.map(_.page), actionBus) - .amend( - cls <-- $m.map(_.extraClasses), - child.maybe <-- $m.map(_.icon), - span( - cls <-- $m.map(_.textClasses.getOrElse("")), - child.text <-- $m.map(_.text) - ) - ) - - object FullBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - ol( - role := "list", - cls := "flex items-center space-x-4", - children <-- $m.map(_.path).split(_.id)((_, _, $p) => - li( - div( - cls := "flex items-center", - child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), - Link( - $p.map(p => - if (p.isRoot) then Link.fullHome(p) - else - Link.ViewModel( - p, - None, - s"ml-4 max-w-xs truncate ${Link.baseClasses}", - p.title - ) - ), - actionBus - ) - ) - ) - ) - ) - - object ShortBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - Link( - $m.map { p => - val target = p.parent.getOrElse(p) - if target.isRoot then Link.shortHome(target) - else - Link.ViewModel( - target, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - s"Zpět na ${target.title}" - ) - }, - actionBus - ) - - def apply(actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - nav( - cls := "flex", - aria.label := "Breadcrumb", - div( - cls := "flex sm:hidden", - ShortBreadcrumbs(router.$currentPage, actionBus) - ), - div( - cls := "hidden sm:block", - FullBreadcrumbs(router.$currentPage, actionBus) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala deleted file mode 100644 index 0db2047..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ /dev/null @@ -1,242 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent -import fiftyforms.ui.components.tailwind.Icons -import fiftyforms.ui.components.tailwind.Avatar -import cz.e_bs.cmi.mdr.pdb.UserInfo -import io.laminext.syntax.core.* - -object NavigationBar: - - case class Logo(img: String, name: String) - case class Link(a: () => Anchor, active: Boolean) - case class MenuItem(title: String) - - case class ViewModel( - userInfo: UserInfo, - pages: List[Link], - userMenu: List[MenuItem], - logo: Logo - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - val $userInfo = $m.map(_.userInfo) - val mobileMenuOpen = Var(false) - - val desktopOnly = cls("hidden md:block") - val mobileOnly = cls("md:hidden") - - inline def avatarImage(size: Int = 8) = - Avatar($userInfo.map(_.img)).avatarImage(size) - - def notificationButton = button( - tpe := "button", - cls := List( - "bg-indigo-600", - "focus:outline-none", - "focus:ring-2", - "focus:ring-offset-2", - "focus:ring-offset-indigo-600", - "focus:ring-white", - "hover:text-white", - "p-1", - "rounded-full", - "text-indigo-200" - ), - span(cls := "sr-only", "View notifications"), - Icons.outline.bell() - ) - - def userProfile: HtmlElement = - val menuOpen = Var(false) - - def menuItem(item: MenuItem, idx: Int): HtmlElement = - a( - href := "#", - cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", - role := "menuitem", - tabIndex := -1, - idAttr := s"user-menu-item-$idx", - item.title - ) - - div( - cls := "ml-3 relative", - div( - button( - tpe := "button", - cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - idAttr := "user-menu-button", - aria.expanded <-- menuOpen.signal, - aria.hasPopup := true, - span(cls := "sr-only", "Open user menu"), - child <-- avatarImage(), - onClick.preventDefault.mapTo( - !menuOpen.now() - ) --> menuOpen.writer - ) - ), - /* - * */ - div( - cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", - cls <-- menuOpen.signal.map { o => - if (o) "md:block" else "md:hidden" - }, - role := "menu", - aria.orientation := "vertical", - aria.labelledBy := "user-menu-button", - tabIndex := -1, - // : keyboard navigation - children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) - ) - ) - - def mobileProfile = - def menuItem(item: MenuItem): HtmlElement = - a( - href := "#", - cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", - item.title - ) - - div( - cls := "pt-4 pb-3 border-t border-indigo-700", - div( - cls := "flex items-center px-5", - div( - cls := "flex-shrink-0", - child <-- avatarImage(10) - ), - div( - cls := "ml-3", - div( - cls := "text-base font-medium text-white", - child.text <-- $userInfo.map(_.name) - ), - child.maybe <-- $userInfo.map( - _.email.map(e => - div( - cls := "text-sm font-medium text-indigo-300", - e - ) - ) - ) - ), - notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) - ), - div( - cls := "mt-3 px-2 space-y-1", - children <-- $m.map(_.userMenu.map(menuItem)) - ) - ) - - def pageLink(page: Link): Anchor = - page - .a() - .amend( - cls := "text-white px-3 py-2 rounded-md text-sm font-medium", - cls := Seq( - "bg-indigo-700" -> page.active, - "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active - ), - ariaCurrent := (if page.active then "page" else "false") - ) - - def logoImg: Image = - img( - cls := "h-8 w-8", - src <-- $m.map(_.logo.img), - alt <-- $m.map(_.logo.name) - ) - - def mobileMenuButton = button( - tpe := "button", - cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - aria.controls := "mobile-menu", - aria.expanded <-- mobileMenuOpen.signal, - span(cls := "sr-only", "Open main menu"), - Icons.outline - .menu() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") - ), - Icons.outline - .x() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") - ), - onClick.preventDefault.mapTo( - !mobileMenuOpen.now() - ) --> mobileMenuOpen.writer - ) - - def navBarLeft = - div( - cls := "flex items-center", - div(cls := "flex-shrink-0", logoImg), - div( - desktopOnly, - div( - cls := "ml-10 flex items-baseline space-x-4", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ) - ) - ) - - def navBarRight = - div( - desktopOnly, - div( - cls := "ml-4 flex items-center md:ml-6", - notificationButton, - userProfile - ) - ) - - def navBarMobile = - div( - cls := "-mr-2 flex", - mobileOnly, - mobileMenuButton - ) - - def navBar = - div( - cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex items-center justify-between h-16", - navBarLeft, - navBarRight, - navBarMobile - ) - ) - - def mobileMenu = - div( - mobileOnly, - cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }, - idAttr := "mobile-menu", - div( - cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ), - mobileProfile - ) - - nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala deleted file mode 100644 index 75339df..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object PageHeader: - def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = - header( - cls := "bg-white shadow-sm", - div( - cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", - h1( - cls := "text-lg leading-6 font-semibold text-gray-900", - Breadcrumbs(actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala deleted file mode 100644 index 5096463..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ /dev/null @@ -1,30 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import fiftyforms.ui.components.tailwind.Loading -import io.laminext.syntax.core.* - -object PageLayout: - case class ViewModel( - navigation: NavigationBar.ViewModel, - content: Option[HtmlElement] - ) - def apply(actionBus: Observer[Action])( - $m: Signal[ViewModel], - mods: Modifier[HtmlElement]* - )(using router: Router[Page]): HtmlElement = - val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) - div( - cls := "h-full flex flex-col", - NavigationBar($m.map(_.navigation)), - child.maybe <-- router.$currentPage.map(_.isRoot) - .switch(None, Some(PageHeader(actionBus))), - main( - cls := "flex-grow-1 overflow-y-auto", - mods, - child <-- $maybeContent.map(_.getOrElse(Loading)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala deleted file mode 100644 index 62d820f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.LinkSupport.* -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo - -object PageLink: - type ViewModel = Page - def apply($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = - a(mods($m, actions), child.text <-- $m.map(_.title)) - - def apply(m: ViewModel, actions: Observer[Action])(using - router: Router[Page] - ): Anchor = apply(Val(m), actions) - - def container($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = a(mods($m, actions)) - - def container(m: ViewModel, actions: Observer[Action])(using - Router[Page] - ): Anchor = container(Val(m), actions) - - private def mods($m: Signal[Page], actions: Observer[Action])(using - router: Router[Page] - ): Modifier[Anchor] = - Seq( - href <-- $m.map(router.absoluteUrlForPage).recover { case _ => - Some("invalid url") - }, - composeEvents(onClick.noKeyMod.preventDefault)( - _.sample($m) - .map(NavigateTo.apply) - ) --> actions - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala deleted file mode 100644 index 87f825a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.pages.dashboard.DashboardPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -class DashboardPageConnector(actionBus: Observer[Action])(using - router: Router[Page] -): - def apply: HtmlElement = - AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala deleted file mode 100644 index e9b0f71..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala +++ /dev/null @@ -1,76 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailKriteriaPage -import pages.detail.components.DukazyKriteria -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -object DetailKriteriaPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailKriteriaPageConnector( - state: DetailKriteriaPageConnector.AppState -)( - $page: Signal[Page.DetailKriteria] -)(using Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => - (p.osobniCislo.value, p.parametr.value, p.kriterium.value) - )((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map( - FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) - ) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, s, $s) => - DetailKriteriaPage($s)(state.actionBus.contramap { - case DukazyKriteria.Add => - NavigateTo( - Page.UpravDukazKriteria( - Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), - Page.Titled(s.parametr.id, Some(s.parametr.nazev)), - Page.Titled(s.kriterium.id, Some(s.kriterium.id)) - ) - ) - }) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): DetailKriteriaPage.ViewModel = - DetailKriteriaPage.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala deleted file mode 100644 index a3b00af..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala +++ /dev/null @@ -1,103 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import scala.scalajs.js.annotation.JSExportTopLevel -import scala.scalajs.js.annotation.JSExport -import scala.scalajs.js.annotation.JSImport -import scala.scalajs.js -import org.scalajs.dom -import com.raquo.laminar.api.L.{*, given} -import scala.scalajs.js.Date -import com.raquo.waypoint.Router -import com.raquo.waypoint.SplitRender -import cz.e_bs.cmi.mdr.pdb.app.services.DataFetcher -import scala.scalajs.js.JSON -import zio.json._ -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.state.AppState - -@js.native -@JSImport("stylesheets/main.css", JSImport.Namespace) -object Css extends js.Any - -@js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native -@JSImport("params/pdb-params.json", JSImport.Default) -object pdbParams extends js.Object - -@JSExportTopLevel("app") -object Main: - - @JSExport - def main(args: Array[String]): Unit = - import Routes.given - onLoad { - setupAirstream() - val appContainer = dom.document.querySelector("#app") - val _ = - render( - appContainer, - renderPage(state.MockAppState(using unsafeWindowOwner, router)) - ) - } - - private def onLoad(f: => Unit): Unit = - documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) - - private def setupAirstream()(using router: Router[Page]): Unit = - AirstreamError.registerUnhandledErrorCallback(err => - router.forcePage( - Page.UnhandledError( - Some(err.getClass.getName), // TODO: Fill only in dev mode - Some(err.getMessage) - ) - ) - ) - - def renderPage(state: AppState)(using - router: Router[Page] - ): HtmlElement = - val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) - .collectSignal[Page.Detail]( - connectors - .DetailPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailParametru]( - connectors - .DetailParametruPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailKriteria]( - connectors - .DetailKriteriaPageConnector(state)(_) - .apply - ) - .collectSignal[Page.UpravDukazKriteria]( - pages.detail.UpravDukaz.Connector(state)(_).apply - ) - .collectStatic(Page.Dashboard)( - connectors.DashboardPageConnector(state.actionBus).apply - ) - .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) - ) - .collect[Page.UnhandledError](pg => - pages.errors - .UnhandledErrorPage( - pages.errors.UnhandledErrorPage - .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), - state.actionBus - ) - ) - .collectStatic(Page.Directory)( - connectors - .DirectoryPageConnector(state.users, state.actionBus) - .apply - ) - div(cls := "h-full", child <-- pageSplitter.$view) - - // Pull in the stylesheet - val css: Css.type = Css diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala deleted file mode 100644 index 3bbc8c0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ /dev/null @@ -1,198 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.* -import org.scalajs.dom -import zio.json.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import scala.scalajs.js -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.Page.Titled - -// enum is not working with Waypoints' SplitRender collectStatic -sealed abstract class Page( - val id: String, - val title: String, - val parent: Option[Page] -) { - val path: Vector[Page] = - parent match - case None => Vector(this) - case Some(p) => p.path :+ this - - val isRoot: Boolean = parent.isEmpty -} - -object Page: - - case class Titled[V](value: V, title: Option[String] = None): - val show: String = title.getOrElse(value.toString) - - case object Directory extends Page("directory", "Adresář", None) - - case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) - - case class Detail(osobniCislo: Titled[OsobniCislo]) - extends Page("user", osobniCislo.show, Some(Directory)) - - object Detail { - def apply(o: UserInfo): Detail = Detail( - Titled(o.personalNumber, Some(o.name)) - ) - } - - case class DetailParametru( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String] - ) extends Page( - "parameter", - parametr.show, - Some(Detail(osobniCislo)) - ) - - object DetailParametru { - def apply(o: UserInfo, p: Parameter): DetailParametru = - DetailParametru( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)) - ) - } - - case class DetailKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "criteria", - kriterium.show, - Some(DetailParametru(osobniCislo, parametr)) - ) - - object DetailKriteria { - def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = - DetailKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class UpravDukazKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "addProof", - "Důkaz", - Some(DetailKriteria(osobniCislo, parametr, kriterium)) - ) - - object UpravDukazKriteria { - def apply( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): UpravDukazKriteria = - UpravDukazKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class NotFound(url: String) extends Page("404", "404", Some(Directory)) - - case class UnhandledError( - errorName: Option[String], - errorMessage: Option[String] - ) extends Page("500", "Unexpected error", Some(Directory)) - -object Routes: - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) - given [V: JsonEncoder]: JsonEncoder[Titled[V]] = - DeriveJsonEncoder.gen[Titled[V]] - given [V: JsonDecoder]: JsonDecoder[Titled[V]] = - DeriveJsonDecoder.gen[Titled[V]] - given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] - given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] - - val base = - js.`import`.meta.env.BASE_URL - .asInstanceOf[String] + "app" - - val homePage: Page = Page.Directory - - given router: Router[Page] = Router[Page]( - routes = List( - Route.static(homePage, root / endOfSegments, basePath = base), - Route.static( - Page.Dashboard, - root / "dashboard" / endOfSegments, - basePath = base - ), - Route[Page.Detail, String]( - encode = _.osobniCislo.value.toString, - decode = osc => Page.Detail(Titled(OsobniCislo(osc))), - root / "osoba" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.DetailParametru, (String, String)]( - encode = p => (p.osobniCislo.value.toString, p.parametr.value), - decode = - p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / endOfSegments, - basePath = base - ), - Route[Page.DetailKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.DetailKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.UpravDukazKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.UpravDukazKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / "edit" / endOfSegments, - basePath = base - ) - ), - serializePage = _.toJson, - deserializePage = _.fromJson[Page] - .fold(s => throw IllegalStateException(s), identity), - getPageTitle = _.title, - routeFallback = url => Page.NotFound(url), - deserializeFallback = _ => Page.Dashboard - )( - $popStateEvent = windowEvents.onPopState, - owner = unsafeWindowOwner - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala deleted file mode 100644 index b24b82b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -sealed trait Action - -case object FetchDirectory extends Action -case class FetchUserDetails(osc: OsobniCislo) extends Action -case class FetchParameters(osc: OsobniCislo) extends Action -case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action -case class FetchParameterCriteria( - osc: OsobniCislo, - paramId: String, - critId: String, - page: (UserInfo, Parameter, ParameterCriteria) => Page -) extends Action -case class FetchAvailableFiles(osc: OsobniCislo) extends Action -case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala deleted file mode 100644 index c637d35..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala +++ /dev/null @@ -1,82 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.{UserProfile, UserInfo, OsobniCislo} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo -import cz.e_bs.cmi.mdr.pdb.UserFunction - -object AppPage: - // TODO: pages by logged in user - val pages: List[Page] = List(Page.Directory, Page.Dashboard) - - import NavigationBar.{Logo, MenuItem} - - val logo = Logo( - "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", - "Workflow" - ) - - // TODO: menu items by user profile - val userMenu = - List( - MenuItem("Your Profile"), - MenuItem("Settings"), - MenuItem("Sign out") - ) - - // TODO: load user profile - val $userProfile = Var( - UserProfile( - "tom", - UserInfo( - OsobniCislo("1031"), - "tom", - "Tom", - "Cook", - None, - None, - Some("tom@example.com"), - Some("+420 222 866 180"), - Some( - UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") - ), - Nil, - None - ) - ) - ) - - val $userInfo = $userProfile.signal.map(_.userInfo) - - type ViewModel = Option[HtmlElement] - def apply( - actionBus: Observer[Action] - )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using - router: Router[Page] - ): HtmlElement = - PageLayout(actionBus)( - $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => - PageLayout.ViewModel( - NavigationBar.ViewModel( - u, - pages.map(p => - NavigationBar.Link( - () => - PageLink( - p, - actionBus - ), - p == cp - ) - ), - userMenu, - logo - ), - c - ) - ), - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala deleted file mode 100644 index bf473c6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ /dev/null @@ -1,143 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden -import fiftyforms.ui.components.tailwind.list.IconText.ViewModel -import fiftyforms.ui.components.tailwind.Icons -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Action - -object Breadcrumbs: - - private def slash = - import svg.{*, given} - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-300", - xmlns := "http://www.w3.org/2000/svg", - fill := "currentColor", - viewBox := "0 0 20 20", - ariaHidden := true, - path( - d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" - ) - ) - - def backIcon = - Icons.solid - .`arrow-narrow-left`() - .amend( - svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" - ) - - object Link: - case class ViewModel( - page: Page, - icon: Option[SvgElement], - extraClasses: String, - text: String, - textClasses: Option[String] = None - ) - - val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" - - def shortHome(p: Page) = ViewModel( - p, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - "Zpět na úvodní stránku", - None - ) - - def fullHome(p: Page) = ViewModel( - p, - Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), - "text-gray-400 hover:text-gray-500", - "Domů", - Some("sr-only") - ) - - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - PageLink - .container($m.map(_.page), actionBus) - .amend( - cls <-- $m.map(_.extraClasses), - child.maybe <-- $m.map(_.icon), - span( - cls <-- $m.map(_.textClasses.getOrElse("")), - child.text <-- $m.map(_.text) - ) - ) - - object FullBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - ol( - role := "list", - cls := "flex items-center space-x-4", - children <-- $m.map(_.path).split(_.id)((_, _, $p) => - li( - div( - cls := "flex items-center", - child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), - Link( - $p.map(p => - if (p.isRoot) then Link.fullHome(p) - else - Link.ViewModel( - p, - None, - s"ml-4 max-w-xs truncate ${Link.baseClasses}", - p.title - ) - ), - actionBus - ) - ) - ) - ) - ) - - object ShortBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - Link( - $m.map { p => - val target = p.parent.getOrElse(p) - if target.isRoot then Link.shortHome(target) - else - Link.ViewModel( - target, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - s"Zpět na ${target.title}" - ) - }, - actionBus - ) - - def apply(actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - nav( - cls := "flex", - aria.label := "Breadcrumb", - div( - cls := "flex sm:hidden", - ShortBreadcrumbs(router.$currentPage, actionBus) - ), - div( - cls := "hidden sm:block", - FullBreadcrumbs(router.$currentPage, actionBus) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala deleted file mode 100644 index 0db2047..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ /dev/null @@ -1,242 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent -import fiftyforms.ui.components.tailwind.Icons -import fiftyforms.ui.components.tailwind.Avatar -import cz.e_bs.cmi.mdr.pdb.UserInfo -import io.laminext.syntax.core.* - -object NavigationBar: - - case class Logo(img: String, name: String) - case class Link(a: () => Anchor, active: Boolean) - case class MenuItem(title: String) - - case class ViewModel( - userInfo: UserInfo, - pages: List[Link], - userMenu: List[MenuItem], - logo: Logo - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - val $userInfo = $m.map(_.userInfo) - val mobileMenuOpen = Var(false) - - val desktopOnly = cls("hidden md:block") - val mobileOnly = cls("md:hidden") - - inline def avatarImage(size: Int = 8) = - Avatar($userInfo.map(_.img)).avatarImage(size) - - def notificationButton = button( - tpe := "button", - cls := List( - "bg-indigo-600", - "focus:outline-none", - "focus:ring-2", - "focus:ring-offset-2", - "focus:ring-offset-indigo-600", - "focus:ring-white", - "hover:text-white", - "p-1", - "rounded-full", - "text-indigo-200" - ), - span(cls := "sr-only", "View notifications"), - Icons.outline.bell() - ) - - def userProfile: HtmlElement = - val menuOpen = Var(false) - - def menuItem(item: MenuItem, idx: Int): HtmlElement = - a( - href := "#", - cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", - role := "menuitem", - tabIndex := -1, - idAttr := s"user-menu-item-$idx", - item.title - ) - - div( - cls := "ml-3 relative", - div( - button( - tpe := "button", - cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - idAttr := "user-menu-button", - aria.expanded <-- menuOpen.signal, - aria.hasPopup := true, - span(cls := "sr-only", "Open user menu"), - child <-- avatarImage(), - onClick.preventDefault.mapTo( - !menuOpen.now() - ) --> menuOpen.writer - ) - ), - /* - * */ - div( - cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", - cls <-- menuOpen.signal.map { o => - if (o) "md:block" else "md:hidden" - }, - role := "menu", - aria.orientation := "vertical", - aria.labelledBy := "user-menu-button", - tabIndex := -1, - // : keyboard navigation - children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) - ) - ) - - def mobileProfile = - def menuItem(item: MenuItem): HtmlElement = - a( - href := "#", - cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", - item.title - ) - - div( - cls := "pt-4 pb-3 border-t border-indigo-700", - div( - cls := "flex items-center px-5", - div( - cls := "flex-shrink-0", - child <-- avatarImage(10) - ), - div( - cls := "ml-3", - div( - cls := "text-base font-medium text-white", - child.text <-- $userInfo.map(_.name) - ), - child.maybe <-- $userInfo.map( - _.email.map(e => - div( - cls := "text-sm font-medium text-indigo-300", - e - ) - ) - ) - ), - notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) - ), - div( - cls := "mt-3 px-2 space-y-1", - children <-- $m.map(_.userMenu.map(menuItem)) - ) - ) - - def pageLink(page: Link): Anchor = - page - .a() - .amend( - cls := "text-white px-3 py-2 rounded-md text-sm font-medium", - cls := Seq( - "bg-indigo-700" -> page.active, - "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active - ), - ariaCurrent := (if page.active then "page" else "false") - ) - - def logoImg: Image = - img( - cls := "h-8 w-8", - src <-- $m.map(_.logo.img), - alt <-- $m.map(_.logo.name) - ) - - def mobileMenuButton = button( - tpe := "button", - cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - aria.controls := "mobile-menu", - aria.expanded <-- mobileMenuOpen.signal, - span(cls := "sr-only", "Open main menu"), - Icons.outline - .menu() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") - ), - Icons.outline - .x() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") - ), - onClick.preventDefault.mapTo( - !mobileMenuOpen.now() - ) --> mobileMenuOpen.writer - ) - - def navBarLeft = - div( - cls := "flex items-center", - div(cls := "flex-shrink-0", logoImg), - div( - desktopOnly, - div( - cls := "ml-10 flex items-baseline space-x-4", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ) - ) - ) - - def navBarRight = - div( - desktopOnly, - div( - cls := "ml-4 flex items-center md:ml-6", - notificationButton, - userProfile - ) - ) - - def navBarMobile = - div( - cls := "-mr-2 flex", - mobileOnly, - mobileMenuButton - ) - - def navBar = - div( - cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex items-center justify-between h-16", - navBarLeft, - navBarRight, - navBarMobile - ) - ) - - def mobileMenu = - div( - mobileOnly, - cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }, - idAttr := "mobile-menu", - div( - cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ), - mobileProfile - ) - - nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala deleted file mode 100644 index 75339df..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object PageHeader: - def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = - header( - cls := "bg-white shadow-sm", - div( - cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", - h1( - cls := "text-lg leading-6 font-semibold text-gray-900", - Breadcrumbs(actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala deleted file mode 100644 index 5096463..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ /dev/null @@ -1,30 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import fiftyforms.ui.components.tailwind.Loading -import io.laminext.syntax.core.* - -object PageLayout: - case class ViewModel( - navigation: NavigationBar.ViewModel, - content: Option[HtmlElement] - ) - def apply(actionBus: Observer[Action])( - $m: Signal[ViewModel], - mods: Modifier[HtmlElement]* - )(using router: Router[Page]): HtmlElement = - val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) - div( - cls := "h-full flex flex-col", - NavigationBar($m.map(_.navigation)), - child.maybe <-- router.$currentPage.map(_.isRoot) - .switch(None, Some(PageHeader(actionBus))), - main( - cls := "flex-grow-1 overflow-y-auto", - mods, - child <-- $maybeContent.map(_.getOrElse(Loading)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala deleted file mode 100644 index 62d820f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.LinkSupport.* -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo - -object PageLink: - type ViewModel = Page - def apply($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = - a(mods($m, actions), child.text <-- $m.map(_.title)) - - def apply(m: ViewModel, actions: Observer[Action])(using - router: Router[Page] - ): Anchor = apply(Val(m), actions) - - def container($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = a(mods($m, actions)) - - def container(m: ViewModel, actions: Observer[Action])(using - Router[Page] - ): Anchor = container(Val(m), actions) - - private def mods($m: Signal[Page], actions: Observer[Action])(using - router: Router[Page] - ): Modifier[Anchor] = - Seq( - href <-- $m.map(router.absoluteUrlForPage).recover { case _ => - Some("invalid url") - }, - composeEvents(onClick.noKeyMod.preventDefault)( - _.sample($m) - .map(NavigateTo.apply) - ) --> actions - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala deleted file mode 100644 index 87f825a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.pages.dashboard.DashboardPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -class DashboardPageConnector(actionBus: Observer[Action])(using - router: Router[Page] -): - def apply: HtmlElement = - AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala deleted file mode 100644 index e9b0f71..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala +++ /dev/null @@ -1,76 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailKriteriaPage -import pages.detail.components.DukazyKriteria -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -object DetailKriteriaPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailKriteriaPageConnector( - state: DetailKriteriaPageConnector.AppState -)( - $page: Signal[Page.DetailKriteria] -)(using Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => - (p.osobniCislo.value, p.parametr.value, p.kriterium.value) - )((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map( - FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) - ) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, s, $s) => - DetailKriteriaPage($s)(state.actionBus.contramap { - case DukazyKriteria.Add => - NavigateTo( - Page.UpravDukazKriteria( - Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), - Page.Titled(s.parametr.id, Some(s.parametr.nazev)), - Page.Titled(s.kriterium.id, Some(s.kriterium.id)) - ) - ) - }) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): DetailKriteriaPage.ViewModel = - DetailKriteriaPage.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala deleted file mode 100644 index 4d8024f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ /dev/null @@ -1,57 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import pages.detail.DetailPage -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import fiftyforms.ui.components.tailwind.Color - -object DetailPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailPageConnector(state: DetailPageConnector.AppState)( - $page: Signal[Page.Detail] -)(using router: Router[Page]): - val $oscChangeSignal = $page.splitOne(_.osobniCislo.value)((osc, _, _) => osc) - val $pageChangeSignal = - $oscChangeSignal.flatMap(osc => - EventStream.fromSeq(Seq(FetchUserDetails(osc), FetchParameters(osc))) - ) - // TODO: filter the value based on the current osc - // OSC change will fetch new data, but still - // - we need to be sure that what we got is really what we ought to display - // - we want to display stale data accordingly (at least with loading indicator) - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - def apply: HtmlElement = - AppPage(state.actionBus)( - $data.combineWithFn($params)(_ zip _) - .map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: List[Parameter] - ): DetailPage.ViewModel = - DetailPage.ViewModel( - o.toDetailOsoby, - p.map( - _.toParametr(param => - PageLink.container(Page.DetailParametru(o, param), state.actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala deleted file mode 100644 index a3b00af..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala +++ /dev/null @@ -1,103 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import scala.scalajs.js.annotation.JSExportTopLevel -import scala.scalajs.js.annotation.JSExport -import scala.scalajs.js.annotation.JSImport -import scala.scalajs.js -import org.scalajs.dom -import com.raquo.laminar.api.L.{*, given} -import scala.scalajs.js.Date -import com.raquo.waypoint.Router -import com.raquo.waypoint.SplitRender -import cz.e_bs.cmi.mdr.pdb.app.services.DataFetcher -import scala.scalajs.js.JSON -import zio.json._ -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.state.AppState - -@js.native -@JSImport("stylesheets/main.css", JSImport.Namespace) -object Css extends js.Any - -@js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native -@JSImport("params/pdb-params.json", JSImport.Default) -object pdbParams extends js.Object - -@JSExportTopLevel("app") -object Main: - - @JSExport - def main(args: Array[String]): Unit = - import Routes.given - onLoad { - setupAirstream() - val appContainer = dom.document.querySelector("#app") - val _ = - render( - appContainer, - renderPage(state.MockAppState(using unsafeWindowOwner, router)) - ) - } - - private def onLoad(f: => Unit): Unit = - documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) - - private def setupAirstream()(using router: Router[Page]): Unit = - AirstreamError.registerUnhandledErrorCallback(err => - router.forcePage( - Page.UnhandledError( - Some(err.getClass.getName), // TODO: Fill only in dev mode - Some(err.getMessage) - ) - ) - ) - - def renderPage(state: AppState)(using - router: Router[Page] - ): HtmlElement = - val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) - .collectSignal[Page.Detail]( - connectors - .DetailPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailParametru]( - connectors - .DetailParametruPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailKriteria]( - connectors - .DetailKriteriaPageConnector(state)(_) - .apply - ) - .collectSignal[Page.UpravDukazKriteria]( - pages.detail.UpravDukaz.Connector(state)(_).apply - ) - .collectStatic(Page.Dashboard)( - connectors.DashboardPageConnector(state.actionBus).apply - ) - .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) - ) - .collect[Page.UnhandledError](pg => - pages.errors - .UnhandledErrorPage( - pages.errors.UnhandledErrorPage - .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), - state.actionBus - ) - ) - .collectStatic(Page.Directory)( - connectors - .DirectoryPageConnector(state.users, state.actionBus) - .apply - ) - div(cls := "h-full", child <-- pageSplitter.$view) - - // Pull in the stylesheet - val css: Css.type = Css diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala deleted file mode 100644 index 3bbc8c0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ /dev/null @@ -1,198 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.* -import org.scalajs.dom -import zio.json.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import scala.scalajs.js -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.Page.Titled - -// enum is not working with Waypoints' SplitRender collectStatic -sealed abstract class Page( - val id: String, - val title: String, - val parent: Option[Page] -) { - val path: Vector[Page] = - parent match - case None => Vector(this) - case Some(p) => p.path :+ this - - val isRoot: Boolean = parent.isEmpty -} - -object Page: - - case class Titled[V](value: V, title: Option[String] = None): - val show: String = title.getOrElse(value.toString) - - case object Directory extends Page("directory", "Adresář", None) - - case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) - - case class Detail(osobniCislo: Titled[OsobniCislo]) - extends Page("user", osobniCislo.show, Some(Directory)) - - object Detail { - def apply(o: UserInfo): Detail = Detail( - Titled(o.personalNumber, Some(o.name)) - ) - } - - case class DetailParametru( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String] - ) extends Page( - "parameter", - parametr.show, - Some(Detail(osobniCislo)) - ) - - object DetailParametru { - def apply(o: UserInfo, p: Parameter): DetailParametru = - DetailParametru( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)) - ) - } - - case class DetailKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "criteria", - kriterium.show, - Some(DetailParametru(osobniCislo, parametr)) - ) - - object DetailKriteria { - def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = - DetailKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class UpravDukazKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "addProof", - "Důkaz", - Some(DetailKriteria(osobniCislo, parametr, kriterium)) - ) - - object UpravDukazKriteria { - def apply( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): UpravDukazKriteria = - UpravDukazKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class NotFound(url: String) extends Page("404", "404", Some(Directory)) - - case class UnhandledError( - errorName: Option[String], - errorMessage: Option[String] - ) extends Page("500", "Unexpected error", Some(Directory)) - -object Routes: - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) - given [V: JsonEncoder]: JsonEncoder[Titled[V]] = - DeriveJsonEncoder.gen[Titled[V]] - given [V: JsonDecoder]: JsonDecoder[Titled[V]] = - DeriveJsonDecoder.gen[Titled[V]] - given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] - given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] - - val base = - js.`import`.meta.env.BASE_URL - .asInstanceOf[String] + "app" - - val homePage: Page = Page.Directory - - given router: Router[Page] = Router[Page]( - routes = List( - Route.static(homePage, root / endOfSegments, basePath = base), - Route.static( - Page.Dashboard, - root / "dashboard" / endOfSegments, - basePath = base - ), - Route[Page.Detail, String]( - encode = _.osobniCislo.value.toString, - decode = osc => Page.Detail(Titled(OsobniCislo(osc))), - root / "osoba" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.DetailParametru, (String, String)]( - encode = p => (p.osobniCislo.value.toString, p.parametr.value), - decode = - p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / endOfSegments, - basePath = base - ), - Route[Page.DetailKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.DetailKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.UpravDukazKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.UpravDukazKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / "edit" / endOfSegments, - basePath = base - ) - ), - serializePage = _.toJson, - deserializePage = _.fromJson[Page] - .fold(s => throw IllegalStateException(s), identity), - getPageTitle = _.title, - routeFallback = url => Page.NotFound(url), - deserializeFallback = _ => Page.Dashboard - )( - $popStateEvent = windowEvents.onPopState, - owner = unsafeWindowOwner - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala deleted file mode 100644 index b24b82b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -sealed trait Action - -case object FetchDirectory extends Action -case class FetchUserDetails(osc: OsobniCislo) extends Action -case class FetchParameters(osc: OsobniCislo) extends Action -case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action -case class FetchParameterCriteria( - osc: OsobniCislo, - paramId: String, - critId: String, - page: (UserInfo, Parameter, ParameterCriteria) => Page -) extends Action -case class FetchAvailableFiles(osc: OsobniCislo) extends Action -case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala deleted file mode 100644 index c637d35..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala +++ /dev/null @@ -1,82 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.{UserProfile, UserInfo, OsobniCislo} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo -import cz.e_bs.cmi.mdr.pdb.UserFunction - -object AppPage: - // TODO: pages by logged in user - val pages: List[Page] = List(Page.Directory, Page.Dashboard) - - import NavigationBar.{Logo, MenuItem} - - val logo = Logo( - "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", - "Workflow" - ) - - // TODO: menu items by user profile - val userMenu = - List( - MenuItem("Your Profile"), - MenuItem("Settings"), - MenuItem("Sign out") - ) - - // TODO: load user profile - val $userProfile = Var( - UserProfile( - "tom", - UserInfo( - OsobniCislo("1031"), - "tom", - "Tom", - "Cook", - None, - None, - Some("tom@example.com"), - Some("+420 222 866 180"), - Some( - UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") - ), - Nil, - None - ) - ) - ) - - val $userInfo = $userProfile.signal.map(_.userInfo) - - type ViewModel = Option[HtmlElement] - def apply( - actionBus: Observer[Action] - )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using - router: Router[Page] - ): HtmlElement = - PageLayout(actionBus)( - $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => - PageLayout.ViewModel( - NavigationBar.ViewModel( - u, - pages.map(p => - NavigationBar.Link( - () => - PageLink( - p, - actionBus - ), - p == cp - ) - ), - userMenu, - logo - ), - c - ) - ), - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala deleted file mode 100644 index bf473c6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ /dev/null @@ -1,143 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden -import fiftyforms.ui.components.tailwind.list.IconText.ViewModel -import fiftyforms.ui.components.tailwind.Icons -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Action - -object Breadcrumbs: - - private def slash = - import svg.{*, given} - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-300", - xmlns := "http://www.w3.org/2000/svg", - fill := "currentColor", - viewBox := "0 0 20 20", - ariaHidden := true, - path( - d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" - ) - ) - - def backIcon = - Icons.solid - .`arrow-narrow-left`() - .amend( - svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" - ) - - object Link: - case class ViewModel( - page: Page, - icon: Option[SvgElement], - extraClasses: String, - text: String, - textClasses: Option[String] = None - ) - - val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" - - def shortHome(p: Page) = ViewModel( - p, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - "Zpět na úvodní stránku", - None - ) - - def fullHome(p: Page) = ViewModel( - p, - Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), - "text-gray-400 hover:text-gray-500", - "Domů", - Some("sr-only") - ) - - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - PageLink - .container($m.map(_.page), actionBus) - .amend( - cls <-- $m.map(_.extraClasses), - child.maybe <-- $m.map(_.icon), - span( - cls <-- $m.map(_.textClasses.getOrElse("")), - child.text <-- $m.map(_.text) - ) - ) - - object FullBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - ol( - role := "list", - cls := "flex items-center space-x-4", - children <-- $m.map(_.path).split(_.id)((_, _, $p) => - li( - div( - cls := "flex items-center", - child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), - Link( - $p.map(p => - if (p.isRoot) then Link.fullHome(p) - else - Link.ViewModel( - p, - None, - s"ml-4 max-w-xs truncate ${Link.baseClasses}", - p.title - ) - ), - actionBus - ) - ) - ) - ) - ) - - object ShortBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - Link( - $m.map { p => - val target = p.parent.getOrElse(p) - if target.isRoot then Link.shortHome(target) - else - Link.ViewModel( - target, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - s"Zpět na ${target.title}" - ) - }, - actionBus - ) - - def apply(actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - nav( - cls := "flex", - aria.label := "Breadcrumb", - div( - cls := "flex sm:hidden", - ShortBreadcrumbs(router.$currentPage, actionBus) - ), - div( - cls := "hidden sm:block", - FullBreadcrumbs(router.$currentPage, actionBus) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala deleted file mode 100644 index 0db2047..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ /dev/null @@ -1,242 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent -import fiftyforms.ui.components.tailwind.Icons -import fiftyforms.ui.components.tailwind.Avatar -import cz.e_bs.cmi.mdr.pdb.UserInfo -import io.laminext.syntax.core.* - -object NavigationBar: - - case class Logo(img: String, name: String) - case class Link(a: () => Anchor, active: Boolean) - case class MenuItem(title: String) - - case class ViewModel( - userInfo: UserInfo, - pages: List[Link], - userMenu: List[MenuItem], - logo: Logo - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - val $userInfo = $m.map(_.userInfo) - val mobileMenuOpen = Var(false) - - val desktopOnly = cls("hidden md:block") - val mobileOnly = cls("md:hidden") - - inline def avatarImage(size: Int = 8) = - Avatar($userInfo.map(_.img)).avatarImage(size) - - def notificationButton = button( - tpe := "button", - cls := List( - "bg-indigo-600", - "focus:outline-none", - "focus:ring-2", - "focus:ring-offset-2", - "focus:ring-offset-indigo-600", - "focus:ring-white", - "hover:text-white", - "p-1", - "rounded-full", - "text-indigo-200" - ), - span(cls := "sr-only", "View notifications"), - Icons.outline.bell() - ) - - def userProfile: HtmlElement = - val menuOpen = Var(false) - - def menuItem(item: MenuItem, idx: Int): HtmlElement = - a( - href := "#", - cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", - role := "menuitem", - tabIndex := -1, - idAttr := s"user-menu-item-$idx", - item.title - ) - - div( - cls := "ml-3 relative", - div( - button( - tpe := "button", - cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - idAttr := "user-menu-button", - aria.expanded <-- menuOpen.signal, - aria.hasPopup := true, - span(cls := "sr-only", "Open user menu"), - child <-- avatarImage(), - onClick.preventDefault.mapTo( - !menuOpen.now() - ) --> menuOpen.writer - ) - ), - /* - * */ - div( - cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", - cls <-- menuOpen.signal.map { o => - if (o) "md:block" else "md:hidden" - }, - role := "menu", - aria.orientation := "vertical", - aria.labelledBy := "user-menu-button", - tabIndex := -1, - // : keyboard navigation - children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) - ) - ) - - def mobileProfile = - def menuItem(item: MenuItem): HtmlElement = - a( - href := "#", - cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", - item.title - ) - - div( - cls := "pt-4 pb-3 border-t border-indigo-700", - div( - cls := "flex items-center px-5", - div( - cls := "flex-shrink-0", - child <-- avatarImage(10) - ), - div( - cls := "ml-3", - div( - cls := "text-base font-medium text-white", - child.text <-- $userInfo.map(_.name) - ), - child.maybe <-- $userInfo.map( - _.email.map(e => - div( - cls := "text-sm font-medium text-indigo-300", - e - ) - ) - ) - ), - notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) - ), - div( - cls := "mt-3 px-2 space-y-1", - children <-- $m.map(_.userMenu.map(menuItem)) - ) - ) - - def pageLink(page: Link): Anchor = - page - .a() - .amend( - cls := "text-white px-3 py-2 rounded-md text-sm font-medium", - cls := Seq( - "bg-indigo-700" -> page.active, - "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active - ), - ariaCurrent := (if page.active then "page" else "false") - ) - - def logoImg: Image = - img( - cls := "h-8 w-8", - src <-- $m.map(_.logo.img), - alt <-- $m.map(_.logo.name) - ) - - def mobileMenuButton = button( - tpe := "button", - cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - aria.controls := "mobile-menu", - aria.expanded <-- mobileMenuOpen.signal, - span(cls := "sr-only", "Open main menu"), - Icons.outline - .menu() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") - ), - Icons.outline - .x() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") - ), - onClick.preventDefault.mapTo( - !mobileMenuOpen.now() - ) --> mobileMenuOpen.writer - ) - - def navBarLeft = - div( - cls := "flex items-center", - div(cls := "flex-shrink-0", logoImg), - div( - desktopOnly, - div( - cls := "ml-10 flex items-baseline space-x-4", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ) - ) - ) - - def navBarRight = - div( - desktopOnly, - div( - cls := "ml-4 flex items-center md:ml-6", - notificationButton, - userProfile - ) - ) - - def navBarMobile = - div( - cls := "-mr-2 flex", - mobileOnly, - mobileMenuButton - ) - - def navBar = - div( - cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex items-center justify-between h-16", - navBarLeft, - navBarRight, - navBarMobile - ) - ) - - def mobileMenu = - div( - mobileOnly, - cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }, - idAttr := "mobile-menu", - div( - cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ), - mobileProfile - ) - - nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala deleted file mode 100644 index 75339df..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object PageHeader: - def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = - header( - cls := "bg-white shadow-sm", - div( - cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", - h1( - cls := "text-lg leading-6 font-semibold text-gray-900", - Breadcrumbs(actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala deleted file mode 100644 index 5096463..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ /dev/null @@ -1,30 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import fiftyforms.ui.components.tailwind.Loading -import io.laminext.syntax.core.* - -object PageLayout: - case class ViewModel( - navigation: NavigationBar.ViewModel, - content: Option[HtmlElement] - ) - def apply(actionBus: Observer[Action])( - $m: Signal[ViewModel], - mods: Modifier[HtmlElement]* - )(using router: Router[Page]): HtmlElement = - val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) - div( - cls := "h-full flex flex-col", - NavigationBar($m.map(_.navigation)), - child.maybe <-- router.$currentPage.map(_.isRoot) - .switch(None, Some(PageHeader(actionBus))), - main( - cls := "flex-grow-1 overflow-y-auto", - mods, - child <-- $maybeContent.map(_.getOrElse(Loading)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala deleted file mode 100644 index 62d820f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.LinkSupport.* -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo - -object PageLink: - type ViewModel = Page - def apply($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = - a(mods($m, actions), child.text <-- $m.map(_.title)) - - def apply(m: ViewModel, actions: Observer[Action])(using - router: Router[Page] - ): Anchor = apply(Val(m), actions) - - def container($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = a(mods($m, actions)) - - def container(m: ViewModel, actions: Observer[Action])(using - Router[Page] - ): Anchor = container(Val(m), actions) - - private def mods($m: Signal[Page], actions: Observer[Action])(using - router: Router[Page] - ): Modifier[Anchor] = - Seq( - href <-- $m.map(router.absoluteUrlForPage).recover { case _ => - Some("invalid url") - }, - composeEvents(onClick.noKeyMod.preventDefault)( - _.sample($m) - .map(NavigateTo.apply) - ) --> actions - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala deleted file mode 100644 index 87f825a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.pages.dashboard.DashboardPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -class DashboardPageConnector(actionBus: Observer[Action])(using - router: Router[Page] -): - def apply: HtmlElement = - AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala deleted file mode 100644 index e9b0f71..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala +++ /dev/null @@ -1,76 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailKriteriaPage -import pages.detail.components.DukazyKriteria -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -object DetailKriteriaPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailKriteriaPageConnector( - state: DetailKriteriaPageConnector.AppState -)( - $page: Signal[Page.DetailKriteria] -)(using Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => - (p.osobniCislo.value, p.parametr.value, p.kriterium.value) - )((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map( - FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) - ) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, s, $s) => - DetailKriteriaPage($s)(state.actionBus.contramap { - case DukazyKriteria.Add => - NavigateTo( - Page.UpravDukazKriteria( - Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), - Page.Titled(s.parametr.id, Some(s.parametr.nazev)), - Page.Titled(s.kriterium.id, Some(s.kriterium.id)) - ) - ) - }) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): DetailKriteriaPage.ViewModel = - DetailKriteriaPage.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala deleted file mode 100644 index 4d8024f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ /dev/null @@ -1,57 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import pages.detail.DetailPage -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import fiftyforms.ui.components.tailwind.Color - -object DetailPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailPageConnector(state: DetailPageConnector.AppState)( - $page: Signal[Page.Detail] -)(using router: Router[Page]): - val $oscChangeSignal = $page.splitOne(_.osobniCislo.value)((osc, _, _) => osc) - val $pageChangeSignal = - $oscChangeSignal.flatMap(osc => - EventStream.fromSeq(Seq(FetchUserDetails(osc), FetchParameters(osc))) - ) - // TODO: filter the value based on the current osc - // OSC change will fetch new data, but still - // - we need to be sure that what we got is really what we ought to display - // - we want to display stale data accordingly (at least with loading indicator) - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - def apply: HtmlElement = - AppPage(state.actionBus)( - $data.combineWithFn($params)(_ zip _) - .map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: List[Parameter] - ): DetailPage.ViewModel = - DetailPage.ViewModel( - o.toDetailOsoby, - p.map( - _.toParametr(param => - PageLink.container(Page.DetailParametru(o, param), state.actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala deleted file mode 100644 index fdaa7f8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailParametruPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink - -object DetailParametruPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailParametruPageConnector( - state: DetailParametruPageConnector.AppState -)( - $page: Signal[Page.DetailParametru] -)(using router: Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => (p.osobniCislo.value, p.parametr.value))((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map(FetchParameter(_, _)) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - } yield (da, pb) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailParametruPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter - ): DetailParametruPage.ViewModel = - DetailParametruPage.ViewModel( - o.toDetailOsoby, - p.toParametr(p => - PageLink.container(Page.DetailParametru(o, p), state.actionBus) - ), - p.criteria.map( - _.toKriterium { c => - PageLink.container( - Page.DetailKriteria(o, p, c), - state.actionBus - ) - } - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala deleted file mode 100644 index a3b00af..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala +++ /dev/null @@ -1,103 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import scala.scalajs.js.annotation.JSExportTopLevel -import scala.scalajs.js.annotation.JSExport -import scala.scalajs.js.annotation.JSImport -import scala.scalajs.js -import org.scalajs.dom -import com.raquo.laminar.api.L.{*, given} -import scala.scalajs.js.Date -import com.raquo.waypoint.Router -import com.raquo.waypoint.SplitRender -import cz.e_bs.cmi.mdr.pdb.app.services.DataFetcher -import scala.scalajs.js.JSON -import zio.json._ -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.state.AppState - -@js.native -@JSImport("stylesheets/main.css", JSImport.Namespace) -object Css extends js.Any - -@js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native -@JSImport("params/pdb-params.json", JSImport.Default) -object pdbParams extends js.Object - -@JSExportTopLevel("app") -object Main: - - @JSExport - def main(args: Array[String]): Unit = - import Routes.given - onLoad { - setupAirstream() - val appContainer = dom.document.querySelector("#app") - val _ = - render( - appContainer, - renderPage(state.MockAppState(using unsafeWindowOwner, router)) - ) - } - - private def onLoad(f: => Unit): Unit = - documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) - - private def setupAirstream()(using router: Router[Page]): Unit = - AirstreamError.registerUnhandledErrorCallback(err => - router.forcePage( - Page.UnhandledError( - Some(err.getClass.getName), // TODO: Fill only in dev mode - Some(err.getMessage) - ) - ) - ) - - def renderPage(state: AppState)(using - router: Router[Page] - ): HtmlElement = - val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) - .collectSignal[Page.Detail]( - connectors - .DetailPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailParametru]( - connectors - .DetailParametruPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailKriteria]( - connectors - .DetailKriteriaPageConnector(state)(_) - .apply - ) - .collectSignal[Page.UpravDukazKriteria]( - pages.detail.UpravDukaz.Connector(state)(_).apply - ) - .collectStatic(Page.Dashboard)( - connectors.DashboardPageConnector(state.actionBus).apply - ) - .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) - ) - .collect[Page.UnhandledError](pg => - pages.errors - .UnhandledErrorPage( - pages.errors.UnhandledErrorPage - .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), - state.actionBus - ) - ) - .collectStatic(Page.Directory)( - connectors - .DirectoryPageConnector(state.users, state.actionBus) - .apply - ) - div(cls := "h-full", child <-- pageSplitter.$view) - - // Pull in the stylesheet - val css: Css.type = Css diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala deleted file mode 100644 index 3bbc8c0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ /dev/null @@ -1,198 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.* -import org.scalajs.dom -import zio.json.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import scala.scalajs.js -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.Page.Titled - -// enum is not working with Waypoints' SplitRender collectStatic -sealed abstract class Page( - val id: String, - val title: String, - val parent: Option[Page] -) { - val path: Vector[Page] = - parent match - case None => Vector(this) - case Some(p) => p.path :+ this - - val isRoot: Boolean = parent.isEmpty -} - -object Page: - - case class Titled[V](value: V, title: Option[String] = None): - val show: String = title.getOrElse(value.toString) - - case object Directory extends Page("directory", "Adresář", None) - - case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) - - case class Detail(osobniCislo: Titled[OsobniCislo]) - extends Page("user", osobniCislo.show, Some(Directory)) - - object Detail { - def apply(o: UserInfo): Detail = Detail( - Titled(o.personalNumber, Some(o.name)) - ) - } - - case class DetailParametru( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String] - ) extends Page( - "parameter", - parametr.show, - Some(Detail(osobniCislo)) - ) - - object DetailParametru { - def apply(o: UserInfo, p: Parameter): DetailParametru = - DetailParametru( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)) - ) - } - - case class DetailKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "criteria", - kriterium.show, - Some(DetailParametru(osobniCislo, parametr)) - ) - - object DetailKriteria { - def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = - DetailKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class UpravDukazKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "addProof", - "Důkaz", - Some(DetailKriteria(osobniCislo, parametr, kriterium)) - ) - - object UpravDukazKriteria { - def apply( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): UpravDukazKriteria = - UpravDukazKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class NotFound(url: String) extends Page("404", "404", Some(Directory)) - - case class UnhandledError( - errorName: Option[String], - errorMessage: Option[String] - ) extends Page("500", "Unexpected error", Some(Directory)) - -object Routes: - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) - given [V: JsonEncoder]: JsonEncoder[Titled[V]] = - DeriveJsonEncoder.gen[Titled[V]] - given [V: JsonDecoder]: JsonDecoder[Titled[V]] = - DeriveJsonDecoder.gen[Titled[V]] - given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] - given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] - - val base = - js.`import`.meta.env.BASE_URL - .asInstanceOf[String] + "app" - - val homePage: Page = Page.Directory - - given router: Router[Page] = Router[Page]( - routes = List( - Route.static(homePage, root / endOfSegments, basePath = base), - Route.static( - Page.Dashboard, - root / "dashboard" / endOfSegments, - basePath = base - ), - Route[Page.Detail, String]( - encode = _.osobniCislo.value.toString, - decode = osc => Page.Detail(Titled(OsobniCislo(osc))), - root / "osoba" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.DetailParametru, (String, String)]( - encode = p => (p.osobniCislo.value.toString, p.parametr.value), - decode = - p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / endOfSegments, - basePath = base - ), - Route[Page.DetailKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.DetailKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.UpravDukazKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.UpravDukazKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / "edit" / endOfSegments, - basePath = base - ) - ), - serializePage = _.toJson, - deserializePage = _.fromJson[Page] - .fold(s => throw IllegalStateException(s), identity), - getPageTitle = _.title, - routeFallback = url => Page.NotFound(url), - deserializeFallback = _ => Page.Dashboard - )( - $popStateEvent = windowEvents.onPopState, - owner = unsafeWindowOwner - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala deleted file mode 100644 index b24b82b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -sealed trait Action - -case object FetchDirectory extends Action -case class FetchUserDetails(osc: OsobniCislo) extends Action -case class FetchParameters(osc: OsobniCislo) extends Action -case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action -case class FetchParameterCriteria( - osc: OsobniCislo, - paramId: String, - critId: String, - page: (UserInfo, Parameter, ParameterCriteria) => Page -) extends Action -case class FetchAvailableFiles(osc: OsobniCislo) extends Action -case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala deleted file mode 100644 index c637d35..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala +++ /dev/null @@ -1,82 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.{UserProfile, UserInfo, OsobniCislo} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo -import cz.e_bs.cmi.mdr.pdb.UserFunction - -object AppPage: - // TODO: pages by logged in user - val pages: List[Page] = List(Page.Directory, Page.Dashboard) - - import NavigationBar.{Logo, MenuItem} - - val logo = Logo( - "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", - "Workflow" - ) - - // TODO: menu items by user profile - val userMenu = - List( - MenuItem("Your Profile"), - MenuItem("Settings"), - MenuItem("Sign out") - ) - - // TODO: load user profile - val $userProfile = Var( - UserProfile( - "tom", - UserInfo( - OsobniCislo("1031"), - "tom", - "Tom", - "Cook", - None, - None, - Some("tom@example.com"), - Some("+420 222 866 180"), - Some( - UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") - ), - Nil, - None - ) - ) - ) - - val $userInfo = $userProfile.signal.map(_.userInfo) - - type ViewModel = Option[HtmlElement] - def apply( - actionBus: Observer[Action] - )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using - router: Router[Page] - ): HtmlElement = - PageLayout(actionBus)( - $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => - PageLayout.ViewModel( - NavigationBar.ViewModel( - u, - pages.map(p => - NavigationBar.Link( - () => - PageLink( - p, - actionBus - ), - p == cp - ) - ), - userMenu, - logo - ), - c - ) - ), - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala deleted file mode 100644 index bf473c6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ /dev/null @@ -1,143 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden -import fiftyforms.ui.components.tailwind.list.IconText.ViewModel -import fiftyforms.ui.components.tailwind.Icons -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Action - -object Breadcrumbs: - - private def slash = - import svg.{*, given} - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-300", - xmlns := "http://www.w3.org/2000/svg", - fill := "currentColor", - viewBox := "0 0 20 20", - ariaHidden := true, - path( - d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" - ) - ) - - def backIcon = - Icons.solid - .`arrow-narrow-left`() - .amend( - svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" - ) - - object Link: - case class ViewModel( - page: Page, - icon: Option[SvgElement], - extraClasses: String, - text: String, - textClasses: Option[String] = None - ) - - val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" - - def shortHome(p: Page) = ViewModel( - p, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - "Zpět na úvodní stránku", - None - ) - - def fullHome(p: Page) = ViewModel( - p, - Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), - "text-gray-400 hover:text-gray-500", - "Domů", - Some("sr-only") - ) - - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - PageLink - .container($m.map(_.page), actionBus) - .amend( - cls <-- $m.map(_.extraClasses), - child.maybe <-- $m.map(_.icon), - span( - cls <-- $m.map(_.textClasses.getOrElse("")), - child.text <-- $m.map(_.text) - ) - ) - - object FullBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - ol( - role := "list", - cls := "flex items-center space-x-4", - children <-- $m.map(_.path).split(_.id)((_, _, $p) => - li( - div( - cls := "flex items-center", - child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), - Link( - $p.map(p => - if (p.isRoot) then Link.fullHome(p) - else - Link.ViewModel( - p, - None, - s"ml-4 max-w-xs truncate ${Link.baseClasses}", - p.title - ) - ), - actionBus - ) - ) - ) - ) - ) - - object ShortBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - Link( - $m.map { p => - val target = p.parent.getOrElse(p) - if target.isRoot then Link.shortHome(target) - else - Link.ViewModel( - target, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - s"Zpět na ${target.title}" - ) - }, - actionBus - ) - - def apply(actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - nav( - cls := "flex", - aria.label := "Breadcrumb", - div( - cls := "flex sm:hidden", - ShortBreadcrumbs(router.$currentPage, actionBus) - ), - div( - cls := "hidden sm:block", - FullBreadcrumbs(router.$currentPage, actionBus) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala deleted file mode 100644 index 0db2047..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ /dev/null @@ -1,242 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent -import fiftyforms.ui.components.tailwind.Icons -import fiftyforms.ui.components.tailwind.Avatar -import cz.e_bs.cmi.mdr.pdb.UserInfo -import io.laminext.syntax.core.* - -object NavigationBar: - - case class Logo(img: String, name: String) - case class Link(a: () => Anchor, active: Boolean) - case class MenuItem(title: String) - - case class ViewModel( - userInfo: UserInfo, - pages: List[Link], - userMenu: List[MenuItem], - logo: Logo - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - val $userInfo = $m.map(_.userInfo) - val mobileMenuOpen = Var(false) - - val desktopOnly = cls("hidden md:block") - val mobileOnly = cls("md:hidden") - - inline def avatarImage(size: Int = 8) = - Avatar($userInfo.map(_.img)).avatarImage(size) - - def notificationButton = button( - tpe := "button", - cls := List( - "bg-indigo-600", - "focus:outline-none", - "focus:ring-2", - "focus:ring-offset-2", - "focus:ring-offset-indigo-600", - "focus:ring-white", - "hover:text-white", - "p-1", - "rounded-full", - "text-indigo-200" - ), - span(cls := "sr-only", "View notifications"), - Icons.outline.bell() - ) - - def userProfile: HtmlElement = - val menuOpen = Var(false) - - def menuItem(item: MenuItem, idx: Int): HtmlElement = - a( - href := "#", - cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", - role := "menuitem", - tabIndex := -1, - idAttr := s"user-menu-item-$idx", - item.title - ) - - div( - cls := "ml-3 relative", - div( - button( - tpe := "button", - cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - idAttr := "user-menu-button", - aria.expanded <-- menuOpen.signal, - aria.hasPopup := true, - span(cls := "sr-only", "Open user menu"), - child <-- avatarImage(), - onClick.preventDefault.mapTo( - !menuOpen.now() - ) --> menuOpen.writer - ) - ), - /* - * */ - div( - cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", - cls <-- menuOpen.signal.map { o => - if (o) "md:block" else "md:hidden" - }, - role := "menu", - aria.orientation := "vertical", - aria.labelledBy := "user-menu-button", - tabIndex := -1, - // : keyboard navigation - children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) - ) - ) - - def mobileProfile = - def menuItem(item: MenuItem): HtmlElement = - a( - href := "#", - cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", - item.title - ) - - div( - cls := "pt-4 pb-3 border-t border-indigo-700", - div( - cls := "flex items-center px-5", - div( - cls := "flex-shrink-0", - child <-- avatarImage(10) - ), - div( - cls := "ml-3", - div( - cls := "text-base font-medium text-white", - child.text <-- $userInfo.map(_.name) - ), - child.maybe <-- $userInfo.map( - _.email.map(e => - div( - cls := "text-sm font-medium text-indigo-300", - e - ) - ) - ) - ), - notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) - ), - div( - cls := "mt-3 px-2 space-y-1", - children <-- $m.map(_.userMenu.map(menuItem)) - ) - ) - - def pageLink(page: Link): Anchor = - page - .a() - .amend( - cls := "text-white px-3 py-2 rounded-md text-sm font-medium", - cls := Seq( - "bg-indigo-700" -> page.active, - "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active - ), - ariaCurrent := (if page.active then "page" else "false") - ) - - def logoImg: Image = - img( - cls := "h-8 w-8", - src <-- $m.map(_.logo.img), - alt <-- $m.map(_.logo.name) - ) - - def mobileMenuButton = button( - tpe := "button", - cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - aria.controls := "mobile-menu", - aria.expanded <-- mobileMenuOpen.signal, - span(cls := "sr-only", "Open main menu"), - Icons.outline - .menu() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") - ), - Icons.outline - .x() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") - ), - onClick.preventDefault.mapTo( - !mobileMenuOpen.now() - ) --> mobileMenuOpen.writer - ) - - def navBarLeft = - div( - cls := "flex items-center", - div(cls := "flex-shrink-0", logoImg), - div( - desktopOnly, - div( - cls := "ml-10 flex items-baseline space-x-4", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ) - ) - ) - - def navBarRight = - div( - desktopOnly, - div( - cls := "ml-4 flex items-center md:ml-6", - notificationButton, - userProfile - ) - ) - - def navBarMobile = - div( - cls := "-mr-2 flex", - mobileOnly, - mobileMenuButton - ) - - def navBar = - div( - cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex items-center justify-between h-16", - navBarLeft, - navBarRight, - navBarMobile - ) - ) - - def mobileMenu = - div( - mobileOnly, - cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }, - idAttr := "mobile-menu", - div( - cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ), - mobileProfile - ) - - nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala deleted file mode 100644 index 75339df..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object PageHeader: - def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = - header( - cls := "bg-white shadow-sm", - div( - cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", - h1( - cls := "text-lg leading-6 font-semibold text-gray-900", - Breadcrumbs(actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala deleted file mode 100644 index 5096463..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ /dev/null @@ -1,30 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import fiftyforms.ui.components.tailwind.Loading -import io.laminext.syntax.core.* - -object PageLayout: - case class ViewModel( - navigation: NavigationBar.ViewModel, - content: Option[HtmlElement] - ) - def apply(actionBus: Observer[Action])( - $m: Signal[ViewModel], - mods: Modifier[HtmlElement]* - )(using router: Router[Page]): HtmlElement = - val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) - div( - cls := "h-full flex flex-col", - NavigationBar($m.map(_.navigation)), - child.maybe <-- router.$currentPage.map(_.isRoot) - .switch(None, Some(PageHeader(actionBus))), - main( - cls := "flex-grow-1 overflow-y-auto", - mods, - child <-- $maybeContent.map(_.getOrElse(Loading)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala deleted file mode 100644 index 62d820f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.LinkSupport.* -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo - -object PageLink: - type ViewModel = Page - def apply($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = - a(mods($m, actions), child.text <-- $m.map(_.title)) - - def apply(m: ViewModel, actions: Observer[Action])(using - router: Router[Page] - ): Anchor = apply(Val(m), actions) - - def container($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = a(mods($m, actions)) - - def container(m: ViewModel, actions: Observer[Action])(using - Router[Page] - ): Anchor = container(Val(m), actions) - - private def mods($m: Signal[Page], actions: Observer[Action])(using - router: Router[Page] - ): Modifier[Anchor] = - Seq( - href <-- $m.map(router.absoluteUrlForPage).recover { case _ => - Some("invalid url") - }, - composeEvents(onClick.noKeyMod.preventDefault)( - _.sample($m) - .map(NavigateTo.apply) - ) --> actions - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala deleted file mode 100644 index 87f825a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.pages.dashboard.DashboardPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -class DashboardPageConnector(actionBus: Observer[Action])(using - router: Router[Page] -): - def apply: HtmlElement = - AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala deleted file mode 100644 index e9b0f71..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala +++ /dev/null @@ -1,76 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailKriteriaPage -import pages.detail.components.DukazyKriteria -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -object DetailKriteriaPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailKriteriaPageConnector( - state: DetailKriteriaPageConnector.AppState -)( - $page: Signal[Page.DetailKriteria] -)(using Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => - (p.osobniCislo.value, p.parametr.value, p.kriterium.value) - )((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map( - FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) - ) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, s, $s) => - DetailKriteriaPage($s)(state.actionBus.contramap { - case DukazyKriteria.Add => - NavigateTo( - Page.UpravDukazKriteria( - Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), - Page.Titled(s.parametr.id, Some(s.parametr.nazev)), - Page.Titled(s.kriterium.id, Some(s.kriterium.id)) - ) - ) - }) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): DetailKriteriaPage.ViewModel = - DetailKriteriaPage.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala deleted file mode 100644 index 4d8024f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ /dev/null @@ -1,57 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import pages.detail.DetailPage -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import fiftyforms.ui.components.tailwind.Color - -object DetailPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailPageConnector(state: DetailPageConnector.AppState)( - $page: Signal[Page.Detail] -)(using router: Router[Page]): - val $oscChangeSignal = $page.splitOne(_.osobniCislo.value)((osc, _, _) => osc) - val $pageChangeSignal = - $oscChangeSignal.flatMap(osc => - EventStream.fromSeq(Seq(FetchUserDetails(osc), FetchParameters(osc))) - ) - // TODO: filter the value based on the current osc - // OSC change will fetch new data, but still - // - we need to be sure that what we got is really what we ought to display - // - we want to display stale data accordingly (at least with loading indicator) - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - def apply: HtmlElement = - AppPage(state.actionBus)( - $data.combineWithFn($params)(_ zip _) - .map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: List[Parameter] - ): DetailPage.ViewModel = - DetailPage.ViewModel( - o.toDetailOsoby, - p.map( - _.toParametr(param => - PageLink.container(Page.DetailParametru(o, param), state.actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala deleted file mode 100644 index fdaa7f8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailParametruPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink - -object DetailParametruPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailParametruPageConnector( - state: DetailParametruPageConnector.AppState -)( - $page: Signal[Page.DetailParametru] -)(using router: Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => (p.osobniCislo.value, p.parametr.value))((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map(FetchParameter(_, _)) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - } yield (da, pb) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailParametruPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter - ): DetailParametruPage.ViewModel = - DetailParametruPage.ViewModel( - o.toDetailOsoby, - p.toParametr(p => - PageLink.container(Page.DetailParametru(o, p), state.actionBus) - ), - p.criteria.map( - _.toKriterium { c => - PageLink.container( - Page.DetailKriteria(o, p, c), - state.actionBus - ) - } - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala deleted file mode 100644 index 049aa22..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -case class DirectoryPageConnector( - $input: EventStream[List[UserInfo]], - actionBus: Observer[Action] -)(using router: Router[Page]): - val $data = $input.startWithNone - val $actionSignal = EventStream.fromValue(FetchDirectory) - - def apply: HtmlElement = - AppPage(actionBus)( - $data.split(_ => ())((_, _, s) => - pages.directory.DirectoryPage( - s.map( - _.map( - _.toUserRow(u => - PageLink.container( - Page.Detail(Page.Titled(u.personalNumber)), - actionBus - ) - ) - ) - ) - ) - ), - $actionSignal --> actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala deleted file mode 100644 index a3b00af..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala +++ /dev/null @@ -1,103 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import scala.scalajs.js.annotation.JSExportTopLevel -import scala.scalajs.js.annotation.JSExport -import scala.scalajs.js.annotation.JSImport -import scala.scalajs.js -import org.scalajs.dom -import com.raquo.laminar.api.L.{*, given} -import scala.scalajs.js.Date -import com.raquo.waypoint.Router -import com.raquo.waypoint.SplitRender -import cz.e_bs.cmi.mdr.pdb.app.services.DataFetcher -import scala.scalajs.js.JSON -import zio.json._ -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.state.AppState - -@js.native -@JSImport("stylesheets/main.css", JSImport.Namespace) -object Css extends js.Any - -@js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native -@JSImport("params/pdb-params.json", JSImport.Default) -object pdbParams extends js.Object - -@JSExportTopLevel("app") -object Main: - - @JSExport - def main(args: Array[String]): Unit = - import Routes.given - onLoad { - setupAirstream() - val appContainer = dom.document.querySelector("#app") - val _ = - render( - appContainer, - renderPage(state.MockAppState(using unsafeWindowOwner, router)) - ) - } - - private def onLoad(f: => Unit): Unit = - documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) - - private def setupAirstream()(using router: Router[Page]): Unit = - AirstreamError.registerUnhandledErrorCallback(err => - router.forcePage( - Page.UnhandledError( - Some(err.getClass.getName), // TODO: Fill only in dev mode - Some(err.getMessage) - ) - ) - ) - - def renderPage(state: AppState)(using - router: Router[Page] - ): HtmlElement = - val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) - .collectSignal[Page.Detail]( - connectors - .DetailPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailParametru]( - connectors - .DetailParametruPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailKriteria]( - connectors - .DetailKriteriaPageConnector(state)(_) - .apply - ) - .collectSignal[Page.UpravDukazKriteria]( - pages.detail.UpravDukaz.Connector(state)(_).apply - ) - .collectStatic(Page.Dashboard)( - connectors.DashboardPageConnector(state.actionBus).apply - ) - .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) - ) - .collect[Page.UnhandledError](pg => - pages.errors - .UnhandledErrorPage( - pages.errors.UnhandledErrorPage - .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), - state.actionBus - ) - ) - .collectStatic(Page.Directory)( - connectors - .DirectoryPageConnector(state.users, state.actionBus) - .apply - ) - div(cls := "h-full", child <-- pageSplitter.$view) - - // Pull in the stylesheet - val css: Css.type = Css diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala deleted file mode 100644 index 3bbc8c0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ /dev/null @@ -1,198 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.* -import org.scalajs.dom -import zio.json.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import scala.scalajs.js -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.Page.Titled - -// enum is not working with Waypoints' SplitRender collectStatic -sealed abstract class Page( - val id: String, - val title: String, - val parent: Option[Page] -) { - val path: Vector[Page] = - parent match - case None => Vector(this) - case Some(p) => p.path :+ this - - val isRoot: Boolean = parent.isEmpty -} - -object Page: - - case class Titled[V](value: V, title: Option[String] = None): - val show: String = title.getOrElse(value.toString) - - case object Directory extends Page("directory", "Adresář", None) - - case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) - - case class Detail(osobniCislo: Titled[OsobniCislo]) - extends Page("user", osobniCislo.show, Some(Directory)) - - object Detail { - def apply(o: UserInfo): Detail = Detail( - Titled(o.personalNumber, Some(o.name)) - ) - } - - case class DetailParametru( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String] - ) extends Page( - "parameter", - parametr.show, - Some(Detail(osobniCislo)) - ) - - object DetailParametru { - def apply(o: UserInfo, p: Parameter): DetailParametru = - DetailParametru( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)) - ) - } - - case class DetailKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "criteria", - kriterium.show, - Some(DetailParametru(osobniCislo, parametr)) - ) - - object DetailKriteria { - def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = - DetailKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class UpravDukazKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "addProof", - "Důkaz", - Some(DetailKriteria(osobniCislo, parametr, kriterium)) - ) - - object UpravDukazKriteria { - def apply( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): UpravDukazKriteria = - UpravDukazKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class NotFound(url: String) extends Page("404", "404", Some(Directory)) - - case class UnhandledError( - errorName: Option[String], - errorMessage: Option[String] - ) extends Page("500", "Unexpected error", Some(Directory)) - -object Routes: - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) - given [V: JsonEncoder]: JsonEncoder[Titled[V]] = - DeriveJsonEncoder.gen[Titled[V]] - given [V: JsonDecoder]: JsonDecoder[Titled[V]] = - DeriveJsonDecoder.gen[Titled[V]] - given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] - given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] - - val base = - js.`import`.meta.env.BASE_URL - .asInstanceOf[String] + "app" - - val homePage: Page = Page.Directory - - given router: Router[Page] = Router[Page]( - routes = List( - Route.static(homePage, root / endOfSegments, basePath = base), - Route.static( - Page.Dashboard, - root / "dashboard" / endOfSegments, - basePath = base - ), - Route[Page.Detail, String]( - encode = _.osobniCislo.value.toString, - decode = osc => Page.Detail(Titled(OsobniCislo(osc))), - root / "osoba" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.DetailParametru, (String, String)]( - encode = p => (p.osobniCislo.value.toString, p.parametr.value), - decode = - p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / endOfSegments, - basePath = base - ), - Route[Page.DetailKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.DetailKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.UpravDukazKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.UpravDukazKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / "edit" / endOfSegments, - basePath = base - ) - ), - serializePage = _.toJson, - deserializePage = _.fromJson[Page] - .fold(s => throw IllegalStateException(s), identity), - getPageTitle = _.title, - routeFallback = url => Page.NotFound(url), - deserializeFallback = _ => Page.Dashboard - )( - $popStateEvent = windowEvents.onPopState, - owner = unsafeWindowOwner - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala deleted file mode 100644 index b24b82b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -sealed trait Action - -case object FetchDirectory extends Action -case class FetchUserDetails(osc: OsobniCislo) extends Action -case class FetchParameters(osc: OsobniCislo) extends Action -case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action -case class FetchParameterCriteria( - osc: OsobniCislo, - paramId: String, - critId: String, - page: (UserInfo, Parameter, ParameterCriteria) => Page -) extends Action -case class FetchAvailableFiles(osc: OsobniCislo) extends Action -case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala deleted file mode 100644 index c637d35..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala +++ /dev/null @@ -1,82 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.{UserProfile, UserInfo, OsobniCislo} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo -import cz.e_bs.cmi.mdr.pdb.UserFunction - -object AppPage: - // TODO: pages by logged in user - val pages: List[Page] = List(Page.Directory, Page.Dashboard) - - import NavigationBar.{Logo, MenuItem} - - val logo = Logo( - "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", - "Workflow" - ) - - // TODO: menu items by user profile - val userMenu = - List( - MenuItem("Your Profile"), - MenuItem("Settings"), - MenuItem("Sign out") - ) - - // TODO: load user profile - val $userProfile = Var( - UserProfile( - "tom", - UserInfo( - OsobniCislo("1031"), - "tom", - "Tom", - "Cook", - None, - None, - Some("tom@example.com"), - Some("+420 222 866 180"), - Some( - UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") - ), - Nil, - None - ) - ) - ) - - val $userInfo = $userProfile.signal.map(_.userInfo) - - type ViewModel = Option[HtmlElement] - def apply( - actionBus: Observer[Action] - )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using - router: Router[Page] - ): HtmlElement = - PageLayout(actionBus)( - $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => - PageLayout.ViewModel( - NavigationBar.ViewModel( - u, - pages.map(p => - NavigationBar.Link( - () => - PageLink( - p, - actionBus - ), - p == cp - ) - ), - userMenu, - logo - ), - c - ) - ), - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala deleted file mode 100644 index bf473c6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ /dev/null @@ -1,143 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden -import fiftyforms.ui.components.tailwind.list.IconText.ViewModel -import fiftyforms.ui.components.tailwind.Icons -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Action - -object Breadcrumbs: - - private def slash = - import svg.{*, given} - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-300", - xmlns := "http://www.w3.org/2000/svg", - fill := "currentColor", - viewBox := "0 0 20 20", - ariaHidden := true, - path( - d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" - ) - ) - - def backIcon = - Icons.solid - .`arrow-narrow-left`() - .amend( - svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" - ) - - object Link: - case class ViewModel( - page: Page, - icon: Option[SvgElement], - extraClasses: String, - text: String, - textClasses: Option[String] = None - ) - - val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" - - def shortHome(p: Page) = ViewModel( - p, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - "Zpět na úvodní stránku", - None - ) - - def fullHome(p: Page) = ViewModel( - p, - Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), - "text-gray-400 hover:text-gray-500", - "Domů", - Some("sr-only") - ) - - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - PageLink - .container($m.map(_.page), actionBus) - .amend( - cls <-- $m.map(_.extraClasses), - child.maybe <-- $m.map(_.icon), - span( - cls <-- $m.map(_.textClasses.getOrElse("")), - child.text <-- $m.map(_.text) - ) - ) - - object FullBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - ol( - role := "list", - cls := "flex items-center space-x-4", - children <-- $m.map(_.path).split(_.id)((_, _, $p) => - li( - div( - cls := "flex items-center", - child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), - Link( - $p.map(p => - if (p.isRoot) then Link.fullHome(p) - else - Link.ViewModel( - p, - None, - s"ml-4 max-w-xs truncate ${Link.baseClasses}", - p.title - ) - ), - actionBus - ) - ) - ) - ) - ) - - object ShortBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - Link( - $m.map { p => - val target = p.parent.getOrElse(p) - if target.isRoot then Link.shortHome(target) - else - Link.ViewModel( - target, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - s"Zpět na ${target.title}" - ) - }, - actionBus - ) - - def apply(actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - nav( - cls := "flex", - aria.label := "Breadcrumb", - div( - cls := "flex sm:hidden", - ShortBreadcrumbs(router.$currentPage, actionBus) - ), - div( - cls := "hidden sm:block", - FullBreadcrumbs(router.$currentPage, actionBus) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala deleted file mode 100644 index 0db2047..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ /dev/null @@ -1,242 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent -import fiftyforms.ui.components.tailwind.Icons -import fiftyforms.ui.components.tailwind.Avatar -import cz.e_bs.cmi.mdr.pdb.UserInfo -import io.laminext.syntax.core.* - -object NavigationBar: - - case class Logo(img: String, name: String) - case class Link(a: () => Anchor, active: Boolean) - case class MenuItem(title: String) - - case class ViewModel( - userInfo: UserInfo, - pages: List[Link], - userMenu: List[MenuItem], - logo: Logo - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - val $userInfo = $m.map(_.userInfo) - val mobileMenuOpen = Var(false) - - val desktopOnly = cls("hidden md:block") - val mobileOnly = cls("md:hidden") - - inline def avatarImage(size: Int = 8) = - Avatar($userInfo.map(_.img)).avatarImage(size) - - def notificationButton = button( - tpe := "button", - cls := List( - "bg-indigo-600", - "focus:outline-none", - "focus:ring-2", - "focus:ring-offset-2", - "focus:ring-offset-indigo-600", - "focus:ring-white", - "hover:text-white", - "p-1", - "rounded-full", - "text-indigo-200" - ), - span(cls := "sr-only", "View notifications"), - Icons.outline.bell() - ) - - def userProfile: HtmlElement = - val menuOpen = Var(false) - - def menuItem(item: MenuItem, idx: Int): HtmlElement = - a( - href := "#", - cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", - role := "menuitem", - tabIndex := -1, - idAttr := s"user-menu-item-$idx", - item.title - ) - - div( - cls := "ml-3 relative", - div( - button( - tpe := "button", - cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - idAttr := "user-menu-button", - aria.expanded <-- menuOpen.signal, - aria.hasPopup := true, - span(cls := "sr-only", "Open user menu"), - child <-- avatarImage(), - onClick.preventDefault.mapTo( - !menuOpen.now() - ) --> menuOpen.writer - ) - ), - /* - * */ - div( - cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", - cls <-- menuOpen.signal.map { o => - if (o) "md:block" else "md:hidden" - }, - role := "menu", - aria.orientation := "vertical", - aria.labelledBy := "user-menu-button", - tabIndex := -1, - // : keyboard navigation - children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) - ) - ) - - def mobileProfile = - def menuItem(item: MenuItem): HtmlElement = - a( - href := "#", - cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", - item.title - ) - - div( - cls := "pt-4 pb-3 border-t border-indigo-700", - div( - cls := "flex items-center px-5", - div( - cls := "flex-shrink-0", - child <-- avatarImage(10) - ), - div( - cls := "ml-3", - div( - cls := "text-base font-medium text-white", - child.text <-- $userInfo.map(_.name) - ), - child.maybe <-- $userInfo.map( - _.email.map(e => - div( - cls := "text-sm font-medium text-indigo-300", - e - ) - ) - ) - ), - notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) - ), - div( - cls := "mt-3 px-2 space-y-1", - children <-- $m.map(_.userMenu.map(menuItem)) - ) - ) - - def pageLink(page: Link): Anchor = - page - .a() - .amend( - cls := "text-white px-3 py-2 rounded-md text-sm font-medium", - cls := Seq( - "bg-indigo-700" -> page.active, - "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active - ), - ariaCurrent := (if page.active then "page" else "false") - ) - - def logoImg: Image = - img( - cls := "h-8 w-8", - src <-- $m.map(_.logo.img), - alt <-- $m.map(_.logo.name) - ) - - def mobileMenuButton = button( - tpe := "button", - cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - aria.controls := "mobile-menu", - aria.expanded <-- mobileMenuOpen.signal, - span(cls := "sr-only", "Open main menu"), - Icons.outline - .menu() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") - ), - Icons.outline - .x() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") - ), - onClick.preventDefault.mapTo( - !mobileMenuOpen.now() - ) --> mobileMenuOpen.writer - ) - - def navBarLeft = - div( - cls := "flex items-center", - div(cls := "flex-shrink-0", logoImg), - div( - desktopOnly, - div( - cls := "ml-10 flex items-baseline space-x-4", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ) - ) - ) - - def navBarRight = - div( - desktopOnly, - div( - cls := "ml-4 flex items-center md:ml-6", - notificationButton, - userProfile - ) - ) - - def navBarMobile = - div( - cls := "-mr-2 flex", - mobileOnly, - mobileMenuButton - ) - - def navBar = - div( - cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex items-center justify-between h-16", - navBarLeft, - navBarRight, - navBarMobile - ) - ) - - def mobileMenu = - div( - mobileOnly, - cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }, - idAttr := "mobile-menu", - div( - cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ), - mobileProfile - ) - - nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala deleted file mode 100644 index 75339df..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object PageHeader: - def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = - header( - cls := "bg-white shadow-sm", - div( - cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", - h1( - cls := "text-lg leading-6 font-semibold text-gray-900", - Breadcrumbs(actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala deleted file mode 100644 index 5096463..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ /dev/null @@ -1,30 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import fiftyforms.ui.components.tailwind.Loading -import io.laminext.syntax.core.* - -object PageLayout: - case class ViewModel( - navigation: NavigationBar.ViewModel, - content: Option[HtmlElement] - ) - def apply(actionBus: Observer[Action])( - $m: Signal[ViewModel], - mods: Modifier[HtmlElement]* - )(using router: Router[Page]): HtmlElement = - val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) - div( - cls := "h-full flex flex-col", - NavigationBar($m.map(_.navigation)), - child.maybe <-- router.$currentPage.map(_.isRoot) - .switch(None, Some(PageHeader(actionBus))), - main( - cls := "flex-grow-1 overflow-y-auto", - mods, - child <-- $maybeContent.map(_.getOrElse(Loading)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala deleted file mode 100644 index 62d820f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.LinkSupport.* -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo - -object PageLink: - type ViewModel = Page - def apply($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = - a(mods($m, actions), child.text <-- $m.map(_.title)) - - def apply(m: ViewModel, actions: Observer[Action])(using - router: Router[Page] - ): Anchor = apply(Val(m), actions) - - def container($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = a(mods($m, actions)) - - def container(m: ViewModel, actions: Observer[Action])(using - Router[Page] - ): Anchor = container(Val(m), actions) - - private def mods($m: Signal[Page], actions: Observer[Action])(using - router: Router[Page] - ): Modifier[Anchor] = - Seq( - href <-- $m.map(router.absoluteUrlForPage).recover { case _ => - Some("invalid url") - }, - composeEvents(onClick.noKeyMod.preventDefault)( - _.sample($m) - .map(NavigateTo.apply) - ) --> actions - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala deleted file mode 100644 index 87f825a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.pages.dashboard.DashboardPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -class DashboardPageConnector(actionBus: Observer[Action])(using - router: Router[Page] -): - def apply: HtmlElement = - AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala deleted file mode 100644 index e9b0f71..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala +++ /dev/null @@ -1,76 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailKriteriaPage -import pages.detail.components.DukazyKriteria -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -object DetailKriteriaPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailKriteriaPageConnector( - state: DetailKriteriaPageConnector.AppState -)( - $page: Signal[Page.DetailKriteria] -)(using Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => - (p.osobniCislo.value, p.parametr.value, p.kriterium.value) - )((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map( - FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) - ) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, s, $s) => - DetailKriteriaPage($s)(state.actionBus.contramap { - case DukazyKriteria.Add => - NavigateTo( - Page.UpravDukazKriteria( - Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), - Page.Titled(s.parametr.id, Some(s.parametr.nazev)), - Page.Titled(s.kriterium.id, Some(s.kriterium.id)) - ) - ) - }) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): DetailKriteriaPage.ViewModel = - DetailKriteriaPage.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala deleted file mode 100644 index 4d8024f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ /dev/null @@ -1,57 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import pages.detail.DetailPage -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import fiftyforms.ui.components.tailwind.Color - -object DetailPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailPageConnector(state: DetailPageConnector.AppState)( - $page: Signal[Page.Detail] -)(using router: Router[Page]): - val $oscChangeSignal = $page.splitOne(_.osobniCislo.value)((osc, _, _) => osc) - val $pageChangeSignal = - $oscChangeSignal.flatMap(osc => - EventStream.fromSeq(Seq(FetchUserDetails(osc), FetchParameters(osc))) - ) - // TODO: filter the value based on the current osc - // OSC change will fetch new data, but still - // - we need to be sure that what we got is really what we ought to display - // - we want to display stale data accordingly (at least with loading indicator) - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - def apply: HtmlElement = - AppPage(state.actionBus)( - $data.combineWithFn($params)(_ zip _) - .map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: List[Parameter] - ): DetailPage.ViewModel = - DetailPage.ViewModel( - o.toDetailOsoby, - p.map( - _.toParametr(param => - PageLink.container(Page.DetailParametru(o, param), state.actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala deleted file mode 100644 index fdaa7f8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailParametruPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink - -object DetailParametruPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailParametruPageConnector( - state: DetailParametruPageConnector.AppState -)( - $page: Signal[Page.DetailParametru] -)(using router: Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => (p.osobniCislo.value, p.parametr.value))((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map(FetchParameter(_, _)) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - } yield (da, pb) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailParametruPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter - ): DetailParametruPage.ViewModel = - DetailParametruPage.ViewModel( - o.toDetailOsoby, - p.toParametr(p => - PageLink.container(Page.DetailParametru(o, p), state.actionBus) - ), - p.criteria.map( - _.toKriterium { c => - PageLink.container( - Page.DetailKriteria(o, p, c), - state.actionBus - ) - } - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala deleted file mode 100644 index 049aa22..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -case class DirectoryPageConnector( - $input: EventStream[List[UserInfo]], - actionBus: Observer[Action] -)(using router: Router[Page]): - val $data = $input.startWithNone - val $actionSignal = EventStream.fromValue(FetchDirectory) - - def apply: HtmlElement = - AppPage(actionBus)( - $data.split(_ => ())((_, _, s) => - pages.directory.DirectoryPage( - s.map( - _.map( - _.toUserRow(u => - PageLink.container( - Page.Detail(Page.Titled(u.personalNumber)), - actionBus - ) - ) - ) - ) - ) - ), - $actionSignal --> actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala deleted file mode 100644 index 88e7450..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ /dev/null @@ -1,68 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii -import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria -import fiftyforms.ui.components.tailwind.Color - -extension (o: UserInfo) - def toDetailOsoby: DetailOsoby.ViewModel = - DetailOsoby.ViewModel( - o.personalNumber, - o.name, - o.email, - o.phone, - o.img, - o.mainFunction.map(f => - DetailOsoby.Funkce.ViewModel(f.name, f.dept, f.ou) - ), - o.userContracts.headOption.map(c => - DetailOsoby.PracovniPomer.ViewModel(c.rel, c.startDate, c.endDate) - ) - ) - -extension (param: Parameter) - def toParametr(container: Parameter => Anchor): DetailParametru.ViewModel = - DetailParametru.ViewModel( - id = param.id, - nazev = param.name, - popis = param.description, - status = "Nesplněno", - statusColor = Color.red, - a = container(param) - ) - -extension (crit: ParameterCriteria) - def toKriterium( - container: ParameterCriteria => Anchor - ): DetailKriteria.ViewModel = - DetailKriteria.ViewModel( - nazev = crit.criteriumText, - kapitola = crit.chapterId, - bod = crit.itemId, - status = "Nesplněno", - statusColor = Color.red, - splneno = false, - dukazy = Nil, - container = container(crit) - ) - -extension (user: UserInfo) - def toUserRow( - container: UserInfo => HtmlElement = (_: UserInfo) => div() - ): UserRow.ViewModel = - UserRow.ViewModel( - osobniCislo = user.personalNumber.toString, - celeJmeno = user.name, - prijmeni = user.surname, - hlavniFunkce = user.mainFunction.map(_.name), - img = user.img, - container = () => container(user) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala deleted file mode 100644 index a3b00af..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala +++ /dev/null @@ -1,103 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import scala.scalajs.js.annotation.JSExportTopLevel -import scala.scalajs.js.annotation.JSExport -import scala.scalajs.js.annotation.JSImport -import scala.scalajs.js -import org.scalajs.dom -import com.raquo.laminar.api.L.{*, given} -import scala.scalajs.js.Date -import com.raquo.waypoint.Router -import com.raquo.waypoint.SplitRender -import cz.e_bs.cmi.mdr.pdb.app.services.DataFetcher -import scala.scalajs.js.JSON -import zio.json._ -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.state.AppState - -@js.native -@JSImport("stylesheets/main.css", JSImport.Namespace) -object Css extends js.Any - -@js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native -@JSImport("params/pdb-params.json", JSImport.Default) -object pdbParams extends js.Object - -@JSExportTopLevel("app") -object Main: - - @JSExport - def main(args: Array[String]): Unit = - import Routes.given - onLoad { - setupAirstream() - val appContainer = dom.document.querySelector("#app") - val _ = - render( - appContainer, - renderPage(state.MockAppState(using unsafeWindowOwner, router)) - ) - } - - private def onLoad(f: => Unit): Unit = - documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) - - private def setupAirstream()(using router: Router[Page]): Unit = - AirstreamError.registerUnhandledErrorCallback(err => - router.forcePage( - Page.UnhandledError( - Some(err.getClass.getName), // TODO: Fill only in dev mode - Some(err.getMessage) - ) - ) - ) - - def renderPage(state: AppState)(using - router: Router[Page] - ): HtmlElement = - val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) - .collectSignal[Page.Detail]( - connectors - .DetailPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailParametru]( - connectors - .DetailParametruPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailKriteria]( - connectors - .DetailKriteriaPageConnector(state)(_) - .apply - ) - .collectSignal[Page.UpravDukazKriteria]( - pages.detail.UpravDukaz.Connector(state)(_).apply - ) - .collectStatic(Page.Dashboard)( - connectors.DashboardPageConnector(state.actionBus).apply - ) - .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) - ) - .collect[Page.UnhandledError](pg => - pages.errors - .UnhandledErrorPage( - pages.errors.UnhandledErrorPage - .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), - state.actionBus - ) - ) - .collectStatic(Page.Directory)( - connectors - .DirectoryPageConnector(state.users, state.actionBus) - .apply - ) - div(cls := "h-full", child <-- pageSplitter.$view) - - // Pull in the stylesheet - val css: Css.type = Css diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala deleted file mode 100644 index 3bbc8c0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ /dev/null @@ -1,198 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.* -import org.scalajs.dom -import zio.json.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import scala.scalajs.js -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.Page.Titled - -// enum is not working with Waypoints' SplitRender collectStatic -sealed abstract class Page( - val id: String, - val title: String, - val parent: Option[Page] -) { - val path: Vector[Page] = - parent match - case None => Vector(this) - case Some(p) => p.path :+ this - - val isRoot: Boolean = parent.isEmpty -} - -object Page: - - case class Titled[V](value: V, title: Option[String] = None): - val show: String = title.getOrElse(value.toString) - - case object Directory extends Page("directory", "Adresář", None) - - case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) - - case class Detail(osobniCislo: Titled[OsobniCislo]) - extends Page("user", osobniCislo.show, Some(Directory)) - - object Detail { - def apply(o: UserInfo): Detail = Detail( - Titled(o.personalNumber, Some(o.name)) - ) - } - - case class DetailParametru( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String] - ) extends Page( - "parameter", - parametr.show, - Some(Detail(osobniCislo)) - ) - - object DetailParametru { - def apply(o: UserInfo, p: Parameter): DetailParametru = - DetailParametru( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)) - ) - } - - case class DetailKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "criteria", - kriterium.show, - Some(DetailParametru(osobniCislo, parametr)) - ) - - object DetailKriteria { - def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = - DetailKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class UpravDukazKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "addProof", - "Důkaz", - Some(DetailKriteria(osobniCislo, parametr, kriterium)) - ) - - object UpravDukazKriteria { - def apply( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): UpravDukazKriteria = - UpravDukazKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class NotFound(url: String) extends Page("404", "404", Some(Directory)) - - case class UnhandledError( - errorName: Option[String], - errorMessage: Option[String] - ) extends Page("500", "Unexpected error", Some(Directory)) - -object Routes: - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) - given [V: JsonEncoder]: JsonEncoder[Titled[V]] = - DeriveJsonEncoder.gen[Titled[V]] - given [V: JsonDecoder]: JsonDecoder[Titled[V]] = - DeriveJsonDecoder.gen[Titled[V]] - given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] - given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] - - val base = - js.`import`.meta.env.BASE_URL - .asInstanceOf[String] + "app" - - val homePage: Page = Page.Directory - - given router: Router[Page] = Router[Page]( - routes = List( - Route.static(homePage, root / endOfSegments, basePath = base), - Route.static( - Page.Dashboard, - root / "dashboard" / endOfSegments, - basePath = base - ), - Route[Page.Detail, String]( - encode = _.osobniCislo.value.toString, - decode = osc => Page.Detail(Titled(OsobniCislo(osc))), - root / "osoba" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.DetailParametru, (String, String)]( - encode = p => (p.osobniCislo.value.toString, p.parametr.value), - decode = - p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / endOfSegments, - basePath = base - ), - Route[Page.DetailKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.DetailKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.UpravDukazKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.UpravDukazKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / "edit" / endOfSegments, - basePath = base - ) - ), - serializePage = _.toJson, - deserializePage = _.fromJson[Page] - .fold(s => throw IllegalStateException(s), identity), - getPageTitle = _.title, - routeFallback = url => Page.NotFound(url), - deserializeFallback = _ => Page.Dashboard - )( - $popStateEvent = windowEvents.onPopState, - owner = unsafeWindowOwner - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala deleted file mode 100644 index b24b82b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -sealed trait Action - -case object FetchDirectory extends Action -case class FetchUserDetails(osc: OsobniCislo) extends Action -case class FetchParameters(osc: OsobniCislo) extends Action -case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action -case class FetchParameterCriteria( - osc: OsobniCislo, - paramId: String, - critId: String, - page: (UserInfo, Parameter, ParameterCriteria) => Page -) extends Action -case class FetchAvailableFiles(osc: OsobniCislo) extends Action -case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala deleted file mode 100644 index c637d35..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala +++ /dev/null @@ -1,82 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.{UserProfile, UserInfo, OsobniCislo} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo -import cz.e_bs.cmi.mdr.pdb.UserFunction - -object AppPage: - // TODO: pages by logged in user - val pages: List[Page] = List(Page.Directory, Page.Dashboard) - - import NavigationBar.{Logo, MenuItem} - - val logo = Logo( - "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", - "Workflow" - ) - - // TODO: menu items by user profile - val userMenu = - List( - MenuItem("Your Profile"), - MenuItem("Settings"), - MenuItem("Sign out") - ) - - // TODO: load user profile - val $userProfile = Var( - UserProfile( - "tom", - UserInfo( - OsobniCislo("1031"), - "tom", - "Tom", - "Cook", - None, - None, - Some("tom@example.com"), - Some("+420 222 866 180"), - Some( - UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") - ), - Nil, - None - ) - ) - ) - - val $userInfo = $userProfile.signal.map(_.userInfo) - - type ViewModel = Option[HtmlElement] - def apply( - actionBus: Observer[Action] - )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using - router: Router[Page] - ): HtmlElement = - PageLayout(actionBus)( - $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => - PageLayout.ViewModel( - NavigationBar.ViewModel( - u, - pages.map(p => - NavigationBar.Link( - () => - PageLink( - p, - actionBus - ), - p == cp - ) - ), - userMenu, - logo - ), - c - ) - ), - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala deleted file mode 100644 index bf473c6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ /dev/null @@ -1,143 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden -import fiftyforms.ui.components.tailwind.list.IconText.ViewModel -import fiftyforms.ui.components.tailwind.Icons -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Action - -object Breadcrumbs: - - private def slash = - import svg.{*, given} - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-300", - xmlns := "http://www.w3.org/2000/svg", - fill := "currentColor", - viewBox := "0 0 20 20", - ariaHidden := true, - path( - d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" - ) - ) - - def backIcon = - Icons.solid - .`arrow-narrow-left`() - .amend( - svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" - ) - - object Link: - case class ViewModel( - page: Page, - icon: Option[SvgElement], - extraClasses: String, - text: String, - textClasses: Option[String] = None - ) - - val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" - - def shortHome(p: Page) = ViewModel( - p, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - "Zpět na úvodní stránku", - None - ) - - def fullHome(p: Page) = ViewModel( - p, - Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), - "text-gray-400 hover:text-gray-500", - "Domů", - Some("sr-only") - ) - - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - PageLink - .container($m.map(_.page), actionBus) - .amend( - cls <-- $m.map(_.extraClasses), - child.maybe <-- $m.map(_.icon), - span( - cls <-- $m.map(_.textClasses.getOrElse("")), - child.text <-- $m.map(_.text) - ) - ) - - object FullBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - ol( - role := "list", - cls := "flex items-center space-x-4", - children <-- $m.map(_.path).split(_.id)((_, _, $p) => - li( - div( - cls := "flex items-center", - child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), - Link( - $p.map(p => - if (p.isRoot) then Link.fullHome(p) - else - Link.ViewModel( - p, - None, - s"ml-4 max-w-xs truncate ${Link.baseClasses}", - p.title - ) - ), - actionBus - ) - ) - ) - ) - ) - - object ShortBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - Link( - $m.map { p => - val target = p.parent.getOrElse(p) - if target.isRoot then Link.shortHome(target) - else - Link.ViewModel( - target, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - s"Zpět na ${target.title}" - ) - }, - actionBus - ) - - def apply(actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - nav( - cls := "flex", - aria.label := "Breadcrumb", - div( - cls := "flex sm:hidden", - ShortBreadcrumbs(router.$currentPage, actionBus) - ), - div( - cls := "hidden sm:block", - FullBreadcrumbs(router.$currentPage, actionBus) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala deleted file mode 100644 index 0db2047..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ /dev/null @@ -1,242 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent -import fiftyforms.ui.components.tailwind.Icons -import fiftyforms.ui.components.tailwind.Avatar -import cz.e_bs.cmi.mdr.pdb.UserInfo -import io.laminext.syntax.core.* - -object NavigationBar: - - case class Logo(img: String, name: String) - case class Link(a: () => Anchor, active: Boolean) - case class MenuItem(title: String) - - case class ViewModel( - userInfo: UserInfo, - pages: List[Link], - userMenu: List[MenuItem], - logo: Logo - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - val $userInfo = $m.map(_.userInfo) - val mobileMenuOpen = Var(false) - - val desktopOnly = cls("hidden md:block") - val mobileOnly = cls("md:hidden") - - inline def avatarImage(size: Int = 8) = - Avatar($userInfo.map(_.img)).avatarImage(size) - - def notificationButton = button( - tpe := "button", - cls := List( - "bg-indigo-600", - "focus:outline-none", - "focus:ring-2", - "focus:ring-offset-2", - "focus:ring-offset-indigo-600", - "focus:ring-white", - "hover:text-white", - "p-1", - "rounded-full", - "text-indigo-200" - ), - span(cls := "sr-only", "View notifications"), - Icons.outline.bell() - ) - - def userProfile: HtmlElement = - val menuOpen = Var(false) - - def menuItem(item: MenuItem, idx: Int): HtmlElement = - a( - href := "#", - cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", - role := "menuitem", - tabIndex := -1, - idAttr := s"user-menu-item-$idx", - item.title - ) - - div( - cls := "ml-3 relative", - div( - button( - tpe := "button", - cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - idAttr := "user-menu-button", - aria.expanded <-- menuOpen.signal, - aria.hasPopup := true, - span(cls := "sr-only", "Open user menu"), - child <-- avatarImage(), - onClick.preventDefault.mapTo( - !menuOpen.now() - ) --> menuOpen.writer - ) - ), - /* - * */ - div( - cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", - cls <-- menuOpen.signal.map { o => - if (o) "md:block" else "md:hidden" - }, - role := "menu", - aria.orientation := "vertical", - aria.labelledBy := "user-menu-button", - tabIndex := -1, - // : keyboard navigation - children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) - ) - ) - - def mobileProfile = - def menuItem(item: MenuItem): HtmlElement = - a( - href := "#", - cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", - item.title - ) - - div( - cls := "pt-4 pb-3 border-t border-indigo-700", - div( - cls := "flex items-center px-5", - div( - cls := "flex-shrink-0", - child <-- avatarImage(10) - ), - div( - cls := "ml-3", - div( - cls := "text-base font-medium text-white", - child.text <-- $userInfo.map(_.name) - ), - child.maybe <-- $userInfo.map( - _.email.map(e => - div( - cls := "text-sm font-medium text-indigo-300", - e - ) - ) - ) - ), - notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) - ), - div( - cls := "mt-3 px-2 space-y-1", - children <-- $m.map(_.userMenu.map(menuItem)) - ) - ) - - def pageLink(page: Link): Anchor = - page - .a() - .amend( - cls := "text-white px-3 py-2 rounded-md text-sm font-medium", - cls := Seq( - "bg-indigo-700" -> page.active, - "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active - ), - ariaCurrent := (if page.active then "page" else "false") - ) - - def logoImg: Image = - img( - cls := "h-8 w-8", - src <-- $m.map(_.logo.img), - alt <-- $m.map(_.logo.name) - ) - - def mobileMenuButton = button( - tpe := "button", - cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - aria.controls := "mobile-menu", - aria.expanded <-- mobileMenuOpen.signal, - span(cls := "sr-only", "Open main menu"), - Icons.outline - .menu() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") - ), - Icons.outline - .x() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") - ), - onClick.preventDefault.mapTo( - !mobileMenuOpen.now() - ) --> mobileMenuOpen.writer - ) - - def navBarLeft = - div( - cls := "flex items-center", - div(cls := "flex-shrink-0", logoImg), - div( - desktopOnly, - div( - cls := "ml-10 flex items-baseline space-x-4", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ) - ) - ) - - def navBarRight = - div( - desktopOnly, - div( - cls := "ml-4 flex items-center md:ml-6", - notificationButton, - userProfile - ) - ) - - def navBarMobile = - div( - cls := "-mr-2 flex", - mobileOnly, - mobileMenuButton - ) - - def navBar = - div( - cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex items-center justify-between h-16", - navBarLeft, - navBarRight, - navBarMobile - ) - ) - - def mobileMenu = - div( - mobileOnly, - cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }, - idAttr := "mobile-menu", - div( - cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ), - mobileProfile - ) - - nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala deleted file mode 100644 index 75339df..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object PageHeader: - def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = - header( - cls := "bg-white shadow-sm", - div( - cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", - h1( - cls := "text-lg leading-6 font-semibold text-gray-900", - Breadcrumbs(actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala deleted file mode 100644 index 5096463..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ /dev/null @@ -1,30 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import fiftyforms.ui.components.tailwind.Loading -import io.laminext.syntax.core.* - -object PageLayout: - case class ViewModel( - navigation: NavigationBar.ViewModel, - content: Option[HtmlElement] - ) - def apply(actionBus: Observer[Action])( - $m: Signal[ViewModel], - mods: Modifier[HtmlElement]* - )(using router: Router[Page]): HtmlElement = - val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) - div( - cls := "h-full flex flex-col", - NavigationBar($m.map(_.navigation)), - child.maybe <-- router.$currentPage.map(_.isRoot) - .switch(None, Some(PageHeader(actionBus))), - main( - cls := "flex-grow-1 overflow-y-auto", - mods, - child <-- $maybeContent.map(_.getOrElse(Loading)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala deleted file mode 100644 index 62d820f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.LinkSupport.* -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo - -object PageLink: - type ViewModel = Page - def apply($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = - a(mods($m, actions), child.text <-- $m.map(_.title)) - - def apply(m: ViewModel, actions: Observer[Action])(using - router: Router[Page] - ): Anchor = apply(Val(m), actions) - - def container($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = a(mods($m, actions)) - - def container(m: ViewModel, actions: Observer[Action])(using - Router[Page] - ): Anchor = container(Val(m), actions) - - private def mods($m: Signal[Page], actions: Observer[Action])(using - router: Router[Page] - ): Modifier[Anchor] = - Seq( - href <-- $m.map(router.absoluteUrlForPage).recover { case _ => - Some("invalid url") - }, - composeEvents(onClick.noKeyMod.preventDefault)( - _.sample($m) - .map(NavigateTo.apply) - ) --> actions - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala deleted file mode 100644 index 87f825a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.pages.dashboard.DashboardPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -class DashboardPageConnector(actionBus: Observer[Action])(using - router: Router[Page] -): - def apply: HtmlElement = - AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala deleted file mode 100644 index e9b0f71..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala +++ /dev/null @@ -1,76 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailKriteriaPage -import pages.detail.components.DukazyKriteria -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -object DetailKriteriaPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailKriteriaPageConnector( - state: DetailKriteriaPageConnector.AppState -)( - $page: Signal[Page.DetailKriteria] -)(using Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => - (p.osobniCislo.value, p.parametr.value, p.kriterium.value) - )((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map( - FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) - ) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, s, $s) => - DetailKriteriaPage($s)(state.actionBus.contramap { - case DukazyKriteria.Add => - NavigateTo( - Page.UpravDukazKriteria( - Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), - Page.Titled(s.parametr.id, Some(s.parametr.nazev)), - Page.Titled(s.kriterium.id, Some(s.kriterium.id)) - ) - ) - }) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): DetailKriteriaPage.ViewModel = - DetailKriteriaPage.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala deleted file mode 100644 index 4d8024f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ /dev/null @@ -1,57 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import pages.detail.DetailPage -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import fiftyforms.ui.components.tailwind.Color - -object DetailPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailPageConnector(state: DetailPageConnector.AppState)( - $page: Signal[Page.Detail] -)(using router: Router[Page]): - val $oscChangeSignal = $page.splitOne(_.osobniCislo.value)((osc, _, _) => osc) - val $pageChangeSignal = - $oscChangeSignal.flatMap(osc => - EventStream.fromSeq(Seq(FetchUserDetails(osc), FetchParameters(osc))) - ) - // TODO: filter the value based on the current osc - // OSC change will fetch new data, but still - // - we need to be sure that what we got is really what we ought to display - // - we want to display stale data accordingly (at least with loading indicator) - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - def apply: HtmlElement = - AppPage(state.actionBus)( - $data.combineWithFn($params)(_ zip _) - .map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: List[Parameter] - ): DetailPage.ViewModel = - DetailPage.ViewModel( - o.toDetailOsoby, - p.map( - _.toParametr(param => - PageLink.container(Page.DetailParametru(o, param), state.actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala deleted file mode 100644 index fdaa7f8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailParametruPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink - -object DetailParametruPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailParametruPageConnector( - state: DetailParametruPageConnector.AppState -)( - $page: Signal[Page.DetailParametru] -)(using router: Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => (p.osobniCislo.value, p.parametr.value))((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map(FetchParameter(_, _)) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - } yield (da, pb) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailParametruPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter - ): DetailParametruPage.ViewModel = - DetailParametruPage.ViewModel( - o.toDetailOsoby, - p.toParametr(p => - PageLink.container(Page.DetailParametru(o, p), state.actionBus) - ), - p.criteria.map( - _.toKriterium { c => - PageLink.container( - Page.DetailKriteria(o, p, c), - state.actionBus - ) - } - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala deleted file mode 100644 index 049aa22..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -case class DirectoryPageConnector( - $input: EventStream[List[UserInfo]], - actionBus: Observer[Action] -)(using router: Router[Page]): - val $data = $input.startWithNone - val $actionSignal = EventStream.fromValue(FetchDirectory) - - def apply: HtmlElement = - AppPage(actionBus)( - $data.split(_ => ())((_, _, s) => - pages.directory.DirectoryPage( - s.map( - _.map( - _.toUserRow(u => - PageLink.container( - Page.Detail(Page.Titled(u.personalNumber)), - actionBus - ) - ) - ) - ) - ) - ), - $actionSignal --> actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala deleted file mode 100644 index 88e7450..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ /dev/null @@ -1,68 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii -import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria -import fiftyforms.ui.components.tailwind.Color - -extension (o: UserInfo) - def toDetailOsoby: DetailOsoby.ViewModel = - DetailOsoby.ViewModel( - o.personalNumber, - o.name, - o.email, - o.phone, - o.img, - o.mainFunction.map(f => - DetailOsoby.Funkce.ViewModel(f.name, f.dept, f.ou) - ), - o.userContracts.headOption.map(c => - DetailOsoby.PracovniPomer.ViewModel(c.rel, c.startDate, c.endDate) - ) - ) - -extension (param: Parameter) - def toParametr(container: Parameter => Anchor): DetailParametru.ViewModel = - DetailParametru.ViewModel( - id = param.id, - nazev = param.name, - popis = param.description, - status = "Nesplněno", - statusColor = Color.red, - a = container(param) - ) - -extension (crit: ParameterCriteria) - def toKriterium( - container: ParameterCriteria => Anchor - ): DetailKriteria.ViewModel = - DetailKriteria.ViewModel( - nazev = crit.criteriumText, - kapitola = crit.chapterId, - bod = crit.itemId, - status = "Nesplněno", - statusColor = Color.red, - splneno = false, - dukazy = Nil, - container = container(crit) - ) - -extension (user: UserInfo) - def toUserRow( - container: UserInfo => HtmlElement = (_: UserInfo) => div() - ): UserRow.ViewModel = - UserRow.ViewModel( - osobniCislo = user.personalNumber.toString, - celeJmeno = user.name, - prijmeni = user.surname, - hlavniFunkce = user.mainFunction.map(_.name), - img = user.img, - container = () => container(user) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala deleted file mode 100644 index 7d5b2c1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.dashboard - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.Page - -object DashboardPage: - - def render: HtmlElement = - div("Dashboard page") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala deleted file mode 100644 index a3b00af..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala +++ /dev/null @@ -1,103 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import scala.scalajs.js.annotation.JSExportTopLevel -import scala.scalajs.js.annotation.JSExport -import scala.scalajs.js.annotation.JSImport -import scala.scalajs.js -import org.scalajs.dom -import com.raquo.laminar.api.L.{*, given} -import scala.scalajs.js.Date -import com.raquo.waypoint.Router -import com.raquo.waypoint.SplitRender -import cz.e_bs.cmi.mdr.pdb.app.services.DataFetcher -import scala.scalajs.js.JSON -import zio.json._ -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.state.AppState - -@js.native -@JSImport("stylesheets/main.css", JSImport.Namespace) -object Css extends js.Any - -@js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native -@JSImport("params/pdb-params.json", JSImport.Default) -object pdbParams extends js.Object - -@JSExportTopLevel("app") -object Main: - - @JSExport - def main(args: Array[String]): Unit = - import Routes.given - onLoad { - setupAirstream() - val appContainer = dom.document.querySelector("#app") - val _ = - render( - appContainer, - renderPage(state.MockAppState(using unsafeWindowOwner, router)) - ) - } - - private def onLoad(f: => Unit): Unit = - documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) - - private def setupAirstream()(using router: Router[Page]): Unit = - AirstreamError.registerUnhandledErrorCallback(err => - router.forcePage( - Page.UnhandledError( - Some(err.getClass.getName), // TODO: Fill only in dev mode - Some(err.getMessage) - ) - ) - ) - - def renderPage(state: AppState)(using - router: Router[Page] - ): HtmlElement = - val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) - .collectSignal[Page.Detail]( - connectors - .DetailPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailParametru]( - connectors - .DetailParametruPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailKriteria]( - connectors - .DetailKriteriaPageConnector(state)(_) - .apply - ) - .collectSignal[Page.UpravDukazKriteria]( - pages.detail.UpravDukaz.Connector(state)(_).apply - ) - .collectStatic(Page.Dashboard)( - connectors.DashboardPageConnector(state.actionBus).apply - ) - .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) - ) - .collect[Page.UnhandledError](pg => - pages.errors - .UnhandledErrorPage( - pages.errors.UnhandledErrorPage - .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), - state.actionBus - ) - ) - .collectStatic(Page.Directory)( - connectors - .DirectoryPageConnector(state.users, state.actionBus) - .apply - ) - div(cls := "h-full", child <-- pageSplitter.$view) - - // Pull in the stylesheet - val css: Css.type = Css diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala deleted file mode 100644 index 3bbc8c0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ /dev/null @@ -1,198 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.* -import org.scalajs.dom -import zio.json.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import scala.scalajs.js -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.Page.Titled - -// enum is not working with Waypoints' SplitRender collectStatic -sealed abstract class Page( - val id: String, - val title: String, - val parent: Option[Page] -) { - val path: Vector[Page] = - parent match - case None => Vector(this) - case Some(p) => p.path :+ this - - val isRoot: Boolean = parent.isEmpty -} - -object Page: - - case class Titled[V](value: V, title: Option[String] = None): - val show: String = title.getOrElse(value.toString) - - case object Directory extends Page("directory", "Adresář", None) - - case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) - - case class Detail(osobniCislo: Titled[OsobniCislo]) - extends Page("user", osobniCislo.show, Some(Directory)) - - object Detail { - def apply(o: UserInfo): Detail = Detail( - Titled(o.personalNumber, Some(o.name)) - ) - } - - case class DetailParametru( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String] - ) extends Page( - "parameter", - parametr.show, - Some(Detail(osobniCislo)) - ) - - object DetailParametru { - def apply(o: UserInfo, p: Parameter): DetailParametru = - DetailParametru( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)) - ) - } - - case class DetailKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "criteria", - kriterium.show, - Some(DetailParametru(osobniCislo, parametr)) - ) - - object DetailKriteria { - def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = - DetailKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class UpravDukazKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "addProof", - "Důkaz", - Some(DetailKriteria(osobniCislo, parametr, kriterium)) - ) - - object UpravDukazKriteria { - def apply( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): UpravDukazKriteria = - UpravDukazKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class NotFound(url: String) extends Page("404", "404", Some(Directory)) - - case class UnhandledError( - errorName: Option[String], - errorMessage: Option[String] - ) extends Page("500", "Unexpected error", Some(Directory)) - -object Routes: - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) - given [V: JsonEncoder]: JsonEncoder[Titled[V]] = - DeriveJsonEncoder.gen[Titled[V]] - given [V: JsonDecoder]: JsonDecoder[Titled[V]] = - DeriveJsonDecoder.gen[Titled[V]] - given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] - given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] - - val base = - js.`import`.meta.env.BASE_URL - .asInstanceOf[String] + "app" - - val homePage: Page = Page.Directory - - given router: Router[Page] = Router[Page]( - routes = List( - Route.static(homePage, root / endOfSegments, basePath = base), - Route.static( - Page.Dashboard, - root / "dashboard" / endOfSegments, - basePath = base - ), - Route[Page.Detail, String]( - encode = _.osobniCislo.value.toString, - decode = osc => Page.Detail(Titled(OsobniCislo(osc))), - root / "osoba" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.DetailParametru, (String, String)]( - encode = p => (p.osobniCislo.value.toString, p.parametr.value), - decode = - p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / endOfSegments, - basePath = base - ), - Route[Page.DetailKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.DetailKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.UpravDukazKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.UpravDukazKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / "edit" / endOfSegments, - basePath = base - ) - ), - serializePage = _.toJson, - deserializePage = _.fromJson[Page] - .fold(s => throw IllegalStateException(s), identity), - getPageTitle = _.title, - routeFallback = url => Page.NotFound(url), - deserializeFallback = _ => Page.Dashboard - )( - $popStateEvent = windowEvents.onPopState, - owner = unsafeWindowOwner - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala deleted file mode 100644 index b24b82b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -sealed trait Action - -case object FetchDirectory extends Action -case class FetchUserDetails(osc: OsobniCislo) extends Action -case class FetchParameters(osc: OsobniCislo) extends Action -case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action -case class FetchParameterCriteria( - osc: OsobniCislo, - paramId: String, - critId: String, - page: (UserInfo, Parameter, ParameterCriteria) => Page -) extends Action -case class FetchAvailableFiles(osc: OsobniCislo) extends Action -case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala deleted file mode 100644 index c637d35..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala +++ /dev/null @@ -1,82 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.{UserProfile, UserInfo, OsobniCislo} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo -import cz.e_bs.cmi.mdr.pdb.UserFunction - -object AppPage: - // TODO: pages by logged in user - val pages: List[Page] = List(Page.Directory, Page.Dashboard) - - import NavigationBar.{Logo, MenuItem} - - val logo = Logo( - "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", - "Workflow" - ) - - // TODO: menu items by user profile - val userMenu = - List( - MenuItem("Your Profile"), - MenuItem("Settings"), - MenuItem("Sign out") - ) - - // TODO: load user profile - val $userProfile = Var( - UserProfile( - "tom", - UserInfo( - OsobniCislo("1031"), - "tom", - "Tom", - "Cook", - None, - None, - Some("tom@example.com"), - Some("+420 222 866 180"), - Some( - UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") - ), - Nil, - None - ) - ) - ) - - val $userInfo = $userProfile.signal.map(_.userInfo) - - type ViewModel = Option[HtmlElement] - def apply( - actionBus: Observer[Action] - )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using - router: Router[Page] - ): HtmlElement = - PageLayout(actionBus)( - $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => - PageLayout.ViewModel( - NavigationBar.ViewModel( - u, - pages.map(p => - NavigationBar.Link( - () => - PageLink( - p, - actionBus - ), - p == cp - ) - ), - userMenu, - logo - ), - c - ) - ), - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala deleted file mode 100644 index bf473c6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ /dev/null @@ -1,143 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden -import fiftyforms.ui.components.tailwind.list.IconText.ViewModel -import fiftyforms.ui.components.tailwind.Icons -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Action - -object Breadcrumbs: - - private def slash = - import svg.{*, given} - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-300", - xmlns := "http://www.w3.org/2000/svg", - fill := "currentColor", - viewBox := "0 0 20 20", - ariaHidden := true, - path( - d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" - ) - ) - - def backIcon = - Icons.solid - .`arrow-narrow-left`() - .amend( - svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" - ) - - object Link: - case class ViewModel( - page: Page, - icon: Option[SvgElement], - extraClasses: String, - text: String, - textClasses: Option[String] = None - ) - - val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" - - def shortHome(p: Page) = ViewModel( - p, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - "Zpět na úvodní stránku", - None - ) - - def fullHome(p: Page) = ViewModel( - p, - Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), - "text-gray-400 hover:text-gray-500", - "Domů", - Some("sr-only") - ) - - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - PageLink - .container($m.map(_.page), actionBus) - .amend( - cls <-- $m.map(_.extraClasses), - child.maybe <-- $m.map(_.icon), - span( - cls <-- $m.map(_.textClasses.getOrElse("")), - child.text <-- $m.map(_.text) - ) - ) - - object FullBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - ol( - role := "list", - cls := "flex items-center space-x-4", - children <-- $m.map(_.path).split(_.id)((_, _, $p) => - li( - div( - cls := "flex items-center", - child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), - Link( - $p.map(p => - if (p.isRoot) then Link.fullHome(p) - else - Link.ViewModel( - p, - None, - s"ml-4 max-w-xs truncate ${Link.baseClasses}", - p.title - ) - ), - actionBus - ) - ) - ) - ) - ) - - object ShortBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - Link( - $m.map { p => - val target = p.parent.getOrElse(p) - if target.isRoot then Link.shortHome(target) - else - Link.ViewModel( - target, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - s"Zpět na ${target.title}" - ) - }, - actionBus - ) - - def apply(actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - nav( - cls := "flex", - aria.label := "Breadcrumb", - div( - cls := "flex sm:hidden", - ShortBreadcrumbs(router.$currentPage, actionBus) - ), - div( - cls := "hidden sm:block", - FullBreadcrumbs(router.$currentPage, actionBus) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala deleted file mode 100644 index 0db2047..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ /dev/null @@ -1,242 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent -import fiftyforms.ui.components.tailwind.Icons -import fiftyforms.ui.components.tailwind.Avatar -import cz.e_bs.cmi.mdr.pdb.UserInfo -import io.laminext.syntax.core.* - -object NavigationBar: - - case class Logo(img: String, name: String) - case class Link(a: () => Anchor, active: Boolean) - case class MenuItem(title: String) - - case class ViewModel( - userInfo: UserInfo, - pages: List[Link], - userMenu: List[MenuItem], - logo: Logo - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - val $userInfo = $m.map(_.userInfo) - val mobileMenuOpen = Var(false) - - val desktopOnly = cls("hidden md:block") - val mobileOnly = cls("md:hidden") - - inline def avatarImage(size: Int = 8) = - Avatar($userInfo.map(_.img)).avatarImage(size) - - def notificationButton = button( - tpe := "button", - cls := List( - "bg-indigo-600", - "focus:outline-none", - "focus:ring-2", - "focus:ring-offset-2", - "focus:ring-offset-indigo-600", - "focus:ring-white", - "hover:text-white", - "p-1", - "rounded-full", - "text-indigo-200" - ), - span(cls := "sr-only", "View notifications"), - Icons.outline.bell() - ) - - def userProfile: HtmlElement = - val menuOpen = Var(false) - - def menuItem(item: MenuItem, idx: Int): HtmlElement = - a( - href := "#", - cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", - role := "menuitem", - tabIndex := -1, - idAttr := s"user-menu-item-$idx", - item.title - ) - - div( - cls := "ml-3 relative", - div( - button( - tpe := "button", - cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - idAttr := "user-menu-button", - aria.expanded <-- menuOpen.signal, - aria.hasPopup := true, - span(cls := "sr-only", "Open user menu"), - child <-- avatarImage(), - onClick.preventDefault.mapTo( - !menuOpen.now() - ) --> menuOpen.writer - ) - ), - /* - * */ - div( - cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", - cls <-- menuOpen.signal.map { o => - if (o) "md:block" else "md:hidden" - }, - role := "menu", - aria.orientation := "vertical", - aria.labelledBy := "user-menu-button", - tabIndex := -1, - // : keyboard navigation - children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) - ) - ) - - def mobileProfile = - def menuItem(item: MenuItem): HtmlElement = - a( - href := "#", - cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", - item.title - ) - - div( - cls := "pt-4 pb-3 border-t border-indigo-700", - div( - cls := "flex items-center px-5", - div( - cls := "flex-shrink-0", - child <-- avatarImage(10) - ), - div( - cls := "ml-3", - div( - cls := "text-base font-medium text-white", - child.text <-- $userInfo.map(_.name) - ), - child.maybe <-- $userInfo.map( - _.email.map(e => - div( - cls := "text-sm font-medium text-indigo-300", - e - ) - ) - ) - ), - notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) - ), - div( - cls := "mt-3 px-2 space-y-1", - children <-- $m.map(_.userMenu.map(menuItem)) - ) - ) - - def pageLink(page: Link): Anchor = - page - .a() - .amend( - cls := "text-white px-3 py-2 rounded-md text-sm font-medium", - cls := Seq( - "bg-indigo-700" -> page.active, - "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active - ), - ariaCurrent := (if page.active then "page" else "false") - ) - - def logoImg: Image = - img( - cls := "h-8 w-8", - src <-- $m.map(_.logo.img), - alt <-- $m.map(_.logo.name) - ) - - def mobileMenuButton = button( - tpe := "button", - cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - aria.controls := "mobile-menu", - aria.expanded <-- mobileMenuOpen.signal, - span(cls := "sr-only", "Open main menu"), - Icons.outline - .menu() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") - ), - Icons.outline - .x() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") - ), - onClick.preventDefault.mapTo( - !mobileMenuOpen.now() - ) --> mobileMenuOpen.writer - ) - - def navBarLeft = - div( - cls := "flex items-center", - div(cls := "flex-shrink-0", logoImg), - div( - desktopOnly, - div( - cls := "ml-10 flex items-baseline space-x-4", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ) - ) - ) - - def navBarRight = - div( - desktopOnly, - div( - cls := "ml-4 flex items-center md:ml-6", - notificationButton, - userProfile - ) - ) - - def navBarMobile = - div( - cls := "-mr-2 flex", - mobileOnly, - mobileMenuButton - ) - - def navBar = - div( - cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex items-center justify-between h-16", - navBarLeft, - navBarRight, - navBarMobile - ) - ) - - def mobileMenu = - div( - mobileOnly, - cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }, - idAttr := "mobile-menu", - div( - cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ), - mobileProfile - ) - - nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala deleted file mode 100644 index 75339df..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object PageHeader: - def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = - header( - cls := "bg-white shadow-sm", - div( - cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", - h1( - cls := "text-lg leading-6 font-semibold text-gray-900", - Breadcrumbs(actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala deleted file mode 100644 index 5096463..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ /dev/null @@ -1,30 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import fiftyforms.ui.components.tailwind.Loading -import io.laminext.syntax.core.* - -object PageLayout: - case class ViewModel( - navigation: NavigationBar.ViewModel, - content: Option[HtmlElement] - ) - def apply(actionBus: Observer[Action])( - $m: Signal[ViewModel], - mods: Modifier[HtmlElement]* - )(using router: Router[Page]): HtmlElement = - val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) - div( - cls := "h-full flex flex-col", - NavigationBar($m.map(_.navigation)), - child.maybe <-- router.$currentPage.map(_.isRoot) - .switch(None, Some(PageHeader(actionBus))), - main( - cls := "flex-grow-1 overflow-y-auto", - mods, - child <-- $maybeContent.map(_.getOrElse(Loading)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala deleted file mode 100644 index 62d820f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.LinkSupport.* -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo - -object PageLink: - type ViewModel = Page - def apply($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = - a(mods($m, actions), child.text <-- $m.map(_.title)) - - def apply(m: ViewModel, actions: Observer[Action])(using - router: Router[Page] - ): Anchor = apply(Val(m), actions) - - def container($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = a(mods($m, actions)) - - def container(m: ViewModel, actions: Observer[Action])(using - Router[Page] - ): Anchor = container(Val(m), actions) - - private def mods($m: Signal[Page], actions: Observer[Action])(using - router: Router[Page] - ): Modifier[Anchor] = - Seq( - href <-- $m.map(router.absoluteUrlForPage).recover { case _ => - Some("invalid url") - }, - composeEvents(onClick.noKeyMod.preventDefault)( - _.sample($m) - .map(NavigateTo.apply) - ) --> actions - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala deleted file mode 100644 index 87f825a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.pages.dashboard.DashboardPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -class DashboardPageConnector(actionBus: Observer[Action])(using - router: Router[Page] -): - def apply: HtmlElement = - AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala deleted file mode 100644 index e9b0f71..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala +++ /dev/null @@ -1,76 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailKriteriaPage -import pages.detail.components.DukazyKriteria -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -object DetailKriteriaPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailKriteriaPageConnector( - state: DetailKriteriaPageConnector.AppState -)( - $page: Signal[Page.DetailKriteria] -)(using Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => - (p.osobniCislo.value, p.parametr.value, p.kriterium.value) - )((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map( - FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) - ) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, s, $s) => - DetailKriteriaPage($s)(state.actionBus.contramap { - case DukazyKriteria.Add => - NavigateTo( - Page.UpravDukazKriteria( - Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), - Page.Titled(s.parametr.id, Some(s.parametr.nazev)), - Page.Titled(s.kriterium.id, Some(s.kriterium.id)) - ) - ) - }) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): DetailKriteriaPage.ViewModel = - DetailKriteriaPage.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala deleted file mode 100644 index 4d8024f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ /dev/null @@ -1,57 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import pages.detail.DetailPage -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import fiftyforms.ui.components.tailwind.Color - -object DetailPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailPageConnector(state: DetailPageConnector.AppState)( - $page: Signal[Page.Detail] -)(using router: Router[Page]): - val $oscChangeSignal = $page.splitOne(_.osobniCislo.value)((osc, _, _) => osc) - val $pageChangeSignal = - $oscChangeSignal.flatMap(osc => - EventStream.fromSeq(Seq(FetchUserDetails(osc), FetchParameters(osc))) - ) - // TODO: filter the value based on the current osc - // OSC change will fetch new data, but still - // - we need to be sure that what we got is really what we ought to display - // - we want to display stale data accordingly (at least with loading indicator) - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - def apply: HtmlElement = - AppPage(state.actionBus)( - $data.combineWithFn($params)(_ zip _) - .map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: List[Parameter] - ): DetailPage.ViewModel = - DetailPage.ViewModel( - o.toDetailOsoby, - p.map( - _.toParametr(param => - PageLink.container(Page.DetailParametru(o, param), state.actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala deleted file mode 100644 index fdaa7f8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailParametruPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink - -object DetailParametruPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailParametruPageConnector( - state: DetailParametruPageConnector.AppState -)( - $page: Signal[Page.DetailParametru] -)(using router: Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => (p.osobniCislo.value, p.parametr.value))((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map(FetchParameter(_, _)) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - } yield (da, pb) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailParametruPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter - ): DetailParametruPage.ViewModel = - DetailParametruPage.ViewModel( - o.toDetailOsoby, - p.toParametr(p => - PageLink.container(Page.DetailParametru(o, p), state.actionBus) - ), - p.criteria.map( - _.toKriterium { c => - PageLink.container( - Page.DetailKriteria(o, p, c), - state.actionBus - ) - } - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala deleted file mode 100644 index 049aa22..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -case class DirectoryPageConnector( - $input: EventStream[List[UserInfo]], - actionBus: Observer[Action] -)(using router: Router[Page]): - val $data = $input.startWithNone - val $actionSignal = EventStream.fromValue(FetchDirectory) - - def apply: HtmlElement = - AppPage(actionBus)( - $data.split(_ => ())((_, _, s) => - pages.directory.DirectoryPage( - s.map( - _.map( - _.toUserRow(u => - PageLink.container( - Page.Detail(Page.Titled(u.personalNumber)), - actionBus - ) - ) - ) - ) - ) - ), - $actionSignal --> actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala deleted file mode 100644 index 88e7450..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ /dev/null @@ -1,68 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii -import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria -import fiftyforms.ui.components.tailwind.Color - -extension (o: UserInfo) - def toDetailOsoby: DetailOsoby.ViewModel = - DetailOsoby.ViewModel( - o.personalNumber, - o.name, - o.email, - o.phone, - o.img, - o.mainFunction.map(f => - DetailOsoby.Funkce.ViewModel(f.name, f.dept, f.ou) - ), - o.userContracts.headOption.map(c => - DetailOsoby.PracovniPomer.ViewModel(c.rel, c.startDate, c.endDate) - ) - ) - -extension (param: Parameter) - def toParametr(container: Parameter => Anchor): DetailParametru.ViewModel = - DetailParametru.ViewModel( - id = param.id, - nazev = param.name, - popis = param.description, - status = "Nesplněno", - statusColor = Color.red, - a = container(param) - ) - -extension (crit: ParameterCriteria) - def toKriterium( - container: ParameterCriteria => Anchor - ): DetailKriteria.ViewModel = - DetailKriteria.ViewModel( - nazev = crit.criteriumText, - kapitola = crit.chapterId, - bod = crit.itemId, - status = "Nesplněno", - statusColor = Color.red, - splneno = false, - dukazy = Nil, - container = container(crit) - ) - -extension (user: UserInfo) - def toUserRow( - container: UserInfo => HtmlElement = (_: UserInfo) => div() - ): UserRow.ViewModel = - UserRow.ViewModel( - osobniCislo = user.personalNumber.toString, - celeJmeno = user.name, - prijmeni = user.surname, - hlavniFunkce = user.mainFunction.map(_.name), - img = user.img, - container = () => container(user) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala deleted file mode 100644 index 7d5b2c1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.dashboard - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.Page - -object DashboardPage: - - def render: HtmlElement = - div("Dashboard page") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala deleted file mode 100644 index 9ef0698..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala +++ /dev/null @@ -1,31 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailKriteriaPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply($m: Signal[ViewModel])( - action: Observer[DukazyKriteria.Action] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - DukazyKriteria($m.map(_.kriterium.dukazy))(action) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala deleted file mode 100644 index a3b00af..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala +++ /dev/null @@ -1,103 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import scala.scalajs.js.annotation.JSExportTopLevel -import scala.scalajs.js.annotation.JSExport -import scala.scalajs.js.annotation.JSImport -import scala.scalajs.js -import org.scalajs.dom -import com.raquo.laminar.api.L.{*, given} -import scala.scalajs.js.Date -import com.raquo.waypoint.Router -import com.raquo.waypoint.SplitRender -import cz.e_bs.cmi.mdr.pdb.app.services.DataFetcher -import scala.scalajs.js.JSON -import zio.json._ -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.state.AppState - -@js.native -@JSImport("stylesheets/main.css", JSImport.Namespace) -object Css extends js.Any - -@js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native -@JSImport("params/pdb-params.json", JSImport.Default) -object pdbParams extends js.Object - -@JSExportTopLevel("app") -object Main: - - @JSExport - def main(args: Array[String]): Unit = - import Routes.given - onLoad { - setupAirstream() - val appContainer = dom.document.querySelector("#app") - val _ = - render( - appContainer, - renderPage(state.MockAppState(using unsafeWindowOwner, router)) - ) - } - - private def onLoad(f: => Unit): Unit = - documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) - - private def setupAirstream()(using router: Router[Page]): Unit = - AirstreamError.registerUnhandledErrorCallback(err => - router.forcePage( - Page.UnhandledError( - Some(err.getClass.getName), // TODO: Fill only in dev mode - Some(err.getMessage) - ) - ) - ) - - def renderPage(state: AppState)(using - router: Router[Page] - ): HtmlElement = - val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) - .collectSignal[Page.Detail]( - connectors - .DetailPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailParametru]( - connectors - .DetailParametruPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailKriteria]( - connectors - .DetailKriteriaPageConnector(state)(_) - .apply - ) - .collectSignal[Page.UpravDukazKriteria]( - pages.detail.UpravDukaz.Connector(state)(_).apply - ) - .collectStatic(Page.Dashboard)( - connectors.DashboardPageConnector(state.actionBus).apply - ) - .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) - ) - .collect[Page.UnhandledError](pg => - pages.errors - .UnhandledErrorPage( - pages.errors.UnhandledErrorPage - .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), - state.actionBus - ) - ) - .collectStatic(Page.Directory)( - connectors - .DirectoryPageConnector(state.users, state.actionBus) - .apply - ) - div(cls := "h-full", child <-- pageSplitter.$view) - - // Pull in the stylesheet - val css: Css.type = Css diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala deleted file mode 100644 index 3bbc8c0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ /dev/null @@ -1,198 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.* -import org.scalajs.dom -import zio.json.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import scala.scalajs.js -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.Page.Titled - -// enum is not working with Waypoints' SplitRender collectStatic -sealed abstract class Page( - val id: String, - val title: String, - val parent: Option[Page] -) { - val path: Vector[Page] = - parent match - case None => Vector(this) - case Some(p) => p.path :+ this - - val isRoot: Boolean = parent.isEmpty -} - -object Page: - - case class Titled[V](value: V, title: Option[String] = None): - val show: String = title.getOrElse(value.toString) - - case object Directory extends Page("directory", "Adresář", None) - - case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) - - case class Detail(osobniCislo: Titled[OsobniCislo]) - extends Page("user", osobniCislo.show, Some(Directory)) - - object Detail { - def apply(o: UserInfo): Detail = Detail( - Titled(o.personalNumber, Some(o.name)) - ) - } - - case class DetailParametru( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String] - ) extends Page( - "parameter", - parametr.show, - Some(Detail(osobniCislo)) - ) - - object DetailParametru { - def apply(o: UserInfo, p: Parameter): DetailParametru = - DetailParametru( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)) - ) - } - - case class DetailKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "criteria", - kriterium.show, - Some(DetailParametru(osobniCislo, parametr)) - ) - - object DetailKriteria { - def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = - DetailKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class UpravDukazKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "addProof", - "Důkaz", - Some(DetailKriteria(osobniCislo, parametr, kriterium)) - ) - - object UpravDukazKriteria { - def apply( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): UpravDukazKriteria = - UpravDukazKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class NotFound(url: String) extends Page("404", "404", Some(Directory)) - - case class UnhandledError( - errorName: Option[String], - errorMessage: Option[String] - ) extends Page("500", "Unexpected error", Some(Directory)) - -object Routes: - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) - given [V: JsonEncoder]: JsonEncoder[Titled[V]] = - DeriveJsonEncoder.gen[Titled[V]] - given [V: JsonDecoder]: JsonDecoder[Titled[V]] = - DeriveJsonDecoder.gen[Titled[V]] - given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] - given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] - - val base = - js.`import`.meta.env.BASE_URL - .asInstanceOf[String] + "app" - - val homePage: Page = Page.Directory - - given router: Router[Page] = Router[Page]( - routes = List( - Route.static(homePage, root / endOfSegments, basePath = base), - Route.static( - Page.Dashboard, - root / "dashboard" / endOfSegments, - basePath = base - ), - Route[Page.Detail, String]( - encode = _.osobniCislo.value.toString, - decode = osc => Page.Detail(Titled(OsobniCislo(osc))), - root / "osoba" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.DetailParametru, (String, String)]( - encode = p => (p.osobniCislo.value.toString, p.parametr.value), - decode = - p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / endOfSegments, - basePath = base - ), - Route[Page.DetailKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.DetailKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.UpravDukazKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.UpravDukazKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / "edit" / endOfSegments, - basePath = base - ) - ), - serializePage = _.toJson, - deserializePage = _.fromJson[Page] - .fold(s => throw IllegalStateException(s), identity), - getPageTitle = _.title, - routeFallback = url => Page.NotFound(url), - deserializeFallback = _ => Page.Dashboard - )( - $popStateEvent = windowEvents.onPopState, - owner = unsafeWindowOwner - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala deleted file mode 100644 index b24b82b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -sealed trait Action - -case object FetchDirectory extends Action -case class FetchUserDetails(osc: OsobniCislo) extends Action -case class FetchParameters(osc: OsobniCislo) extends Action -case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action -case class FetchParameterCriteria( - osc: OsobniCislo, - paramId: String, - critId: String, - page: (UserInfo, Parameter, ParameterCriteria) => Page -) extends Action -case class FetchAvailableFiles(osc: OsobniCislo) extends Action -case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala deleted file mode 100644 index c637d35..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala +++ /dev/null @@ -1,82 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.{UserProfile, UserInfo, OsobniCislo} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo -import cz.e_bs.cmi.mdr.pdb.UserFunction - -object AppPage: - // TODO: pages by logged in user - val pages: List[Page] = List(Page.Directory, Page.Dashboard) - - import NavigationBar.{Logo, MenuItem} - - val logo = Logo( - "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", - "Workflow" - ) - - // TODO: menu items by user profile - val userMenu = - List( - MenuItem("Your Profile"), - MenuItem("Settings"), - MenuItem("Sign out") - ) - - // TODO: load user profile - val $userProfile = Var( - UserProfile( - "tom", - UserInfo( - OsobniCislo("1031"), - "tom", - "Tom", - "Cook", - None, - None, - Some("tom@example.com"), - Some("+420 222 866 180"), - Some( - UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") - ), - Nil, - None - ) - ) - ) - - val $userInfo = $userProfile.signal.map(_.userInfo) - - type ViewModel = Option[HtmlElement] - def apply( - actionBus: Observer[Action] - )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using - router: Router[Page] - ): HtmlElement = - PageLayout(actionBus)( - $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => - PageLayout.ViewModel( - NavigationBar.ViewModel( - u, - pages.map(p => - NavigationBar.Link( - () => - PageLink( - p, - actionBus - ), - p == cp - ) - ), - userMenu, - logo - ), - c - ) - ), - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala deleted file mode 100644 index bf473c6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ /dev/null @@ -1,143 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden -import fiftyforms.ui.components.tailwind.list.IconText.ViewModel -import fiftyforms.ui.components.tailwind.Icons -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Action - -object Breadcrumbs: - - private def slash = - import svg.{*, given} - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-300", - xmlns := "http://www.w3.org/2000/svg", - fill := "currentColor", - viewBox := "0 0 20 20", - ariaHidden := true, - path( - d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" - ) - ) - - def backIcon = - Icons.solid - .`arrow-narrow-left`() - .amend( - svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" - ) - - object Link: - case class ViewModel( - page: Page, - icon: Option[SvgElement], - extraClasses: String, - text: String, - textClasses: Option[String] = None - ) - - val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" - - def shortHome(p: Page) = ViewModel( - p, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - "Zpět na úvodní stránku", - None - ) - - def fullHome(p: Page) = ViewModel( - p, - Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), - "text-gray-400 hover:text-gray-500", - "Domů", - Some("sr-only") - ) - - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - PageLink - .container($m.map(_.page), actionBus) - .amend( - cls <-- $m.map(_.extraClasses), - child.maybe <-- $m.map(_.icon), - span( - cls <-- $m.map(_.textClasses.getOrElse("")), - child.text <-- $m.map(_.text) - ) - ) - - object FullBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - ol( - role := "list", - cls := "flex items-center space-x-4", - children <-- $m.map(_.path).split(_.id)((_, _, $p) => - li( - div( - cls := "flex items-center", - child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), - Link( - $p.map(p => - if (p.isRoot) then Link.fullHome(p) - else - Link.ViewModel( - p, - None, - s"ml-4 max-w-xs truncate ${Link.baseClasses}", - p.title - ) - ), - actionBus - ) - ) - ) - ) - ) - - object ShortBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - Link( - $m.map { p => - val target = p.parent.getOrElse(p) - if target.isRoot then Link.shortHome(target) - else - Link.ViewModel( - target, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - s"Zpět na ${target.title}" - ) - }, - actionBus - ) - - def apply(actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - nav( - cls := "flex", - aria.label := "Breadcrumb", - div( - cls := "flex sm:hidden", - ShortBreadcrumbs(router.$currentPage, actionBus) - ), - div( - cls := "hidden sm:block", - FullBreadcrumbs(router.$currentPage, actionBus) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala deleted file mode 100644 index 0db2047..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ /dev/null @@ -1,242 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent -import fiftyforms.ui.components.tailwind.Icons -import fiftyforms.ui.components.tailwind.Avatar -import cz.e_bs.cmi.mdr.pdb.UserInfo -import io.laminext.syntax.core.* - -object NavigationBar: - - case class Logo(img: String, name: String) - case class Link(a: () => Anchor, active: Boolean) - case class MenuItem(title: String) - - case class ViewModel( - userInfo: UserInfo, - pages: List[Link], - userMenu: List[MenuItem], - logo: Logo - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - val $userInfo = $m.map(_.userInfo) - val mobileMenuOpen = Var(false) - - val desktopOnly = cls("hidden md:block") - val mobileOnly = cls("md:hidden") - - inline def avatarImage(size: Int = 8) = - Avatar($userInfo.map(_.img)).avatarImage(size) - - def notificationButton = button( - tpe := "button", - cls := List( - "bg-indigo-600", - "focus:outline-none", - "focus:ring-2", - "focus:ring-offset-2", - "focus:ring-offset-indigo-600", - "focus:ring-white", - "hover:text-white", - "p-1", - "rounded-full", - "text-indigo-200" - ), - span(cls := "sr-only", "View notifications"), - Icons.outline.bell() - ) - - def userProfile: HtmlElement = - val menuOpen = Var(false) - - def menuItem(item: MenuItem, idx: Int): HtmlElement = - a( - href := "#", - cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", - role := "menuitem", - tabIndex := -1, - idAttr := s"user-menu-item-$idx", - item.title - ) - - div( - cls := "ml-3 relative", - div( - button( - tpe := "button", - cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - idAttr := "user-menu-button", - aria.expanded <-- menuOpen.signal, - aria.hasPopup := true, - span(cls := "sr-only", "Open user menu"), - child <-- avatarImage(), - onClick.preventDefault.mapTo( - !menuOpen.now() - ) --> menuOpen.writer - ) - ), - /* - * */ - div( - cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", - cls <-- menuOpen.signal.map { o => - if (o) "md:block" else "md:hidden" - }, - role := "menu", - aria.orientation := "vertical", - aria.labelledBy := "user-menu-button", - tabIndex := -1, - // : keyboard navigation - children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) - ) - ) - - def mobileProfile = - def menuItem(item: MenuItem): HtmlElement = - a( - href := "#", - cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", - item.title - ) - - div( - cls := "pt-4 pb-3 border-t border-indigo-700", - div( - cls := "flex items-center px-5", - div( - cls := "flex-shrink-0", - child <-- avatarImage(10) - ), - div( - cls := "ml-3", - div( - cls := "text-base font-medium text-white", - child.text <-- $userInfo.map(_.name) - ), - child.maybe <-- $userInfo.map( - _.email.map(e => - div( - cls := "text-sm font-medium text-indigo-300", - e - ) - ) - ) - ), - notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) - ), - div( - cls := "mt-3 px-2 space-y-1", - children <-- $m.map(_.userMenu.map(menuItem)) - ) - ) - - def pageLink(page: Link): Anchor = - page - .a() - .amend( - cls := "text-white px-3 py-2 rounded-md text-sm font-medium", - cls := Seq( - "bg-indigo-700" -> page.active, - "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active - ), - ariaCurrent := (if page.active then "page" else "false") - ) - - def logoImg: Image = - img( - cls := "h-8 w-8", - src <-- $m.map(_.logo.img), - alt <-- $m.map(_.logo.name) - ) - - def mobileMenuButton = button( - tpe := "button", - cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - aria.controls := "mobile-menu", - aria.expanded <-- mobileMenuOpen.signal, - span(cls := "sr-only", "Open main menu"), - Icons.outline - .menu() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") - ), - Icons.outline - .x() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") - ), - onClick.preventDefault.mapTo( - !mobileMenuOpen.now() - ) --> mobileMenuOpen.writer - ) - - def navBarLeft = - div( - cls := "flex items-center", - div(cls := "flex-shrink-0", logoImg), - div( - desktopOnly, - div( - cls := "ml-10 flex items-baseline space-x-4", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ) - ) - ) - - def navBarRight = - div( - desktopOnly, - div( - cls := "ml-4 flex items-center md:ml-6", - notificationButton, - userProfile - ) - ) - - def navBarMobile = - div( - cls := "-mr-2 flex", - mobileOnly, - mobileMenuButton - ) - - def navBar = - div( - cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex items-center justify-between h-16", - navBarLeft, - navBarRight, - navBarMobile - ) - ) - - def mobileMenu = - div( - mobileOnly, - cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }, - idAttr := "mobile-menu", - div( - cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ), - mobileProfile - ) - - nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala deleted file mode 100644 index 75339df..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object PageHeader: - def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = - header( - cls := "bg-white shadow-sm", - div( - cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", - h1( - cls := "text-lg leading-6 font-semibold text-gray-900", - Breadcrumbs(actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala deleted file mode 100644 index 5096463..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ /dev/null @@ -1,30 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import fiftyforms.ui.components.tailwind.Loading -import io.laminext.syntax.core.* - -object PageLayout: - case class ViewModel( - navigation: NavigationBar.ViewModel, - content: Option[HtmlElement] - ) - def apply(actionBus: Observer[Action])( - $m: Signal[ViewModel], - mods: Modifier[HtmlElement]* - )(using router: Router[Page]): HtmlElement = - val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) - div( - cls := "h-full flex flex-col", - NavigationBar($m.map(_.navigation)), - child.maybe <-- router.$currentPage.map(_.isRoot) - .switch(None, Some(PageHeader(actionBus))), - main( - cls := "flex-grow-1 overflow-y-auto", - mods, - child <-- $maybeContent.map(_.getOrElse(Loading)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala deleted file mode 100644 index 62d820f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.LinkSupport.* -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo - -object PageLink: - type ViewModel = Page - def apply($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = - a(mods($m, actions), child.text <-- $m.map(_.title)) - - def apply(m: ViewModel, actions: Observer[Action])(using - router: Router[Page] - ): Anchor = apply(Val(m), actions) - - def container($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = a(mods($m, actions)) - - def container(m: ViewModel, actions: Observer[Action])(using - Router[Page] - ): Anchor = container(Val(m), actions) - - private def mods($m: Signal[Page], actions: Observer[Action])(using - router: Router[Page] - ): Modifier[Anchor] = - Seq( - href <-- $m.map(router.absoluteUrlForPage).recover { case _ => - Some("invalid url") - }, - composeEvents(onClick.noKeyMod.preventDefault)( - _.sample($m) - .map(NavigateTo.apply) - ) --> actions - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala deleted file mode 100644 index 87f825a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.pages.dashboard.DashboardPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -class DashboardPageConnector(actionBus: Observer[Action])(using - router: Router[Page] -): - def apply: HtmlElement = - AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala deleted file mode 100644 index e9b0f71..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala +++ /dev/null @@ -1,76 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailKriteriaPage -import pages.detail.components.DukazyKriteria -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -object DetailKriteriaPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailKriteriaPageConnector( - state: DetailKriteriaPageConnector.AppState -)( - $page: Signal[Page.DetailKriteria] -)(using Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => - (p.osobniCislo.value, p.parametr.value, p.kriterium.value) - )((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map( - FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) - ) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, s, $s) => - DetailKriteriaPage($s)(state.actionBus.contramap { - case DukazyKriteria.Add => - NavigateTo( - Page.UpravDukazKriteria( - Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), - Page.Titled(s.parametr.id, Some(s.parametr.nazev)), - Page.Titled(s.kriterium.id, Some(s.kriterium.id)) - ) - ) - }) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): DetailKriteriaPage.ViewModel = - DetailKriteriaPage.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala deleted file mode 100644 index 4d8024f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ /dev/null @@ -1,57 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import pages.detail.DetailPage -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import fiftyforms.ui.components.tailwind.Color - -object DetailPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailPageConnector(state: DetailPageConnector.AppState)( - $page: Signal[Page.Detail] -)(using router: Router[Page]): - val $oscChangeSignal = $page.splitOne(_.osobniCislo.value)((osc, _, _) => osc) - val $pageChangeSignal = - $oscChangeSignal.flatMap(osc => - EventStream.fromSeq(Seq(FetchUserDetails(osc), FetchParameters(osc))) - ) - // TODO: filter the value based on the current osc - // OSC change will fetch new data, but still - // - we need to be sure that what we got is really what we ought to display - // - we want to display stale data accordingly (at least with loading indicator) - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - def apply: HtmlElement = - AppPage(state.actionBus)( - $data.combineWithFn($params)(_ zip _) - .map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: List[Parameter] - ): DetailPage.ViewModel = - DetailPage.ViewModel( - o.toDetailOsoby, - p.map( - _.toParametr(param => - PageLink.container(Page.DetailParametru(o, param), state.actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala deleted file mode 100644 index fdaa7f8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailParametruPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink - -object DetailParametruPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailParametruPageConnector( - state: DetailParametruPageConnector.AppState -)( - $page: Signal[Page.DetailParametru] -)(using router: Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => (p.osobniCislo.value, p.parametr.value))((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map(FetchParameter(_, _)) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - } yield (da, pb) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailParametruPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter - ): DetailParametruPage.ViewModel = - DetailParametruPage.ViewModel( - o.toDetailOsoby, - p.toParametr(p => - PageLink.container(Page.DetailParametru(o, p), state.actionBus) - ), - p.criteria.map( - _.toKriterium { c => - PageLink.container( - Page.DetailKriteria(o, p, c), - state.actionBus - ) - } - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala deleted file mode 100644 index 049aa22..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -case class DirectoryPageConnector( - $input: EventStream[List[UserInfo]], - actionBus: Observer[Action] -)(using router: Router[Page]): - val $data = $input.startWithNone - val $actionSignal = EventStream.fromValue(FetchDirectory) - - def apply: HtmlElement = - AppPage(actionBus)( - $data.split(_ => ())((_, _, s) => - pages.directory.DirectoryPage( - s.map( - _.map( - _.toUserRow(u => - PageLink.container( - Page.Detail(Page.Titled(u.personalNumber)), - actionBus - ) - ) - ) - ) - ) - ), - $actionSignal --> actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala deleted file mode 100644 index 88e7450..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ /dev/null @@ -1,68 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii -import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria -import fiftyforms.ui.components.tailwind.Color - -extension (o: UserInfo) - def toDetailOsoby: DetailOsoby.ViewModel = - DetailOsoby.ViewModel( - o.personalNumber, - o.name, - o.email, - o.phone, - o.img, - o.mainFunction.map(f => - DetailOsoby.Funkce.ViewModel(f.name, f.dept, f.ou) - ), - o.userContracts.headOption.map(c => - DetailOsoby.PracovniPomer.ViewModel(c.rel, c.startDate, c.endDate) - ) - ) - -extension (param: Parameter) - def toParametr(container: Parameter => Anchor): DetailParametru.ViewModel = - DetailParametru.ViewModel( - id = param.id, - nazev = param.name, - popis = param.description, - status = "Nesplněno", - statusColor = Color.red, - a = container(param) - ) - -extension (crit: ParameterCriteria) - def toKriterium( - container: ParameterCriteria => Anchor - ): DetailKriteria.ViewModel = - DetailKriteria.ViewModel( - nazev = crit.criteriumText, - kapitola = crit.chapterId, - bod = crit.itemId, - status = "Nesplněno", - statusColor = Color.red, - splneno = false, - dukazy = Nil, - container = container(crit) - ) - -extension (user: UserInfo) - def toUserRow( - container: UserInfo => HtmlElement = (_: UserInfo) => div() - ): UserRow.ViewModel = - UserRow.ViewModel( - osobniCislo = user.personalNumber.toString, - celeJmeno = user.name, - prijmeni = user.surname, - hlavniFunkce = user.mainFunction.map(_.name), - img = user.img, - container = () => container(user) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala deleted file mode 100644 index 7d5b2c1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.dashboard - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.Page - -object DashboardPage: - - def render: HtmlElement = - div("Dashboard page") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala deleted file mode 100644 index 9ef0698..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala +++ /dev/null @@ -1,31 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailKriteriaPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply($m: Signal[ViewModel])( - action: Observer[DukazyKriteria.Action] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - DukazyKriteria($m.map(_.kriterium.dukazy))(action) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala deleted file mode 100644 index 3b316e1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object DetailPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametry: SeznamParametru.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby($m.map(_.osoba)), - SeznamParametru($m.map(_.parametry)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala deleted file mode 100644 index a3b00af..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala +++ /dev/null @@ -1,103 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import scala.scalajs.js.annotation.JSExportTopLevel -import scala.scalajs.js.annotation.JSExport -import scala.scalajs.js.annotation.JSImport -import scala.scalajs.js -import org.scalajs.dom -import com.raquo.laminar.api.L.{*, given} -import scala.scalajs.js.Date -import com.raquo.waypoint.Router -import com.raquo.waypoint.SplitRender -import cz.e_bs.cmi.mdr.pdb.app.services.DataFetcher -import scala.scalajs.js.JSON -import zio.json._ -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.state.AppState - -@js.native -@JSImport("stylesheets/main.css", JSImport.Namespace) -object Css extends js.Any - -@js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native -@JSImport("params/pdb-params.json", JSImport.Default) -object pdbParams extends js.Object - -@JSExportTopLevel("app") -object Main: - - @JSExport - def main(args: Array[String]): Unit = - import Routes.given - onLoad { - setupAirstream() - val appContainer = dom.document.querySelector("#app") - val _ = - render( - appContainer, - renderPage(state.MockAppState(using unsafeWindowOwner, router)) - ) - } - - private def onLoad(f: => Unit): Unit = - documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) - - private def setupAirstream()(using router: Router[Page]): Unit = - AirstreamError.registerUnhandledErrorCallback(err => - router.forcePage( - Page.UnhandledError( - Some(err.getClass.getName), // TODO: Fill only in dev mode - Some(err.getMessage) - ) - ) - ) - - def renderPage(state: AppState)(using - router: Router[Page] - ): HtmlElement = - val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) - .collectSignal[Page.Detail]( - connectors - .DetailPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailParametru]( - connectors - .DetailParametruPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailKriteria]( - connectors - .DetailKriteriaPageConnector(state)(_) - .apply - ) - .collectSignal[Page.UpravDukazKriteria]( - pages.detail.UpravDukaz.Connector(state)(_).apply - ) - .collectStatic(Page.Dashboard)( - connectors.DashboardPageConnector(state.actionBus).apply - ) - .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) - ) - .collect[Page.UnhandledError](pg => - pages.errors - .UnhandledErrorPage( - pages.errors.UnhandledErrorPage - .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), - state.actionBus - ) - ) - .collectStatic(Page.Directory)( - connectors - .DirectoryPageConnector(state.users, state.actionBus) - .apply - ) - div(cls := "h-full", child <-- pageSplitter.$view) - - // Pull in the stylesheet - val css: Css.type = Css diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala deleted file mode 100644 index 3bbc8c0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ /dev/null @@ -1,198 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.* -import org.scalajs.dom -import zio.json.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import scala.scalajs.js -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.Page.Titled - -// enum is not working with Waypoints' SplitRender collectStatic -sealed abstract class Page( - val id: String, - val title: String, - val parent: Option[Page] -) { - val path: Vector[Page] = - parent match - case None => Vector(this) - case Some(p) => p.path :+ this - - val isRoot: Boolean = parent.isEmpty -} - -object Page: - - case class Titled[V](value: V, title: Option[String] = None): - val show: String = title.getOrElse(value.toString) - - case object Directory extends Page("directory", "Adresář", None) - - case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) - - case class Detail(osobniCislo: Titled[OsobniCislo]) - extends Page("user", osobniCislo.show, Some(Directory)) - - object Detail { - def apply(o: UserInfo): Detail = Detail( - Titled(o.personalNumber, Some(o.name)) - ) - } - - case class DetailParametru( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String] - ) extends Page( - "parameter", - parametr.show, - Some(Detail(osobniCislo)) - ) - - object DetailParametru { - def apply(o: UserInfo, p: Parameter): DetailParametru = - DetailParametru( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)) - ) - } - - case class DetailKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "criteria", - kriterium.show, - Some(DetailParametru(osobniCislo, parametr)) - ) - - object DetailKriteria { - def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = - DetailKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class UpravDukazKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "addProof", - "Důkaz", - Some(DetailKriteria(osobniCislo, parametr, kriterium)) - ) - - object UpravDukazKriteria { - def apply( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): UpravDukazKriteria = - UpravDukazKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class NotFound(url: String) extends Page("404", "404", Some(Directory)) - - case class UnhandledError( - errorName: Option[String], - errorMessage: Option[String] - ) extends Page("500", "Unexpected error", Some(Directory)) - -object Routes: - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) - given [V: JsonEncoder]: JsonEncoder[Titled[V]] = - DeriveJsonEncoder.gen[Titled[V]] - given [V: JsonDecoder]: JsonDecoder[Titled[V]] = - DeriveJsonDecoder.gen[Titled[V]] - given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] - given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] - - val base = - js.`import`.meta.env.BASE_URL - .asInstanceOf[String] + "app" - - val homePage: Page = Page.Directory - - given router: Router[Page] = Router[Page]( - routes = List( - Route.static(homePage, root / endOfSegments, basePath = base), - Route.static( - Page.Dashboard, - root / "dashboard" / endOfSegments, - basePath = base - ), - Route[Page.Detail, String]( - encode = _.osobniCislo.value.toString, - decode = osc => Page.Detail(Titled(OsobniCislo(osc))), - root / "osoba" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.DetailParametru, (String, String)]( - encode = p => (p.osobniCislo.value.toString, p.parametr.value), - decode = - p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / endOfSegments, - basePath = base - ), - Route[Page.DetailKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.DetailKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.UpravDukazKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.UpravDukazKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / "edit" / endOfSegments, - basePath = base - ) - ), - serializePage = _.toJson, - deserializePage = _.fromJson[Page] - .fold(s => throw IllegalStateException(s), identity), - getPageTitle = _.title, - routeFallback = url => Page.NotFound(url), - deserializeFallback = _ => Page.Dashboard - )( - $popStateEvent = windowEvents.onPopState, - owner = unsafeWindowOwner - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala deleted file mode 100644 index b24b82b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -sealed trait Action - -case object FetchDirectory extends Action -case class FetchUserDetails(osc: OsobniCislo) extends Action -case class FetchParameters(osc: OsobniCislo) extends Action -case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action -case class FetchParameterCriteria( - osc: OsobniCislo, - paramId: String, - critId: String, - page: (UserInfo, Parameter, ParameterCriteria) => Page -) extends Action -case class FetchAvailableFiles(osc: OsobniCislo) extends Action -case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala deleted file mode 100644 index c637d35..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala +++ /dev/null @@ -1,82 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.{UserProfile, UserInfo, OsobniCislo} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo -import cz.e_bs.cmi.mdr.pdb.UserFunction - -object AppPage: - // TODO: pages by logged in user - val pages: List[Page] = List(Page.Directory, Page.Dashboard) - - import NavigationBar.{Logo, MenuItem} - - val logo = Logo( - "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", - "Workflow" - ) - - // TODO: menu items by user profile - val userMenu = - List( - MenuItem("Your Profile"), - MenuItem("Settings"), - MenuItem("Sign out") - ) - - // TODO: load user profile - val $userProfile = Var( - UserProfile( - "tom", - UserInfo( - OsobniCislo("1031"), - "tom", - "Tom", - "Cook", - None, - None, - Some("tom@example.com"), - Some("+420 222 866 180"), - Some( - UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") - ), - Nil, - None - ) - ) - ) - - val $userInfo = $userProfile.signal.map(_.userInfo) - - type ViewModel = Option[HtmlElement] - def apply( - actionBus: Observer[Action] - )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using - router: Router[Page] - ): HtmlElement = - PageLayout(actionBus)( - $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => - PageLayout.ViewModel( - NavigationBar.ViewModel( - u, - pages.map(p => - NavigationBar.Link( - () => - PageLink( - p, - actionBus - ), - p == cp - ) - ), - userMenu, - logo - ), - c - ) - ), - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala deleted file mode 100644 index bf473c6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ /dev/null @@ -1,143 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden -import fiftyforms.ui.components.tailwind.list.IconText.ViewModel -import fiftyforms.ui.components.tailwind.Icons -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Action - -object Breadcrumbs: - - private def slash = - import svg.{*, given} - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-300", - xmlns := "http://www.w3.org/2000/svg", - fill := "currentColor", - viewBox := "0 0 20 20", - ariaHidden := true, - path( - d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" - ) - ) - - def backIcon = - Icons.solid - .`arrow-narrow-left`() - .amend( - svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" - ) - - object Link: - case class ViewModel( - page: Page, - icon: Option[SvgElement], - extraClasses: String, - text: String, - textClasses: Option[String] = None - ) - - val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" - - def shortHome(p: Page) = ViewModel( - p, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - "Zpět na úvodní stránku", - None - ) - - def fullHome(p: Page) = ViewModel( - p, - Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), - "text-gray-400 hover:text-gray-500", - "Domů", - Some("sr-only") - ) - - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - PageLink - .container($m.map(_.page), actionBus) - .amend( - cls <-- $m.map(_.extraClasses), - child.maybe <-- $m.map(_.icon), - span( - cls <-- $m.map(_.textClasses.getOrElse("")), - child.text <-- $m.map(_.text) - ) - ) - - object FullBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - ol( - role := "list", - cls := "flex items-center space-x-4", - children <-- $m.map(_.path).split(_.id)((_, _, $p) => - li( - div( - cls := "flex items-center", - child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), - Link( - $p.map(p => - if (p.isRoot) then Link.fullHome(p) - else - Link.ViewModel( - p, - None, - s"ml-4 max-w-xs truncate ${Link.baseClasses}", - p.title - ) - ), - actionBus - ) - ) - ) - ) - ) - - object ShortBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - Link( - $m.map { p => - val target = p.parent.getOrElse(p) - if target.isRoot then Link.shortHome(target) - else - Link.ViewModel( - target, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - s"Zpět na ${target.title}" - ) - }, - actionBus - ) - - def apply(actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - nav( - cls := "flex", - aria.label := "Breadcrumb", - div( - cls := "flex sm:hidden", - ShortBreadcrumbs(router.$currentPage, actionBus) - ), - div( - cls := "hidden sm:block", - FullBreadcrumbs(router.$currentPage, actionBus) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala deleted file mode 100644 index 0db2047..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ /dev/null @@ -1,242 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent -import fiftyforms.ui.components.tailwind.Icons -import fiftyforms.ui.components.tailwind.Avatar -import cz.e_bs.cmi.mdr.pdb.UserInfo -import io.laminext.syntax.core.* - -object NavigationBar: - - case class Logo(img: String, name: String) - case class Link(a: () => Anchor, active: Boolean) - case class MenuItem(title: String) - - case class ViewModel( - userInfo: UserInfo, - pages: List[Link], - userMenu: List[MenuItem], - logo: Logo - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - val $userInfo = $m.map(_.userInfo) - val mobileMenuOpen = Var(false) - - val desktopOnly = cls("hidden md:block") - val mobileOnly = cls("md:hidden") - - inline def avatarImage(size: Int = 8) = - Avatar($userInfo.map(_.img)).avatarImage(size) - - def notificationButton = button( - tpe := "button", - cls := List( - "bg-indigo-600", - "focus:outline-none", - "focus:ring-2", - "focus:ring-offset-2", - "focus:ring-offset-indigo-600", - "focus:ring-white", - "hover:text-white", - "p-1", - "rounded-full", - "text-indigo-200" - ), - span(cls := "sr-only", "View notifications"), - Icons.outline.bell() - ) - - def userProfile: HtmlElement = - val menuOpen = Var(false) - - def menuItem(item: MenuItem, idx: Int): HtmlElement = - a( - href := "#", - cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", - role := "menuitem", - tabIndex := -1, - idAttr := s"user-menu-item-$idx", - item.title - ) - - div( - cls := "ml-3 relative", - div( - button( - tpe := "button", - cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - idAttr := "user-menu-button", - aria.expanded <-- menuOpen.signal, - aria.hasPopup := true, - span(cls := "sr-only", "Open user menu"), - child <-- avatarImage(), - onClick.preventDefault.mapTo( - !menuOpen.now() - ) --> menuOpen.writer - ) - ), - /* - * */ - div( - cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", - cls <-- menuOpen.signal.map { o => - if (o) "md:block" else "md:hidden" - }, - role := "menu", - aria.orientation := "vertical", - aria.labelledBy := "user-menu-button", - tabIndex := -1, - // : keyboard navigation - children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) - ) - ) - - def mobileProfile = - def menuItem(item: MenuItem): HtmlElement = - a( - href := "#", - cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", - item.title - ) - - div( - cls := "pt-4 pb-3 border-t border-indigo-700", - div( - cls := "flex items-center px-5", - div( - cls := "flex-shrink-0", - child <-- avatarImage(10) - ), - div( - cls := "ml-3", - div( - cls := "text-base font-medium text-white", - child.text <-- $userInfo.map(_.name) - ), - child.maybe <-- $userInfo.map( - _.email.map(e => - div( - cls := "text-sm font-medium text-indigo-300", - e - ) - ) - ) - ), - notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) - ), - div( - cls := "mt-3 px-2 space-y-1", - children <-- $m.map(_.userMenu.map(menuItem)) - ) - ) - - def pageLink(page: Link): Anchor = - page - .a() - .amend( - cls := "text-white px-3 py-2 rounded-md text-sm font-medium", - cls := Seq( - "bg-indigo-700" -> page.active, - "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active - ), - ariaCurrent := (if page.active then "page" else "false") - ) - - def logoImg: Image = - img( - cls := "h-8 w-8", - src <-- $m.map(_.logo.img), - alt <-- $m.map(_.logo.name) - ) - - def mobileMenuButton = button( - tpe := "button", - cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - aria.controls := "mobile-menu", - aria.expanded <-- mobileMenuOpen.signal, - span(cls := "sr-only", "Open main menu"), - Icons.outline - .menu() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") - ), - Icons.outline - .x() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") - ), - onClick.preventDefault.mapTo( - !mobileMenuOpen.now() - ) --> mobileMenuOpen.writer - ) - - def navBarLeft = - div( - cls := "flex items-center", - div(cls := "flex-shrink-0", logoImg), - div( - desktopOnly, - div( - cls := "ml-10 flex items-baseline space-x-4", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ) - ) - ) - - def navBarRight = - div( - desktopOnly, - div( - cls := "ml-4 flex items-center md:ml-6", - notificationButton, - userProfile - ) - ) - - def navBarMobile = - div( - cls := "-mr-2 flex", - mobileOnly, - mobileMenuButton - ) - - def navBar = - div( - cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex items-center justify-between h-16", - navBarLeft, - navBarRight, - navBarMobile - ) - ) - - def mobileMenu = - div( - mobileOnly, - cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }, - idAttr := "mobile-menu", - div( - cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ), - mobileProfile - ) - - nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala deleted file mode 100644 index 75339df..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object PageHeader: - def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = - header( - cls := "bg-white shadow-sm", - div( - cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", - h1( - cls := "text-lg leading-6 font-semibold text-gray-900", - Breadcrumbs(actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala deleted file mode 100644 index 5096463..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ /dev/null @@ -1,30 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import fiftyforms.ui.components.tailwind.Loading -import io.laminext.syntax.core.* - -object PageLayout: - case class ViewModel( - navigation: NavigationBar.ViewModel, - content: Option[HtmlElement] - ) - def apply(actionBus: Observer[Action])( - $m: Signal[ViewModel], - mods: Modifier[HtmlElement]* - )(using router: Router[Page]): HtmlElement = - val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) - div( - cls := "h-full flex flex-col", - NavigationBar($m.map(_.navigation)), - child.maybe <-- router.$currentPage.map(_.isRoot) - .switch(None, Some(PageHeader(actionBus))), - main( - cls := "flex-grow-1 overflow-y-auto", - mods, - child <-- $maybeContent.map(_.getOrElse(Loading)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala deleted file mode 100644 index 62d820f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.LinkSupport.* -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo - -object PageLink: - type ViewModel = Page - def apply($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = - a(mods($m, actions), child.text <-- $m.map(_.title)) - - def apply(m: ViewModel, actions: Observer[Action])(using - router: Router[Page] - ): Anchor = apply(Val(m), actions) - - def container($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = a(mods($m, actions)) - - def container(m: ViewModel, actions: Observer[Action])(using - Router[Page] - ): Anchor = container(Val(m), actions) - - private def mods($m: Signal[Page], actions: Observer[Action])(using - router: Router[Page] - ): Modifier[Anchor] = - Seq( - href <-- $m.map(router.absoluteUrlForPage).recover { case _ => - Some("invalid url") - }, - composeEvents(onClick.noKeyMod.preventDefault)( - _.sample($m) - .map(NavigateTo.apply) - ) --> actions - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala deleted file mode 100644 index 87f825a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.pages.dashboard.DashboardPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -class DashboardPageConnector(actionBus: Observer[Action])(using - router: Router[Page] -): - def apply: HtmlElement = - AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala deleted file mode 100644 index e9b0f71..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala +++ /dev/null @@ -1,76 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailKriteriaPage -import pages.detail.components.DukazyKriteria -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -object DetailKriteriaPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailKriteriaPageConnector( - state: DetailKriteriaPageConnector.AppState -)( - $page: Signal[Page.DetailKriteria] -)(using Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => - (p.osobniCislo.value, p.parametr.value, p.kriterium.value) - )((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map( - FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) - ) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, s, $s) => - DetailKriteriaPage($s)(state.actionBus.contramap { - case DukazyKriteria.Add => - NavigateTo( - Page.UpravDukazKriteria( - Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), - Page.Titled(s.parametr.id, Some(s.parametr.nazev)), - Page.Titled(s.kriterium.id, Some(s.kriterium.id)) - ) - ) - }) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): DetailKriteriaPage.ViewModel = - DetailKriteriaPage.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala deleted file mode 100644 index 4d8024f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ /dev/null @@ -1,57 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import pages.detail.DetailPage -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import fiftyforms.ui.components.tailwind.Color - -object DetailPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailPageConnector(state: DetailPageConnector.AppState)( - $page: Signal[Page.Detail] -)(using router: Router[Page]): - val $oscChangeSignal = $page.splitOne(_.osobniCislo.value)((osc, _, _) => osc) - val $pageChangeSignal = - $oscChangeSignal.flatMap(osc => - EventStream.fromSeq(Seq(FetchUserDetails(osc), FetchParameters(osc))) - ) - // TODO: filter the value based on the current osc - // OSC change will fetch new data, but still - // - we need to be sure that what we got is really what we ought to display - // - we want to display stale data accordingly (at least with loading indicator) - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - def apply: HtmlElement = - AppPage(state.actionBus)( - $data.combineWithFn($params)(_ zip _) - .map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: List[Parameter] - ): DetailPage.ViewModel = - DetailPage.ViewModel( - o.toDetailOsoby, - p.map( - _.toParametr(param => - PageLink.container(Page.DetailParametru(o, param), state.actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala deleted file mode 100644 index fdaa7f8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailParametruPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink - -object DetailParametruPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailParametruPageConnector( - state: DetailParametruPageConnector.AppState -)( - $page: Signal[Page.DetailParametru] -)(using router: Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => (p.osobniCislo.value, p.parametr.value))((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map(FetchParameter(_, _)) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - } yield (da, pb) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailParametruPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter - ): DetailParametruPage.ViewModel = - DetailParametruPage.ViewModel( - o.toDetailOsoby, - p.toParametr(p => - PageLink.container(Page.DetailParametru(o, p), state.actionBus) - ), - p.criteria.map( - _.toKriterium { c => - PageLink.container( - Page.DetailKriteria(o, p, c), - state.actionBus - ) - } - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala deleted file mode 100644 index 049aa22..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -case class DirectoryPageConnector( - $input: EventStream[List[UserInfo]], - actionBus: Observer[Action] -)(using router: Router[Page]): - val $data = $input.startWithNone - val $actionSignal = EventStream.fromValue(FetchDirectory) - - def apply: HtmlElement = - AppPage(actionBus)( - $data.split(_ => ())((_, _, s) => - pages.directory.DirectoryPage( - s.map( - _.map( - _.toUserRow(u => - PageLink.container( - Page.Detail(Page.Titled(u.personalNumber)), - actionBus - ) - ) - ) - ) - ) - ), - $actionSignal --> actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala deleted file mode 100644 index 88e7450..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ /dev/null @@ -1,68 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii -import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria -import fiftyforms.ui.components.tailwind.Color - -extension (o: UserInfo) - def toDetailOsoby: DetailOsoby.ViewModel = - DetailOsoby.ViewModel( - o.personalNumber, - o.name, - o.email, - o.phone, - o.img, - o.mainFunction.map(f => - DetailOsoby.Funkce.ViewModel(f.name, f.dept, f.ou) - ), - o.userContracts.headOption.map(c => - DetailOsoby.PracovniPomer.ViewModel(c.rel, c.startDate, c.endDate) - ) - ) - -extension (param: Parameter) - def toParametr(container: Parameter => Anchor): DetailParametru.ViewModel = - DetailParametru.ViewModel( - id = param.id, - nazev = param.name, - popis = param.description, - status = "Nesplněno", - statusColor = Color.red, - a = container(param) - ) - -extension (crit: ParameterCriteria) - def toKriterium( - container: ParameterCriteria => Anchor - ): DetailKriteria.ViewModel = - DetailKriteria.ViewModel( - nazev = crit.criteriumText, - kapitola = crit.chapterId, - bod = crit.itemId, - status = "Nesplněno", - statusColor = Color.red, - splneno = false, - dukazy = Nil, - container = container(crit) - ) - -extension (user: UserInfo) - def toUserRow( - container: UserInfo => HtmlElement = (_: UserInfo) => div() - ): UserRow.ViewModel = - UserRow.ViewModel( - osobniCislo = user.personalNumber.toString, - celeJmeno = user.name, - prijmeni = user.surname, - hlavniFunkce = user.mainFunction.map(_.name), - img = user.img, - container = () => container(user) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala deleted file mode 100644 index 7d5b2c1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.dashboard - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.Page - -object DashboardPage: - - def render: HtmlElement = - div("Dashboard page") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala deleted file mode 100644 index 9ef0698..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala +++ /dev/null @@ -1,31 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailKriteriaPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply($m: Signal[ViewModel])( - action: Observer[DukazyKriteria.Action] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - DukazyKriteria($m.map(_.kriterium.dukazy))(action) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala deleted file mode 100644 index 3b316e1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object DetailPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametry: SeznamParametru.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby($m.map(_.osoba)), - SeznamParametru($m.map(_.parametry)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala deleted file mode 100644 index 17224cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala +++ /dev/null @@ -1,24 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailParametruPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriteria: SeznamKriterii.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby.header($m.map(_.osoba)), - DetailParametru($m.map(_.parametr)), - SeznamKriterii($m.map(_.kriteria)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala deleted file mode 100644 index a3b00af..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala +++ /dev/null @@ -1,103 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import scala.scalajs.js.annotation.JSExportTopLevel -import scala.scalajs.js.annotation.JSExport -import scala.scalajs.js.annotation.JSImport -import scala.scalajs.js -import org.scalajs.dom -import com.raquo.laminar.api.L.{*, given} -import scala.scalajs.js.Date -import com.raquo.waypoint.Router -import com.raquo.waypoint.SplitRender -import cz.e_bs.cmi.mdr.pdb.app.services.DataFetcher -import scala.scalajs.js.JSON -import zio.json._ -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.state.AppState - -@js.native -@JSImport("stylesheets/main.css", JSImport.Namespace) -object Css extends js.Any - -@js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native -@JSImport("params/pdb-params.json", JSImport.Default) -object pdbParams extends js.Object - -@JSExportTopLevel("app") -object Main: - - @JSExport - def main(args: Array[String]): Unit = - import Routes.given - onLoad { - setupAirstream() - val appContainer = dom.document.querySelector("#app") - val _ = - render( - appContainer, - renderPage(state.MockAppState(using unsafeWindowOwner, router)) - ) - } - - private def onLoad(f: => Unit): Unit = - documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) - - private def setupAirstream()(using router: Router[Page]): Unit = - AirstreamError.registerUnhandledErrorCallback(err => - router.forcePage( - Page.UnhandledError( - Some(err.getClass.getName), // TODO: Fill only in dev mode - Some(err.getMessage) - ) - ) - ) - - def renderPage(state: AppState)(using - router: Router[Page] - ): HtmlElement = - val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) - .collectSignal[Page.Detail]( - connectors - .DetailPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailParametru]( - connectors - .DetailParametruPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailKriteria]( - connectors - .DetailKriteriaPageConnector(state)(_) - .apply - ) - .collectSignal[Page.UpravDukazKriteria]( - pages.detail.UpravDukaz.Connector(state)(_).apply - ) - .collectStatic(Page.Dashboard)( - connectors.DashboardPageConnector(state.actionBus).apply - ) - .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) - ) - .collect[Page.UnhandledError](pg => - pages.errors - .UnhandledErrorPage( - pages.errors.UnhandledErrorPage - .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), - state.actionBus - ) - ) - .collectStatic(Page.Directory)( - connectors - .DirectoryPageConnector(state.users, state.actionBus) - .apply - ) - div(cls := "h-full", child <-- pageSplitter.$view) - - // Pull in the stylesheet - val css: Css.type = Css diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala deleted file mode 100644 index 3bbc8c0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ /dev/null @@ -1,198 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.* -import org.scalajs.dom -import zio.json.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import scala.scalajs.js -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.Page.Titled - -// enum is not working with Waypoints' SplitRender collectStatic -sealed abstract class Page( - val id: String, - val title: String, - val parent: Option[Page] -) { - val path: Vector[Page] = - parent match - case None => Vector(this) - case Some(p) => p.path :+ this - - val isRoot: Boolean = parent.isEmpty -} - -object Page: - - case class Titled[V](value: V, title: Option[String] = None): - val show: String = title.getOrElse(value.toString) - - case object Directory extends Page("directory", "Adresář", None) - - case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) - - case class Detail(osobniCislo: Titled[OsobniCislo]) - extends Page("user", osobniCislo.show, Some(Directory)) - - object Detail { - def apply(o: UserInfo): Detail = Detail( - Titled(o.personalNumber, Some(o.name)) - ) - } - - case class DetailParametru( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String] - ) extends Page( - "parameter", - parametr.show, - Some(Detail(osobniCislo)) - ) - - object DetailParametru { - def apply(o: UserInfo, p: Parameter): DetailParametru = - DetailParametru( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)) - ) - } - - case class DetailKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "criteria", - kriterium.show, - Some(DetailParametru(osobniCislo, parametr)) - ) - - object DetailKriteria { - def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = - DetailKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class UpravDukazKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "addProof", - "Důkaz", - Some(DetailKriteria(osobniCislo, parametr, kriterium)) - ) - - object UpravDukazKriteria { - def apply( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): UpravDukazKriteria = - UpravDukazKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class NotFound(url: String) extends Page("404", "404", Some(Directory)) - - case class UnhandledError( - errorName: Option[String], - errorMessage: Option[String] - ) extends Page("500", "Unexpected error", Some(Directory)) - -object Routes: - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) - given [V: JsonEncoder]: JsonEncoder[Titled[V]] = - DeriveJsonEncoder.gen[Titled[V]] - given [V: JsonDecoder]: JsonDecoder[Titled[V]] = - DeriveJsonDecoder.gen[Titled[V]] - given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] - given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] - - val base = - js.`import`.meta.env.BASE_URL - .asInstanceOf[String] + "app" - - val homePage: Page = Page.Directory - - given router: Router[Page] = Router[Page]( - routes = List( - Route.static(homePage, root / endOfSegments, basePath = base), - Route.static( - Page.Dashboard, - root / "dashboard" / endOfSegments, - basePath = base - ), - Route[Page.Detail, String]( - encode = _.osobniCislo.value.toString, - decode = osc => Page.Detail(Titled(OsobniCislo(osc))), - root / "osoba" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.DetailParametru, (String, String)]( - encode = p => (p.osobniCislo.value.toString, p.parametr.value), - decode = - p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / endOfSegments, - basePath = base - ), - Route[Page.DetailKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.DetailKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.UpravDukazKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.UpravDukazKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / "edit" / endOfSegments, - basePath = base - ) - ), - serializePage = _.toJson, - deserializePage = _.fromJson[Page] - .fold(s => throw IllegalStateException(s), identity), - getPageTitle = _.title, - routeFallback = url => Page.NotFound(url), - deserializeFallback = _ => Page.Dashboard - )( - $popStateEvent = windowEvents.onPopState, - owner = unsafeWindowOwner - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala deleted file mode 100644 index b24b82b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -sealed trait Action - -case object FetchDirectory extends Action -case class FetchUserDetails(osc: OsobniCislo) extends Action -case class FetchParameters(osc: OsobniCislo) extends Action -case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action -case class FetchParameterCriteria( - osc: OsobniCislo, - paramId: String, - critId: String, - page: (UserInfo, Parameter, ParameterCriteria) => Page -) extends Action -case class FetchAvailableFiles(osc: OsobniCislo) extends Action -case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala deleted file mode 100644 index c637d35..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala +++ /dev/null @@ -1,82 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.{UserProfile, UserInfo, OsobniCislo} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo -import cz.e_bs.cmi.mdr.pdb.UserFunction - -object AppPage: - // TODO: pages by logged in user - val pages: List[Page] = List(Page.Directory, Page.Dashboard) - - import NavigationBar.{Logo, MenuItem} - - val logo = Logo( - "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", - "Workflow" - ) - - // TODO: menu items by user profile - val userMenu = - List( - MenuItem("Your Profile"), - MenuItem("Settings"), - MenuItem("Sign out") - ) - - // TODO: load user profile - val $userProfile = Var( - UserProfile( - "tom", - UserInfo( - OsobniCislo("1031"), - "tom", - "Tom", - "Cook", - None, - None, - Some("tom@example.com"), - Some("+420 222 866 180"), - Some( - UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") - ), - Nil, - None - ) - ) - ) - - val $userInfo = $userProfile.signal.map(_.userInfo) - - type ViewModel = Option[HtmlElement] - def apply( - actionBus: Observer[Action] - )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using - router: Router[Page] - ): HtmlElement = - PageLayout(actionBus)( - $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => - PageLayout.ViewModel( - NavigationBar.ViewModel( - u, - pages.map(p => - NavigationBar.Link( - () => - PageLink( - p, - actionBus - ), - p == cp - ) - ), - userMenu, - logo - ), - c - ) - ), - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala deleted file mode 100644 index bf473c6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ /dev/null @@ -1,143 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden -import fiftyforms.ui.components.tailwind.list.IconText.ViewModel -import fiftyforms.ui.components.tailwind.Icons -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Action - -object Breadcrumbs: - - private def slash = - import svg.{*, given} - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-300", - xmlns := "http://www.w3.org/2000/svg", - fill := "currentColor", - viewBox := "0 0 20 20", - ariaHidden := true, - path( - d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" - ) - ) - - def backIcon = - Icons.solid - .`arrow-narrow-left`() - .amend( - svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" - ) - - object Link: - case class ViewModel( - page: Page, - icon: Option[SvgElement], - extraClasses: String, - text: String, - textClasses: Option[String] = None - ) - - val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" - - def shortHome(p: Page) = ViewModel( - p, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - "Zpět na úvodní stránku", - None - ) - - def fullHome(p: Page) = ViewModel( - p, - Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), - "text-gray-400 hover:text-gray-500", - "Domů", - Some("sr-only") - ) - - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - PageLink - .container($m.map(_.page), actionBus) - .amend( - cls <-- $m.map(_.extraClasses), - child.maybe <-- $m.map(_.icon), - span( - cls <-- $m.map(_.textClasses.getOrElse("")), - child.text <-- $m.map(_.text) - ) - ) - - object FullBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - ol( - role := "list", - cls := "flex items-center space-x-4", - children <-- $m.map(_.path).split(_.id)((_, _, $p) => - li( - div( - cls := "flex items-center", - child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), - Link( - $p.map(p => - if (p.isRoot) then Link.fullHome(p) - else - Link.ViewModel( - p, - None, - s"ml-4 max-w-xs truncate ${Link.baseClasses}", - p.title - ) - ), - actionBus - ) - ) - ) - ) - ) - - object ShortBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - Link( - $m.map { p => - val target = p.parent.getOrElse(p) - if target.isRoot then Link.shortHome(target) - else - Link.ViewModel( - target, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - s"Zpět na ${target.title}" - ) - }, - actionBus - ) - - def apply(actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - nav( - cls := "flex", - aria.label := "Breadcrumb", - div( - cls := "flex sm:hidden", - ShortBreadcrumbs(router.$currentPage, actionBus) - ), - div( - cls := "hidden sm:block", - FullBreadcrumbs(router.$currentPage, actionBus) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala deleted file mode 100644 index 0db2047..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ /dev/null @@ -1,242 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent -import fiftyforms.ui.components.tailwind.Icons -import fiftyforms.ui.components.tailwind.Avatar -import cz.e_bs.cmi.mdr.pdb.UserInfo -import io.laminext.syntax.core.* - -object NavigationBar: - - case class Logo(img: String, name: String) - case class Link(a: () => Anchor, active: Boolean) - case class MenuItem(title: String) - - case class ViewModel( - userInfo: UserInfo, - pages: List[Link], - userMenu: List[MenuItem], - logo: Logo - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - val $userInfo = $m.map(_.userInfo) - val mobileMenuOpen = Var(false) - - val desktopOnly = cls("hidden md:block") - val mobileOnly = cls("md:hidden") - - inline def avatarImage(size: Int = 8) = - Avatar($userInfo.map(_.img)).avatarImage(size) - - def notificationButton = button( - tpe := "button", - cls := List( - "bg-indigo-600", - "focus:outline-none", - "focus:ring-2", - "focus:ring-offset-2", - "focus:ring-offset-indigo-600", - "focus:ring-white", - "hover:text-white", - "p-1", - "rounded-full", - "text-indigo-200" - ), - span(cls := "sr-only", "View notifications"), - Icons.outline.bell() - ) - - def userProfile: HtmlElement = - val menuOpen = Var(false) - - def menuItem(item: MenuItem, idx: Int): HtmlElement = - a( - href := "#", - cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", - role := "menuitem", - tabIndex := -1, - idAttr := s"user-menu-item-$idx", - item.title - ) - - div( - cls := "ml-3 relative", - div( - button( - tpe := "button", - cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - idAttr := "user-menu-button", - aria.expanded <-- menuOpen.signal, - aria.hasPopup := true, - span(cls := "sr-only", "Open user menu"), - child <-- avatarImage(), - onClick.preventDefault.mapTo( - !menuOpen.now() - ) --> menuOpen.writer - ) - ), - /* - * */ - div( - cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", - cls <-- menuOpen.signal.map { o => - if (o) "md:block" else "md:hidden" - }, - role := "menu", - aria.orientation := "vertical", - aria.labelledBy := "user-menu-button", - tabIndex := -1, - // : keyboard navigation - children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) - ) - ) - - def mobileProfile = - def menuItem(item: MenuItem): HtmlElement = - a( - href := "#", - cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", - item.title - ) - - div( - cls := "pt-4 pb-3 border-t border-indigo-700", - div( - cls := "flex items-center px-5", - div( - cls := "flex-shrink-0", - child <-- avatarImage(10) - ), - div( - cls := "ml-3", - div( - cls := "text-base font-medium text-white", - child.text <-- $userInfo.map(_.name) - ), - child.maybe <-- $userInfo.map( - _.email.map(e => - div( - cls := "text-sm font-medium text-indigo-300", - e - ) - ) - ) - ), - notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) - ), - div( - cls := "mt-3 px-2 space-y-1", - children <-- $m.map(_.userMenu.map(menuItem)) - ) - ) - - def pageLink(page: Link): Anchor = - page - .a() - .amend( - cls := "text-white px-3 py-2 rounded-md text-sm font-medium", - cls := Seq( - "bg-indigo-700" -> page.active, - "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active - ), - ariaCurrent := (if page.active then "page" else "false") - ) - - def logoImg: Image = - img( - cls := "h-8 w-8", - src <-- $m.map(_.logo.img), - alt <-- $m.map(_.logo.name) - ) - - def mobileMenuButton = button( - tpe := "button", - cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - aria.controls := "mobile-menu", - aria.expanded <-- mobileMenuOpen.signal, - span(cls := "sr-only", "Open main menu"), - Icons.outline - .menu() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") - ), - Icons.outline - .x() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") - ), - onClick.preventDefault.mapTo( - !mobileMenuOpen.now() - ) --> mobileMenuOpen.writer - ) - - def navBarLeft = - div( - cls := "flex items-center", - div(cls := "flex-shrink-0", logoImg), - div( - desktopOnly, - div( - cls := "ml-10 flex items-baseline space-x-4", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ) - ) - ) - - def navBarRight = - div( - desktopOnly, - div( - cls := "ml-4 flex items-center md:ml-6", - notificationButton, - userProfile - ) - ) - - def navBarMobile = - div( - cls := "-mr-2 flex", - mobileOnly, - mobileMenuButton - ) - - def navBar = - div( - cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex items-center justify-between h-16", - navBarLeft, - navBarRight, - navBarMobile - ) - ) - - def mobileMenu = - div( - mobileOnly, - cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }, - idAttr := "mobile-menu", - div( - cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ), - mobileProfile - ) - - nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala deleted file mode 100644 index 75339df..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object PageHeader: - def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = - header( - cls := "bg-white shadow-sm", - div( - cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", - h1( - cls := "text-lg leading-6 font-semibold text-gray-900", - Breadcrumbs(actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala deleted file mode 100644 index 5096463..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ /dev/null @@ -1,30 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import fiftyforms.ui.components.tailwind.Loading -import io.laminext.syntax.core.* - -object PageLayout: - case class ViewModel( - navigation: NavigationBar.ViewModel, - content: Option[HtmlElement] - ) - def apply(actionBus: Observer[Action])( - $m: Signal[ViewModel], - mods: Modifier[HtmlElement]* - )(using router: Router[Page]): HtmlElement = - val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) - div( - cls := "h-full flex flex-col", - NavigationBar($m.map(_.navigation)), - child.maybe <-- router.$currentPage.map(_.isRoot) - .switch(None, Some(PageHeader(actionBus))), - main( - cls := "flex-grow-1 overflow-y-auto", - mods, - child <-- $maybeContent.map(_.getOrElse(Loading)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala deleted file mode 100644 index 62d820f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.LinkSupport.* -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo - -object PageLink: - type ViewModel = Page - def apply($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = - a(mods($m, actions), child.text <-- $m.map(_.title)) - - def apply(m: ViewModel, actions: Observer[Action])(using - router: Router[Page] - ): Anchor = apply(Val(m), actions) - - def container($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = a(mods($m, actions)) - - def container(m: ViewModel, actions: Observer[Action])(using - Router[Page] - ): Anchor = container(Val(m), actions) - - private def mods($m: Signal[Page], actions: Observer[Action])(using - router: Router[Page] - ): Modifier[Anchor] = - Seq( - href <-- $m.map(router.absoluteUrlForPage).recover { case _ => - Some("invalid url") - }, - composeEvents(onClick.noKeyMod.preventDefault)( - _.sample($m) - .map(NavigateTo.apply) - ) --> actions - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala deleted file mode 100644 index 87f825a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.pages.dashboard.DashboardPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -class DashboardPageConnector(actionBus: Observer[Action])(using - router: Router[Page] -): - def apply: HtmlElement = - AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala deleted file mode 100644 index e9b0f71..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala +++ /dev/null @@ -1,76 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailKriteriaPage -import pages.detail.components.DukazyKriteria -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -object DetailKriteriaPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailKriteriaPageConnector( - state: DetailKriteriaPageConnector.AppState -)( - $page: Signal[Page.DetailKriteria] -)(using Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => - (p.osobniCislo.value, p.parametr.value, p.kriterium.value) - )((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map( - FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) - ) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, s, $s) => - DetailKriteriaPage($s)(state.actionBus.contramap { - case DukazyKriteria.Add => - NavigateTo( - Page.UpravDukazKriteria( - Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), - Page.Titled(s.parametr.id, Some(s.parametr.nazev)), - Page.Titled(s.kriterium.id, Some(s.kriterium.id)) - ) - ) - }) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): DetailKriteriaPage.ViewModel = - DetailKriteriaPage.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala deleted file mode 100644 index 4d8024f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ /dev/null @@ -1,57 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import pages.detail.DetailPage -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import fiftyforms.ui.components.tailwind.Color - -object DetailPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailPageConnector(state: DetailPageConnector.AppState)( - $page: Signal[Page.Detail] -)(using router: Router[Page]): - val $oscChangeSignal = $page.splitOne(_.osobniCislo.value)((osc, _, _) => osc) - val $pageChangeSignal = - $oscChangeSignal.flatMap(osc => - EventStream.fromSeq(Seq(FetchUserDetails(osc), FetchParameters(osc))) - ) - // TODO: filter the value based on the current osc - // OSC change will fetch new data, but still - // - we need to be sure that what we got is really what we ought to display - // - we want to display stale data accordingly (at least with loading indicator) - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - def apply: HtmlElement = - AppPage(state.actionBus)( - $data.combineWithFn($params)(_ zip _) - .map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: List[Parameter] - ): DetailPage.ViewModel = - DetailPage.ViewModel( - o.toDetailOsoby, - p.map( - _.toParametr(param => - PageLink.container(Page.DetailParametru(o, param), state.actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala deleted file mode 100644 index fdaa7f8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailParametruPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink - -object DetailParametruPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailParametruPageConnector( - state: DetailParametruPageConnector.AppState -)( - $page: Signal[Page.DetailParametru] -)(using router: Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => (p.osobniCislo.value, p.parametr.value))((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map(FetchParameter(_, _)) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - } yield (da, pb) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailParametruPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter - ): DetailParametruPage.ViewModel = - DetailParametruPage.ViewModel( - o.toDetailOsoby, - p.toParametr(p => - PageLink.container(Page.DetailParametru(o, p), state.actionBus) - ), - p.criteria.map( - _.toKriterium { c => - PageLink.container( - Page.DetailKriteria(o, p, c), - state.actionBus - ) - } - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala deleted file mode 100644 index 049aa22..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -case class DirectoryPageConnector( - $input: EventStream[List[UserInfo]], - actionBus: Observer[Action] -)(using router: Router[Page]): - val $data = $input.startWithNone - val $actionSignal = EventStream.fromValue(FetchDirectory) - - def apply: HtmlElement = - AppPage(actionBus)( - $data.split(_ => ())((_, _, s) => - pages.directory.DirectoryPage( - s.map( - _.map( - _.toUserRow(u => - PageLink.container( - Page.Detail(Page.Titled(u.personalNumber)), - actionBus - ) - ) - ) - ) - ) - ), - $actionSignal --> actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala deleted file mode 100644 index 88e7450..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ /dev/null @@ -1,68 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii -import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria -import fiftyforms.ui.components.tailwind.Color - -extension (o: UserInfo) - def toDetailOsoby: DetailOsoby.ViewModel = - DetailOsoby.ViewModel( - o.personalNumber, - o.name, - o.email, - o.phone, - o.img, - o.mainFunction.map(f => - DetailOsoby.Funkce.ViewModel(f.name, f.dept, f.ou) - ), - o.userContracts.headOption.map(c => - DetailOsoby.PracovniPomer.ViewModel(c.rel, c.startDate, c.endDate) - ) - ) - -extension (param: Parameter) - def toParametr(container: Parameter => Anchor): DetailParametru.ViewModel = - DetailParametru.ViewModel( - id = param.id, - nazev = param.name, - popis = param.description, - status = "Nesplněno", - statusColor = Color.red, - a = container(param) - ) - -extension (crit: ParameterCriteria) - def toKriterium( - container: ParameterCriteria => Anchor - ): DetailKriteria.ViewModel = - DetailKriteria.ViewModel( - nazev = crit.criteriumText, - kapitola = crit.chapterId, - bod = crit.itemId, - status = "Nesplněno", - statusColor = Color.red, - splneno = false, - dukazy = Nil, - container = container(crit) - ) - -extension (user: UserInfo) - def toUserRow( - container: UserInfo => HtmlElement = (_: UserInfo) => div() - ): UserRow.ViewModel = - UserRow.ViewModel( - osobniCislo = user.personalNumber.toString, - celeJmeno = user.name, - prijmeni = user.surname, - hlavniFunkce = user.mainFunction.map(_.name), - img = user.img, - container = () => container(user) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala deleted file mode 100644 index 7d5b2c1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.dashboard - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.Page - -object DashboardPage: - - def render: HtmlElement = - div("Dashboard page") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala deleted file mode 100644 index 9ef0698..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala +++ /dev/null @@ -1,31 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailKriteriaPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply($m: Signal[ViewModel])( - action: Observer[DukazyKriteria.Action] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - DukazyKriteria($m.map(_.kriterium.dukazy))(action) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala deleted file mode 100644 index 3b316e1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object DetailPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametry: SeznamParametru.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby($m.map(_.osoba)), - SeznamParametru($m.map(_.parametry)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala deleted file mode 100644 index 17224cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala +++ /dev/null @@ -1,24 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailParametruPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriteria: SeznamKriterii.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby.header($m.map(_.osoba)), - DetailParametru($m.map(_.parametr)), - SeznamKriterii($m.map(_.kriteria)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala deleted file mode 100644 index dc912fa..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ /dev/null @@ -1,107 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb -package app -package pages.detail - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.UpravDukazForm -import fiftyforms.services.files.File - -object UpravDukaz: - - type ThisPage = Page.UpravDukazKriteria - type PageKey = (OsobniCislo, String, String) - - trait State { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def availableFiles: EventStream[List[File]] - def actionBus: Observer[Action] - } - - def keyOfPage(page: ThisPage): PageKey = - (page.osobniCislo.value, page.parametr.value, page.kriterium.value) - - def onChangeAction(key: PageKey): Action = - FetchParameterCriteria( - key._1, - key._2, - key._3, - Page.UpravDukazKriteria(_, _, _) - ) - - class Connector(state: State)($page: Signal[ThisPage])(using Router[Page]): - val $paramChangeSignal = $page.splitOne(keyOfPage)((x, _, _) => x) - val $pageChangeSignal = $paramChangeSignal.map(onChangeAction) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.split(_ => ())((_, s, $s) => - PageComponent( - $s.map(buildModel), - state.availableFiles, - state.actionBus.contramap { - case UpravDukazForm.Cancelled => - NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) - case UpravDukazForm.AvailableFilesRequested => - FetchAvailableFiles(s._1.personalNumber) - } - ) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): PageComponent.ViewModel = - import connectors.* - PageComponent.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) - - object PageComponent: - import components.* - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply( - $m: Signal[ViewModel], - availableFilesStream: EventStream[List[File]], - events: Observer[UpravDukazForm.Event] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - UpravDukazForm(availableFilesStream)(events) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala deleted file mode 100644 index a3b00af..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala +++ /dev/null @@ -1,103 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import scala.scalajs.js.annotation.JSExportTopLevel -import scala.scalajs.js.annotation.JSExport -import scala.scalajs.js.annotation.JSImport -import scala.scalajs.js -import org.scalajs.dom -import com.raquo.laminar.api.L.{*, given} -import scala.scalajs.js.Date -import com.raquo.waypoint.Router -import com.raquo.waypoint.SplitRender -import cz.e_bs.cmi.mdr.pdb.app.services.DataFetcher -import scala.scalajs.js.JSON -import zio.json._ -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.state.AppState - -@js.native -@JSImport("stylesheets/main.css", JSImport.Namespace) -object Css extends js.Any - -@js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native -@JSImport("params/pdb-params.json", JSImport.Default) -object pdbParams extends js.Object - -@JSExportTopLevel("app") -object Main: - - @JSExport - def main(args: Array[String]): Unit = - import Routes.given - onLoad { - setupAirstream() - val appContainer = dom.document.querySelector("#app") - val _ = - render( - appContainer, - renderPage(state.MockAppState(using unsafeWindowOwner, router)) - ) - } - - private def onLoad(f: => Unit): Unit = - documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) - - private def setupAirstream()(using router: Router[Page]): Unit = - AirstreamError.registerUnhandledErrorCallback(err => - router.forcePage( - Page.UnhandledError( - Some(err.getClass.getName), // TODO: Fill only in dev mode - Some(err.getMessage) - ) - ) - ) - - def renderPage(state: AppState)(using - router: Router[Page] - ): HtmlElement = - val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) - .collectSignal[Page.Detail]( - connectors - .DetailPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailParametru]( - connectors - .DetailParametruPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailKriteria]( - connectors - .DetailKriteriaPageConnector(state)(_) - .apply - ) - .collectSignal[Page.UpravDukazKriteria]( - pages.detail.UpravDukaz.Connector(state)(_).apply - ) - .collectStatic(Page.Dashboard)( - connectors.DashboardPageConnector(state.actionBus).apply - ) - .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) - ) - .collect[Page.UnhandledError](pg => - pages.errors - .UnhandledErrorPage( - pages.errors.UnhandledErrorPage - .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), - state.actionBus - ) - ) - .collectStatic(Page.Directory)( - connectors - .DirectoryPageConnector(state.users, state.actionBus) - .apply - ) - div(cls := "h-full", child <-- pageSplitter.$view) - - // Pull in the stylesheet - val css: Css.type = Css diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala deleted file mode 100644 index 3bbc8c0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ /dev/null @@ -1,198 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.* -import org.scalajs.dom -import zio.json.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import scala.scalajs.js -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.Page.Titled - -// enum is not working with Waypoints' SplitRender collectStatic -sealed abstract class Page( - val id: String, - val title: String, - val parent: Option[Page] -) { - val path: Vector[Page] = - parent match - case None => Vector(this) - case Some(p) => p.path :+ this - - val isRoot: Boolean = parent.isEmpty -} - -object Page: - - case class Titled[V](value: V, title: Option[String] = None): - val show: String = title.getOrElse(value.toString) - - case object Directory extends Page("directory", "Adresář", None) - - case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) - - case class Detail(osobniCislo: Titled[OsobniCislo]) - extends Page("user", osobniCislo.show, Some(Directory)) - - object Detail { - def apply(o: UserInfo): Detail = Detail( - Titled(o.personalNumber, Some(o.name)) - ) - } - - case class DetailParametru( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String] - ) extends Page( - "parameter", - parametr.show, - Some(Detail(osobniCislo)) - ) - - object DetailParametru { - def apply(o: UserInfo, p: Parameter): DetailParametru = - DetailParametru( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)) - ) - } - - case class DetailKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "criteria", - kriterium.show, - Some(DetailParametru(osobniCislo, parametr)) - ) - - object DetailKriteria { - def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = - DetailKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class UpravDukazKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "addProof", - "Důkaz", - Some(DetailKriteria(osobniCislo, parametr, kriterium)) - ) - - object UpravDukazKriteria { - def apply( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): UpravDukazKriteria = - UpravDukazKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class NotFound(url: String) extends Page("404", "404", Some(Directory)) - - case class UnhandledError( - errorName: Option[String], - errorMessage: Option[String] - ) extends Page("500", "Unexpected error", Some(Directory)) - -object Routes: - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) - given [V: JsonEncoder]: JsonEncoder[Titled[V]] = - DeriveJsonEncoder.gen[Titled[V]] - given [V: JsonDecoder]: JsonDecoder[Titled[V]] = - DeriveJsonDecoder.gen[Titled[V]] - given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] - given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] - - val base = - js.`import`.meta.env.BASE_URL - .asInstanceOf[String] + "app" - - val homePage: Page = Page.Directory - - given router: Router[Page] = Router[Page]( - routes = List( - Route.static(homePage, root / endOfSegments, basePath = base), - Route.static( - Page.Dashboard, - root / "dashboard" / endOfSegments, - basePath = base - ), - Route[Page.Detail, String]( - encode = _.osobniCislo.value.toString, - decode = osc => Page.Detail(Titled(OsobniCislo(osc))), - root / "osoba" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.DetailParametru, (String, String)]( - encode = p => (p.osobniCislo.value.toString, p.parametr.value), - decode = - p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / endOfSegments, - basePath = base - ), - Route[Page.DetailKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.DetailKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.UpravDukazKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.UpravDukazKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / "edit" / endOfSegments, - basePath = base - ) - ), - serializePage = _.toJson, - deserializePage = _.fromJson[Page] - .fold(s => throw IllegalStateException(s), identity), - getPageTitle = _.title, - routeFallback = url => Page.NotFound(url), - deserializeFallback = _ => Page.Dashboard - )( - $popStateEvent = windowEvents.onPopState, - owner = unsafeWindowOwner - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala deleted file mode 100644 index b24b82b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -sealed trait Action - -case object FetchDirectory extends Action -case class FetchUserDetails(osc: OsobniCislo) extends Action -case class FetchParameters(osc: OsobniCislo) extends Action -case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action -case class FetchParameterCriteria( - osc: OsobniCislo, - paramId: String, - critId: String, - page: (UserInfo, Parameter, ParameterCriteria) => Page -) extends Action -case class FetchAvailableFiles(osc: OsobniCislo) extends Action -case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala deleted file mode 100644 index c637d35..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala +++ /dev/null @@ -1,82 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.{UserProfile, UserInfo, OsobniCislo} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo -import cz.e_bs.cmi.mdr.pdb.UserFunction - -object AppPage: - // TODO: pages by logged in user - val pages: List[Page] = List(Page.Directory, Page.Dashboard) - - import NavigationBar.{Logo, MenuItem} - - val logo = Logo( - "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", - "Workflow" - ) - - // TODO: menu items by user profile - val userMenu = - List( - MenuItem("Your Profile"), - MenuItem("Settings"), - MenuItem("Sign out") - ) - - // TODO: load user profile - val $userProfile = Var( - UserProfile( - "tom", - UserInfo( - OsobniCislo("1031"), - "tom", - "Tom", - "Cook", - None, - None, - Some("tom@example.com"), - Some("+420 222 866 180"), - Some( - UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") - ), - Nil, - None - ) - ) - ) - - val $userInfo = $userProfile.signal.map(_.userInfo) - - type ViewModel = Option[HtmlElement] - def apply( - actionBus: Observer[Action] - )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using - router: Router[Page] - ): HtmlElement = - PageLayout(actionBus)( - $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => - PageLayout.ViewModel( - NavigationBar.ViewModel( - u, - pages.map(p => - NavigationBar.Link( - () => - PageLink( - p, - actionBus - ), - p == cp - ) - ), - userMenu, - logo - ), - c - ) - ), - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala deleted file mode 100644 index bf473c6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ /dev/null @@ -1,143 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden -import fiftyforms.ui.components.tailwind.list.IconText.ViewModel -import fiftyforms.ui.components.tailwind.Icons -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Action - -object Breadcrumbs: - - private def slash = - import svg.{*, given} - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-300", - xmlns := "http://www.w3.org/2000/svg", - fill := "currentColor", - viewBox := "0 0 20 20", - ariaHidden := true, - path( - d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" - ) - ) - - def backIcon = - Icons.solid - .`arrow-narrow-left`() - .amend( - svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" - ) - - object Link: - case class ViewModel( - page: Page, - icon: Option[SvgElement], - extraClasses: String, - text: String, - textClasses: Option[String] = None - ) - - val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" - - def shortHome(p: Page) = ViewModel( - p, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - "Zpět na úvodní stránku", - None - ) - - def fullHome(p: Page) = ViewModel( - p, - Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), - "text-gray-400 hover:text-gray-500", - "Domů", - Some("sr-only") - ) - - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - PageLink - .container($m.map(_.page), actionBus) - .amend( - cls <-- $m.map(_.extraClasses), - child.maybe <-- $m.map(_.icon), - span( - cls <-- $m.map(_.textClasses.getOrElse("")), - child.text <-- $m.map(_.text) - ) - ) - - object FullBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - ol( - role := "list", - cls := "flex items-center space-x-4", - children <-- $m.map(_.path).split(_.id)((_, _, $p) => - li( - div( - cls := "flex items-center", - child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), - Link( - $p.map(p => - if (p.isRoot) then Link.fullHome(p) - else - Link.ViewModel( - p, - None, - s"ml-4 max-w-xs truncate ${Link.baseClasses}", - p.title - ) - ), - actionBus - ) - ) - ) - ) - ) - - object ShortBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - Link( - $m.map { p => - val target = p.parent.getOrElse(p) - if target.isRoot then Link.shortHome(target) - else - Link.ViewModel( - target, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - s"Zpět na ${target.title}" - ) - }, - actionBus - ) - - def apply(actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - nav( - cls := "flex", - aria.label := "Breadcrumb", - div( - cls := "flex sm:hidden", - ShortBreadcrumbs(router.$currentPage, actionBus) - ), - div( - cls := "hidden sm:block", - FullBreadcrumbs(router.$currentPage, actionBus) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala deleted file mode 100644 index 0db2047..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ /dev/null @@ -1,242 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent -import fiftyforms.ui.components.tailwind.Icons -import fiftyforms.ui.components.tailwind.Avatar -import cz.e_bs.cmi.mdr.pdb.UserInfo -import io.laminext.syntax.core.* - -object NavigationBar: - - case class Logo(img: String, name: String) - case class Link(a: () => Anchor, active: Boolean) - case class MenuItem(title: String) - - case class ViewModel( - userInfo: UserInfo, - pages: List[Link], - userMenu: List[MenuItem], - logo: Logo - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - val $userInfo = $m.map(_.userInfo) - val mobileMenuOpen = Var(false) - - val desktopOnly = cls("hidden md:block") - val mobileOnly = cls("md:hidden") - - inline def avatarImage(size: Int = 8) = - Avatar($userInfo.map(_.img)).avatarImage(size) - - def notificationButton = button( - tpe := "button", - cls := List( - "bg-indigo-600", - "focus:outline-none", - "focus:ring-2", - "focus:ring-offset-2", - "focus:ring-offset-indigo-600", - "focus:ring-white", - "hover:text-white", - "p-1", - "rounded-full", - "text-indigo-200" - ), - span(cls := "sr-only", "View notifications"), - Icons.outline.bell() - ) - - def userProfile: HtmlElement = - val menuOpen = Var(false) - - def menuItem(item: MenuItem, idx: Int): HtmlElement = - a( - href := "#", - cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", - role := "menuitem", - tabIndex := -1, - idAttr := s"user-menu-item-$idx", - item.title - ) - - div( - cls := "ml-3 relative", - div( - button( - tpe := "button", - cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - idAttr := "user-menu-button", - aria.expanded <-- menuOpen.signal, - aria.hasPopup := true, - span(cls := "sr-only", "Open user menu"), - child <-- avatarImage(), - onClick.preventDefault.mapTo( - !menuOpen.now() - ) --> menuOpen.writer - ) - ), - /* - * */ - div( - cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", - cls <-- menuOpen.signal.map { o => - if (o) "md:block" else "md:hidden" - }, - role := "menu", - aria.orientation := "vertical", - aria.labelledBy := "user-menu-button", - tabIndex := -1, - // : keyboard navigation - children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) - ) - ) - - def mobileProfile = - def menuItem(item: MenuItem): HtmlElement = - a( - href := "#", - cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", - item.title - ) - - div( - cls := "pt-4 pb-3 border-t border-indigo-700", - div( - cls := "flex items-center px-5", - div( - cls := "flex-shrink-0", - child <-- avatarImage(10) - ), - div( - cls := "ml-3", - div( - cls := "text-base font-medium text-white", - child.text <-- $userInfo.map(_.name) - ), - child.maybe <-- $userInfo.map( - _.email.map(e => - div( - cls := "text-sm font-medium text-indigo-300", - e - ) - ) - ) - ), - notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) - ), - div( - cls := "mt-3 px-2 space-y-1", - children <-- $m.map(_.userMenu.map(menuItem)) - ) - ) - - def pageLink(page: Link): Anchor = - page - .a() - .amend( - cls := "text-white px-3 py-2 rounded-md text-sm font-medium", - cls := Seq( - "bg-indigo-700" -> page.active, - "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active - ), - ariaCurrent := (if page.active then "page" else "false") - ) - - def logoImg: Image = - img( - cls := "h-8 w-8", - src <-- $m.map(_.logo.img), - alt <-- $m.map(_.logo.name) - ) - - def mobileMenuButton = button( - tpe := "button", - cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - aria.controls := "mobile-menu", - aria.expanded <-- mobileMenuOpen.signal, - span(cls := "sr-only", "Open main menu"), - Icons.outline - .menu() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") - ), - Icons.outline - .x() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") - ), - onClick.preventDefault.mapTo( - !mobileMenuOpen.now() - ) --> mobileMenuOpen.writer - ) - - def navBarLeft = - div( - cls := "flex items-center", - div(cls := "flex-shrink-0", logoImg), - div( - desktopOnly, - div( - cls := "ml-10 flex items-baseline space-x-4", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ) - ) - ) - - def navBarRight = - div( - desktopOnly, - div( - cls := "ml-4 flex items-center md:ml-6", - notificationButton, - userProfile - ) - ) - - def navBarMobile = - div( - cls := "-mr-2 flex", - mobileOnly, - mobileMenuButton - ) - - def navBar = - div( - cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex items-center justify-between h-16", - navBarLeft, - navBarRight, - navBarMobile - ) - ) - - def mobileMenu = - div( - mobileOnly, - cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }, - idAttr := "mobile-menu", - div( - cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ), - mobileProfile - ) - - nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala deleted file mode 100644 index 75339df..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object PageHeader: - def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = - header( - cls := "bg-white shadow-sm", - div( - cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", - h1( - cls := "text-lg leading-6 font-semibold text-gray-900", - Breadcrumbs(actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala deleted file mode 100644 index 5096463..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ /dev/null @@ -1,30 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import fiftyforms.ui.components.tailwind.Loading -import io.laminext.syntax.core.* - -object PageLayout: - case class ViewModel( - navigation: NavigationBar.ViewModel, - content: Option[HtmlElement] - ) - def apply(actionBus: Observer[Action])( - $m: Signal[ViewModel], - mods: Modifier[HtmlElement]* - )(using router: Router[Page]): HtmlElement = - val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) - div( - cls := "h-full flex flex-col", - NavigationBar($m.map(_.navigation)), - child.maybe <-- router.$currentPage.map(_.isRoot) - .switch(None, Some(PageHeader(actionBus))), - main( - cls := "flex-grow-1 overflow-y-auto", - mods, - child <-- $maybeContent.map(_.getOrElse(Loading)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala deleted file mode 100644 index 62d820f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.LinkSupport.* -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo - -object PageLink: - type ViewModel = Page - def apply($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = - a(mods($m, actions), child.text <-- $m.map(_.title)) - - def apply(m: ViewModel, actions: Observer[Action])(using - router: Router[Page] - ): Anchor = apply(Val(m), actions) - - def container($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = a(mods($m, actions)) - - def container(m: ViewModel, actions: Observer[Action])(using - Router[Page] - ): Anchor = container(Val(m), actions) - - private def mods($m: Signal[Page], actions: Observer[Action])(using - router: Router[Page] - ): Modifier[Anchor] = - Seq( - href <-- $m.map(router.absoluteUrlForPage).recover { case _ => - Some("invalid url") - }, - composeEvents(onClick.noKeyMod.preventDefault)( - _.sample($m) - .map(NavigateTo.apply) - ) --> actions - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala deleted file mode 100644 index 87f825a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.pages.dashboard.DashboardPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -class DashboardPageConnector(actionBus: Observer[Action])(using - router: Router[Page] -): - def apply: HtmlElement = - AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala deleted file mode 100644 index e9b0f71..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala +++ /dev/null @@ -1,76 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailKriteriaPage -import pages.detail.components.DukazyKriteria -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -object DetailKriteriaPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailKriteriaPageConnector( - state: DetailKriteriaPageConnector.AppState -)( - $page: Signal[Page.DetailKriteria] -)(using Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => - (p.osobniCislo.value, p.parametr.value, p.kriterium.value) - )((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map( - FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) - ) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, s, $s) => - DetailKriteriaPage($s)(state.actionBus.contramap { - case DukazyKriteria.Add => - NavigateTo( - Page.UpravDukazKriteria( - Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), - Page.Titled(s.parametr.id, Some(s.parametr.nazev)), - Page.Titled(s.kriterium.id, Some(s.kriterium.id)) - ) - ) - }) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): DetailKriteriaPage.ViewModel = - DetailKriteriaPage.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala deleted file mode 100644 index 4d8024f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ /dev/null @@ -1,57 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import pages.detail.DetailPage -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import fiftyforms.ui.components.tailwind.Color - -object DetailPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailPageConnector(state: DetailPageConnector.AppState)( - $page: Signal[Page.Detail] -)(using router: Router[Page]): - val $oscChangeSignal = $page.splitOne(_.osobniCislo.value)((osc, _, _) => osc) - val $pageChangeSignal = - $oscChangeSignal.flatMap(osc => - EventStream.fromSeq(Seq(FetchUserDetails(osc), FetchParameters(osc))) - ) - // TODO: filter the value based on the current osc - // OSC change will fetch new data, but still - // - we need to be sure that what we got is really what we ought to display - // - we want to display stale data accordingly (at least with loading indicator) - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - def apply: HtmlElement = - AppPage(state.actionBus)( - $data.combineWithFn($params)(_ zip _) - .map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: List[Parameter] - ): DetailPage.ViewModel = - DetailPage.ViewModel( - o.toDetailOsoby, - p.map( - _.toParametr(param => - PageLink.container(Page.DetailParametru(o, param), state.actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala deleted file mode 100644 index fdaa7f8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailParametruPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink - -object DetailParametruPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailParametruPageConnector( - state: DetailParametruPageConnector.AppState -)( - $page: Signal[Page.DetailParametru] -)(using router: Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => (p.osobniCislo.value, p.parametr.value))((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map(FetchParameter(_, _)) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - } yield (da, pb) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailParametruPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter - ): DetailParametruPage.ViewModel = - DetailParametruPage.ViewModel( - o.toDetailOsoby, - p.toParametr(p => - PageLink.container(Page.DetailParametru(o, p), state.actionBus) - ), - p.criteria.map( - _.toKriterium { c => - PageLink.container( - Page.DetailKriteria(o, p, c), - state.actionBus - ) - } - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala deleted file mode 100644 index 049aa22..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -case class DirectoryPageConnector( - $input: EventStream[List[UserInfo]], - actionBus: Observer[Action] -)(using router: Router[Page]): - val $data = $input.startWithNone - val $actionSignal = EventStream.fromValue(FetchDirectory) - - def apply: HtmlElement = - AppPage(actionBus)( - $data.split(_ => ())((_, _, s) => - pages.directory.DirectoryPage( - s.map( - _.map( - _.toUserRow(u => - PageLink.container( - Page.Detail(Page.Titled(u.personalNumber)), - actionBus - ) - ) - ) - ) - ) - ), - $actionSignal --> actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala deleted file mode 100644 index 88e7450..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ /dev/null @@ -1,68 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii -import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria -import fiftyforms.ui.components.tailwind.Color - -extension (o: UserInfo) - def toDetailOsoby: DetailOsoby.ViewModel = - DetailOsoby.ViewModel( - o.personalNumber, - o.name, - o.email, - o.phone, - o.img, - o.mainFunction.map(f => - DetailOsoby.Funkce.ViewModel(f.name, f.dept, f.ou) - ), - o.userContracts.headOption.map(c => - DetailOsoby.PracovniPomer.ViewModel(c.rel, c.startDate, c.endDate) - ) - ) - -extension (param: Parameter) - def toParametr(container: Parameter => Anchor): DetailParametru.ViewModel = - DetailParametru.ViewModel( - id = param.id, - nazev = param.name, - popis = param.description, - status = "Nesplněno", - statusColor = Color.red, - a = container(param) - ) - -extension (crit: ParameterCriteria) - def toKriterium( - container: ParameterCriteria => Anchor - ): DetailKriteria.ViewModel = - DetailKriteria.ViewModel( - nazev = crit.criteriumText, - kapitola = crit.chapterId, - bod = crit.itemId, - status = "Nesplněno", - statusColor = Color.red, - splneno = false, - dukazy = Nil, - container = container(crit) - ) - -extension (user: UserInfo) - def toUserRow( - container: UserInfo => HtmlElement = (_: UserInfo) => div() - ): UserRow.ViewModel = - UserRow.ViewModel( - osobniCislo = user.personalNumber.toString, - celeJmeno = user.name, - prijmeni = user.surname, - hlavniFunkce = user.mainFunction.map(_.name), - img = user.img, - container = () => container(user) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala deleted file mode 100644 index 7d5b2c1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.dashboard - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.Page - -object DashboardPage: - - def render: HtmlElement = - div("Dashboard page") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala deleted file mode 100644 index 9ef0698..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala +++ /dev/null @@ -1,31 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailKriteriaPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply($m: Signal[ViewModel])( - action: Observer[DukazyKriteria.Action] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - DukazyKriteria($m.map(_.kriterium.dukazy))(action) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala deleted file mode 100644 index 3b316e1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object DetailPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametry: SeznamParametru.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby($m.map(_.osoba)), - SeznamParametru($m.map(_.parametry)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala deleted file mode 100644 index 17224cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala +++ /dev/null @@ -1,24 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailParametruPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriteria: SeznamKriterii.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby.header($m.map(_.osoba)), - DetailParametru($m.map(_.parametr)), - SeznamKriterii($m.map(_.kriteria)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala deleted file mode 100644 index dc912fa..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ /dev/null @@ -1,107 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb -package app -package pages.detail - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.UpravDukazForm -import fiftyforms.services.files.File - -object UpravDukaz: - - type ThisPage = Page.UpravDukazKriteria - type PageKey = (OsobniCislo, String, String) - - trait State { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def availableFiles: EventStream[List[File]] - def actionBus: Observer[Action] - } - - def keyOfPage(page: ThisPage): PageKey = - (page.osobniCislo.value, page.parametr.value, page.kriterium.value) - - def onChangeAction(key: PageKey): Action = - FetchParameterCriteria( - key._1, - key._2, - key._3, - Page.UpravDukazKriteria(_, _, _) - ) - - class Connector(state: State)($page: Signal[ThisPage])(using Router[Page]): - val $paramChangeSignal = $page.splitOne(keyOfPage)((x, _, _) => x) - val $pageChangeSignal = $paramChangeSignal.map(onChangeAction) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.split(_ => ())((_, s, $s) => - PageComponent( - $s.map(buildModel), - state.availableFiles, - state.actionBus.contramap { - case UpravDukazForm.Cancelled => - NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) - case UpravDukazForm.AvailableFilesRequested => - FetchAvailableFiles(s._1.personalNumber) - } - ) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): PageComponent.ViewModel = - import connectors.* - PageComponent.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) - - object PageComponent: - import components.* - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply( - $m: Signal[ViewModel], - availableFilesStream: EventStream[List[File]], - events: Observer[UpravDukazForm.Event] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - UpravDukazForm(availableFilesStream)(events) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala deleted file mode 100644 index ca311b0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.Icons -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Color - -object DetailKriteria: - case class ViewModel( - nazev: String, - kapitola: String, - bod: String, - status: String, - statusColor: Color, - splneno: Boolean, - dukazy: List[DukazKriteria.ViewModel], - container: HtmlElement = div() - ) { - val id = s"${kapitola}${bod}" - } - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h3( - cls := "text-xl leading-6 font-bold text-gray-900", - child.text <-- $m.map(_.nazev) - ), - h3( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - "Kapitola ", - child.text <-- $m.map(_.kapitola), - " bod ", - child <-- $m.map(_.bod) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala deleted file mode 100644 index a3b00af..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala +++ /dev/null @@ -1,103 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import scala.scalajs.js.annotation.JSExportTopLevel -import scala.scalajs.js.annotation.JSExport -import scala.scalajs.js.annotation.JSImport -import scala.scalajs.js -import org.scalajs.dom -import com.raquo.laminar.api.L.{*, given} -import scala.scalajs.js.Date -import com.raquo.waypoint.Router -import com.raquo.waypoint.SplitRender -import cz.e_bs.cmi.mdr.pdb.app.services.DataFetcher -import scala.scalajs.js.JSON -import zio.json._ -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.state.AppState - -@js.native -@JSImport("stylesheets/main.css", JSImport.Namespace) -object Css extends js.Any - -@js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native -@JSImport("params/pdb-params.json", JSImport.Default) -object pdbParams extends js.Object - -@JSExportTopLevel("app") -object Main: - - @JSExport - def main(args: Array[String]): Unit = - import Routes.given - onLoad { - setupAirstream() - val appContainer = dom.document.querySelector("#app") - val _ = - render( - appContainer, - renderPage(state.MockAppState(using unsafeWindowOwner, router)) - ) - } - - private def onLoad(f: => Unit): Unit = - documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) - - private def setupAirstream()(using router: Router[Page]): Unit = - AirstreamError.registerUnhandledErrorCallback(err => - router.forcePage( - Page.UnhandledError( - Some(err.getClass.getName), // TODO: Fill only in dev mode - Some(err.getMessage) - ) - ) - ) - - def renderPage(state: AppState)(using - router: Router[Page] - ): HtmlElement = - val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) - .collectSignal[Page.Detail]( - connectors - .DetailPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailParametru]( - connectors - .DetailParametruPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailKriteria]( - connectors - .DetailKriteriaPageConnector(state)(_) - .apply - ) - .collectSignal[Page.UpravDukazKriteria]( - pages.detail.UpravDukaz.Connector(state)(_).apply - ) - .collectStatic(Page.Dashboard)( - connectors.DashboardPageConnector(state.actionBus).apply - ) - .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) - ) - .collect[Page.UnhandledError](pg => - pages.errors - .UnhandledErrorPage( - pages.errors.UnhandledErrorPage - .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), - state.actionBus - ) - ) - .collectStatic(Page.Directory)( - connectors - .DirectoryPageConnector(state.users, state.actionBus) - .apply - ) - div(cls := "h-full", child <-- pageSplitter.$view) - - // Pull in the stylesheet - val css: Css.type = Css diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala deleted file mode 100644 index 3bbc8c0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ /dev/null @@ -1,198 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.* -import org.scalajs.dom -import zio.json.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import scala.scalajs.js -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.Page.Titled - -// enum is not working with Waypoints' SplitRender collectStatic -sealed abstract class Page( - val id: String, - val title: String, - val parent: Option[Page] -) { - val path: Vector[Page] = - parent match - case None => Vector(this) - case Some(p) => p.path :+ this - - val isRoot: Boolean = parent.isEmpty -} - -object Page: - - case class Titled[V](value: V, title: Option[String] = None): - val show: String = title.getOrElse(value.toString) - - case object Directory extends Page("directory", "Adresář", None) - - case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) - - case class Detail(osobniCislo: Titled[OsobniCislo]) - extends Page("user", osobniCislo.show, Some(Directory)) - - object Detail { - def apply(o: UserInfo): Detail = Detail( - Titled(o.personalNumber, Some(o.name)) - ) - } - - case class DetailParametru( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String] - ) extends Page( - "parameter", - parametr.show, - Some(Detail(osobniCislo)) - ) - - object DetailParametru { - def apply(o: UserInfo, p: Parameter): DetailParametru = - DetailParametru( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)) - ) - } - - case class DetailKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "criteria", - kriterium.show, - Some(DetailParametru(osobniCislo, parametr)) - ) - - object DetailKriteria { - def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = - DetailKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class UpravDukazKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "addProof", - "Důkaz", - Some(DetailKriteria(osobniCislo, parametr, kriterium)) - ) - - object UpravDukazKriteria { - def apply( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): UpravDukazKriteria = - UpravDukazKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class NotFound(url: String) extends Page("404", "404", Some(Directory)) - - case class UnhandledError( - errorName: Option[String], - errorMessage: Option[String] - ) extends Page("500", "Unexpected error", Some(Directory)) - -object Routes: - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) - given [V: JsonEncoder]: JsonEncoder[Titled[V]] = - DeriveJsonEncoder.gen[Titled[V]] - given [V: JsonDecoder]: JsonDecoder[Titled[V]] = - DeriveJsonDecoder.gen[Titled[V]] - given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] - given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] - - val base = - js.`import`.meta.env.BASE_URL - .asInstanceOf[String] + "app" - - val homePage: Page = Page.Directory - - given router: Router[Page] = Router[Page]( - routes = List( - Route.static(homePage, root / endOfSegments, basePath = base), - Route.static( - Page.Dashboard, - root / "dashboard" / endOfSegments, - basePath = base - ), - Route[Page.Detail, String]( - encode = _.osobniCislo.value.toString, - decode = osc => Page.Detail(Titled(OsobniCislo(osc))), - root / "osoba" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.DetailParametru, (String, String)]( - encode = p => (p.osobniCislo.value.toString, p.parametr.value), - decode = - p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / endOfSegments, - basePath = base - ), - Route[Page.DetailKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.DetailKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.UpravDukazKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.UpravDukazKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / "edit" / endOfSegments, - basePath = base - ) - ), - serializePage = _.toJson, - deserializePage = _.fromJson[Page] - .fold(s => throw IllegalStateException(s), identity), - getPageTitle = _.title, - routeFallback = url => Page.NotFound(url), - deserializeFallback = _ => Page.Dashboard - )( - $popStateEvent = windowEvents.onPopState, - owner = unsafeWindowOwner - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala deleted file mode 100644 index b24b82b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -sealed trait Action - -case object FetchDirectory extends Action -case class FetchUserDetails(osc: OsobniCislo) extends Action -case class FetchParameters(osc: OsobniCislo) extends Action -case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action -case class FetchParameterCriteria( - osc: OsobniCislo, - paramId: String, - critId: String, - page: (UserInfo, Parameter, ParameterCriteria) => Page -) extends Action -case class FetchAvailableFiles(osc: OsobniCislo) extends Action -case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala deleted file mode 100644 index c637d35..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala +++ /dev/null @@ -1,82 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.{UserProfile, UserInfo, OsobniCislo} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo -import cz.e_bs.cmi.mdr.pdb.UserFunction - -object AppPage: - // TODO: pages by logged in user - val pages: List[Page] = List(Page.Directory, Page.Dashboard) - - import NavigationBar.{Logo, MenuItem} - - val logo = Logo( - "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", - "Workflow" - ) - - // TODO: menu items by user profile - val userMenu = - List( - MenuItem("Your Profile"), - MenuItem("Settings"), - MenuItem("Sign out") - ) - - // TODO: load user profile - val $userProfile = Var( - UserProfile( - "tom", - UserInfo( - OsobniCislo("1031"), - "tom", - "Tom", - "Cook", - None, - None, - Some("tom@example.com"), - Some("+420 222 866 180"), - Some( - UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") - ), - Nil, - None - ) - ) - ) - - val $userInfo = $userProfile.signal.map(_.userInfo) - - type ViewModel = Option[HtmlElement] - def apply( - actionBus: Observer[Action] - )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using - router: Router[Page] - ): HtmlElement = - PageLayout(actionBus)( - $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => - PageLayout.ViewModel( - NavigationBar.ViewModel( - u, - pages.map(p => - NavigationBar.Link( - () => - PageLink( - p, - actionBus - ), - p == cp - ) - ), - userMenu, - logo - ), - c - ) - ), - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala deleted file mode 100644 index bf473c6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ /dev/null @@ -1,143 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden -import fiftyforms.ui.components.tailwind.list.IconText.ViewModel -import fiftyforms.ui.components.tailwind.Icons -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Action - -object Breadcrumbs: - - private def slash = - import svg.{*, given} - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-300", - xmlns := "http://www.w3.org/2000/svg", - fill := "currentColor", - viewBox := "0 0 20 20", - ariaHidden := true, - path( - d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" - ) - ) - - def backIcon = - Icons.solid - .`arrow-narrow-left`() - .amend( - svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" - ) - - object Link: - case class ViewModel( - page: Page, - icon: Option[SvgElement], - extraClasses: String, - text: String, - textClasses: Option[String] = None - ) - - val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" - - def shortHome(p: Page) = ViewModel( - p, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - "Zpět na úvodní stránku", - None - ) - - def fullHome(p: Page) = ViewModel( - p, - Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), - "text-gray-400 hover:text-gray-500", - "Domů", - Some("sr-only") - ) - - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - PageLink - .container($m.map(_.page), actionBus) - .amend( - cls <-- $m.map(_.extraClasses), - child.maybe <-- $m.map(_.icon), - span( - cls <-- $m.map(_.textClasses.getOrElse("")), - child.text <-- $m.map(_.text) - ) - ) - - object FullBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - ol( - role := "list", - cls := "flex items-center space-x-4", - children <-- $m.map(_.path).split(_.id)((_, _, $p) => - li( - div( - cls := "flex items-center", - child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), - Link( - $p.map(p => - if (p.isRoot) then Link.fullHome(p) - else - Link.ViewModel( - p, - None, - s"ml-4 max-w-xs truncate ${Link.baseClasses}", - p.title - ) - ), - actionBus - ) - ) - ) - ) - ) - - object ShortBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - Link( - $m.map { p => - val target = p.parent.getOrElse(p) - if target.isRoot then Link.shortHome(target) - else - Link.ViewModel( - target, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - s"Zpět na ${target.title}" - ) - }, - actionBus - ) - - def apply(actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - nav( - cls := "flex", - aria.label := "Breadcrumb", - div( - cls := "flex sm:hidden", - ShortBreadcrumbs(router.$currentPage, actionBus) - ), - div( - cls := "hidden sm:block", - FullBreadcrumbs(router.$currentPage, actionBus) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala deleted file mode 100644 index 0db2047..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ /dev/null @@ -1,242 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent -import fiftyforms.ui.components.tailwind.Icons -import fiftyforms.ui.components.tailwind.Avatar -import cz.e_bs.cmi.mdr.pdb.UserInfo -import io.laminext.syntax.core.* - -object NavigationBar: - - case class Logo(img: String, name: String) - case class Link(a: () => Anchor, active: Boolean) - case class MenuItem(title: String) - - case class ViewModel( - userInfo: UserInfo, - pages: List[Link], - userMenu: List[MenuItem], - logo: Logo - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - val $userInfo = $m.map(_.userInfo) - val mobileMenuOpen = Var(false) - - val desktopOnly = cls("hidden md:block") - val mobileOnly = cls("md:hidden") - - inline def avatarImage(size: Int = 8) = - Avatar($userInfo.map(_.img)).avatarImage(size) - - def notificationButton = button( - tpe := "button", - cls := List( - "bg-indigo-600", - "focus:outline-none", - "focus:ring-2", - "focus:ring-offset-2", - "focus:ring-offset-indigo-600", - "focus:ring-white", - "hover:text-white", - "p-1", - "rounded-full", - "text-indigo-200" - ), - span(cls := "sr-only", "View notifications"), - Icons.outline.bell() - ) - - def userProfile: HtmlElement = - val menuOpen = Var(false) - - def menuItem(item: MenuItem, idx: Int): HtmlElement = - a( - href := "#", - cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", - role := "menuitem", - tabIndex := -1, - idAttr := s"user-menu-item-$idx", - item.title - ) - - div( - cls := "ml-3 relative", - div( - button( - tpe := "button", - cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - idAttr := "user-menu-button", - aria.expanded <-- menuOpen.signal, - aria.hasPopup := true, - span(cls := "sr-only", "Open user menu"), - child <-- avatarImage(), - onClick.preventDefault.mapTo( - !menuOpen.now() - ) --> menuOpen.writer - ) - ), - /* - * */ - div( - cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", - cls <-- menuOpen.signal.map { o => - if (o) "md:block" else "md:hidden" - }, - role := "menu", - aria.orientation := "vertical", - aria.labelledBy := "user-menu-button", - tabIndex := -1, - // : keyboard navigation - children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) - ) - ) - - def mobileProfile = - def menuItem(item: MenuItem): HtmlElement = - a( - href := "#", - cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", - item.title - ) - - div( - cls := "pt-4 pb-3 border-t border-indigo-700", - div( - cls := "flex items-center px-5", - div( - cls := "flex-shrink-0", - child <-- avatarImage(10) - ), - div( - cls := "ml-3", - div( - cls := "text-base font-medium text-white", - child.text <-- $userInfo.map(_.name) - ), - child.maybe <-- $userInfo.map( - _.email.map(e => - div( - cls := "text-sm font-medium text-indigo-300", - e - ) - ) - ) - ), - notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) - ), - div( - cls := "mt-3 px-2 space-y-1", - children <-- $m.map(_.userMenu.map(menuItem)) - ) - ) - - def pageLink(page: Link): Anchor = - page - .a() - .amend( - cls := "text-white px-3 py-2 rounded-md text-sm font-medium", - cls := Seq( - "bg-indigo-700" -> page.active, - "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active - ), - ariaCurrent := (if page.active then "page" else "false") - ) - - def logoImg: Image = - img( - cls := "h-8 w-8", - src <-- $m.map(_.logo.img), - alt <-- $m.map(_.logo.name) - ) - - def mobileMenuButton = button( - tpe := "button", - cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - aria.controls := "mobile-menu", - aria.expanded <-- mobileMenuOpen.signal, - span(cls := "sr-only", "Open main menu"), - Icons.outline - .menu() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") - ), - Icons.outline - .x() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") - ), - onClick.preventDefault.mapTo( - !mobileMenuOpen.now() - ) --> mobileMenuOpen.writer - ) - - def navBarLeft = - div( - cls := "flex items-center", - div(cls := "flex-shrink-0", logoImg), - div( - desktopOnly, - div( - cls := "ml-10 flex items-baseline space-x-4", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ) - ) - ) - - def navBarRight = - div( - desktopOnly, - div( - cls := "ml-4 flex items-center md:ml-6", - notificationButton, - userProfile - ) - ) - - def navBarMobile = - div( - cls := "-mr-2 flex", - mobileOnly, - mobileMenuButton - ) - - def navBar = - div( - cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex items-center justify-between h-16", - navBarLeft, - navBarRight, - navBarMobile - ) - ) - - def mobileMenu = - div( - mobileOnly, - cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }, - idAttr := "mobile-menu", - div( - cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ), - mobileProfile - ) - - nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala deleted file mode 100644 index 75339df..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object PageHeader: - def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = - header( - cls := "bg-white shadow-sm", - div( - cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", - h1( - cls := "text-lg leading-6 font-semibold text-gray-900", - Breadcrumbs(actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala deleted file mode 100644 index 5096463..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ /dev/null @@ -1,30 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import fiftyforms.ui.components.tailwind.Loading -import io.laminext.syntax.core.* - -object PageLayout: - case class ViewModel( - navigation: NavigationBar.ViewModel, - content: Option[HtmlElement] - ) - def apply(actionBus: Observer[Action])( - $m: Signal[ViewModel], - mods: Modifier[HtmlElement]* - )(using router: Router[Page]): HtmlElement = - val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) - div( - cls := "h-full flex flex-col", - NavigationBar($m.map(_.navigation)), - child.maybe <-- router.$currentPage.map(_.isRoot) - .switch(None, Some(PageHeader(actionBus))), - main( - cls := "flex-grow-1 overflow-y-auto", - mods, - child <-- $maybeContent.map(_.getOrElse(Loading)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala deleted file mode 100644 index 62d820f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.LinkSupport.* -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo - -object PageLink: - type ViewModel = Page - def apply($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = - a(mods($m, actions), child.text <-- $m.map(_.title)) - - def apply(m: ViewModel, actions: Observer[Action])(using - router: Router[Page] - ): Anchor = apply(Val(m), actions) - - def container($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = a(mods($m, actions)) - - def container(m: ViewModel, actions: Observer[Action])(using - Router[Page] - ): Anchor = container(Val(m), actions) - - private def mods($m: Signal[Page], actions: Observer[Action])(using - router: Router[Page] - ): Modifier[Anchor] = - Seq( - href <-- $m.map(router.absoluteUrlForPage).recover { case _ => - Some("invalid url") - }, - composeEvents(onClick.noKeyMod.preventDefault)( - _.sample($m) - .map(NavigateTo.apply) - ) --> actions - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala deleted file mode 100644 index 87f825a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.pages.dashboard.DashboardPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -class DashboardPageConnector(actionBus: Observer[Action])(using - router: Router[Page] -): - def apply: HtmlElement = - AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala deleted file mode 100644 index e9b0f71..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala +++ /dev/null @@ -1,76 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailKriteriaPage -import pages.detail.components.DukazyKriteria -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -object DetailKriteriaPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailKriteriaPageConnector( - state: DetailKriteriaPageConnector.AppState -)( - $page: Signal[Page.DetailKriteria] -)(using Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => - (p.osobniCislo.value, p.parametr.value, p.kriterium.value) - )((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map( - FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) - ) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, s, $s) => - DetailKriteriaPage($s)(state.actionBus.contramap { - case DukazyKriteria.Add => - NavigateTo( - Page.UpravDukazKriteria( - Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), - Page.Titled(s.parametr.id, Some(s.parametr.nazev)), - Page.Titled(s.kriterium.id, Some(s.kriterium.id)) - ) - ) - }) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): DetailKriteriaPage.ViewModel = - DetailKriteriaPage.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala deleted file mode 100644 index 4d8024f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ /dev/null @@ -1,57 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import pages.detail.DetailPage -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import fiftyforms.ui.components.tailwind.Color - -object DetailPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailPageConnector(state: DetailPageConnector.AppState)( - $page: Signal[Page.Detail] -)(using router: Router[Page]): - val $oscChangeSignal = $page.splitOne(_.osobniCislo.value)((osc, _, _) => osc) - val $pageChangeSignal = - $oscChangeSignal.flatMap(osc => - EventStream.fromSeq(Seq(FetchUserDetails(osc), FetchParameters(osc))) - ) - // TODO: filter the value based on the current osc - // OSC change will fetch new data, but still - // - we need to be sure that what we got is really what we ought to display - // - we want to display stale data accordingly (at least with loading indicator) - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - def apply: HtmlElement = - AppPage(state.actionBus)( - $data.combineWithFn($params)(_ zip _) - .map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: List[Parameter] - ): DetailPage.ViewModel = - DetailPage.ViewModel( - o.toDetailOsoby, - p.map( - _.toParametr(param => - PageLink.container(Page.DetailParametru(o, param), state.actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala deleted file mode 100644 index fdaa7f8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailParametruPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink - -object DetailParametruPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailParametruPageConnector( - state: DetailParametruPageConnector.AppState -)( - $page: Signal[Page.DetailParametru] -)(using router: Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => (p.osobniCislo.value, p.parametr.value))((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map(FetchParameter(_, _)) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - } yield (da, pb) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailParametruPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter - ): DetailParametruPage.ViewModel = - DetailParametruPage.ViewModel( - o.toDetailOsoby, - p.toParametr(p => - PageLink.container(Page.DetailParametru(o, p), state.actionBus) - ), - p.criteria.map( - _.toKriterium { c => - PageLink.container( - Page.DetailKriteria(o, p, c), - state.actionBus - ) - } - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala deleted file mode 100644 index 049aa22..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -case class DirectoryPageConnector( - $input: EventStream[List[UserInfo]], - actionBus: Observer[Action] -)(using router: Router[Page]): - val $data = $input.startWithNone - val $actionSignal = EventStream.fromValue(FetchDirectory) - - def apply: HtmlElement = - AppPage(actionBus)( - $data.split(_ => ())((_, _, s) => - pages.directory.DirectoryPage( - s.map( - _.map( - _.toUserRow(u => - PageLink.container( - Page.Detail(Page.Titled(u.personalNumber)), - actionBus - ) - ) - ) - ) - ) - ), - $actionSignal --> actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala deleted file mode 100644 index 88e7450..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ /dev/null @@ -1,68 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii -import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria -import fiftyforms.ui.components.tailwind.Color - -extension (o: UserInfo) - def toDetailOsoby: DetailOsoby.ViewModel = - DetailOsoby.ViewModel( - o.personalNumber, - o.name, - o.email, - o.phone, - o.img, - o.mainFunction.map(f => - DetailOsoby.Funkce.ViewModel(f.name, f.dept, f.ou) - ), - o.userContracts.headOption.map(c => - DetailOsoby.PracovniPomer.ViewModel(c.rel, c.startDate, c.endDate) - ) - ) - -extension (param: Parameter) - def toParametr(container: Parameter => Anchor): DetailParametru.ViewModel = - DetailParametru.ViewModel( - id = param.id, - nazev = param.name, - popis = param.description, - status = "Nesplněno", - statusColor = Color.red, - a = container(param) - ) - -extension (crit: ParameterCriteria) - def toKriterium( - container: ParameterCriteria => Anchor - ): DetailKriteria.ViewModel = - DetailKriteria.ViewModel( - nazev = crit.criteriumText, - kapitola = crit.chapterId, - bod = crit.itemId, - status = "Nesplněno", - statusColor = Color.red, - splneno = false, - dukazy = Nil, - container = container(crit) - ) - -extension (user: UserInfo) - def toUserRow( - container: UserInfo => HtmlElement = (_: UserInfo) => div() - ): UserRow.ViewModel = - UserRow.ViewModel( - osobniCislo = user.personalNumber.toString, - celeJmeno = user.name, - prijmeni = user.surname, - hlavniFunkce = user.mainFunction.map(_.name), - img = user.img, - container = () => container(user) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala deleted file mode 100644 index 7d5b2c1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.dashboard - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.Page - -object DashboardPage: - - def render: HtmlElement = - div("Dashboard page") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala deleted file mode 100644 index 9ef0698..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala +++ /dev/null @@ -1,31 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailKriteriaPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply($m: Signal[ViewModel])( - action: Observer[DukazyKriteria.Action] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - DukazyKriteria($m.map(_.kriterium.dukazy))(action) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala deleted file mode 100644 index 3b316e1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object DetailPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametry: SeznamParametru.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby($m.map(_.osoba)), - SeznamParametru($m.map(_.parametry)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala deleted file mode 100644 index 17224cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala +++ /dev/null @@ -1,24 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailParametruPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriteria: SeznamKriterii.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby.header($m.map(_.osoba)), - DetailParametru($m.map(_.parametr)), - SeznamKriterii($m.map(_.kriteria)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala deleted file mode 100644 index dc912fa..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ /dev/null @@ -1,107 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb -package app -package pages.detail - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.UpravDukazForm -import fiftyforms.services.files.File - -object UpravDukaz: - - type ThisPage = Page.UpravDukazKriteria - type PageKey = (OsobniCislo, String, String) - - trait State { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def availableFiles: EventStream[List[File]] - def actionBus: Observer[Action] - } - - def keyOfPage(page: ThisPage): PageKey = - (page.osobniCislo.value, page.parametr.value, page.kriterium.value) - - def onChangeAction(key: PageKey): Action = - FetchParameterCriteria( - key._1, - key._2, - key._3, - Page.UpravDukazKriteria(_, _, _) - ) - - class Connector(state: State)($page: Signal[ThisPage])(using Router[Page]): - val $paramChangeSignal = $page.splitOne(keyOfPage)((x, _, _) => x) - val $pageChangeSignal = $paramChangeSignal.map(onChangeAction) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.split(_ => ())((_, s, $s) => - PageComponent( - $s.map(buildModel), - state.availableFiles, - state.actionBus.contramap { - case UpravDukazForm.Cancelled => - NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) - case UpravDukazForm.AvailableFilesRequested => - FetchAvailableFiles(s._1.personalNumber) - } - ) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): PageComponent.ViewModel = - import connectors.* - PageComponent.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) - - object PageComponent: - import components.* - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply( - $m: Signal[ViewModel], - availableFilesStream: EventStream[List[File]], - events: Observer[UpravDukazForm.Event] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - UpravDukazForm(availableFilesStream)(events) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala deleted file mode 100644 index ca311b0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.Icons -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Color - -object DetailKriteria: - case class ViewModel( - nazev: String, - kapitola: String, - bod: String, - status: String, - statusColor: Color, - splneno: Boolean, - dukazy: List[DukazKriteria.ViewModel], - container: HtmlElement = div() - ) { - val id = s"${kapitola}${bod}" - } - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h3( - cls := "text-xl leading-6 font-bold text-gray-900", - child.text <-- $m.map(_.nazev) - ), - h3( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - "Kapitola ", - child.text <-- $m.map(_.kapitola), - " bod ", - child <-- $m.map(_.bod) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala deleted file mode 100644 index ae90290..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala +++ /dev/null @@ -1,100 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Avatar - -object DetailOsoby: - - object Funkce: - case class ViewModel( - nazev: String, - stredisko: String, - voj: String - ) - - def render($m: Signal[ViewModel]): HtmlElement = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.nazev), - span( - cls := "hidden md:inline", - " @ ", - child.text <-- $m.map(_.stredisko), - ", ", - child.text <-- $m.map(_.voj) - ) - ) - - object PracovniPomer: - case class ViewModel( - druh: String, - pocatek: LocalDate, - konec: Option[LocalDate] - ) - - def render($m: Signal[ViewModel]): HtmlElement = - import fiftyforms.ui.components.tailwind.CustomAttrs.datetime - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.druh), - " od ", - time( - datetime <-- $m.map(_.pocatek.toString), - child.text <-- $m.map(_.pocatek.toString) - ) - ) - - case class ViewModel( - osobniCislo: OsobniCislo, - jmeno: String, - email: Option[String], - telefon: Option[String], - img: Option[String], - hlavniFunkce: Option[Funkce.ViewModel], - pracovniPomer: Option[PracovniPomer.ViewModel] - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-4", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(16) - ), - div( - h1( - cls := "text-2xl font-bold text-gray-900", - child.text <-- $m.map(_.jmeno) - ), - child.maybe <-- $m.map(_.hlavniFunkce).split(_ => ())((_, _, d) => - Funkce.render(d) - ), - child.maybe <-- $m.map(_.pracovniPomer).split(_ => ())((_, _, d) => - PracovniPomer.render(d) - ) - ) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-5", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(8) - ), - div( - h1( - cls := "text-lg font-medium text-gray-800", - child.text <-- $m.map(_.jmeno) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala deleted file mode 100644 index a3b00af..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala +++ /dev/null @@ -1,103 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import scala.scalajs.js.annotation.JSExportTopLevel -import scala.scalajs.js.annotation.JSExport -import scala.scalajs.js.annotation.JSImport -import scala.scalajs.js -import org.scalajs.dom -import com.raquo.laminar.api.L.{*, given} -import scala.scalajs.js.Date -import com.raquo.waypoint.Router -import com.raquo.waypoint.SplitRender -import cz.e_bs.cmi.mdr.pdb.app.services.DataFetcher -import scala.scalajs.js.JSON -import zio.json._ -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.state.AppState - -@js.native -@JSImport("stylesheets/main.css", JSImport.Namespace) -object Css extends js.Any - -@js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native -@JSImport("params/pdb-params.json", JSImport.Default) -object pdbParams extends js.Object - -@JSExportTopLevel("app") -object Main: - - @JSExport - def main(args: Array[String]): Unit = - import Routes.given - onLoad { - setupAirstream() - val appContainer = dom.document.querySelector("#app") - val _ = - render( - appContainer, - renderPage(state.MockAppState(using unsafeWindowOwner, router)) - ) - } - - private def onLoad(f: => Unit): Unit = - documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) - - private def setupAirstream()(using router: Router[Page]): Unit = - AirstreamError.registerUnhandledErrorCallback(err => - router.forcePage( - Page.UnhandledError( - Some(err.getClass.getName), // TODO: Fill only in dev mode - Some(err.getMessage) - ) - ) - ) - - def renderPage(state: AppState)(using - router: Router[Page] - ): HtmlElement = - val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) - .collectSignal[Page.Detail]( - connectors - .DetailPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailParametru]( - connectors - .DetailParametruPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailKriteria]( - connectors - .DetailKriteriaPageConnector(state)(_) - .apply - ) - .collectSignal[Page.UpravDukazKriteria]( - pages.detail.UpravDukaz.Connector(state)(_).apply - ) - .collectStatic(Page.Dashboard)( - connectors.DashboardPageConnector(state.actionBus).apply - ) - .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) - ) - .collect[Page.UnhandledError](pg => - pages.errors - .UnhandledErrorPage( - pages.errors.UnhandledErrorPage - .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), - state.actionBus - ) - ) - .collectStatic(Page.Directory)( - connectors - .DirectoryPageConnector(state.users, state.actionBus) - .apply - ) - div(cls := "h-full", child <-- pageSplitter.$view) - - // Pull in the stylesheet - val css: Css.type = Css diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala deleted file mode 100644 index 3bbc8c0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ /dev/null @@ -1,198 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.* -import org.scalajs.dom -import zio.json.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import scala.scalajs.js -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.Page.Titled - -// enum is not working with Waypoints' SplitRender collectStatic -sealed abstract class Page( - val id: String, - val title: String, - val parent: Option[Page] -) { - val path: Vector[Page] = - parent match - case None => Vector(this) - case Some(p) => p.path :+ this - - val isRoot: Boolean = parent.isEmpty -} - -object Page: - - case class Titled[V](value: V, title: Option[String] = None): - val show: String = title.getOrElse(value.toString) - - case object Directory extends Page("directory", "Adresář", None) - - case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) - - case class Detail(osobniCislo: Titled[OsobniCislo]) - extends Page("user", osobniCislo.show, Some(Directory)) - - object Detail { - def apply(o: UserInfo): Detail = Detail( - Titled(o.personalNumber, Some(o.name)) - ) - } - - case class DetailParametru( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String] - ) extends Page( - "parameter", - parametr.show, - Some(Detail(osobniCislo)) - ) - - object DetailParametru { - def apply(o: UserInfo, p: Parameter): DetailParametru = - DetailParametru( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)) - ) - } - - case class DetailKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "criteria", - kriterium.show, - Some(DetailParametru(osobniCislo, parametr)) - ) - - object DetailKriteria { - def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = - DetailKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class UpravDukazKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "addProof", - "Důkaz", - Some(DetailKriteria(osobniCislo, parametr, kriterium)) - ) - - object UpravDukazKriteria { - def apply( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): UpravDukazKriteria = - UpravDukazKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class NotFound(url: String) extends Page("404", "404", Some(Directory)) - - case class UnhandledError( - errorName: Option[String], - errorMessage: Option[String] - ) extends Page("500", "Unexpected error", Some(Directory)) - -object Routes: - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) - given [V: JsonEncoder]: JsonEncoder[Titled[V]] = - DeriveJsonEncoder.gen[Titled[V]] - given [V: JsonDecoder]: JsonDecoder[Titled[V]] = - DeriveJsonDecoder.gen[Titled[V]] - given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] - given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] - - val base = - js.`import`.meta.env.BASE_URL - .asInstanceOf[String] + "app" - - val homePage: Page = Page.Directory - - given router: Router[Page] = Router[Page]( - routes = List( - Route.static(homePage, root / endOfSegments, basePath = base), - Route.static( - Page.Dashboard, - root / "dashboard" / endOfSegments, - basePath = base - ), - Route[Page.Detail, String]( - encode = _.osobniCislo.value.toString, - decode = osc => Page.Detail(Titled(OsobniCislo(osc))), - root / "osoba" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.DetailParametru, (String, String)]( - encode = p => (p.osobniCislo.value.toString, p.parametr.value), - decode = - p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / endOfSegments, - basePath = base - ), - Route[Page.DetailKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.DetailKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.UpravDukazKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.UpravDukazKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / "edit" / endOfSegments, - basePath = base - ) - ), - serializePage = _.toJson, - deserializePage = _.fromJson[Page] - .fold(s => throw IllegalStateException(s), identity), - getPageTitle = _.title, - routeFallback = url => Page.NotFound(url), - deserializeFallback = _ => Page.Dashboard - )( - $popStateEvent = windowEvents.onPopState, - owner = unsafeWindowOwner - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala deleted file mode 100644 index b24b82b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -sealed trait Action - -case object FetchDirectory extends Action -case class FetchUserDetails(osc: OsobniCislo) extends Action -case class FetchParameters(osc: OsobniCislo) extends Action -case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action -case class FetchParameterCriteria( - osc: OsobniCislo, - paramId: String, - critId: String, - page: (UserInfo, Parameter, ParameterCriteria) => Page -) extends Action -case class FetchAvailableFiles(osc: OsobniCislo) extends Action -case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala deleted file mode 100644 index c637d35..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala +++ /dev/null @@ -1,82 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.{UserProfile, UserInfo, OsobniCislo} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo -import cz.e_bs.cmi.mdr.pdb.UserFunction - -object AppPage: - // TODO: pages by logged in user - val pages: List[Page] = List(Page.Directory, Page.Dashboard) - - import NavigationBar.{Logo, MenuItem} - - val logo = Logo( - "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", - "Workflow" - ) - - // TODO: menu items by user profile - val userMenu = - List( - MenuItem("Your Profile"), - MenuItem("Settings"), - MenuItem("Sign out") - ) - - // TODO: load user profile - val $userProfile = Var( - UserProfile( - "tom", - UserInfo( - OsobniCislo("1031"), - "tom", - "Tom", - "Cook", - None, - None, - Some("tom@example.com"), - Some("+420 222 866 180"), - Some( - UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") - ), - Nil, - None - ) - ) - ) - - val $userInfo = $userProfile.signal.map(_.userInfo) - - type ViewModel = Option[HtmlElement] - def apply( - actionBus: Observer[Action] - )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using - router: Router[Page] - ): HtmlElement = - PageLayout(actionBus)( - $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => - PageLayout.ViewModel( - NavigationBar.ViewModel( - u, - pages.map(p => - NavigationBar.Link( - () => - PageLink( - p, - actionBus - ), - p == cp - ) - ), - userMenu, - logo - ), - c - ) - ), - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala deleted file mode 100644 index bf473c6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ /dev/null @@ -1,143 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden -import fiftyforms.ui.components.tailwind.list.IconText.ViewModel -import fiftyforms.ui.components.tailwind.Icons -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Action - -object Breadcrumbs: - - private def slash = - import svg.{*, given} - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-300", - xmlns := "http://www.w3.org/2000/svg", - fill := "currentColor", - viewBox := "0 0 20 20", - ariaHidden := true, - path( - d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" - ) - ) - - def backIcon = - Icons.solid - .`arrow-narrow-left`() - .amend( - svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" - ) - - object Link: - case class ViewModel( - page: Page, - icon: Option[SvgElement], - extraClasses: String, - text: String, - textClasses: Option[String] = None - ) - - val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" - - def shortHome(p: Page) = ViewModel( - p, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - "Zpět na úvodní stránku", - None - ) - - def fullHome(p: Page) = ViewModel( - p, - Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), - "text-gray-400 hover:text-gray-500", - "Domů", - Some("sr-only") - ) - - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - PageLink - .container($m.map(_.page), actionBus) - .amend( - cls <-- $m.map(_.extraClasses), - child.maybe <-- $m.map(_.icon), - span( - cls <-- $m.map(_.textClasses.getOrElse("")), - child.text <-- $m.map(_.text) - ) - ) - - object FullBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - ol( - role := "list", - cls := "flex items-center space-x-4", - children <-- $m.map(_.path).split(_.id)((_, _, $p) => - li( - div( - cls := "flex items-center", - child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), - Link( - $p.map(p => - if (p.isRoot) then Link.fullHome(p) - else - Link.ViewModel( - p, - None, - s"ml-4 max-w-xs truncate ${Link.baseClasses}", - p.title - ) - ), - actionBus - ) - ) - ) - ) - ) - - object ShortBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - Link( - $m.map { p => - val target = p.parent.getOrElse(p) - if target.isRoot then Link.shortHome(target) - else - Link.ViewModel( - target, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - s"Zpět na ${target.title}" - ) - }, - actionBus - ) - - def apply(actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - nav( - cls := "flex", - aria.label := "Breadcrumb", - div( - cls := "flex sm:hidden", - ShortBreadcrumbs(router.$currentPage, actionBus) - ), - div( - cls := "hidden sm:block", - FullBreadcrumbs(router.$currentPage, actionBus) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala deleted file mode 100644 index 0db2047..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ /dev/null @@ -1,242 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent -import fiftyforms.ui.components.tailwind.Icons -import fiftyforms.ui.components.tailwind.Avatar -import cz.e_bs.cmi.mdr.pdb.UserInfo -import io.laminext.syntax.core.* - -object NavigationBar: - - case class Logo(img: String, name: String) - case class Link(a: () => Anchor, active: Boolean) - case class MenuItem(title: String) - - case class ViewModel( - userInfo: UserInfo, - pages: List[Link], - userMenu: List[MenuItem], - logo: Logo - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - val $userInfo = $m.map(_.userInfo) - val mobileMenuOpen = Var(false) - - val desktopOnly = cls("hidden md:block") - val mobileOnly = cls("md:hidden") - - inline def avatarImage(size: Int = 8) = - Avatar($userInfo.map(_.img)).avatarImage(size) - - def notificationButton = button( - tpe := "button", - cls := List( - "bg-indigo-600", - "focus:outline-none", - "focus:ring-2", - "focus:ring-offset-2", - "focus:ring-offset-indigo-600", - "focus:ring-white", - "hover:text-white", - "p-1", - "rounded-full", - "text-indigo-200" - ), - span(cls := "sr-only", "View notifications"), - Icons.outline.bell() - ) - - def userProfile: HtmlElement = - val menuOpen = Var(false) - - def menuItem(item: MenuItem, idx: Int): HtmlElement = - a( - href := "#", - cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", - role := "menuitem", - tabIndex := -1, - idAttr := s"user-menu-item-$idx", - item.title - ) - - div( - cls := "ml-3 relative", - div( - button( - tpe := "button", - cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - idAttr := "user-menu-button", - aria.expanded <-- menuOpen.signal, - aria.hasPopup := true, - span(cls := "sr-only", "Open user menu"), - child <-- avatarImage(), - onClick.preventDefault.mapTo( - !menuOpen.now() - ) --> menuOpen.writer - ) - ), - /* - * */ - div( - cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", - cls <-- menuOpen.signal.map { o => - if (o) "md:block" else "md:hidden" - }, - role := "menu", - aria.orientation := "vertical", - aria.labelledBy := "user-menu-button", - tabIndex := -1, - // : keyboard navigation - children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) - ) - ) - - def mobileProfile = - def menuItem(item: MenuItem): HtmlElement = - a( - href := "#", - cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", - item.title - ) - - div( - cls := "pt-4 pb-3 border-t border-indigo-700", - div( - cls := "flex items-center px-5", - div( - cls := "flex-shrink-0", - child <-- avatarImage(10) - ), - div( - cls := "ml-3", - div( - cls := "text-base font-medium text-white", - child.text <-- $userInfo.map(_.name) - ), - child.maybe <-- $userInfo.map( - _.email.map(e => - div( - cls := "text-sm font-medium text-indigo-300", - e - ) - ) - ) - ), - notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) - ), - div( - cls := "mt-3 px-2 space-y-1", - children <-- $m.map(_.userMenu.map(menuItem)) - ) - ) - - def pageLink(page: Link): Anchor = - page - .a() - .amend( - cls := "text-white px-3 py-2 rounded-md text-sm font-medium", - cls := Seq( - "bg-indigo-700" -> page.active, - "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active - ), - ariaCurrent := (if page.active then "page" else "false") - ) - - def logoImg: Image = - img( - cls := "h-8 w-8", - src <-- $m.map(_.logo.img), - alt <-- $m.map(_.logo.name) - ) - - def mobileMenuButton = button( - tpe := "button", - cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - aria.controls := "mobile-menu", - aria.expanded <-- mobileMenuOpen.signal, - span(cls := "sr-only", "Open main menu"), - Icons.outline - .menu() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") - ), - Icons.outline - .x() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") - ), - onClick.preventDefault.mapTo( - !mobileMenuOpen.now() - ) --> mobileMenuOpen.writer - ) - - def navBarLeft = - div( - cls := "flex items-center", - div(cls := "flex-shrink-0", logoImg), - div( - desktopOnly, - div( - cls := "ml-10 flex items-baseline space-x-4", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ) - ) - ) - - def navBarRight = - div( - desktopOnly, - div( - cls := "ml-4 flex items-center md:ml-6", - notificationButton, - userProfile - ) - ) - - def navBarMobile = - div( - cls := "-mr-2 flex", - mobileOnly, - mobileMenuButton - ) - - def navBar = - div( - cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex items-center justify-between h-16", - navBarLeft, - navBarRight, - navBarMobile - ) - ) - - def mobileMenu = - div( - mobileOnly, - cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }, - idAttr := "mobile-menu", - div( - cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ), - mobileProfile - ) - - nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala deleted file mode 100644 index 75339df..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object PageHeader: - def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = - header( - cls := "bg-white shadow-sm", - div( - cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", - h1( - cls := "text-lg leading-6 font-semibold text-gray-900", - Breadcrumbs(actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala deleted file mode 100644 index 5096463..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ /dev/null @@ -1,30 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import fiftyforms.ui.components.tailwind.Loading -import io.laminext.syntax.core.* - -object PageLayout: - case class ViewModel( - navigation: NavigationBar.ViewModel, - content: Option[HtmlElement] - ) - def apply(actionBus: Observer[Action])( - $m: Signal[ViewModel], - mods: Modifier[HtmlElement]* - )(using router: Router[Page]): HtmlElement = - val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) - div( - cls := "h-full flex flex-col", - NavigationBar($m.map(_.navigation)), - child.maybe <-- router.$currentPage.map(_.isRoot) - .switch(None, Some(PageHeader(actionBus))), - main( - cls := "flex-grow-1 overflow-y-auto", - mods, - child <-- $maybeContent.map(_.getOrElse(Loading)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala deleted file mode 100644 index 62d820f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.LinkSupport.* -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo - -object PageLink: - type ViewModel = Page - def apply($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = - a(mods($m, actions), child.text <-- $m.map(_.title)) - - def apply(m: ViewModel, actions: Observer[Action])(using - router: Router[Page] - ): Anchor = apply(Val(m), actions) - - def container($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = a(mods($m, actions)) - - def container(m: ViewModel, actions: Observer[Action])(using - Router[Page] - ): Anchor = container(Val(m), actions) - - private def mods($m: Signal[Page], actions: Observer[Action])(using - router: Router[Page] - ): Modifier[Anchor] = - Seq( - href <-- $m.map(router.absoluteUrlForPage).recover { case _ => - Some("invalid url") - }, - composeEvents(onClick.noKeyMod.preventDefault)( - _.sample($m) - .map(NavigateTo.apply) - ) --> actions - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala deleted file mode 100644 index 87f825a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.pages.dashboard.DashboardPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -class DashboardPageConnector(actionBus: Observer[Action])(using - router: Router[Page] -): - def apply: HtmlElement = - AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala deleted file mode 100644 index e9b0f71..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala +++ /dev/null @@ -1,76 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailKriteriaPage -import pages.detail.components.DukazyKriteria -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -object DetailKriteriaPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailKriteriaPageConnector( - state: DetailKriteriaPageConnector.AppState -)( - $page: Signal[Page.DetailKriteria] -)(using Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => - (p.osobniCislo.value, p.parametr.value, p.kriterium.value) - )((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map( - FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) - ) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, s, $s) => - DetailKriteriaPage($s)(state.actionBus.contramap { - case DukazyKriteria.Add => - NavigateTo( - Page.UpravDukazKriteria( - Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), - Page.Titled(s.parametr.id, Some(s.parametr.nazev)), - Page.Titled(s.kriterium.id, Some(s.kriterium.id)) - ) - ) - }) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): DetailKriteriaPage.ViewModel = - DetailKriteriaPage.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala deleted file mode 100644 index 4d8024f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ /dev/null @@ -1,57 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import pages.detail.DetailPage -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import fiftyforms.ui.components.tailwind.Color - -object DetailPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailPageConnector(state: DetailPageConnector.AppState)( - $page: Signal[Page.Detail] -)(using router: Router[Page]): - val $oscChangeSignal = $page.splitOne(_.osobniCislo.value)((osc, _, _) => osc) - val $pageChangeSignal = - $oscChangeSignal.flatMap(osc => - EventStream.fromSeq(Seq(FetchUserDetails(osc), FetchParameters(osc))) - ) - // TODO: filter the value based on the current osc - // OSC change will fetch new data, but still - // - we need to be sure that what we got is really what we ought to display - // - we want to display stale data accordingly (at least with loading indicator) - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - def apply: HtmlElement = - AppPage(state.actionBus)( - $data.combineWithFn($params)(_ zip _) - .map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: List[Parameter] - ): DetailPage.ViewModel = - DetailPage.ViewModel( - o.toDetailOsoby, - p.map( - _.toParametr(param => - PageLink.container(Page.DetailParametru(o, param), state.actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala deleted file mode 100644 index fdaa7f8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailParametruPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink - -object DetailParametruPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailParametruPageConnector( - state: DetailParametruPageConnector.AppState -)( - $page: Signal[Page.DetailParametru] -)(using router: Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => (p.osobniCislo.value, p.parametr.value))((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map(FetchParameter(_, _)) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - } yield (da, pb) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailParametruPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter - ): DetailParametruPage.ViewModel = - DetailParametruPage.ViewModel( - o.toDetailOsoby, - p.toParametr(p => - PageLink.container(Page.DetailParametru(o, p), state.actionBus) - ), - p.criteria.map( - _.toKriterium { c => - PageLink.container( - Page.DetailKriteria(o, p, c), - state.actionBus - ) - } - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala deleted file mode 100644 index 049aa22..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -case class DirectoryPageConnector( - $input: EventStream[List[UserInfo]], - actionBus: Observer[Action] -)(using router: Router[Page]): - val $data = $input.startWithNone - val $actionSignal = EventStream.fromValue(FetchDirectory) - - def apply: HtmlElement = - AppPage(actionBus)( - $data.split(_ => ())((_, _, s) => - pages.directory.DirectoryPage( - s.map( - _.map( - _.toUserRow(u => - PageLink.container( - Page.Detail(Page.Titled(u.personalNumber)), - actionBus - ) - ) - ) - ) - ) - ), - $actionSignal --> actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala deleted file mode 100644 index 88e7450..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ /dev/null @@ -1,68 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii -import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria -import fiftyforms.ui.components.tailwind.Color - -extension (o: UserInfo) - def toDetailOsoby: DetailOsoby.ViewModel = - DetailOsoby.ViewModel( - o.personalNumber, - o.name, - o.email, - o.phone, - o.img, - o.mainFunction.map(f => - DetailOsoby.Funkce.ViewModel(f.name, f.dept, f.ou) - ), - o.userContracts.headOption.map(c => - DetailOsoby.PracovniPomer.ViewModel(c.rel, c.startDate, c.endDate) - ) - ) - -extension (param: Parameter) - def toParametr(container: Parameter => Anchor): DetailParametru.ViewModel = - DetailParametru.ViewModel( - id = param.id, - nazev = param.name, - popis = param.description, - status = "Nesplněno", - statusColor = Color.red, - a = container(param) - ) - -extension (crit: ParameterCriteria) - def toKriterium( - container: ParameterCriteria => Anchor - ): DetailKriteria.ViewModel = - DetailKriteria.ViewModel( - nazev = crit.criteriumText, - kapitola = crit.chapterId, - bod = crit.itemId, - status = "Nesplněno", - statusColor = Color.red, - splneno = false, - dukazy = Nil, - container = container(crit) - ) - -extension (user: UserInfo) - def toUserRow( - container: UserInfo => HtmlElement = (_: UserInfo) => div() - ): UserRow.ViewModel = - UserRow.ViewModel( - osobniCislo = user.personalNumber.toString, - celeJmeno = user.name, - prijmeni = user.surname, - hlavniFunkce = user.mainFunction.map(_.name), - img = user.img, - container = () => container(user) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala deleted file mode 100644 index 7d5b2c1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.dashboard - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.Page - -object DashboardPage: - - def render: HtmlElement = - div("Dashboard page") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala deleted file mode 100644 index 9ef0698..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala +++ /dev/null @@ -1,31 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailKriteriaPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply($m: Signal[ViewModel])( - action: Observer[DukazyKriteria.Action] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - DukazyKriteria($m.map(_.kriterium.dukazy))(action) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala deleted file mode 100644 index 3b316e1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object DetailPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametry: SeznamParametru.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby($m.map(_.osoba)), - SeznamParametru($m.map(_.parametry)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala deleted file mode 100644 index 17224cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala +++ /dev/null @@ -1,24 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailParametruPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriteria: SeznamKriterii.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby.header($m.map(_.osoba)), - DetailParametru($m.map(_.parametr)), - SeznamKriterii($m.map(_.kriteria)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala deleted file mode 100644 index dc912fa..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ /dev/null @@ -1,107 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb -package app -package pages.detail - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.UpravDukazForm -import fiftyforms.services.files.File - -object UpravDukaz: - - type ThisPage = Page.UpravDukazKriteria - type PageKey = (OsobniCislo, String, String) - - trait State { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def availableFiles: EventStream[List[File]] - def actionBus: Observer[Action] - } - - def keyOfPage(page: ThisPage): PageKey = - (page.osobniCislo.value, page.parametr.value, page.kriterium.value) - - def onChangeAction(key: PageKey): Action = - FetchParameterCriteria( - key._1, - key._2, - key._3, - Page.UpravDukazKriteria(_, _, _) - ) - - class Connector(state: State)($page: Signal[ThisPage])(using Router[Page]): - val $paramChangeSignal = $page.splitOne(keyOfPage)((x, _, _) => x) - val $pageChangeSignal = $paramChangeSignal.map(onChangeAction) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.split(_ => ())((_, s, $s) => - PageComponent( - $s.map(buildModel), - state.availableFiles, - state.actionBus.contramap { - case UpravDukazForm.Cancelled => - NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) - case UpravDukazForm.AvailableFilesRequested => - FetchAvailableFiles(s._1.personalNumber) - } - ) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): PageComponent.ViewModel = - import connectors.* - PageComponent.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) - - object PageComponent: - import components.* - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply( - $m: Signal[ViewModel], - availableFilesStream: EventStream[List[File]], - events: Observer[UpravDukazForm.Event] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - UpravDukazForm(availableFilesStream)(events) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala deleted file mode 100644 index ca311b0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.Icons -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Color - -object DetailKriteria: - case class ViewModel( - nazev: String, - kapitola: String, - bod: String, - status: String, - statusColor: Color, - splneno: Boolean, - dukazy: List[DukazKriteria.ViewModel], - container: HtmlElement = div() - ) { - val id = s"${kapitola}${bod}" - } - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h3( - cls := "text-xl leading-6 font-bold text-gray-900", - child.text <-- $m.map(_.nazev) - ), - h3( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - "Kapitola ", - child.text <-- $m.map(_.kapitola), - " bod ", - child <-- $m.map(_.bod) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala deleted file mode 100644 index ae90290..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala +++ /dev/null @@ -1,100 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Avatar - -object DetailOsoby: - - object Funkce: - case class ViewModel( - nazev: String, - stredisko: String, - voj: String - ) - - def render($m: Signal[ViewModel]): HtmlElement = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.nazev), - span( - cls := "hidden md:inline", - " @ ", - child.text <-- $m.map(_.stredisko), - ", ", - child.text <-- $m.map(_.voj) - ) - ) - - object PracovniPomer: - case class ViewModel( - druh: String, - pocatek: LocalDate, - konec: Option[LocalDate] - ) - - def render($m: Signal[ViewModel]): HtmlElement = - import fiftyforms.ui.components.tailwind.CustomAttrs.datetime - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.druh), - " od ", - time( - datetime <-- $m.map(_.pocatek.toString), - child.text <-- $m.map(_.pocatek.toString) - ) - ) - - case class ViewModel( - osobniCislo: OsobniCislo, - jmeno: String, - email: Option[String], - telefon: Option[String], - img: Option[String], - hlavniFunkce: Option[Funkce.ViewModel], - pracovniPomer: Option[PracovniPomer.ViewModel] - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-4", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(16) - ), - div( - h1( - cls := "text-2xl font-bold text-gray-900", - child.text <-- $m.map(_.jmeno) - ), - child.maybe <-- $m.map(_.hlavniFunkce).split(_ => ())((_, _, d) => - Funkce.render(d) - ), - child.maybe <-- $m.map(_.pracovniPomer).split(_ => ())((_, _, d) => - PracovniPomer.render(d) - ) - ) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-5", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(8) - ), - div( - h1( - cls := "text-lg font-medium text-gray-800", - child.text <-- $m.map(_.jmeno) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala deleted file mode 100644 index e11cb62..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color - -object DetailParametru: - case class ViewModel( - id: String, - nazev: String, - popis: String, - status: String, - statusColor: Color, - a: Anchor - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h2( - cls := "text-2xl leading-6 font-medium text-gray-900", - child.text <-- $m.map(_.nazev) - ), - p( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - child.text <-- $m.map(_.popis) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - h2( - cls := "text-lg leading-6 font-medium text-gray-600", - child.text <-- $m.map(_.nazev) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala deleted file mode 100644 index a3b00af..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala +++ /dev/null @@ -1,103 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import scala.scalajs.js.annotation.JSExportTopLevel -import scala.scalajs.js.annotation.JSExport -import scala.scalajs.js.annotation.JSImport -import scala.scalajs.js -import org.scalajs.dom -import com.raquo.laminar.api.L.{*, given} -import scala.scalajs.js.Date -import com.raquo.waypoint.Router -import com.raquo.waypoint.SplitRender -import cz.e_bs.cmi.mdr.pdb.app.services.DataFetcher -import scala.scalajs.js.JSON -import zio.json._ -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.state.AppState - -@js.native -@JSImport("stylesheets/main.css", JSImport.Namespace) -object Css extends js.Any - -@js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native -@JSImport("params/pdb-params.json", JSImport.Default) -object pdbParams extends js.Object - -@JSExportTopLevel("app") -object Main: - - @JSExport - def main(args: Array[String]): Unit = - import Routes.given - onLoad { - setupAirstream() - val appContainer = dom.document.querySelector("#app") - val _ = - render( - appContainer, - renderPage(state.MockAppState(using unsafeWindowOwner, router)) - ) - } - - private def onLoad(f: => Unit): Unit = - documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) - - private def setupAirstream()(using router: Router[Page]): Unit = - AirstreamError.registerUnhandledErrorCallback(err => - router.forcePage( - Page.UnhandledError( - Some(err.getClass.getName), // TODO: Fill only in dev mode - Some(err.getMessage) - ) - ) - ) - - def renderPage(state: AppState)(using - router: Router[Page] - ): HtmlElement = - val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) - .collectSignal[Page.Detail]( - connectors - .DetailPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailParametru]( - connectors - .DetailParametruPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailKriteria]( - connectors - .DetailKriteriaPageConnector(state)(_) - .apply - ) - .collectSignal[Page.UpravDukazKriteria]( - pages.detail.UpravDukaz.Connector(state)(_).apply - ) - .collectStatic(Page.Dashboard)( - connectors.DashboardPageConnector(state.actionBus).apply - ) - .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) - ) - .collect[Page.UnhandledError](pg => - pages.errors - .UnhandledErrorPage( - pages.errors.UnhandledErrorPage - .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), - state.actionBus - ) - ) - .collectStatic(Page.Directory)( - connectors - .DirectoryPageConnector(state.users, state.actionBus) - .apply - ) - div(cls := "h-full", child <-- pageSplitter.$view) - - // Pull in the stylesheet - val css: Css.type = Css diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala deleted file mode 100644 index 3bbc8c0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ /dev/null @@ -1,198 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.* -import org.scalajs.dom -import zio.json.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import scala.scalajs.js -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.Page.Titled - -// enum is not working with Waypoints' SplitRender collectStatic -sealed abstract class Page( - val id: String, - val title: String, - val parent: Option[Page] -) { - val path: Vector[Page] = - parent match - case None => Vector(this) - case Some(p) => p.path :+ this - - val isRoot: Boolean = parent.isEmpty -} - -object Page: - - case class Titled[V](value: V, title: Option[String] = None): - val show: String = title.getOrElse(value.toString) - - case object Directory extends Page("directory", "Adresář", None) - - case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) - - case class Detail(osobniCislo: Titled[OsobniCislo]) - extends Page("user", osobniCislo.show, Some(Directory)) - - object Detail { - def apply(o: UserInfo): Detail = Detail( - Titled(o.personalNumber, Some(o.name)) - ) - } - - case class DetailParametru( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String] - ) extends Page( - "parameter", - parametr.show, - Some(Detail(osobniCislo)) - ) - - object DetailParametru { - def apply(o: UserInfo, p: Parameter): DetailParametru = - DetailParametru( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)) - ) - } - - case class DetailKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "criteria", - kriterium.show, - Some(DetailParametru(osobniCislo, parametr)) - ) - - object DetailKriteria { - def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = - DetailKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class UpravDukazKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "addProof", - "Důkaz", - Some(DetailKriteria(osobniCislo, parametr, kriterium)) - ) - - object UpravDukazKriteria { - def apply( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): UpravDukazKriteria = - UpravDukazKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class NotFound(url: String) extends Page("404", "404", Some(Directory)) - - case class UnhandledError( - errorName: Option[String], - errorMessage: Option[String] - ) extends Page("500", "Unexpected error", Some(Directory)) - -object Routes: - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) - given [V: JsonEncoder]: JsonEncoder[Titled[V]] = - DeriveJsonEncoder.gen[Titled[V]] - given [V: JsonDecoder]: JsonDecoder[Titled[V]] = - DeriveJsonDecoder.gen[Titled[V]] - given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] - given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] - - val base = - js.`import`.meta.env.BASE_URL - .asInstanceOf[String] + "app" - - val homePage: Page = Page.Directory - - given router: Router[Page] = Router[Page]( - routes = List( - Route.static(homePage, root / endOfSegments, basePath = base), - Route.static( - Page.Dashboard, - root / "dashboard" / endOfSegments, - basePath = base - ), - Route[Page.Detail, String]( - encode = _.osobniCislo.value.toString, - decode = osc => Page.Detail(Titled(OsobniCislo(osc))), - root / "osoba" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.DetailParametru, (String, String)]( - encode = p => (p.osobniCislo.value.toString, p.parametr.value), - decode = - p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / endOfSegments, - basePath = base - ), - Route[Page.DetailKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.DetailKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.UpravDukazKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.UpravDukazKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / "edit" / endOfSegments, - basePath = base - ) - ), - serializePage = _.toJson, - deserializePage = _.fromJson[Page] - .fold(s => throw IllegalStateException(s), identity), - getPageTitle = _.title, - routeFallback = url => Page.NotFound(url), - deserializeFallback = _ => Page.Dashboard - )( - $popStateEvent = windowEvents.onPopState, - owner = unsafeWindowOwner - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala deleted file mode 100644 index b24b82b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -sealed trait Action - -case object FetchDirectory extends Action -case class FetchUserDetails(osc: OsobniCislo) extends Action -case class FetchParameters(osc: OsobniCislo) extends Action -case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action -case class FetchParameterCriteria( - osc: OsobniCislo, - paramId: String, - critId: String, - page: (UserInfo, Parameter, ParameterCriteria) => Page -) extends Action -case class FetchAvailableFiles(osc: OsobniCislo) extends Action -case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala deleted file mode 100644 index c637d35..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala +++ /dev/null @@ -1,82 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.{UserProfile, UserInfo, OsobniCislo} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo -import cz.e_bs.cmi.mdr.pdb.UserFunction - -object AppPage: - // TODO: pages by logged in user - val pages: List[Page] = List(Page.Directory, Page.Dashboard) - - import NavigationBar.{Logo, MenuItem} - - val logo = Logo( - "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", - "Workflow" - ) - - // TODO: menu items by user profile - val userMenu = - List( - MenuItem("Your Profile"), - MenuItem("Settings"), - MenuItem("Sign out") - ) - - // TODO: load user profile - val $userProfile = Var( - UserProfile( - "tom", - UserInfo( - OsobniCislo("1031"), - "tom", - "Tom", - "Cook", - None, - None, - Some("tom@example.com"), - Some("+420 222 866 180"), - Some( - UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") - ), - Nil, - None - ) - ) - ) - - val $userInfo = $userProfile.signal.map(_.userInfo) - - type ViewModel = Option[HtmlElement] - def apply( - actionBus: Observer[Action] - )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using - router: Router[Page] - ): HtmlElement = - PageLayout(actionBus)( - $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => - PageLayout.ViewModel( - NavigationBar.ViewModel( - u, - pages.map(p => - NavigationBar.Link( - () => - PageLink( - p, - actionBus - ), - p == cp - ) - ), - userMenu, - logo - ), - c - ) - ), - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala deleted file mode 100644 index bf473c6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ /dev/null @@ -1,143 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden -import fiftyforms.ui.components.tailwind.list.IconText.ViewModel -import fiftyforms.ui.components.tailwind.Icons -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Action - -object Breadcrumbs: - - private def slash = - import svg.{*, given} - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-300", - xmlns := "http://www.w3.org/2000/svg", - fill := "currentColor", - viewBox := "0 0 20 20", - ariaHidden := true, - path( - d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" - ) - ) - - def backIcon = - Icons.solid - .`arrow-narrow-left`() - .amend( - svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" - ) - - object Link: - case class ViewModel( - page: Page, - icon: Option[SvgElement], - extraClasses: String, - text: String, - textClasses: Option[String] = None - ) - - val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" - - def shortHome(p: Page) = ViewModel( - p, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - "Zpět na úvodní stránku", - None - ) - - def fullHome(p: Page) = ViewModel( - p, - Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), - "text-gray-400 hover:text-gray-500", - "Domů", - Some("sr-only") - ) - - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - PageLink - .container($m.map(_.page), actionBus) - .amend( - cls <-- $m.map(_.extraClasses), - child.maybe <-- $m.map(_.icon), - span( - cls <-- $m.map(_.textClasses.getOrElse("")), - child.text <-- $m.map(_.text) - ) - ) - - object FullBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - ol( - role := "list", - cls := "flex items-center space-x-4", - children <-- $m.map(_.path).split(_.id)((_, _, $p) => - li( - div( - cls := "flex items-center", - child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), - Link( - $p.map(p => - if (p.isRoot) then Link.fullHome(p) - else - Link.ViewModel( - p, - None, - s"ml-4 max-w-xs truncate ${Link.baseClasses}", - p.title - ) - ), - actionBus - ) - ) - ) - ) - ) - - object ShortBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - Link( - $m.map { p => - val target = p.parent.getOrElse(p) - if target.isRoot then Link.shortHome(target) - else - Link.ViewModel( - target, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - s"Zpět na ${target.title}" - ) - }, - actionBus - ) - - def apply(actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - nav( - cls := "flex", - aria.label := "Breadcrumb", - div( - cls := "flex sm:hidden", - ShortBreadcrumbs(router.$currentPage, actionBus) - ), - div( - cls := "hidden sm:block", - FullBreadcrumbs(router.$currentPage, actionBus) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala deleted file mode 100644 index 0db2047..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ /dev/null @@ -1,242 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent -import fiftyforms.ui.components.tailwind.Icons -import fiftyforms.ui.components.tailwind.Avatar -import cz.e_bs.cmi.mdr.pdb.UserInfo -import io.laminext.syntax.core.* - -object NavigationBar: - - case class Logo(img: String, name: String) - case class Link(a: () => Anchor, active: Boolean) - case class MenuItem(title: String) - - case class ViewModel( - userInfo: UserInfo, - pages: List[Link], - userMenu: List[MenuItem], - logo: Logo - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - val $userInfo = $m.map(_.userInfo) - val mobileMenuOpen = Var(false) - - val desktopOnly = cls("hidden md:block") - val mobileOnly = cls("md:hidden") - - inline def avatarImage(size: Int = 8) = - Avatar($userInfo.map(_.img)).avatarImage(size) - - def notificationButton = button( - tpe := "button", - cls := List( - "bg-indigo-600", - "focus:outline-none", - "focus:ring-2", - "focus:ring-offset-2", - "focus:ring-offset-indigo-600", - "focus:ring-white", - "hover:text-white", - "p-1", - "rounded-full", - "text-indigo-200" - ), - span(cls := "sr-only", "View notifications"), - Icons.outline.bell() - ) - - def userProfile: HtmlElement = - val menuOpen = Var(false) - - def menuItem(item: MenuItem, idx: Int): HtmlElement = - a( - href := "#", - cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", - role := "menuitem", - tabIndex := -1, - idAttr := s"user-menu-item-$idx", - item.title - ) - - div( - cls := "ml-3 relative", - div( - button( - tpe := "button", - cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - idAttr := "user-menu-button", - aria.expanded <-- menuOpen.signal, - aria.hasPopup := true, - span(cls := "sr-only", "Open user menu"), - child <-- avatarImage(), - onClick.preventDefault.mapTo( - !menuOpen.now() - ) --> menuOpen.writer - ) - ), - /* - * */ - div( - cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", - cls <-- menuOpen.signal.map { o => - if (o) "md:block" else "md:hidden" - }, - role := "menu", - aria.orientation := "vertical", - aria.labelledBy := "user-menu-button", - tabIndex := -1, - // : keyboard navigation - children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) - ) - ) - - def mobileProfile = - def menuItem(item: MenuItem): HtmlElement = - a( - href := "#", - cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", - item.title - ) - - div( - cls := "pt-4 pb-3 border-t border-indigo-700", - div( - cls := "flex items-center px-5", - div( - cls := "flex-shrink-0", - child <-- avatarImage(10) - ), - div( - cls := "ml-3", - div( - cls := "text-base font-medium text-white", - child.text <-- $userInfo.map(_.name) - ), - child.maybe <-- $userInfo.map( - _.email.map(e => - div( - cls := "text-sm font-medium text-indigo-300", - e - ) - ) - ) - ), - notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) - ), - div( - cls := "mt-3 px-2 space-y-1", - children <-- $m.map(_.userMenu.map(menuItem)) - ) - ) - - def pageLink(page: Link): Anchor = - page - .a() - .amend( - cls := "text-white px-3 py-2 rounded-md text-sm font-medium", - cls := Seq( - "bg-indigo-700" -> page.active, - "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active - ), - ariaCurrent := (if page.active then "page" else "false") - ) - - def logoImg: Image = - img( - cls := "h-8 w-8", - src <-- $m.map(_.logo.img), - alt <-- $m.map(_.logo.name) - ) - - def mobileMenuButton = button( - tpe := "button", - cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - aria.controls := "mobile-menu", - aria.expanded <-- mobileMenuOpen.signal, - span(cls := "sr-only", "Open main menu"), - Icons.outline - .menu() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") - ), - Icons.outline - .x() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") - ), - onClick.preventDefault.mapTo( - !mobileMenuOpen.now() - ) --> mobileMenuOpen.writer - ) - - def navBarLeft = - div( - cls := "flex items-center", - div(cls := "flex-shrink-0", logoImg), - div( - desktopOnly, - div( - cls := "ml-10 flex items-baseline space-x-4", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ) - ) - ) - - def navBarRight = - div( - desktopOnly, - div( - cls := "ml-4 flex items-center md:ml-6", - notificationButton, - userProfile - ) - ) - - def navBarMobile = - div( - cls := "-mr-2 flex", - mobileOnly, - mobileMenuButton - ) - - def navBar = - div( - cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex items-center justify-between h-16", - navBarLeft, - navBarRight, - navBarMobile - ) - ) - - def mobileMenu = - div( - mobileOnly, - cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }, - idAttr := "mobile-menu", - div( - cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ), - mobileProfile - ) - - nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala deleted file mode 100644 index 75339df..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object PageHeader: - def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = - header( - cls := "bg-white shadow-sm", - div( - cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", - h1( - cls := "text-lg leading-6 font-semibold text-gray-900", - Breadcrumbs(actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala deleted file mode 100644 index 5096463..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ /dev/null @@ -1,30 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import fiftyforms.ui.components.tailwind.Loading -import io.laminext.syntax.core.* - -object PageLayout: - case class ViewModel( - navigation: NavigationBar.ViewModel, - content: Option[HtmlElement] - ) - def apply(actionBus: Observer[Action])( - $m: Signal[ViewModel], - mods: Modifier[HtmlElement]* - )(using router: Router[Page]): HtmlElement = - val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) - div( - cls := "h-full flex flex-col", - NavigationBar($m.map(_.navigation)), - child.maybe <-- router.$currentPage.map(_.isRoot) - .switch(None, Some(PageHeader(actionBus))), - main( - cls := "flex-grow-1 overflow-y-auto", - mods, - child <-- $maybeContent.map(_.getOrElse(Loading)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala deleted file mode 100644 index 62d820f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.LinkSupport.* -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo - -object PageLink: - type ViewModel = Page - def apply($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = - a(mods($m, actions), child.text <-- $m.map(_.title)) - - def apply(m: ViewModel, actions: Observer[Action])(using - router: Router[Page] - ): Anchor = apply(Val(m), actions) - - def container($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = a(mods($m, actions)) - - def container(m: ViewModel, actions: Observer[Action])(using - Router[Page] - ): Anchor = container(Val(m), actions) - - private def mods($m: Signal[Page], actions: Observer[Action])(using - router: Router[Page] - ): Modifier[Anchor] = - Seq( - href <-- $m.map(router.absoluteUrlForPage).recover { case _ => - Some("invalid url") - }, - composeEvents(onClick.noKeyMod.preventDefault)( - _.sample($m) - .map(NavigateTo.apply) - ) --> actions - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala deleted file mode 100644 index 87f825a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.pages.dashboard.DashboardPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -class DashboardPageConnector(actionBus: Observer[Action])(using - router: Router[Page] -): - def apply: HtmlElement = - AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala deleted file mode 100644 index e9b0f71..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala +++ /dev/null @@ -1,76 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailKriteriaPage -import pages.detail.components.DukazyKriteria -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -object DetailKriteriaPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailKriteriaPageConnector( - state: DetailKriteriaPageConnector.AppState -)( - $page: Signal[Page.DetailKriteria] -)(using Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => - (p.osobniCislo.value, p.parametr.value, p.kriterium.value) - )((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map( - FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) - ) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, s, $s) => - DetailKriteriaPage($s)(state.actionBus.contramap { - case DukazyKriteria.Add => - NavigateTo( - Page.UpravDukazKriteria( - Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), - Page.Titled(s.parametr.id, Some(s.parametr.nazev)), - Page.Titled(s.kriterium.id, Some(s.kriterium.id)) - ) - ) - }) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): DetailKriteriaPage.ViewModel = - DetailKriteriaPage.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala deleted file mode 100644 index 4d8024f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ /dev/null @@ -1,57 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import pages.detail.DetailPage -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import fiftyforms.ui.components.tailwind.Color - -object DetailPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailPageConnector(state: DetailPageConnector.AppState)( - $page: Signal[Page.Detail] -)(using router: Router[Page]): - val $oscChangeSignal = $page.splitOne(_.osobniCislo.value)((osc, _, _) => osc) - val $pageChangeSignal = - $oscChangeSignal.flatMap(osc => - EventStream.fromSeq(Seq(FetchUserDetails(osc), FetchParameters(osc))) - ) - // TODO: filter the value based on the current osc - // OSC change will fetch new data, but still - // - we need to be sure that what we got is really what we ought to display - // - we want to display stale data accordingly (at least with loading indicator) - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - def apply: HtmlElement = - AppPage(state.actionBus)( - $data.combineWithFn($params)(_ zip _) - .map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: List[Parameter] - ): DetailPage.ViewModel = - DetailPage.ViewModel( - o.toDetailOsoby, - p.map( - _.toParametr(param => - PageLink.container(Page.DetailParametru(o, param), state.actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala deleted file mode 100644 index fdaa7f8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailParametruPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink - -object DetailParametruPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailParametruPageConnector( - state: DetailParametruPageConnector.AppState -)( - $page: Signal[Page.DetailParametru] -)(using router: Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => (p.osobniCislo.value, p.parametr.value))((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map(FetchParameter(_, _)) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - } yield (da, pb) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailParametruPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter - ): DetailParametruPage.ViewModel = - DetailParametruPage.ViewModel( - o.toDetailOsoby, - p.toParametr(p => - PageLink.container(Page.DetailParametru(o, p), state.actionBus) - ), - p.criteria.map( - _.toKriterium { c => - PageLink.container( - Page.DetailKriteria(o, p, c), - state.actionBus - ) - } - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala deleted file mode 100644 index 049aa22..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -case class DirectoryPageConnector( - $input: EventStream[List[UserInfo]], - actionBus: Observer[Action] -)(using router: Router[Page]): - val $data = $input.startWithNone - val $actionSignal = EventStream.fromValue(FetchDirectory) - - def apply: HtmlElement = - AppPage(actionBus)( - $data.split(_ => ())((_, _, s) => - pages.directory.DirectoryPage( - s.map( - _.map( - _.toUserRow(u => - PageLink.container( - Page.Detail(Page.Titled(u.personalNumber)), - actionBus - ) - ) - ) - ) - ) - ), - $actionSignal --> actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala deleted file mode 100644 index 88e7450..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ /dev/null @@ -1,68 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii -import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria -import fiftyforms.ui.components.tailwind.Color - -extension (o: UserInfo) - def toDetailOsoby: DetailOsoby.ViewModel = - DetailOsoby.ViewModel( - o.personalNumber, - o.name, - o.email, - o.phone, - o.img, - o.mainFunction.map(f => - DetailOsoby.Funkce.ViewModel(f.name, f.dept, f.ou) - ), - o.userContracts.headOption.map(c => - DetailOsoby.PracovniPomer.ViewModel(c.rel, c.startDate, c.endDate) - ) - ) - -extension (param: Parameter) - def toParametr(container: Parameter => Anchor): DetailParametru.ViewModel = - DetailParametru.ViewModel( - id = param.id, - nazev = param.name, - popis = param.description, - status = "Nesplněno", - statusColor = Color.red, - a = container(param) - ) - -extension (crit: ParameterCriteria) - def toKriterium( - container: ParameterCriteria => Anchor - ): DetailKriteria.ViewModel = - DetailKriteria.ViewModel( - nazev = crit.criteriumText, - kapitola = crit.chapterId, - bod = crit.itemId, - status = "Nesplněno", - statusColor = Color.red, - splneno = false, - dukazy = Nil, - container = container(crit) - ) - -extension (user: UserInfo) - def toUserRow( - container: UserInfo => HtmlElement = (_: UserInfo) => div() - ): UserRow.ViewModel = - UserRow.ViewModel( - osobniCislo = user.personalNumber.toString, - celeJmeno = user.name, - prijmeni = user.surname, - hlavniFunkce = user.mainFunction.map(_.name), - img = user.img, - container = () => container(user) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala deleted file mode 100644 index 7d5b2c1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.dashboard - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.Page - -object DashboardPage: - - def render: HtmlElement = - div("Dashboard page") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala deleted file mode 100644 index 9ef0698..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala +++ /dev/null @@ -1,31 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailKriteriaPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply($m: Signal[ViewModel])( - action: Observer[DukazyKriteria.Action] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - DukazyKriteria($m.map(_.kriterium.dukazy))(action) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala deleted file mode 100644 index 3b316e1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object DetailPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametry: SeznamParametru.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby($m.map(_.osoba)), - SeznamParametru($m.map(_.parametry)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala deleted file mode 100644 index 17224cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala +++ /dev/null @@ -1,24 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailParametruPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriteria: SeznamKriterii.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby.header($m.map(_.osoba)), - DetailParametru($m.map(_.parametr)), - SeznamKriterii($m.map(_.kriteria)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala deleted file mode 100644 index dc912fa..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ /dev/null @@ -1,107 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb -package app -package pages.detail - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.UpravDukazForm -import fiftyforms.services.files.File - -object UpravDukaz: - - type ThisPage = Page.UpravDukazKriteria - type PageKey = (OsobniCislo, String, String) - - trait State { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def availableFiles: EventStream[List[File]] - def actionBus: Observer[Action] - } - - def keyOfPage(page: ThisPage): PageKey = - (page.osobniCislo.value, page.parametr.value, page.kriterium.value) - - def onChangeAction(key: PageKey): Action = - FetchParameterCriteria( - key._1, - key._2, - key._3, - Page.UpravDukazKriteria(_, _, _) - ) - - class Connector(state: State)($page: Signal[ThisPage])(using Router[Page]): - val $paramChangeSignal = $page.splitOne(keyOfPage)((x, _, _) => x) - val $pageChangeSignal = $paramChangeSignal.map(onChangeAction) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.split(_ => ())((_, s, $s) => - PageComponent( - $s.map(buildModel), - state.availableFiles, - state.actionBus.contramap { - case UpravDukazForm.Cancelled => - NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) - case UpravDukazForm.AvailableFilesRequested => - FetchAvailableFiles(s._1.personalNumber) - } - ) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): PageComponent.ViewModel = - import connectors.* - PageComponent.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) - - object PageComponent: - import components.* - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply( - $m: Signal[ViewModel], - availableFilesStream: EventStream[List[File]], - events: Observer[UpravDukazForm.Event] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - UpravDukazForm(availableFilesStream)(events) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala deleted file mode 100644 index ca311b0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.Icons -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Color - -object DetailKriteria: - case class ViewModel( - nazev: String, - kapitola: String, - bod: String, - status: String, - statusColor: Color, - splneno: Boolean, - dukazy: List[DukazKriteria.ViewModel], - container: HtmlElement = div() - ) { - val id = s"${kapitola}${bod}" - } - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h3( - cls := "text-xl leading-6 font-bold text-gray-900", - child.text <-- $m.map(_.nazev) - ), - h3( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - "Kapitola ", - child.text <-- $m.map(_.kapitola), - " bod ", - child <-- $m.map(_.bod) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala deleted file mode 100644 index ae90290..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala +++ /dev/null @@ -1,100 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Avatar - -object DetailOsoby: - - object Funkce: - case class ViewModel( - nazev: String, - stredisko: String, - voj: String - ) - - def render($m: Signal[ViewModel]): HtmlElement = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.nazev), - span( - cls := "hidden md:inline", - " @ ", - child.text <-- $m.map(_.stredisko), - ", ", - child.text <-- $m.map(_.voj) - ) - ) - - object PracovniPomer: - case class ViewModel( - druh: String, - pocatek: LocalDate, - konec: Option[LocalDate] - ) - - def render($m: Signal[ViewModel]): HtmlElement = - import fiftyforms.ui.components.tailwind.CustomAttrs.datetime - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.druh), - " od ", - time( - datetime <-- $m.map(_.pocatek.toString), - child.text <-- $m.map(_.pocatek.toString) - ) - ) - - case class ViewModel( - osobniCislo: OsobniCislo, - jmeno: String, - email: Option[String], - telefon: Option[String], - img: Option[String], - hlavniFunkce: Option[Funkce.ViewModel], - pracovniPomer: Option[PracovniPomer.ViewModel] - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-4", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(16) - ), - div( - h1( - cls := "text-2xl font-bold text-gray-900", - child.text <-- $m.map(_.jmeno) - ), - child.maybe <-- $m.map(_.hlavniFunkce).split(_ => ())((_, _, d) => - Funkce.render(d) - ), - child.maybe <-- $m.map(_.pracovniPomer).split(_ => ())((_, _, d) => - PracovniPomer.render(d) - ) - ) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-5", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(8) - ), - div( - h1( - cls := "text-lg font-medium text-gray-800", - child.text <-- $m.map(_.jmeno) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala deleted file mode 100644 index e11cb62..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color - -object DetailParametru: - case class ViewModel( - id: String, - nazev: String, - popis: String, - status: String, - statusColor: Color, - a: Anchor - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h2( - cls := "text-2xl leading-6 font-medium text-gray-900", - child.text <-- $m.map(_.nazev) - ), - p( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - child.text <-- $m.map(_.popis) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - h2( - cls := "text-lg leading-6 font-medium text-gray-600", - child.text <-- $m.map(_.nazev) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala deleted file mode 100644 index a00ede8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala +++ /dev/null @@ -1,177 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.CustomAttrs - -object DukazKriteria: - case class Osoba(osobniCislo: String, jmeno: String) - case class Udalost(osoba: Osoba, datum: LocalDate) - case class Dokument( - url: String, - nazev: String, - pridal: Udalost, - odebral: Option[Udalost] - ) - case class ViewModel( - dokumenty: List[Dokument], - autorizoval: Option[Udalost], - platiDo: Option[LocalDate], - poznámka: Option[String] - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-lg", - div( - cls := "px-4 py-5 sm:px-6", - h3( - cls := "text-lg leading-6 font-medium text-gray-900", - """Applicant Information""" - ), - p( - cls := "mt-1 max-w-2xl text-sm text-gray-500", - """Personal details and application.""" - ) - ), - div( - cls := "border-t border-gray-200 px-4 py-5 sm:px-6", - dl( - cls := "grid grid-cols-1 gap-x-4 gap-y-8 sm:grid-cols-2", - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Full name""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Margot Foster""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Application for""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Backend Developer""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Email address""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """margotfoster@example.com""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Salary expectation""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """$120,000""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """About""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Fugiat ipsum ipsum deserunt culpa aute sint do nostrud anim incididunt cillum culpa consequat. Excepteur qui ipsum aliquip consequat sint. Sit id mollit nulla mollit nostrud in ea officia proident. Irure nostrud pariatur mollit ad adipisicing reprehenderit deserunt qui eu.""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """Attachments""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - ul( - role := "list", - cls := "border border-gray-200 rounded-md divide-y divide-gray-200", - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """resume_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ), - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """coverletter_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala deleted file mode 100644 index a3b00af..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala +++ /dev/null @@ -1,103 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import scala.scalajs.js.annotation.JSExportTopLevel -import scala.scalajs.js.annotation.JSExport -import scala.scalajs.js.annotation.JSImport -import scala.scalajs.js -import org.scalajs.dom -import com.raquo.laminar.api.L.{*, given} -import scala.scalajs.js.Date -import com.raquo.waypoint.Router -import com.raquo.waypoint.SplitRender -import cz.e_bs.cmi.mdr.pdb.app.services.DataFetcher -import scala.scalajs.js.JSON -import zio.json._ -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.state.AppState - -@js.native -@JSImport("stylesheets/main.css", JSImport.Namespace) -object Css extends js.Any - -@js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native -@JSImport("params/pdb-params.json", JSImport.Default) -object pdbParams extends js.Object - -@JSExportTopLevel("app") -object Main: - - @JSExport - def main(args: Array[String]): Unit = - import Routes.given - onLoad { - setupAirstream() - val appContainer = dom.document.querySelector("#app") - val _ = - render( - appContainer, - renderPage(state.MockAppState(using unsafeWindowOwner, router)) - ) - } - - private def onLoad(f: => Unit): Unit = - documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) - - private def setupAirstream()(using router: Router[Page]): Unit = - AirstreamError.registerUnhandledErrorCallback(err => - router.forcePage( - Page.UnhandledError( - Some(err.getClass.getName), // TODO: Fill only in dev mode - Some(err.getMessage) - ) - ) - ) - - def renderPage(state: AppState)(using - router: Router[Page] - ): HtmlElement = - val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) - .collectSignal[Page.Detail]( - connectors - .DetailPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailParametru]( - connectors - .DetailParametruPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailKriteria]( - connectors - .DetailKriteriaPageConnector(state)(_) - .apply - ) - .collectSignal[Page.UpravDukazKriteria]( - pages.detail.UpravDukaz.Connector(state)(_).apply - ) - .collectStatic(Page.Dashboard)( - connectors.DashboardPageConnector(state.actionBus).apply - ) - .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) - ) - .collect[Page.UnhandledError](pg => - pages.errors - .UnhandledErrorPage( - pages.errors.UnhandledErrorPage - .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), - state.actionBus - ) - ) - .collectStatic(Page.Directory)( - connectors - .DirectoryPageConnector(state.users, state.actionBus) - .apply - ) - div(cls := "h-full", child <-- pageSplitter.$view) - - // Pull in the stylesheet - val css: Css.type = Css diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala deleted file mode 100644 index 3bbc8c0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ /dev/null @@ -1,198 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.* -import org.scalajs.dom -import zio.json.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import scala.scalajs.js -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.Page.Titled - -// enum is not working with Waypoints' SplitRender collectStatic -sealed abstract class Page( - val id: String, - val title: String, - val parent: Option[Page] -) { - val path: Vector[Page] = - parent match - case None => Vector(this) - case Some(p) => p.path :+ this - - val isRoot: Boolean = parent.isEmpty -} - -object Page: - - case class Titled[V](value: V, title: Option[String] = None): - val show: String = title.getOrElse(value.toString) - - case object Directory extends Page("directory", "Adresář", None) - - case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) - - case class Detail(osobniCislo: Titled[OsobniCislo]) - extends Page("user", osobniCislo.show, Some(Directory)) - - object Detail { - def apply(o: UserInfo): Detail = Detail( - Titled(o.personalNumber, Some(o.name)) - ) - } - - case class DetailParametru( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String] - ) extends Page( - "parameter", - parametr.show, - Some(Detail(osobniCislo)) - ) - - object DetailParametru { - def apply(o: UserInfo, p: Parameter): DetailParametru = - DetailParametru( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)) - ) - } - - case class DetailKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "criteria", - kriterium.show, - Some(DetailParametru(osobniCislo, parametr)) - ) - - object DetailKriteria { - def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = - DetailKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class UpravDukazKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "addProof", - "Důkaz", - Some(DetailKriteria(osobniCislo, parametr, kriterium)) - ) - - object UpravDukazKriteria { - def apply( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): UpravDukazKriteria = - UpravDukazKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class NotFound(url: String) extends Page("404", "404", Some(Directory)) - - case class UnhandledError( - errorName: Option[String], - errorMessage: Option[String] - ) extends Page("500", "Unexpected error", Some(Directory)) - -object Routes: - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) - given [V: JsonEncoder]: JsonEncoder[Titled[V]] = - DeriveJsonEncoder.gen[Titled[V]] - given [V: JsonDecoder]: JsonDecoder[Titled[V]] = - DeriveJsonDecoder.gen[Titled[V]] - given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] - given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] - - val base = - js.`import`.meta.env.BASE_URL - .asInstanceOf[String] + "app" - - val homePage: Page = Page.Directory - - given router: Router[Page] = Router[Page]( - routes = List( - Route.static(homePage, root / endOfSegments, basePath = base), - Route.static( - Page.Dashboard, - root / "dashboard" / endOfSegments, - basePath = base - ), - Route[Page.Detail, String]( - encode = _.osobniCislo.value.toString, - decode = osc => Page.Detail(Titled(OsobniCislo(osc))), - root / "osoba" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.DetailParametru, (String, String)]( - encode = p => (p.osobniCislo.value.toString, p.parametr.value), - decode = - p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / endOfSegments, - basePath = base - ), - Route[Page.DetailKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.DetailKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.UpravDukazKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.UpravDukazKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / "edit" / endOfSegments, - basePath = base - ) - ), - serializePage = _.toJson, - deserializePage = _.fromJson[Page] - .fold(s => throw IllegalStateException(s), identity), - getPageTitle = _.title, - routeFallback = url => Page.NotFound(url), - deserializeFallback = _ => Page.Dashboard - )( - $popStateEvent = windowEvents.onPopState, - owner = unsafeWindowOwner - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala deleted file mode 100644 index b24b82b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -sealed trait Action - -case object FetchDirectory extends Action -case class FetchUserDetails(osc: OsobniCislo) extends Action -case class FetchParameters(osc: OsobniCislo) extends Action -case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action -case class FetchParameterCriteria( - osc: OsobniCislo, - paramId: String, - critId: String, - page: (UserInfo, Parameter, ParameterCriteria) => Page -) extends Action -case class FetchAvailableFiles(osc: OsobniCislo) extends Action -case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala deleted file mode 100644 index c637d35..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala +++ /dev/null @@ -1,82 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.{UserProfile, UserInfo, OsobniCislo} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo -import cz.e_bs.cmi.mdr.pdb.UserFunction - -object AppPage: - // TODO: pages by logged in user - val pages: List[Page] = List(Page.Directory, Page.Dashboard) - - import NavigationBar.{Logo, MenuItem} - - val logo = Logo( - "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", - "Workflow" - ) - - // TODO: menu items by user profile - val userMenu = - List( - MenuItem("Your Profile"), - MenuItem("Settings"), - MenuItem("Sign out") - ) - - // TODO: load user profile - val $userProfile = Var( - UserProfile( - "tom", - UserInfo( - OsobniCislo("1031"), - "tom", - "Tom", - "Cook", - None, - None, - Some("tom@example.com"), - Some("+420 222 866 180"), - Some( - UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") - ), - Nil, - None - ) - ) - ) - - val $userInfo = $userProfile.signal.map(_.userInfo) - - type ViewModel = Option[HtmlElement] - def apply( - actionBus: Observer[Action] - )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using - router: Router[Page] - ): HtmlElement = - PageLayout(actionBus)( - $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => - PageLayout.ViewModel( - NavigationBar.ViewModel( - u, - pages.map(p => - NavigationBar.Link( - () => - PageLink( - p, - actionBus - ), - p == cp - ) - ), - userMenu, - logo - ), - c - ) - ), - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala deleted file mode 100644 index bf473c6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ /dev/null @@ -1,143 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden -import fiftyforms.ui.components.tailwind.list.IconText.ViewModel -import fiftyforms.ui.components.tailwind.Icons -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Action - -object Breadcrumbs: - - private def slash = - import svg.{*, given} - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-300", - xmlns := "http://www.w3.org/2000/svg", - fill := "currentColor", - viewBox := "0 0 20 20", - ariaHidden := true, - path( - d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" - ) - ) - - def backIcon = - Icons.solid - .`arrow-narrow-left`() - .amend( - svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" - ) - - object Link: - case class ViewModel( - page: Page, - icon: Option[SvgElement], - extraClasses: String, - text: String, - textClasses: Option[String] = None - ) - - val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" - - def shortHome(p: Page) = ViewModel( - p, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - "Zpět na úvodní stránku", - None - ) - - def fullHome(p: Page) = ViewModel( - p, - Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), - "text-gray-400 hover:text-gray-500", - "Domů", - Some("sr-only") - ) - - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - PageLink - .container($m.map(_.page), actionBus) - .amend( - cls <-- $m.map(_.extraClasses), - child.maybe <-- $m.map(_.icon), - span( - cls <-- $m.map(_.textClasses.getOrElse("")), - child.text <-- $m.map(_.text) - ) - ) - - object FullBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - ol( - role := "list", - cls := "flex items-center space-x-4", - children <-- $m.map(_.path).split(_.id)((_, _, $p) => - li( - div( - cls := "flex items-center", - child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), - Link( - $p.map(p => - if (p.isRoot) then Link.fullHome(p) - else - Link.ViewModel( - p, - None, - s"ml-4 max-w-xs truncate ${Link.baseClasses}", - p.title - ) - ), - actionBus - ) - ) - ) - ) - ) - - object ShortBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - Link( - $m.map { p => - val target = p.parent.getOrElse(p) - if target.isRoot then Link.shortHome(target) - else - Link.ViewModel( - target, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - s"Zpět na ${target.title}" - ) - }, - actionBus - ) - - def apply(actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - nav( - cls := "flex", - aria.label := "Breadcrumb", - div( - cls := "flex sm:hidden", - ShortBreadcrumbs(router.$currentPage, actionBus) - ), - div( - cls := "hidden sm:block", - FullBreadcrumbs(router.$currentPage, actionBus) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala deleted file mode 100644 index 0db2047..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ /dev/null @@ -1,242 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent -import fiftyforms.ui.components.tailwind.Icons -import fiftyforms.ui.components.tailwind.Avatar -import cz.e_bs.cmi.mdr.pdb.UserInfo -import io.laminext.syntax.core.* - -object NavigationBar: - - case class Logo(img: String, name: String) - case class Link(a: () => Anchor, active: Boolean) - case class MenuItem(title: String) - - case class ViewModel( - userInfo: UserInfo, - pages: List[Link], - userMenu: List[MenuItem], - logo: Logo - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - val $userInfo = $m.map(_.userInfo) - val mobileMenuOpen = Var(false) - - val desktopOnly = cls("hidden md:block") - val mobileOnly = cls("md:hidden") - - inline def avatarImage(size: Int = 8) = - Avatar($userInfo.map(_.img)).avatarImage(size) - - def notificationButton = button( - tpe := "button", - cls := List( - "bg-indigo-600", - "focus:outline-none", - "focus:ring-2", - "focus:ring-offset-2", - "focus:ring-offset-indigo-600", - "focus:ring-white", - "hover:text-white", - "p-1", - "rounded-full", - "text-indigo-200" - ), - span(cls := "sr-only", "View notifications"), - Icons.outline.bell() - ) - - def userProfile: HtmlElement = - val menuOpen = Var(false) - - def menuItem(item: MenuItem, idx: Int): HtmlElement = - a( - href := "#", - cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", - role := "menuitem", - tabIndex := -1, - idAttr := s"user-menu-item-$idx", - item.title - ) - - div( - cls := "ml-3 relative", - div( - button( - tpe := "button", - cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - idAttr := "user-menu-button", - aria.expanded <-- menuOpen.signal, - aria.hasPopup := true, - span(cls := "sr-only", "Open user menu"), - child <-- avatarImage(), - onClick.preventDefault.mapTo( - !menuOpen.now() - ) --> menuOpen.writer - ) - ), - /* - * */ - div( - cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", - cls <-- menuOpen.signal.map { o => - if (o) "md:block" else "md:hidden" - }, - role := "menu", - aria.orientation := "vertical", - aria.labelledBy := "user-menu-button", - tabIndex := -1, - // : keyboard navigation - children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) - ) - ) - - def mobileProfile = - def menuItem(item: MenuItem): HtmlElement = - a( - href := "#", - cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", - item.title - ) - - div( - cls := "pt-4 pb-3 border-t border-indigo-700", - div( - cls := "flex items-center px-5", - div( - cls := "flex-shrink-0", - child <-- avatarImage(10) - ), - div( - cls := "ml-3", - div( - cls := "text-base font-medium text-white", - child.text <-- $userInfo.map(_.name) - ), - child.maybe <-- $userInfo.map( - _.email.map(e => - div( - cls := "text-sm font-medium text-indigo-300", - e - ) - ) - ) - ), - notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) - ), - div( - cls := "mt-3 px-2 space-y-1", - children <-- $m.map(_.userMenu.map(menuItem)) - ) - ) - - def pageLink(page: Link): Anchor = - page - .a() - .amend( - cls := "text-white px-3 py-2 rounded-md text-sm font-medium", - cls := Seq( - "bg-indigo-700" -> page.active, - "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active - ), - ariaCurrent := (if page.active then "page" else "false") - ) - - def logoImg: Image = - img( - cls := "h-8 w-8", - src <-- $m.map(_.logo.img), - alt <-- $m.map(_.logo.name) - ) - - def mobileMenuButton = button( - tpe := "button", - cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - aria.controls := "mobile-menu", - aria.expanded <-- mobileMenuOpen.signal, - span(cls := "sr-only", "Open main menu"), - Icons.outline - .menu() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") - ), - Icons.outline - .x() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") - ), - onClick.preventDefault.mapTo( - !mobileMenuOpen.now() - ) --> mobileMenuOpen.writer - ) - - def navBarLeft = - div( - cls := "flex items-center", - div(cls := "flex-shrink-0", logoImg), - div( - desktopOnly, - div( - cls := "ml-10 flex items-baseline space-x-4", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ) - ) - ) - - def navBarRight = - div( - desktopOnly, - div( - cls := "ml-4 flex items-center md:ml-6", - notificationButton, - userProfile - ) - ) - - def navBarMobile = - div( - cls := "-mr-2 flex", - mobileOnly, - mobileMenuButton - ) - - def navBar = - div( - cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex items-center justify-between h-16", - navBarLeft, - navBarRight, - navBarMobile - ) - ) - - def mobileMenu = - div( - mobileOnly, - cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }, - idAttr := "mobile-menu", - div( - cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ), - mobileProfile - ) - - nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala deleted file mode 100644 index 75339df..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object PageHeader: - def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = - header( - cls := "bg-white shadow-sm", - div( - cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", - h1( - cls := "text-lg leading-6 font-semibold text-gray-900", - Breadcrumbs(actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala deleted file mode 100644 index 5096463..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ /dev/null @@ -1,30 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import fiftyforms.ui.components.tailwind.Loading -import io.laminext.syntax.core.* - -object PageLayout: - case class ViewModel( - navigation: NavigationBar.ViewModel, - content: Option[HtmlElement] - ) - def apply(actionBus: Observer[Action])( - $m: Signal[ViewModel], - mods: Modifier[HtmlElement]* - )(using router: Router[Page]): HtmlElement = - val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) - div( - cls := "h-full flex flex-col", - NavigationBar($m.map(_.navigation)), - child.maybe <-- router.$currentPage.map(_.isRoot) - .switch(None, Some(PageHeader(actionBus))), - main( - cls := "flex-grow-1 overflow-y-auto", - mods, - child <-- $maybeContent.map(_.getOrElse(Loading)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala deleted file mode 100644 index 62d820f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.LinkSupport.* -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo - -object PageLink: - type ViewModel = Page - def apply($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = - a(mods($m, actions), child.text <-- $m.map(_.title)) - - def apply(m: ViewModel, actions: Observer[Action])(using - router: Router[Page] - ): Anchor = apply(Val(m), actions) - - def container($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = a(mods($m, actions)) - - def container(m: ViewModel, actions: Observer[Action])(using - Router[Page] - ): Anchor = container(Val(m), actions) - - private def mods($m: Signal[Page], actions: Observer[Action])(using - router: Router[Page] - ): Modifier[Anchor] = - Seq( - href <-- $m.map(router.absoluteUrlForPage).recover { case _ => - Some("invalid url") - }, - composeEvents(onClick.noKeyMod.preventDefault)( - _.sample($m) - .map(NavigateTo.apply) - ) --> actions - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala deleted file mode 100644 index 87f825a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.pages.dashboard.DashboardPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -class DashboardPageConnector(actionBus: Observer[Action])(using - router: Router[Page] -): - def apply: HtmlElement = - AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala deleted file mode 100644 index e9b0f71..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala +++ /dev/null @@ -1,76 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailKriteriaPage -import pages.detail.components.DukazyKriteria -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -object DetailKriteriaPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailKriteriaPageConnector( - state: DetailKriteriaPageConnector.AppState -)( - $page: Signal[Page.DetailKriteria] -)(using Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => - (p.osobniCislo.value, p.parametr.value, p.kriterium.value) - )((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map( - FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) - ) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, s, $s) => - DetailKriteriaPage($s)(state.actionBus.contramap { - case DukazyKriteria.Add => - NavigateTo( - Page.UpravDukazKriteria( - Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), - Page.Titled(s.parametr.id, Some(s.parametr.nazev)), - Page.Titled(s.kriterium.id, Some(s.kriterium.id)) - ) - ) - }) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): DetailKriteriaPage.ViewModel = - DetailKriteriaPage.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala deleted file mode 100644 index 4d8024f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ /dev/null @@ -1,57 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import pages.detail.DetailPage -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import fiftyforms.ui.components.tailwind.Color - -object DetailPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailPageConnector(state: DetailPageConnector.AppState)( - $page: Signal[Page.Detail] -)(using router: Router[Page]): - val $oscChangeSignal = $page.splitOne(_.osobniCislo.value)((osc, _, _) => osc) - val $pageChangeSignal = - $oscChangeSignal.flatMap(osc => - EventStream.fromSeq(Seq(FetchUserDetails(osc), FetchParameters(osc))) - ) - // TODO: filter the value based on the current osc - // OSC change will fetch new data, but still - // - we need to be sure that what we got is really what we ought to display - // - we want to display stale data accordingly (at least with loading indicator) - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - def apply: HtmlElement = - AppPage(state.actionBus)( - $data.combineWithFn($params)(_ zip _) - .map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: List[Parameter] - ): DetailPage.ViewModel = - DetailPage.ViewModel( - o.toDetailOsoby, - p.map( - _.toParametr(param => - PageLink.container(Page.DetailParametru(o, param), state.actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala deleted file mode 100644 index fdaa7f8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailParametruPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink - -object DetailParametruPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailParametruPageConnector( - state: DetailParametruPageConnector.AppState -)( - $page: Signal[Page.DetailParametru] -)(using router: Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => (p.osobniCislo.value, p.parametr.value))((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map(FetchParameter(_, _)) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - } yield (da, pb) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailParametruPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter - ): DetailParametruPage.ViewModel = - DetailParametruPage.ViewModel( - o.toDetailOsoby, - p.toParametr(p => - PageLink.container(Page.DetailParametru(o, p), state.actionBus) - ), - p.criteria.map( - _.toKriterium { c => - PageLink.container( - Page.DetailKriteria(o, p, c), - state.actionBus - ) - } - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala deleted file mode 100644 index 049aa22..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -case class DirectoryPageConnector( - $input: EventStream[List[UserInfo]], - actionBus: Observer[Action] -)(using router: Router[Page]): - val $data = $input.startWithNone - val $actionSignal = EventStream.fromValue(FetchDirectory) - - def apply: HtmlElement = - AppPage(actionBus)( - $data.split(_ => ())((_, _, s) => - pages.directory.DirectoryPage( - s.map( - _.map( - _.toUserRow(u => - PageLink.container( - Page.Detail(Page.Titled(u.personalNumber)), - actionBus - ) - ) - ) - ) - ) - ), - $actionSignal --> actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala deleted file mode 100644 index 88e7450..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ /dev/null @@ -1,68 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii -import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria -import fiftyforms.ui.components.tailwind.Color - -extension (o: UserInfo) - def toDetailOsoby: DetailOsoby.ViewModel = - DetailOsoby.ViewModel( - o.personalNumber, - o.name, - o.email, - o.phone, - o.img, - o.mainFunction.map(f => - DetailOsoby.Funkce.ViewModel(f.name, f.dept, f.ou) - ), - o.userContracts.headOption.map(c => - DetailOsoby.PracovniPomer.ViewModel(c.rel, c.startDate, c.endDate) - ) - ) - -extension (param: Parameter) - def toParametr(container: Parameter => Anchor): DetailParametru.ViewModel = - DetailParametru.ViewModel( - id = param.id, - nazev = param.name, - popis = param.description, - status = "Nesplněno", - statusColor = Color.red, - a = container(param) - ) - -extension (crit: ParameterCriteria) - def toKriterium( - container: ParameterCriteria => Anchor - ): DetailKriteria.ViewModel = - DetailKriteria.ViewModel( - nazev = crit.criteriumText, - kapitola = crit.chapterId, - bod = crit.itemId, - status = "Nesplněno", - statusColor = Color.red, - splneno = false, - dukazy = Nil, - container = container(crit) - ) - -extension (user: UserInfo) - def toUserRow( - container: UserInfo => HtmlElement = (_: UserInfo) => div() - ): UserRow.ViewModel = - UserRow.ViewModel( - osobniCislo = user.personalNumber.toString, - celeJmeno = user.name, - prijmeni = user.surname, - hlavniFunkce = user.mainFunction.map(_.name), - img = user.img, - container = () => container(user) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala deleted file mode 100644 index 7d5b2c1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.dashboard - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.Page - -object DashboardPage: - - def render: HtmlElement = - div("Dashboard page") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala deleted file mode 100644 index 9ef0698..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala +++ /dev/null @@ -1,31 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailKriteriaPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply($m: Signal[ViewModel])( - action: Observer[DukazyKriteria.Action] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - DukazyKriteria($m.map(_.kriterium.dukazy))(action) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala deleted file mode 100644 index 3b316e1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object DetailPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametry: SeznamParametru.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby($m.map(_.osoba)), - SeznamParametru($m.map(_.parametry)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala deleted file mode 100644 index 17224cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala +++ /dev/null @@ -1,24 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailParametruPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriteria: SeznamKriterii.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby.header($m.map(_.osoba)), - DetailParametru($m.map(_.parametr)), - SeznamKriterii($m.map(_.kriteria)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala deleted file mode 100644 index dc912fa..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ /dev/null @@ -1,107 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb -package app -package pages.detail - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.UpravDukazForm -import fiftyforms.services.files.File - -object UpravDukaz: - - type ThisPage = Page.UpravDukazKriteria - type PageKey = (OsobniCislo, String, String) - - trait State { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def availableFiles: EventStream[List[File]] - def actionBus: Observer[Action] - } - - def keyOfPage(page: ThisPage): PageKey = - (page.osobniCislo.value, page.parametr.value, page.kriterium.value) - - def onChangeAction(key: PageKey): Action = - FetchParameterCriteria( - key._1, - key._2, - key._3, - Page.UpravDukazKriteria(_, _, _) - ) - - class Connector(state: State)($page: Signal[ThisPage])(using Router[Page]): - val $paramChangeSignal = $page.splitOne(keyOfPage)((x, _, _) => x) - val $pageChangeSignal = $paramChangeSignal.map(onChangeAction) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.split(_ => ())((_, s, $s) => - PageComponent( - $s.map(buildModel), - state.availableFiles, - state.actionBus.contramap { - case UpravDukazForm.Cancelled => - NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) - case UpravDukazForm.AvailableFilesRequested => - FetchAvailableFiles(s._1.personalNumber) - } - ) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): PageComponent.ViewModel = - import connectors.* - PageComponent.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) - - object PageComponent: - import components.* - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply( - $m: Signal[ViewModel], - availableFilesStream: EventStream[List[File]], - events: Observer[UpravDukazForm.Event] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - UpravDukazForm(availableFilesStream)(events) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala deleted file mode 100644 index ca311b0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.Icons -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Color - -object DetailKriteria: - case class ViewModel( - nazev: String, - kapitola: String, - bod: String, - status: String, - statusColor: Color, - splneno: Boolean, - dukazy: List[DukazKriteria.ViewModel], - container: HtmlElement = div() - ) { - val id = s"${kapitola}${bod}" - } - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h3( - cls := "text-xl leading-6 font-bold text-gray-900", - child.text <-- $m.map(_.nazev) - ), - h3( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - "Kapitola ", - child.text <-- $m.map(_.kapitola), - " bod ", - child <-- $m.map(_.bod) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala deleted file mode 100644 index ae90290..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala +++ /dev/null @@ -1,100 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Avatar - -object DetailOsoby: - - object Funkce: - case class ViewModel( - nazev: String, - stredisko: String, - voj: String - ) - - def render($m: Signal[ViewModel]): HtmlElement = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.nazev), - span( - cls := "hidden md:inline", - " @ ", - child.text <-- $m.map(_.stredisko), - ", ", - child.text <-- $m.map(_.voj) - ) - ) - - object PracovniPomer: - case class ViewModel( - druh: String, - pocatek: LocalDate, - konec: Option[LocalDate] - ) - - def render($m: Signal[ViewModel]): HtmlElement = - import fiftyforms.ui.components.tailwind.CustomAttrs.datetime - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.druh), - " od ", - time( - datetime <-- $m.map(_.pocatek.toString), - child.text <-- $m.map(_.pocatek.toString) - ) - ) - - case class ViewModel( - osobniCislo: OsobniCislo, - jmeno: String, - email: Option[String], - telefon: Option[String], - img: Option[String], - hlavniFunkce: Option[Funkce.ViewModel], - pracovniPomer: Option[PracovniPomer.ViewModel] - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-4", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(16) - ), - div( - h1( - cls := "text-2xl font-bold text-gray-900", - child.text <-- $m.map(_.jmeno) - ), - child.maybe <-- $m.map(_.hlavniFunkce).split(_ => ())((_, _, d) => - Funkce.render(d) - ), - child.maybe <-- $m.map(_.pracovniPomer).split(_ => ())((_, _, d) => - PracovniPomer.render(d) - ) - ) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-5", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(8) - ), - div( - h1( - cls := "text-lg font-medium text-gray-800", - child.text <-- $m.map(_.jmeno) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala deleted file mode 100644 index e11cb62..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color - -object DetailParametru: - case class ViewModel( - id: String, - nazev: String, - popis: String, - status: String, - statusColor: Color, - a: Anchor - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h2( - cls := "text-2xl leading-6 font-medium text-gray-900", - child.text <-- $m.map(_.nazev) - ), - p( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - child.text <-- $m.map(_.popis) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - h2( - cls := "text-lg leading-6 font-medium text-gray-600", - child.text <-- $m.map(_.nazev) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala deleted file mode 100644 index a00ede8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala +++ /dev/null @@ -1,177 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.CustomAttrs - -object DukazKriteria: - case class Osoba(osobniCislo: String, jmeno: String) - case class Udalost(osoba: Osoba, datum: LocalDate) - case class Dokument( - url: String, - nazev: String, - pridal: Udalost, - odebral: Option[Udalost] - ) - case class ViewModel( - dokumenty: List[Dokument], - autorizoval: Option[Udalost], - platiDo: Option[LocalDate], - poznámka: Option[String] - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-lg", - div( - cls := "px-4 py-5 sm:px-6", - h3( - cls := "text-lg leading-6 font-medium text-gray-900", - """Applicant Information""" - ), - p( - cls := "mt-1 max-w-2xl text-sm text-gray-500", - """Personal details and application.""" - ) - ), - div( - cls := "border-t border-gray-200 px-4 py-5 sm:px-6", - dl( - cls := "grid grid-cols-1 gap-x-4 gap-y-8 sm:grid-cols-2", - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Full name""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Margot Foster""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Application for""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Backend Developer""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Email address""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """margotfoster@example.com""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Salary expectation""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """$120,000""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """About""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Fugiat ipsum ipsum deserunt culpa aute sint do nostrud anim incididunt cillum culpa consequat. Excepteur qui ipsum aliquip consequat sint. Sit id mollit nulla mollit nostrud in ea officia proident. Irure nostrud pariatur mollit ad adipisicing reprehenderit deserunt qui eu.""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """Attachments""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - ul( - role := "list", - cls := "border border-gray-200 rounded-md divide-y divide-gray-200", - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """resume_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ), - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """coverletter_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala deleted file mode 100644 index 22f134f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala +++ /dev/null @@ -1,35 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object DukazyKriteria: - sealed trait Action - case object Add extends Action - - type ViewModel = List[DukazKriteria.ViewModel] - - def apply($m: Signal[ViewModel])(actions: Observer[Action]): HtmlElement = - div( - child.maybe <-- $m.map(m => - if (m.isEmpty) then Some(prazdnyDukaz(actions)) - else None - ), - children <-- $m.map(_.zipWithIndex).split(_._2)((_, _, $s) => - DukazKriteria($s.map(_._1)) - ) - ) - - private def prazdnyDukaz(actions: Observer[Action]): HtmlElement = - button( - tpe := "button", - onClick.preventDefault.mapTo(Add) --> actions, - cls := "relative block w-full border-2 border-gray-300 border-dashed rounded-lg p-12 text-center hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - Icons.outline - .`document-add`(12) - .amend(svg.cls := "mx-auto text-gray-400"), - span( - cls := "mt-2 block text-sm font-medium text-gray-900", - "Přidat důkaz" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala deleted file mode 100644 index a3b00af..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala +++ /dev/null @@ -1,103 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import scala.scalajs.js.annotation.JSExportTopLevel -import scala.scalajs.js.annotation.JSExport -import scala.scalajs.js.annotation.JSImport -import scala.scalajs.js -import org.scalajs.dom -import com.raquo.laminar.api.L.{*, given} -import scala.scalajs.js.Date -import com.raquo.waypoint.Router -import com.raquo.waypoint.SplitRender -import cz.e_bs.cmi.mdr.pdb.app.services.DataFetcher -import scala.scalajs.js.JSON -import zio.json._ -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.state.AppState - -@js.native -@JSImport("stylesheets/main.css", JSImport.Namespace) -object Css extends js.Any - -@js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native -@JSImport("params/pdb-params.json", JSImport.Default) -object pdbParams extends js.Object - -@JSExportTopLevel("app") -object Main: - - @JSExport - def main(args: Array[String]): Unit = - import Routes.given - onLoad { - setupAirstream() - val appContainer = dom.document.querySelector("#app") - val _ = - render( - appContainer, - renderPage(state.MockAppState(using unsafeWindowOwner, router)) - ) - } - - private def onLoad(f: => Unit): Unit = - documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) - - private def setupAirstream()(using router: Router[Page]): Unit = - AirstreamError.registerUnhandledErrorCallback(err => - router.forcePage( - Page.UnhandledError( - Some(err.getClass.getName), // TODO: Fill only in dev mode - Some(err.getMessage) - ) - ) - ) - - def renderPage(state: AppState)(using - router: Router[Page] - ): HtmlElement = - val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) - .collectSignal[Page.Detail]( - connectors - .DetailPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailParametru]( - connectors - .DetailParametruPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailKriteria]( - connectors - .DetailKriteriaPageConnector(state)(_) - .apply - ) - .collectSignal[Page.UpravDukazKriteria]( - pages.detail.UpravDukaz.Connector(state)(_).apply - ) - .collectStatic(Page.Dashboard)( - connectors.DashboardPageConnector(state.actionBus).apply - ) - .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) - ) - .collect[Page.UnhandledError](pg => - pages.errors - .UnhandledErrorPage( - pages.errors.UnhandledErrorPage - .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), - state.actionBus - ) - ) - .collectStatic(Page.Directory)( - connectors - .DirectoryPageConnector(state.users, state.actionBus) - .apply - ) - div(cls := "h-full", child <-- pageSplitter.$view) - - // Pull in the stylesheet - val css: Css.type = Css diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala deleted file mode 100644 index 3bbc8c0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ /dev/null @@ -1,198 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.* -import org.scalajs.dom -import zio.json.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import scala.scalajs.js -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.Page.Titled - -// enum is not working with Waypoints' SplitRender collectStatic -sealed abstract class Page( - val id: String, - val title: String, - val parent: Option[Page] -) { - val path: Vector[Page] = - parent match - case None => Vector(this) - case Some(p) => p.path :+ this - - val isRoot: Boolean = parent.isEmpty -} - -object Page: - - case class Titled[V](value: V, title: Option[String] = None): - val show: String = title.getOrElse(value.toString) - - case object Directory extends Page("directory", "Adresář", None) - - case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) - - case class Detail(osobniCislo: Titled[OsobniCislo]) - extends Page("user", osobniCislo.show, Some(Directory)) - - object Detail { - def apply(o: UserInfo): Detail = Detail( - Titled(o.personalNumber, Some(o.name)) - ) - } - - case class DetailParametru( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String] - ) extends Page( - "parameter", - parametr.show, - Some(Detail(osobniCislo)) - ) - - object DetailParametru { - def apply(o: UserInfo, p: Parameter): DetailParametru = - DetailParametru( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)) - ) - } - - case class DetailKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "criteria", - kriterium.show, - Some(DetailParametru(osobniCislo, parametr)) - ) - - object DetailKriteria { - def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = - DetailKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class UpravDukazKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "addProof", - "Důkaz", - Some(DetailKriteria(osobniCislo, parametr, kriterium)) - ) - - object UpravDukazKriteria { - def apply( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): UpravDukazKriteria = - UpravDukazKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class NotFound(url: String) extends Page("404", "404", Some(Directory)) - - case class UnhandledError( - errorName: Option[String], - errorMessage: Option[String] - ) extends Page("500", "Unexpected error", Some(Directory)) - -object Routes: - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) - given [V: JsonEncoder]: JsonEncoder[Titled[V]] = - DeriveJsonEncoder.gen[Titled[V]] - given [V: JsonDecoder]: JsonDecoder[Titled[V]] = - DeriveJsonDecoder.gen[Titled[V]] - given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] - given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] - - val base = - js.`import`.meta.env.BASE_URL - .asInstanceOf[String] + "app" - - val homePage: Page = Page.Directory - - given router: Router[Page] = Router[Page]( - routes = List( - Route.static(homePage, root / endOfSegments, basePath = base), - Route.static( - Page.Dashboard, - root / "dashboard" / endOfSegments, - basePath = base - ), - Route[Page.Detail, String]( - encode = _.osobniCislo.value.toString, - decode = osc => Page.Detail(Titled(OsobniCislo(osc))), - root / "osoba" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.DetailParametru, (String, String)]( - encode = p => (p.osobniCislo.value.toString, p.parametr.value), - decode = - p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / endOfSegments, - basePath = base - ), - Route[Page.DetailKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.DetailKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.UpravDukazKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.UpravDukazKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / "edit" / endOfSegments, - basePath = base - ) - ), - serializePage = _.toJson, - deserializePage = _.fromJson[Page] - .fold(s => throw IllegalStateException(s), identity), - getPageTitle = _.title, - routeFallback = url => Page.NotFound(url), - deserializeFallback = _ => Page.Dashboard - )( - $popStateEvent = windowEvents.onPopState, - owner = unsafeWindowOwner - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala deleted file mode 100644 index b24b82b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -sealed trait Action - -case object FetchDirectory extends Action -case class FetchUserDetails(osc: OsobniCislo) extends Action -case class FetchParameters(osc: OsobniCislo) extends Action -case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action -case class FetchParameterCriteria( - osc: OsobniCislo, - paramId: String, - critId: String, - page: (UserInfo, Parameter, ParameterCriteria) => Page -) extends Action -case class FetchAvailableFiles(osc: OsobniCislo) extends Action -case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala deleted file mode 100644 index c637d35..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala +++ /dev/null @@ -1,82 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.{UserProfile, UserInfo, OsobniCislo} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo -import cz.e_bs.cmi.mdr.pdb.UserFunction - -object AppPage: - // TODO: pages by logged in user - val pages: List[Page] = List(Page.Directory, Page.Dashboard) - - import NavigationBar.{Logo, MenuItem} - - val logo = Logo( - "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", - "Workflow" - ) - - // TODO: menu items by user profile - val userMenu = - List( - MenuItem("Your Profile"), - MenuItem("Settings"), - MenuItem("Sign out") - ) - - // TODO: load user profile - val $userProfile = Var( - UserProfile( - "tom", - UserInfo( - OsobniCislo("1031"), - "tom", - "Tom", - "Cook", - None, - None, - Some("tom@example.com"), - Some("+420 222 866 180"), - Some( - UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") - ), - Nil, - None - ) - ) - ) - - val $userInfo = $userProfile.signal.map(_.userInfo) - - type ViewModel = Option[HtmlElement] - def apply( - actionBus: Observer[Action] - )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using - router: Router[Page] - ): HtmlElement = - PageLayout(actionBus)( - $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => - PageLayout.ViewModel( - NavigationBar.ViewModel( - u, - pages.map(p => - NavigationBar.Link( - () => - PageLink( - p, - actionBus - ), - p == cp - ) - ), - userMenu, - logo - ), - c - ) - ), - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala deleted file mode 100644 index bf473c6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ /dev/null @@ -1,143 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden -import fiftyforms.ui.components.tailwind.list.IconText.ViewModel -import fiftyforms.ui.components.tailwind.Icons -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Action - -object Breadcrumbs: - - private def slash = - import svg.{*, given} - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-300", - xmlns := "http://www.w3.org/2000/svg", - fill := "currentColor", - viewBox := "0 0 20 20", - ariaHidden := true, - path( - d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" - ) - ) - - def backIcon = - Icons.solid - .`arrow-narrow-left`() - .amend( - svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" - ) - - object Link: - case class ViewModel( - page: Page, - icon: Option[SvgElement], - extraClasses: String, - text: String, - textClasses: Option[String] = None - ) - - val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" - - def shortHome(p: Page) = ViewModel( - p, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - "Zpět na úvodní stránku", - None - ) - - def fullHome(p: Page) = ViewModel( - p, - Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), - "text-gray-400 hover:text-gray-500", - "Domů", - Some("sr-only") - ) - - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - PageLink - .container($m.map(_.page), actionBus) - .amend( - cls <-- $m.map(_.extraClasses), - child.maybe <-- $m.map(_.icon), - span( - cls <-- $m.map(_.textClasses.getOrElse("")), - child.text <-- $m.map(_.text) - ) - ) - - object FullBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - ol( - role := "list", - cls := "flex items-center space-x-4", - children <-- $m.map(_.path).split(_.id)((_, _, $p) => - li( - div( - cls := "flex items-center", - child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), - Link( - $p.map(p => - if (p.isRoot) then Link.fullHome(p) - else - Link.ViewModel( - p, - None, - s"ml-4 max-w-xs truncate ${Link.baseClasses}", - p.title - ) - ), - actionBus - ) - ) - ) - ) - ) - - object ShortBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - Link( - $m.map { p => - val target = p.parent.getOrElse(p) - if target.isRoot then Link.shortHome(target) - else - Link.ViewModel( - target, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - s"Zpět na ${target.title}" - ) - }, - actionBus - ) - - def apply(actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - nav( - cls := "flex", - aria.label := "Breadcrumb", - div( - cls := "flex sm:hidden", - ShortBreadcrumbs(router.$currentPage, actionBus) - ), - div( - cls := "hidden sm:block", - FullBreadcrumbs(router.$currentPage, actionBus) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala deleted file mode 100644 index 0db2047..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ /dev/null @@ -1,242 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent -import fiftyforms.ui.components.tailwind.Icons -import fiftyforms.ui.components.tailwind.Avatar -import cz.e_bs.cmi.mdr.pdb.UserInfo -import io.laminext.syntax.core.* - -object NavigationBar: - - case class Logo(img: String, name: String) - case class Link(a: () => Anchor, active: Boolean) - case class MenuItem(title: String) - - case class ViewModel( - userInfo: UserInfo, - pages: List[Link], - userMenu: List[MenuItem], - logo: Logo - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - val $userInfo = $m.map(_.userInfo) - val mobileMenuOpen = Var(false) - - val desktopOnly = cls("hidden md:block") - val mobileOnly = cls("md:hidden") - - inline def avatarImage(size: Int = 8) = - Avatar($userInfo.map(_.img)).avatarImage(size) - - def notificationButton = button( - tpe := "button", - cls := List( - "bg-indigo-600", - "focus:outline-none", - "focus:ring-2", - "focus:ring-offset-2", - "focus:ring-offset-indigo-600", - "focus:ring-white", - "hover:text-white", - "p-1", - "rounded-full", - "text-indigo-200" - ), - span(cls := "sr-only", "View notifications"), - Icons.outline.bell() - ) - - def userProfile: HtmlElement = - val menuOpen = Var(false) - - def menuItem(item: MenuItem, idx: Int): HtmlElement = - a( - href := "#", - cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", - role := "menuitem", - tabIndex := -1, - idAttr := s"user-menu-item-$idx", - item.title - ) - - div( - cls := "ml-3 relative", - div( - button( - tpe := "button", - cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - idAttr := "user-menu-button", - aria.expanded <-- menuOpen.signal, - aria.hasPopup := true, - span(cls := "sr-only", "Open user menu"), - child <-- avatarImage(), - onClick.preventDefault.mapTo( - !menuOpen.now() - ) --> menuOpen.writer - ) - ), - /* - * */ - div( - cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", - cls <-- menuOpen.signal.map { o => - if (o) "md:block" else "md:hidden" - }, - role := "menu", - aria.orientation := "vertical", - aria.labelledBy := "user-menu-button", - tabIndex := -1, - // : keyboard navigation - children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) - ) - ) - - def mobileProfile = - def menuItem(item: MenuItem): HtmlElement = - a( - href := "#", - cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", - item.title - ) - - div( - cls := "pt-4 pb-3 border-t border-indigo-700", - div( - cls := "flex items-center px-5", - div( - cls := "flex-shrink-0", - child <-- avatarImage(10) - ), - div( - cls := "ml-3", - div( - cls := "text-base font-medium text-white", - child.text <-- $userInfo.map(_.name) - ), - child.maybe <-- $userInfo.map( - _.email.map(e => - div( - cls := "text-sm font-medium text-indigo-300", - e - ) - ) - ) - ), - notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) - ), - div( - cls := "mt-3 px-2 space-y-1", - children <-- $m.map(_.userMenu.map(menuItem)) - ) - ) - - def pageLink(page: Link): Anchor = - page - .a() - .amend( - cls := "text-white px-3 py-2 rounded-md text-sm font-medium", - cls := Seq( - "bg-indigo-700" -> page.active, - "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active - ), - ariaCurrent := (if page.active then "page" else "false") - ) - - def logoImg: Image = - img( - cls := "h-8 w-8", - src <-- $m.map(_.logo.img), - alt <-- $m.map(_.logo.name) - ) - - def mobileMenuButton = button( - tpe := "button", - cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - aria.controls := "mobile-menu", - aria.expanded <-- mobileMenuOpen.signal, - span(cls := "sr-only", "Open main menu"), - Icons.outline - .menu() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") - ), - Icons.outline - .x() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") - ), - onClick.preventDefault.mapTo( - !mobileMenuOpen.now() - ) --> mobileMenuOpen.writer - ) - - def navBarLeft = - div( - cls := "flex items-center", - div(cls := "flex-shrink-0", logoImg), - div( - desktopOnly, - div( - cls := "ml-10 flex items-baseline space-x-4", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ) - ) - ) - - def navBarRight = - div( - desktopOnly, - div( - cls := "ml-4 flex items-center md:ml-6", - notificationButton, - userProfile - ) - ) - - def navBarMobile = - div( - cls := "-mr-2 flex", - mobileOnly, - mobileMenuButton - ) - - def navBar = - div( - cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex items-center justify-between h-16", - navBarLeft, - navBarRight, - navBarMobile - ) - ) - - def mobileMenu = - div( - mobileOnly, - cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }, - idAttr := "mobile-menu", - div( - cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ), - mobileProfile - ) - - nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala deleted file mode 100644 index 75339df..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object PageHeader: - def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = - header( - cls := "bg-white shadow-sm", - div( - cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", - h1( - cls := "text-lg leading-6 font-semibold text-gray-900", - Breadcrumbs(actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala deleted file mode 100644 index 5096463..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ /dev/null @@ -1,30 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import fiftyforms.ui.components.tailwind.Loading -import io.laminext.syntax.core.* - -object PageLayout: - case class ViewModel( - navigation: NavigationBar.ViewModel, - content: Option[HtmlElement] - ) - def apply(actionBus: Observer[Action])( - $m: Signal[ViewModel], - mods: Modifier[HtmlElement]* - )(using router: Router[Page]): HtmlElement = - val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) - div( - cls := "h-full flex flex-col", - NavigationBar($m.map(_.navigation)), - child.maybe <-- router.$currentPage.map(_.isRoot) - .switch(None, Some(PageHeader(actionBus))), - main( - cls := "flex-grow-1 overflow-y-auto", - mods, - child <-- $maybeContent.map(_.getOrElse(Loading)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala deleted file mode 100644 index 62d820f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.LinkSupport.* -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo - -object PageLink: - type ViewModel = Page - def apply($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = - a(mods($m, actions), child.text <-- $m.map(_.title)) - - def apply(m: ViewModel, actions: Observer[Action])(using - router: Router[Page] - ): Anchor = apply(Val(m), actions) - - def container($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = a(mods($m, actions)) - - def container(m: ViewModel, actions: Observer[Action])(using - Router[Page] - ): Anchor = container(Val(m), actions) - - private def mods($m: Signal[Page], actions: Observer[Action])(using - router: Router[Page] - ): Modifier[Anchor] = - Seq( - href <-- $m.map(router.absoluteUrlForPage).recover { case _ => - Some("invalid url") - }, - composeEvents(onClick.noKeyMod.preventDefault)( - _.sample($m) - .map(NavigateTo.apply) - ) --> actions - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala deleted file mode 100644 index 87f825a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.pages.dashboard.DashboardPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -class DashboardPageConnector(actionBus: Observer[Action])(using - router: Router[Page] -): - def apply: HtmlElement = - AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala deleted file mode 100644 index e9b0f71..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala +++ /dev/null @@ -1,76 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailKriteriaPage -import pages.detail.components.DukazyKriteria -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -object DetailKriteriaPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailKriteriaPageConnector( - state: DetailKriteriaPageConnector.AppState -)( - $page: Signal[Page.DetailKriteria] -)(using Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => - (p.osobniCislo.value, p.parametr.value, p.kriterium.value) - )((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map( - FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) - ) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, s, $s) => - DetailKriteriaPage($s)(state.actionBus.contramap { - case DukazyKriteria.Add => - NavigateTo( - Page.UpravDukazKriteria( - Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), - Page.Titled(s.parametr.id, Some(s.parametr.nazev)), - Page.Titled(s.kriterium.id, Some(s.kriterium.id)) - ) - ) - }) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): DetailKriteriaPage.ViewModel = - DetailKriteriaPage.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala deleted file mode 100644 index 4d8024f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ /dev/null @@ -1,57 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import pages.detail.DetailPage -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import fiftyforms.ui.components.tailwind.Color - -object DetailPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailPageConnector(state: DetailPageConnector.AppState)( - $page: Signal[Page.Detail] -)(using router: Router[Page]): - val $oscChangeSignal = $page.splitOne(_.osobniCislo.value)((osc, _, _) => osc) - val $pageChangeSignal = - $oscChangeSignal.flatMap(osc => - EventStream.fromSeq(Seq(FetchUserDetails(osc), FetchParameters(osc))) - ) - // TODO: filter the value based on the current osc - // OSC change will fetch new data, but still - // - we need to be sure that what we got is really what we ought to display - // - we want to display stale data accordingly (at least with loading indicator) - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - def apply: HtmlElement = - AppPage(state.actionBus)( - $data.combineWithFn($params)(_ zip _) - .map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: List[Parameter] - ): DetailPage.ViewModel = - DetailPage.ViewModel( - o.toDetailOsoby, - p.map( - _.toParametr(param => - PageLink.container(Page.DetailParametru(o, param), state.actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala deleted file mode 100644 index fdaa7f8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailParametruPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink - -object DetailParametruPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailParametruPageConnector( - state: DetailParametruPageConnector.AppState -)( - $page: Signal[Page.DetailParametru] -)(using router: Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => (p.osobniCislo.value, p.parametr.value))((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map(FetchParameter(_, _)) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - } yield (da, pb) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailParametruPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter - ): DetailParametruPage.ViewModel = - DetailParametruPage.ViewModel( - o.toDetailOsoby, - p.toParametr(p => - PageLink.container(Page.DetailParametru(o, p), state.actionBus) - ), - p.criteria.map( - _.toKriterium { c => - PageLink.container( - Page.DetailKriteria(o, p, c), - state.actionBus - ) - } - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala deleted file mode 100644 index 049aa22..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -case class DirectoryPageConnector( - $input: EventStream[List[UserInfo]], - actionBus: Observer[Action] -)(using router: Router[Page]): - val $data = $input.startWithNone - val $actionSignal = EventStream.fromValue(FetchDirectory) - - def apply: HtmlElement = - AppPage(actionBus)( - $data.split(_ => ())((_, _, s) => - pages.directory.DirectoryPage( - s.map( - _.map( - _.toUserRow(u => - PageLink.container( - Page.Detail(Page.Titled(u.personalNumber)), - actionBus - ) - ) - ) - ) - ) - ), - $actionSignal --> actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala deleted file mode 100644 index 88e7450..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ /dev/null @@ -1,68 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii -import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria -import fiftyforms.ui.components.tailwind.Color - -extension (o: UserInfo) - def toDetailOsoby: DetailOsoby.ViewModel = - DetailOsoby.ViewModel( - o.personalNumber, - o.name, - o.email, - o.phone, - o.img, - o.mainFunction.map(f => - DetailOsoby.Funkce.ViewModel(f.name, f.dept, f.ou) - ), - o.userContracts.headOption.map(c => - DetailOsoby.PracovniPomer.ViewModel(c.rel, c.startDate, c.endDate) - ) - ) - -extension (param: Parameter) - def toParametr(container: Parameter => Anchor): DetailParametru.ViewModel = - DetailParametru.ViewModel( - id = param.id, - nazev = param.name, - popis = param.description, - status = "Nesplněno", - statusColor = Color.red, - a = container(param) - ) - -extension (crit: ParameterCriteria) - def toKriterium( - container: ParameterCriteria => Anchor - ): DetailKriteria.ViewModel = - DetailKriteria.ViewModel( - nazev = crit.criteriumText, - kapitola = crit.chapterId, - bod = crit.itemId, - status = "Nesplněno", - statusColor = Color.red, - splneno = false, - dukazy = Nil, - container = container(crit) - ) - -extension (user: UserInfo) - def toUserRow( - container: UserInfo => HtmlElement = (_: UserInfo) => div() - ): UserRow.ViewModel = - UserRow.ViewModel( - osobniCislo = user.personalNumber.toString, - celeJmeno = user.name, - prijmeni = user.surname, - hlavniFunkce = user.mainFunction.map(_.name), - img = user.img, - container = () => container(user) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala deleted file mode 100644 index 7d5b2c1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.dashboard - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.Page - -object DashboardPage: - - def render: HtmlElement = - div("Dashboard page") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala deleted file mode 100644 index 9ef0698..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala +++ /dev/null @@ -1,31 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailKriteriaPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply($m: Signal[ViewModel])( - action: Observer[DukazyKriteria.Action] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - DukazyKriteria($m.map(_.kriterium.dukazy))(action) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala deleted file mode 100644 index 3b316e1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object DetailPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametry: SeznamParametru.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby($m.map(_.osoba)), - SeznamParametru($m.map(_.parametry)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala deleted file mode 100644 index 17224cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala +++ /dev/null @@ -1,24 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailParametruPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriteria: SeznamKriterii.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby.header($m.map(_.osoba)), - DetailParametru($m.map(_.parametr)), - SeznamKriterii($m.map(_.kriteria)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala deleted file mode 100644 index dc912fa..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ /dev/null @@ -1,107 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb -package app -package pages.detail - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.UpravDukazForm -import fiftyforms.services.files.File - -object UpravDukaz: - - type ThisPage = Page.UpravDukazKriteria - type PageKey = (OsobniCislo, String, String) - - trait State { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def availableFiles: EventStream[List[File]] - def actionBus: Observer[Action] - } - - def keyOfPage(page: ThisPage): PageKey = - (page.osobniCislo.value, page.parametr.value, page.kriterium.value) - - def onChangeAction(key: PageKey): Action = - FetchParameterCriteria( - key._1, - key._2, - key._3, - Page.UpravDukazKriteria(_, _, _) - ) - - class Connector(state: State)($page: Signal[ThisPage])(using Router[Page]): - val $paramChangeSignal = $page.splitOne(keyOfPage)((x, _, _) => x) - val $pageChangeSignal = $paramChangeSignal.map(onChangeAction) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.split(_ => ())((_, s, $s) => - PageComponent( - $s.map(buildModel), - state.availableFiles, - state.actionBus.contramap { - case UpravDukazForm.Cancelled => - NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) - case UpravDukazForm.AvailableFilesRequested => - FetchAvailableFiles(s._1.personalNumber) - } - ) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): PageComponent.ViewModel = - import connectors.* - PageComponent.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) - - object PageComponent: - import components.* - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply( - $m: Signal[ViewModel], - availableFilesStream: EventStream[List[File]], - events: Observer[UpravDukazForm.Event] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - UpravDukazForm(availableFilesStream)(events) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala deleted file mode 100644 index ca311b0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.Icons -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Color - -object DetailKriteria: - case class ViewModel( - nazev: String, - kapitola: String, - bod: String, - status: String, - statusColor: Color, - splneno: Boolean, - dukazy: List[DukazKriteria.ViewModel], - container: HtmlElement = div() - ) { - val id = s"${kapitola}${bod}" - } - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h3( - cls := "text-xl leading-6 font-bold text-gray-900", - child.text <-- $m.map(_.nazev) - ), - h3( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - "Kapitola ", - child.text <-- $m.map(_.kapitola), - " bod ", - child <-- $m.map(_.bod) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala deleted file mode 100644 index ae90290..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala +++ /dev/null @@ -1,100 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Avatar - -object DetailOsoby: - - object Funkce: - case class ViewModel( - nazev: String, - stredisko: String, - voj: String - ) - - def render($m: Signal[ViewModel]): HtmlElement = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.nazev), - span( - cls := "hidden md:inline", - " @ ", - child.text <-- $m.map(_.stredisko), - ", ", - child.text <-- $m.map(_.voj) - ) - ) - - object PracovniPomer: - case class ViewModel( - druh: String, - pocatek: LocalDate, - konec: Option[LocalDate] - ) - - def render($m: Signal[ViewModel]): HtmlElement = - import fiftyforms.ui.components.tailwind.CustomAttrs.datetime - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.druh), - " od ", - time( - datetime <-- $m.map(_.pocatek.toString), - child.text <-- $m.map(_.pocatek.toString) - ) - ) - - case class ViewModel( - osobniCislo: OsobniCislo, - jmeno: String, - email: Option[String], - telefon: Option[String], - img: Option[String], - hlavniFunkce: Option[Funkce.ViewModel], - pracovniPomer: Option[PracovniPomer.ViewModel] - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-4", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(16) - ), - div( - h1( - cls := "text-2xl font-bold text-gray-900", - child.text <-- $m.map(_.jmeno) - ), - child.maybe <-- $m.map(_.hlavniFunkce).split(_ => ())((_, _, d) => - Funkce.render(d) - ), - child.maybe <-- $m.map(_.pracovniPomer).split(_ => ())((_, _, d) => - PracovniPomer.render(d) - ) - ) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-5", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(8) - ), - div( - h1( - cls := "text-lg font-medium text-gray-800", - child.text <-- $m.map(_.jmeno) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala deleted file mode 100644 index e11cb62..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color - -object DetailParametru: - case class ViewModel( - id: String, - nazev: String, - popis: String, - status: String, - statusColor: Color, - a: Anchor - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h2( - cls := "text-2xl leading-6 font-medium text-gray-900", - child.text <-- $m.map(_.nazev) - ), - p( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - child.text <-- $m.map(_.popis) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - h2( - cls := "text-lg leading-6 font-medium text-gray-600", - child.text <-- $m.map(_.nazev) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala deleted file mode 100644 index a00ede8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala +++ /dev/null @@ -1,177 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.CustomAttrs - -object DukazKriteria: - case class Osoba(osobniCislo: String, jmeno: String) - case class Udalost(osoba: Osoba, datum: LocalDate) - case class Dokument( - url: String, - nazev: String, - pridal: Udalost, - odebral: Option[Udalost] - ) - case class ViewModel( - dokumenty: List[Dokument], - autorizoval: Option[Udalost], - platiDo: Option[LocalDate], - poznámka: Option[String] - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-lg", - div( - cls := "px-4 py-5 sm:px-6", - h3( - cls := "text-lg leading-6 font-medium text-gray-900", - """Applicant Information""" - ), - p( - cls := "mt-1 max-w-2xl text-sm text-gray-500", - """Personal details and application.""" - ) - ), - div( - cls := "border-t border-gray-200 px-4 py-5 sm:px-6", - dl( - cls := "grid grid-cols-1 gap-x-4 gap-y-8 sm:grid-cols-2", - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Full name""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Margot Foster""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Application for""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Backend Developer""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Email address""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """margotfoster@example.com""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Salary expectation""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """$120,000""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """About""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Fugiat ipsum ipsum deserunt culpa aute sint do nostrud anim incididunt cillum culpa consequat. Excepteur qui ipsum aliquip consequat sint. Sit id mollit nulla mollit nostrud in ea officia proident. Irure nostrud pariatur mollit ad adipisicing reprehenderit deserunt qui eu.""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """Attachments""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - ul( - role := "list", - cls := "border border-gray-200 rounded-md divide-y divide-gray-200", - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """resume_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ), - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """coverletter_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala deleted file mode 100644 index 22f134f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala +++ /dev/null @@ -1,35 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object DukazyKriteria: - sealed trait Action - case object Add extends Action - - type ViewModel = List[DukazKriteria.ViewModel] - - def apply($m: Signal[ViewModel])(actions: Observer[Action]): HtmlElement = - div( - child.maybe <-- $m.map(m => - if (m.isEmpty) then Some(prazdnyDukaz(actions)) - else None - ), - children <-- $m.map(_.zipWithIndex).split(_._2)((_, _, $s) => - DukazKriteria($s.map(_._1)) - ) - ) - - private def prazdnyDukaz(actions: Observer[Action]): HtmlElement = - button( - tpe := "button", - onClick.preventDefault.mapTo(Add) --> actions, - cls := "relative block w-full border-2 border-gray-300 border-dashed rounded-lg p-12 text-center hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - Icons.outline - .`document-add`(12) - .amend(svg.cls := "mx-auto text-gray-400"), - span( - cls := "mt-2 block text-sm font-medium text-gray-900", - "Přidat důkaz" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala deleted file mode 100644 index f628697..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.list.StackedList -import fiftyforms.ui.components.tailwind.list.ListRow -import fiftyforms.ui.components.tailwind.list.RowTag -import fiftyforms.ui.components.tailwind.list.RowNext -import java.time.LocalDate - -object SeznamKriterii: - type ViewModel = List[DetailKriteria.ViewModel] - - private val kritList = new StackedList[DetailKriteria.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - kritList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = - p(cls := "text-sm text-gray-500", s"${i.kapitola}${i.bod}"), - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.container - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala deleted file mode 100644 index a3b00af..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala +++ /dev/null @@ -1,103 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import scala.scalajs.js.annotation.JSExportTopLevel -import scala.scalajs.js.annotation.JSExport -import scala.scalajs.js.annotation.JSImport -import scala.scalajs.js -import org.scalajs.dom -import com.raquo.laminar.api.L.{*, given} -import scala.scalajs.js.Date -import com.raquo.waypoint.Router -import com.raquo.waypoint.SplitRender -import cz.e_bs.cmi.mdr.pdb.app.services.DataFetcher -import scala.scalajs.js.JSON -import zio.json._ -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.state.AppState - -@js.native -@JSImport("stylesheets/main.css", JSImport.Namespace) -object Css extends js.Any - -@js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native -@JSImport("params/pdb-params.json", JSImport.Default) -object pdbParams extends js.Object - -@JSExportTopLevel("app") -object Main: - - @JSExport - def main(args: Array[String]): Unit = - import Routes.given - onLoad { - setupAirstream() - val appContainer = dom.document.querySelector("#app") - val _ = - render( - appContainer, - renderPage(state.MockAppState(using unsafeWindowOwner, router)) - ) - } - - private def onLoad(f: => Unit): Unit = - documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) - - private def setupAirstream()(using router: Router[Page]): Unit = - AirstreamError.registerUnhandledErrorCallback(err => - router.forcePage( - Page.UnhandledError( - Some(err.getClass.getName), // TODO: Fill only in dev mode - Some(err.getMessage) - ) - ) - ) - - def renderPage(state: AppState)(using - router: Router[Page] - ): HtmlElement = - val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) - .collectSignal[Page.Detail]( - connectors - .DetailPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailParametru]( - connectors - .DetailParametruPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailKriteria]( - connectors - .DetailKriteriaPageConnector(state)(_) - .apply - ) - .collectSignal[Page.UpravDukazKriteria]( - pages.detail.UpravDukaz.Connector(state)(_).apply - ) - .collectStatic(Page.Dashboard)( - connectors.DashboardPageConnector(state.actionBus).apply - ) - .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) - ) - .collect[Page.UnhandledError](pg => - pages.errors - .UnhandledErrorPage( - pages.errors.UnhandledErrorPage - .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), - state.actionBus - ) - ) - .collectStatic(Page.Directory)( - connectors - .DirectoryPageConnector(state.users, state.actionBus) - .apply - ) - div(cls := "h-full", child <-- pageSplitter.$view) - - // Pull in the stylesheet - val css: Css.type = Css diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala deleted file mode 100644 index 3bbc8c0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ /dev/null @@ -1,198 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.* -import org.scalajs.dom -import zio.json.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import scala.scalajs.js -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.Page.Titled - -// enum is not working with Waypoints' SplitRender collectStatic -sealed abstract class Page( - val id: String, - val title: String, - val parent: Option[Page] -) { - val path: Vector[Page] = - parent match - case None => Vector(this) - case Some(p) => p.path :+ this - - val isRoot: Boolean = parent.isEmpty -} - -object Page: - - case class Titled[V](value: V, title: Option[String] = None): - val show: String = title.getOrElse(value.toString) - - case object Directory extends Page("directory", "Adresář", None) - - case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) - - case class Detail(osobniCislo: Titled[OsobniCislo]) - extends Page("user", osobniCislo.show, Some(Directory)) - - object Detail { - def apply(o: UserInfo): Detail = Detail( - Titled(o.personalNumber, Some(o.name)) - ) - } - - case class DetailParametru( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String] - ) extends Page( - "parameter", - parametr.show, - Some(Detail(osobniCislo)) - ) - - object DetailParametru { - def apply(o: UserInfo, p: Parameter): DetailParametru = - DetailParametru( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)) - ) - } - - case class DetailKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "criteria", - kriterium.show, - Some(DetailParametru(osobniCislo, parametr)) - ) - - object DetailKriteria { - def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = - DetailKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class UpravDukazKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "addProof", - "Důkaz", - Some(DetailKriteria(osobniCislo, parametr, kriterium)) - ) - - object UpravDukazKriteria { - def apply( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): UpravDukazKriteria = - UpravDukazKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class NotFound(url: String) extends Page("404", "404", Some(Directory)) - - case class UnhandledError( - errorName: Option[String], - errorMessage: Option[String] - ) extends Page("500", "Unexpected error", Some(Directory)) - -object Routes: - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) - given [V: JsonEncoder]: JsonEncoder[Titled[V]] = - DeriveJsonEncoder.gen[Titled[V]] - given [V: JsonDecoder]: JsonDecoder[Titled[V]] = - DeriveJsonDecoder.gen[Titled[V]] - given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] - given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] - - val base = - js.`import`.meta.env.BASE_URL - .asInstanceOf[String] + "app" - - val homePage: Page = Page.Directory - - given router: Router[Page] = Router[Page]( - routes = List( - Route.static(homePage, root / endOfSegments, basePath = base), - Route.static( - Page.Dashboard, - root / "dashboard" / endOfSegments, - basePath = base - ), - Route[Page.Detail, String]( - encode = _.osobniCislo.value.toString, - decode = osc => Page.Detail(Titled(OsobniCislo(osc))), - root / "osoba" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.DetailParametru, (String, String)]( - encode = p => (p.osobniCislo.value.toString, p.parametr.value), - decode = - p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / endOfSegments, - basePath = base - ), - Route[Page.DetailKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.DetailKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.UpravDukazKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.UpravDukazKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / "edit" / endOfSegments, - basePath = base - ) - ), - serializePage = _.toJson, - deserializePage = _.fromJson[Page] - .fold(s => throw IllegalStateException(s), identity), - getPageTitle = _.title, - routeFallback = url => Page.NotFound(url), - deserializeFallback = _ => Page.Dashboard - )( - $popStateEvent = windowEvents.onPopState, - owner = unsafeWindowOwner - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala deleted file mode 100644 index b24b82b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -sealed trait Action - -case object FetchDirectory extends Action -case class FetchUserDetails(osc: OsobniCislo) extends Action -case class FetchParameters(osc: OsobniCislo) extends Action -case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action -case class FetchParameterCriteria( - osc: OsobniCislo, - paramId: String, - critId: String, - page: (UserInfo, Parameter, ParameterCriteria) => Page -) extends Action -case class FetchAvailableFiles(osc: OsobniCislo) extends Action -case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala deleted file mode 100644 index c637d35..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala +++ /dev/null @@ -1,82 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.{UserProfile, UserInfo, OsobniCislo} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo -import cz.e_bs.cmi.mdr.pdb.UserFunction - -object AppPage: - // TODO: pages by logged in user - val pages: List[Page] = List(Page.Directory, Page.Dashboard) - - import NavigationBar.{Logo, MenuItem} - - val logo = Logo( - "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", - "Workflow" - ) - - // TODO: menu items by user profile - val userMenu = - List( - MenuItem("Your Profile"), - MenuItem("Settings"), - MenuItem("Sign out") - ) - - // TODO: load user profile - val $userProfile = Var( - UserProfile( - "tom", - UserInfo( - OsobniCislo("1031"), - "tom", - "Tom", - "Cook", - None, - None, - Some("tom@example.com"), - Some("+420 222 866 180"), - Some( - UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") - ), - Nil, - None - ) - ) - ) - - val $userInfo = $userProfile.signal.map(_.userInfo) - - type ViewModel = Option[HtmlElement] - def apply( - actionBus: Observer[Action] - )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using - router: Router[Page] - ): HtmlElement = - PageLayout(actionBus)( - $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => - PageLayout.ViewModel( - NavigationBar.ViewModel( - u, - pages.map(p => - NavigationBar.Link( - () => - PageLink( - p, - actionBus - ), - p == cp - ) - ), - userMenu, - logo - ), - c - ) - ), - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala deleted file mode 100644 index bf473c6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ /dev/null @@ -1,143 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden -import fiftyforms.ui.components.tailwind.list.IconText.ViewModel -import fiftyforms.ui.components.tailwind.Icons -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Action - -object Breadcrumbs: - - private def slash = - import svg.{*, given} - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-300", - xmlns := "http://www.w3.org/2000/svg", - fill := "currentColor", - viewBox := "0 0 20 20", - ariaHidden := true, - path( - d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" - ) - ) - - def backIcon = - Icons.solid - .`arrow-narrow-left`() - .amend( - svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" - ) - - object Link: - case class ViewModel( - page: Page, - icon: Option[SvgElement], - extraClasses: String, - text: String, - textClasses: Option[String] = None - ) - - val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" - - def shortHome(p: Page) = ViewModel( - p, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - "Zpět na úvodní stránku", - None - ) - - def fullHome(p: Page) = ViewModel( - p, - Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), - "text-gray-400 hover:text-gray-500", - "Domů", - Some("sr-only") - ) - - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - PageLink - .container($m.map(_.page), actionBus) - .amend( - cls <-- $m.map(_.extraClasses), - child.maybe <-- $m.map(_.icon), - span( - cls <-- $m.map(_.textClasses.getOrElse("")), - child.text <-- $m.map(_.text) - ) - ) - - object FullBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - ol( - role := "list", - cls := "flex items-center space-x-4", - children <-- $m.map(_.path).split(_.id)((_, _, $p) => - li( - div( - cls := "flex items-center", - child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), - Link( - $p.map(p => - if (p.isRoot) then Link.fullHome(p) - else - Link.ViewModel( - p, - None, - s"ml-4 max-w-xs truncate ${Link.baseClasses}", - p.title - ) - ), - actionBus - ) - ) - ) - ) - ) - - object ShortBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - Link( - $m.map { p => - val target = p.parent.getOrElse(p) - if target.isRoot then Link.shortHome(target) - else - Link.ViewModel( - target, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - s"Zpět na ${target.title}" - ) - }, - actionBus - ) - - def apply(actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - nav( - cls := "flex", - aria.label := "Breadcrumb", - div( - cls := "flex sm:hidden", - ShortBreadcrumbs(router.$currentPage, actionBus) - ), - div( - cls := "hidden sm:block", - FullBreadcrumbs(router.$currentPage, actionBus) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala deleted file mode 100644 index 0db2047..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ /dev/null @@ -1,242 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent -import fiftyforms.ui.components.tailwind.Icons -import fiftyforms.ui.components.tailwind.Avatar -import cz.e_bs.cmi.mdr.pdb.UserInfo -import io.laminext.syntax.core.* - -object NavigationBar: - - case class Logo(img: String, name: String) - case class Link(a: () => Anchor, active: Boolean) - case class MenuItem(title: String) - - case class ViewModel( - userInfo: UserInfo, - pages: List[Link], - userMenu: List[MenuItem], - logo: Logo - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - val $userInfo = $m.map(_.userInfo) - val mobileMenuOpen = Var(false) - - val desktopOnly = cls("hidden md:block") - val mobileOnly = cls("md:hidden") - - inline def avatarImage(size: Int = 8) = - Avatar($userInfo.map(_.img)).avatarImage(size) - - def notificationButton = button( - tpe := "button", - cls := List( - "bg-indigo-600", - "focus:outline-none", - "focus:ring-2", - "focus:ring-offset-2", - "focus:ring-offset-indigo-600", - "focus:ring-white", - "hover:text-white", - "p-1", - "rounded-full", - "text-indigo-200" - ), - span(cls := "sr-only", "View notifications"), - Icons.outline.bell() - ) - - def userProfile: HtmlElement = - val menuOpen = Var(false) - - def menuItem(item: MenuItem, idx: Int): HtmlElement = - a( - href := "#", - cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", - role := "menuitem", - tabIndex := -1, - idAttr := s"user-menu-item-$idx", - item.title - ) - - div( - cls := "ml-3 relative", - div( - button( - tpe := "button", - cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - idAttr := "user-menu-button", - aria.expanded <-- menuOpen.signal, - aria.hasPopup := true, - span(cls := "sr-only", "Open user menu"), - child <-- avatarImage(), - onClick.preventDefault.mapTo( - !menuOpen.now() - ) --> menuOpen.writer - ) - ), - /* - * */ - div( - cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", - cls <-- menuOpen.signal.map { o => - if (o) "md:block" else "md:hidden" - }, - role := "menu", - aria.orientation := "vertical", - aria.labelledBy := "user-menu-button", - tabIndex := -1, - // : keyboard navigation - children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) - ) - ) - - def mobileProfile = - def menuItem(item: MenuItem): HtmlElement = - a( - href := "#", - cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", - item.title - ) - - div( - cls := "pt-4 pb-3 border-t border-indigo-700", - div( - cls := "flex items-center px-5", - div( - cls := "flex-shrink-0", - child <-- avatarImage(10) - ), - div( - cls := "ml-3", - div( - cls := "text-base font-medium text-white", - child.text <-- $userInfo.map(_.name) - ), - child.maybe <-- $userInfo.map( - _.email.map(e => - div( - cls := "text-sm font-medium text-indigo-300", - e - ) - ) - ) - ), - notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) - ), - div( - cls := "mt-3 px-2 space-y-1", - children <-- $m.map(_.userMenu.map(menuItem)) - ) - ) - - def pageLink(page: Link): Anchor = - page - .a() - .amend( - cls := "text-white px-3 py-2 rounded-md text-sm font-medium", - cls := Seq( - "bg-indigo-700" -> page.active, - "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active - ), - ariaCurrent := (if page.active then "page" else "false") - ) - - def logoImg: Image = - img( - cls := "h-8 w-8", - src <-- $m.map(_.logo.img), - alt <-- $m.map(_.logo.name) - ) - - def mobileMenuButton = button( - tpe := "button", - cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - aria.controls := "mobile-menu", - aria.expanded <-- mobileMenuOpen.signal, - span(cls := "sr-only", "Open main menu"), - Icons.outline - .menu() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") - ), - Icons.outline - .x() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") - ), - onClick.preventDefault.mapTo( - !mobileMenuOpen.now() - ) --> mobileMenuOpen.writer - ) - - def navBarLeft = - div( - cls := "flex items-center", - div(cls := "flex-shrink-0", logoImg), - div( - desktopOnly, - div( - cls := "ml-10 flex items-baseline space-x-4", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ) - ) - ) - - def navBarRight = - div( - desktopOnly, - div( - cls := "ml-4 flex items-center md:ml-6", - notificationButton, - userProfile - ) - ) - - def navBarMobile = - div( - cls := "-mr-2 flex", - mobileOnly, - mobileMenuButton - ) - - def navBar = - div( - cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex items-center justify-between h-16", - navBarLeft, - navBarRight, - navBarMobile - ) - ) - - def mobileMenu = - div( - mobileOnly, - cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }, - idAttr := "mobile-menu", - div( - cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ), - mobileProfile - ) - - nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala deleted file mode 100644 index 75339df..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object PageHeader: - def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = - header( - cls := "bg-white shadow-sm", - div( - cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", - h1( - cls := "text-lg leading-6 font-semibold text-gray-900", - Breadcrumbs(actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala deleted file mode 100644 index 5096463..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ /dev/null @@ -1,30 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import fiftyforms.ui.components.tailwind.Loading -import io.laminext.syntax.core.* - -object PageLayout: - case class ViewModel( - navigation: NavigationBar.ViewModel, - content: Option[HtmlElement] - ) - def apply(actionBus: Observer[Action])( - $m: Signal[ViewModel], - mods: Modifier[HtmlElement]* - )(using router: Router[Page]): HtmlElement = - val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) - div( - cls := "h-full flex flex-col", - NavigationBar($m.map(_.navigation)), - child.maybe <-- router.$currentPage.map(_.isRoot) - .switch(None, Some(PageHeader(actionBus))), - main( - cls := "flex-grow-1 overflow-y-auto", - mods, - child <-- $maybeContent.map(_.getOrElse(Loading)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala deleted file mode 100644 index 62d820f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.LinkSupport.* -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo - -object PageLink: - type ViewModel = Page - def apply($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = - a(mods($m, actions), child.text <-- $m.map(_.title)) - - def apply(m: ViewModel, actions: Observer[Action])(using - router: Router[Page] - ): Anchor = apply(Val(m), actions) - - def container($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = a(mods($m, actions)) - - def container(m: ViewModel, actions: Observer[Action])(using - Router[Page] - ): Anchor = container(Val(m), actions) - - private def mods($m: Signal[Page], actions: Observer[Action])(using - router: Router[Page] - ): Modifier[Anchor] = - Seq( - href <-- $m.map(router.absoluteUrlForPage).recover { case _ => - Some("invalid url") - }, - composeEvents(onClick.noKeyMod.preventDefault)( - _.sample($m) - .map(NavigateTo.apply) - ) --> actions - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala deleted file mode 100644 index 87f825a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.pages.dashboard.DashboardPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -class DashboardPageConnector(actionBus: Observer[Action])(using - router: Router[Page] -): - def apply: HtmlElement = - AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala deleted file mode 100644 index e9b0f71..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala +++ /dev/null @@ -1,76 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailKriteriaPage -import pages.detail.components.DukazyKriteria -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -object DetailKriteriaPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailKriteriaPageConnector( - state: DetailKriteriaPageConnector.AppState -)( - $page: Signal[Page.DetailKriteria] -)(using Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => - (p.osobniCislo.value, p.parametr.value, p.kriterium.value) - )((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map( - FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) - ) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, s, $s) => - DetailKriteriaPage($s)(state.actionBus.contramap { - case DukazyKriteria.Add => - NavigateTo( - Page.UpravDukazKriteria( - Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), - Page.Titled(s.parametr.id, Some(s.parametr.nazev)), - Page.Titled(s.kriterium.id, Some(s.kriterium.id)) - ) - ) - }) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): DetailKriteriaPage.ViewModel = - DetailKriteriaPage.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala deleted file mode 100644 index 4d8024f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ /dev/null @@ -1,57 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import pages.detail.DetailPage -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import fiftyforms.ui.components.tailwind.Color - -object DetailPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailPageConnector(state: DetailPageConnector.AppState)( - $page: Signal[Page.Detail] -)(using router: Router[Page]): - val $oscChangeSignal = $page.splitOne(_.osobniCislo.value)((osc, _, _) => osc) - val $pageChangeSignal = - $oscChangeSignal.flatMap(osc => - EventStream.fromSeq(Seq(FetchUserDetails(osc), FetchParameters(osc))) - ) - // TODO: filter the value based on the current osc - // OSC change will fetch new data, but still - // - we need to be sure that what we got is really what we ought to display - // - we want to display stale data accordingly (at least with loading indicator) - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - def apply: HtmlElement = - AppPage(state.actionBus)( - $data.combineWithFn($params)(_ zip _) - .map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: List[Parameter] - ): DetailPage.ViewModel = - DetailPage.ViewModel( - o.toDetailOsoby, - p.map( - _.toParametr(param => - PageLink.container(Page.DetailParametru(o, param), state.actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala deleted file mode 100644 index fdaa7f8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailParametruPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink - -object DetailParametruPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailParametruPageConnector( - state: DetailParametruPageConnector.AppState -)( - $page: Signal[Page.DetailParametru] -)(using router: Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => (p.osobniCislo.value, p.parametr.value))((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map(FetchParameter(_, _)) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - } yield (da, pb) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailParametruPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter - ): DetailParametruPage.ViewModel = - DetailParametruPage.ViewModel( - o.toDetailOsoby, - p.toParametr(p => - PageLink.container(Page.DetailParametru(o, p), state.actionBus) - ), - p.criteria.map( - _.toKriterium { c => - PageLink.container( - Page.DetailKriteria(o, p, c), - state.actionBus - ) - } - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala deleted file mode 100644 index 049aa22..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -case class DirectoryPageConnector( - $input: EventStream[List[UserInfo]], - actionBus: Observer[Action] -)(using router: Router[Page]): - val $data = $input.startWithNone - val $actionSignal = EventStream.fromValue(FetchDirectory) - - def apply: HtmlElement = - AppPage(actionBus)( - $data.split(_ => ())((_, _, s) => - pages.directory.DirectoryPage( - s.map( - _.map( - _.toUserRow(u => - PageLink.container( - Page.Detail(Page.Titled(u.personalNumber)), - actionBus - ) - ) - ) - ) - ) - ), - $actionSignal --> actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala deleted file mode 100644 index 88e7450..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ /dev/null @@ -1,68 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii -import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria -import fiftyforms.ui.components.tailwind.Color - -extension (o: UserInfo) - def toDetailOsoby: DetailOsoby.ViewModel = - DetailOsoby.ViewModel( - o.personalNumber, - o.name, - o.email, - o.phone, - o.img, - o.mainFunction.map(f => - DetailOsoby.Funkce.ViewModel(f.name, f.dept, f.ou) - ), - o.userContracts.headOption.map(c => - DetailOsoby.PracovniPomer.ViewModel(c.rel, c.startDate, c.endDate) - ) - ) - -extension (param: Parameter) - def toParametr(container: Parameter => Anchor): DetailParametru.ViewModel = - DetailParametru.ViewModel( - id = param.id, - nazev = param.name, - popis = param.description, - status = "Nesplněno", - statusColor = Color.red, - a = container(param) - ) - -extension (crit: ParameterCriteria) - def toKriterium( - container: ParameterCriteria => Anchor - ): DetailKriteria.ViewModel = - DetailKriteria.ViewModel( - nazev = crit.criteriumText, - kapitola = crit.chapterId, - bod = crit.itemId, - status = "Nesplněno", - statusColor = Color.red, - splneno = false, - dukazy = Nil, - container = container(crit) - ) - -extension (user: UserInfo) - def toUserRow( - container: UserInfo => HtmlElement = (_: UserInfo) => div() - ): UserRow.ViewModel = - UserRow.ViewModel( - osobniCislo = user.personalNumber.toString, - celeJmeno = user.name, - prijmeni = user.surname, - hlavniFunkce = user.mainFunction.map(_.name), - img = user.img, - container = () => container(user) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala deleted file mode 100644 index 7d5b2c1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.dashboard - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.Page - -object DashboardPage: - - def render: HtmlElement = - div("Dashboard page") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala deleted file mode 100644 index 9ef0698..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala +++ /dev/null @@ -1,31 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailKriteriaPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply($m: Signal[ViewModel])( - action: Observer[DukazyKriteria.Action] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - DukazyKriteria($m.map(_.kriterium.dukazy))(action) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala deleted file mode 100644 index 3b316e1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object DetailPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametry: SeznamParametru.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby($m.map(_.osoba)), - SeznamParametru($m.map(_.parametry)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala deleted file mode 100644 index 17224cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala +++ /dev/null @@ -1,24 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailParametruPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriteria: SeznamKriterii.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby.header($m.map(_.osoba)), - DetailParametru($m.map(_.parametr)), - SeznamKriterii($m.map(_.kriteria)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala deleted file mode 100644 index dc912fa..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ /dev/null @@ -1,107 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb -package app -package pages.detail - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.UpravDukazForm -import fiftyforms.services.files.File - -object UpravDukaz: - - type ThisPage = Page.UpravDukazKriteria - type PageKey = (OsobniCislo, String, String) - - trait State { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def availableFiles: EventStream[List[File]] - def actionBus: Observer[Action] - } - - def keyOfPage(page: ThisPage): PageKey = - (page.osobniCislo.value, page.parametr.value, page.kriterium.value) - - def onChangeAction(key: PageKey): Action = - FetchParameterCriteria( - key._1, - key._2, - key._3, - Page.UpravDukazKriteria(_, _, _) - ) - - class Connector(state: State)($page: Signal[ThisPage])(using Router[Page]): - val $paramChangeSignal = $page.splitOne(keyOfPage)((x, _, _) => x) - val $pageChangeSignal = $paramChangeSignal.map(onChangeAction) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.split(_ => ())((_, s, $s) => - PageComponent( - $s.map(buildModel), - state.availableFiles, - state.actionBus.contramap { - case UpravDukazForm.Cancelled => - NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) - case UpravDukazForm.AvailableFilesRequested => - FetchAvailableFiles(s._1.personalNumber) - } - ) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): PageComponent.ViewModel = - import connectors.* - PageComponent.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) - - object PageComponent: - import components.* - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply( - $m: Signal[ViewModel], - availableFilesStream: EventStream[List[File]], - events: Observer[UpravDukazForm.Event] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - UpravDukazForm(availableFilesStream)(events) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala deleted file mode 100644 index ca311b0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.Icons -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Color - -object DetailKriteria: - case class ViewModel( - nazev: String, - kapitola: String, - bod: String, - status: String, - statusColor: Color, - splneno: Boolean, - dukazy: List[DukazKriteria.ViewModel], - container: HtmlElement = div() - ) { - val id = s"${kapitola}${bod}" - } - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h3( - cls := "text-xl leading-6 font-bold text-gray-900", - child.text <-- $m.map(_.nazev) - ), - h3( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - "Kapitola ", - child.text <-- $m.map(_.kapitola), - " bod ", - child <-- $m.map(_.bod) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala deleted file mode 100644 index ae90290..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala +++ /dev/null @@ -1,100 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Avatar - -object DetailOsoby: - - object Funkce: - case class ViewModel( - nazev: String, - stredisko: String, - voj: String - ) - - def render($m: Signal[ViewModel]): HtmlElement = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.nazev), - span( - cls := "hidden md:inline", - " @ ", - child.text <-- $m.map(_.stredisko), - ", ", - child.text <-- $m.map(_.voj) - ) - ) - - object PracovniPomer: - case class ViewModel( - druh: String, - pocatek: LocalDate, - konec: Option[LocalDate] - ) - - def render($m: Signal[ViewModel]): HtmlElement = - import fiftyforms.ui.components.tailwind.CustomAttrs.datetime - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.druh), - " od ", - time( - datetime <-- $m.map(_.pocatek.toString), - child.text <-- $m.map(_.pocatek.toString) - ) - ) - - case class ViewModel( - osobniCislo: OsobniCislo, - jmeno: String, - email: Option[String], - telefon: Option[String], - img: Option[String], - hlavniFunkce: Option[Funkce.ViewModel], - pracovniPomer: Option[PracovniPomer.ViewModel] - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-4", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(16) - ), - div( - h1( - cls := "text-2xl font-bold text-gray-900", - child.text <-- $m.map(_.jmeno) - ), - child.maybe <-- $m.map(_.hlavniFunkce).split(_ => ())((_, _, d) => - Funkce.render(d) - ), - child.maybe <-- $m.map(_.pracovniPomer).split(_ => ())((_, _, d) => - PracovniPomer.render(d) - ) - ) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-5", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(8) - ), - div( - h1( - cls := "text-lg font-medium text-gray-800", - child.text <-- $m.map(_.jmeno) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala deleted file mode 100644 index e11cb62..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color - -object DetailParametru: - case class ViewModel( - id: String, - nazev: String, - popis: String, - status: String, - statusColor: Color, - a: Anchor - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h2( - cls := "text-2xl leading-6 font-medium text-gray-900", - child.text <-- $m.map(_.nazev) - ), - p( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - child.text <-- $m.map(_.popis) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - h2( - cls := "text-lg leading-6 font-medium text-gray-600", - child.text <-- $m.map(_.nazev) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala deleted file mode 100644 index a00ede8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala +++ /dev/null @@ -1,177 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.CustomAttrs - -object DukazKriteria: - case class Osoba(osobniCislo: String, jmeno: String) - case class Udalost(osoba: Osoba, datum: LocalDate) - case class Dokument( - url: String, - nazev: String, - pridal: Udalost, - odebral: Option[Udalost] - ) - case class ViewModel( - dokumenty: List[Dokument], - autorizoval: Option[Udalost], - platiDo: Option[LocalDate], - poznámka: Option[String] - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-lg", - div( - cls := "px-4 py-5 sm:px-6", - h3( - cls := "text-lg leading-6 font-medium text-gray-900", - """Applicant Information""" - ), - p( - cls := "mt-1 max-w-2xl text-sm text-gray-500", - """Personal details and application.""" - ) - ), - div( - cls := "border-t border-gray-200 px-4 py-5 sm:px-6", - dl( - cls := "grid grid-cols-1 gap-x-4 gap-y-8 sm:grid-cols-2", - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Full name""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Margot Foster""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Application for""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Backend Developer""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Email address""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """margotfoster@example.com""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Salary expectation""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """$120,000""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """About""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Fugiat ipsum ipsum deserunt culpa aute sint do nostrud anim incididunt cillum culpa consequat. Excepteur qui ipsum aliquip consequat sint. Sit id mollit nulla mollit nostrud in ea officia proident. Irure nostrud pariatur mollit ad adipisicing reprehenderit deserunt qui eu.""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """Attachments""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - ul( - role := "list", - cls := "border border-gray-200 rounded-md divide-y divide-gray-200", - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """resume_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ), - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """coverletter_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala deleted file mode 100644 index 22f134f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala +++ /dev/null @@ -1,35 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object DukazyKriteria: - sealed trait Action - case object Add extends Action - - type ViewModel = List[DukazKriteria.ViewModel] - - def apply($m: Signal[ViewModel])(actions: Observer[Action]): HtmlElement = - div( - child.maybe <-- $m.map(m => - if (m.isEmpty) then Some(prazdnyDukaz(actions)) - else None - ), - children <-- $m.map(_.zipWithIndex).split(_._2)((_, _, $s) => - DukazKriteria($s.map(_._1)) - ) - ) - - private def prazdnyDukaz(actions: Observer[Action]): HtmlElement = - button( - tpe := "button", - onClick.preventDefault.mapTo(Add) --> actions, - cls := "relative block w-full border-2 border-gray-300 border-dashed rounded-lg p-12 text-center hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - Icons.outline - .`document-add`(12) - .amend(svg.cls := "mx-auto text-gray-400"), - span( - cls := "mt-2 block text-sm font-medium text-gray-900", - "Přidat důkaz" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala deleted file mode 100644 index f628697..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.list.StackedList -import fiftyforms.ui.components.tailwind.list.ListRow -import fiftyforms.ui.components.tailwind.list.RowTag -import fiftyforms.ui.components.tailwind.list.RowNext -import java.time.LocalDate - -object SeznamKriterii: - type ViewModel = List[DetailKriteria.ViewModel] - - private val kritList = new StackedList[DetailKriteria.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - kritList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = - p(cls := "text-sm text-gray-500", s"${i.kapitola}${i.bod}"), - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.container - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala deleted file mode 100644 index 2bfe4bc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.list.{ - StackedList, - ListRow, - RowTag, - PropList, - IconText, - RowNext -} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.LinkSupport.* - -object SeznamParametru: - type ViewModel = List[DetailParametru.ViewModel] - - private val parametrList = new StackedList[DetailParametru.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - parametrList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = emptyNode, - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.a - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala deleted file mode 100644 index a3b00af..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala +++ /dev/null @@ -1,103 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import scala.scalajs.js.annotation.JSExportTopLevel -import scala.scalajs.js.annotation.JSExport -import scala.scalajs.js.annotation.JSImport -import scala.scalajs.js -import org.scalajs.dom -import com.raquo.laminar.api.L.{*, given} -import scala.scalajs.js.Date -import com.raquo.waypoint.Router -import com.raquo.waypoint.SplitRender -import cz.e_bs.cmi.mdr.pdb.app.services.DataFetcher -import scala.scalajs.js.JSON -import zio.json._ -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.state.AppState - -@js.native -@JSImport("stylesheets/main.css", JSImport.Namespace) -object Css extends js.Any - -@js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native -@JSImport("params/pdb-params.json", JSImport.Default) -object pdbParams extends js.Object - -@JSExportTopLevel("app") -object Main: - - @JSExport - def main(args: Array[String]): Unit = - import Routes.given - onLoad { - setupAirstream() - val appContainer = dom.document.querySelector("#app") - val _ = - render( - appContainer, - renderPage(state.MockAppState(using unsafeWindowOwner, router)) - ) - } - - private def onLoad(f: => Unit): Unit = - documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) - - private def setupAirstream()(using router: Router[Page]): Unit = - AirstreamError.registerUnhandledErrorCallback(err => - router.forcePage( - Page.UnhandledError( - Some(err.getClass.getName), // TODO: Fill only in dev mode - Some(err.getMessage) - ) - ) - ) - - def renderPage(state: AppState)(using - router: Router[Page] - ): HtmlElement = - val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) - .collectSignal[Page.Detail]( - connectors - .DetailPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailParametru]( - connectors - .DetailParametruPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailKriteria]( - connectors - .DetailKriteriaPageConnector(state)(_) - .apply - ) - .collectSignal[Page.UpravDukazKriteria]( - pages.detail.UpravDukaz.Connector(state)(_).apply - ) - .collectStatic(Page.Dashboard)( - connectors.DashboardPageConnector(state.actionBus).apply - ) - .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) - ) - .collect[Page.UnhandledError](pg => - pages.errors - .UnhandledErrorPage( - pages.errors.UnhandledErrorPage - .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), - state.actionBus - ) - ) - .collectStatic(Page.Directory)( - connectors - .DirectoryPageConnector(state.users, state.actionBus) - .apply - ) - div(cls := "h-full", child <-- pageSplitter.$view) - - // Pull in the stylesheet - val css: Css.type = Css diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala deleted file mode 100644 index 3bbc8c0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ /dev/null @@ -1,198 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.* -import org.scalajs.dom -import zio.json.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import scala.scalajs.js -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.Page.Titled - -// enum is not working with Waypoints' SplitRender collectStatic -sealed abstract class Page( - val id: String, - val title: String, - val parent: Option[Page] -) { - val path: Vector[Page] = - parent match - case None => Vector(this) - case Some(p) => p.path :+ this - - val isRoot: Boolean = parent.isEmpty -} - -object Page: - - case class Titled[V](value: V, title: Option[String] = None): - val show: String = title.getOrElse(value.toString) - - case object Directory extends Page("directory", "Adresář", None) - - case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) - - case class Detail(osobniCislo: Titled[OsobniCislo]) - extends Page("user", osobniCislo.show, Some(Directory)) - - object Detail { - def apply(o: UserInfo): Detail = Detail( - Titled(o.personalNumber, Some(o.name)) - ) - } - - case class DetailParametru( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String] - ) extends Page( - "parameter", - parametr.show, - Some(Detail(osobniCislo)) - ) - - object DetailParametru { - def apply(o: UserInfo, p: Parameter): DetailParametru = - DetailParametru( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)) - ) - } - - case class DetailKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "criteria", - kriterium.show, - Some(DetailParametru(osobniCislo, parametr)) - ) - - object DetailKriteria { - def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = - DetailKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class UpravDukazKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "addProof", - "Důkaz", - Some(DetailKriteria(osobniCislo, parametr, kriterium)) - ) - - object UpravDukazKriteria { - def apply( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): UpravDukazKriteria = - UpravDukazKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class NotFound(url: String) extends Page("404", "404", Some(Directory)) - - case class UnhandledError( - errorName: Option[String], - errorMessage: Option[String] - ) extends Page("500", "Unexpected error", Some(Directory)) - -object Routes: - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) - given [V: JsonEncoder]: JsonEncoder[Titled[V]] = - DeriveJsonEncoder.gen[Titled[V]] - given [V: JsonDecoder]: JsonDecoder[Titled[V]] = - DeriveJsonDecoder.gen[Titled[V]] - given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] - given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] - - val base = - js.`import`.meta.env.BASE_URL - .asInstanceOf[String] + "app" - - val homePage: Page = Page.Directory - - given router: Router[Page] = Router[Page]( - routes = List( - Route.static(homePage, root / endOfSegments, basePath = base), - Route.static( - Page.Dashboard, - root / "dashboard" / endOfSegments, - basePath = base - ), - Route[Page.Detail, String]( - encode = _.osobniCislo.value.toString, - decode = osc => Page.Detail(Titled(OsobniCislo(osc))), - root / "osoba" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.DetailParametru, (String, String)]( - encode = p => (p.osobniCislo.value.toString, p.parametr.value), - decode = - p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / endOfSegments, - basePath = base - ), - Route[Page.DetailKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.DetailKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.UpravDukazKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.UpravDukazKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / "edit" / endOfSegments, - basePath = base - ) - ), - serializePage = _.toJson, - deserializePage = _.fromJson[Page] - .fold(s => throw IllegalStateException(s), identity), - getPageTitle = _.title, - routeFallback = url => Page.NotFound(url), - deserializeFallback = _ => Page.Dashboard - )( - $popStateEvent = windowEvents.onPopState, - owner = unsafeWindowOwner - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala deleted file mode 100644 index b24b82b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -sealed trait Action - -case object FetchDirectory extends Action -case class FetchUserDetails(osc: OsobniCislo) extends Action -case class FetchParameters(osc: OsobniCislo) extends Action -case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action -case class FetchParameterCriteria( - osc: OsobniCislo, - paramId: String, - critId: String, - page: (UserInfo, Parameter, ParameterCriteria) => Page -) extends Action -case class FetchAvailableFiles(osc: OsobniCislo) extends Action -case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala deleted file mode 100644 index c637d35..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala +++ /dev/null @@ -1,82 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.{UserProfile, UserInfo, OsobniCislo} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo -import cz.e_bs.cmi.mdr.pdb.UserFunction - -object AppPage: - // TODO: pages by logged in user - val pages: List[Page] = List(Page.Directory, Page.Dashboard) - - import NavigationBar.{Logo, MenuItem} - - val logo = Logo( - "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", - "Workflow" - ) - - // TODO: menu items by user profile - val userMenu = - List( - MenuItem("Your Profile"), - MenuItem("Settings"), - MenuItem("Sign out") - ) - - // TODO: load user profile - val $userProfile = Var( - UserProfile( - "tom", - UserInfo( - OsobniCislo("1031"), - "tom", - "Tom", - "Cook", - None, - None, - Some("tom@example.com"), - Some("+420 222 866 180"), - Some( - UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") - ), - Nil, - None - ) - ) - ) - - val $userInfo = $userProfile.signal.map(_.userInfo) - - type ViewModel = Option[HtmlElement] - def apply( - actionBus: Observer[Action] - )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using - router: Router[Page] - ): HtmlElement = - PageLayout(actionBus)( - $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => - PageLayout.ViewModel( - NavigationBar.ViewModel( - u, - pages.map(p => - NavigationBar.Link( - () => - PageLink( - p, - actionBus - ), - p == cp - ) - ), - userMenu, - logo - ), - c - ) - ), - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala deleted file mode 100644 index bf473c6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ /dev/null @@ -1,143 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden -import fiftyforms.ui.components.tailwind.list.IconText.ViewModel -import fiftyforms.ui.components.tailwind.Icons -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Action - -object Breadcrumbs: - - private def slash = - import svg.{*, given} - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-300", - xmlns := "http://www.w3.org/2000/svg", - fill := "currentColor", - viewBox := "0 0 20 20", - ariaHidden := true, - path( - d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" - ) - ) - - def backIcon = - Icons.solid - .`arrow-narrow-left`() - .amend( - svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" - ) - - object Link: - case class ViewModel( - page: Page, - icon: Option[SvgElement], - extraClasses: String, - text: String, - textClasses: Option[String] = None - ) - - val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" - - def shortHome(p: Page) = ViewModel( - p, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - "Zpět na úvodní stránku", - None - ) - - def fullHome(p: Page) = ViewModel( - p, - Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), - "text-gray-400 hover:text-gray-500", - "Domů", - Some("sr-only") - ) - - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - PageLink - .container($m.map(_.page), actionBus) - .amend( - cls <-- $m.map(_.extraClasses), - child.maybe <-- $m.map(_.icon), - span( - cls <-- $m.map(_.textClasses.getOrElse("")), - child.text <-- $m.map(_.text) - ) - ) - - object FullBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - ol( - role := "list", - cls := "flex items-center space-x-4", - children <-- $m.map(_.path).split(_.id)((_, _, $p) => - li( - div( - cls := "flex items-center", - child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), - Link( - $p.map(p => - if (p.isRoot) then Link.fullHome(p) - else - Link.ViewModel( - p, - None, - s"ml-4 max-w-xs truncate ${Link.baseClasses}", - p.title - ) - ), - actionBus - ) - ) - ) - ) - ) - - object ShortBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - Link( - $m.map { p => - val target = p.parent.getOrElse(p) - if target.isRoot then Link.shortHome(target) - else - Link.ViewModel( - target, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - s"Zpět na ${target.title}" - ) - }, - actionBus - ) - - def apply(actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - nav( - cls := "flex", - aria.label := "Breadcrumb", - div( - cls := "flex sm:hidden", - ShortBreadcrumbs(router.$currentPage, actionBus) - ), - div( - cls := "hidden sm:block", - FullBreadcrumbs(router.$currentPage, actionBus) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala deleted file mode 100644 index 0db2047..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ /dev/null @@ -1,242 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent -import fiftyforms.ui.components.tailwind.Icons -import fiftyforms.ui.components.tailwind.Avatar -import cz.e_bs.cmi.mdr.pdb.UserInfo -import io.laminext.syntax.core.* - -object NavigationBar: - - case class Logo(img: String, name: String) - case class Link(a: () => Anchor, active: Boolean) - case class MenuItem(title: String) - - case class ViewModel( - userInfo: UserInfo, - pages: List[Link], - userMenu: List[MenuItem], - logo: Logo - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - val $userInfo = $m.map(_.userInfo) - val mobileMenuOpen = Var(false) - - val desktopOnly = cls("hidden md:block") - val mobileOnly = cls("md:hidden") - - inline def avatarImage(size: Int = 8) = - Avatar($userInfo.map(_.img)).avatarImage(size) - - def notificationButton = button( - tpe := "button", - cls := List( - "bg-indigo-600", - "focus:outline-none", - "focus:ring-2", - "focus:ring-offset-2", - "focus:ring-offset-indigo-600", - "focus:ring-white", - "hover:text-white", - "p-1", - "rounded-full", - "text-indigo-200" - ), - span(cls := "sr-only", "View notifications"), - Icons.outline.bell() - ) - - def userProfile: HtmlElement = - val menuOpen = Var(false) - - def menuItem(item: MenuItem, idx: Int): HtmlElement = - a( - href := "#", - cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", - role := "menuitem", - tabIndex := -1, - idAttr := s"user-menu-item-$idx", - item.title - ) - - div( - cls := "ml-3 relative", - div( - button( - tpe := "button", - cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - idAttr := "user-menu-button", - aria.expanded <-- menuOpen.signal, - aria.hasPopup := true, - span(cls := "sr-only", "Open user menu"), - child <-- avatarImage(), - onClick.preventDefault.mapTo( - !menuOpen.now() - ) --> menuOpen.writer - ) - ), - /* - * */ - div( - cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", - cls <-- menuOpen.signal.map { o => - if (o) "md:block" else "md:hidden" - }, - role := "menu", - aria.orientation := "vertical", - aria.labelledBy := "user-menu-button", - tabIndex := -1, - // : keyboard navigation - children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) - ) - ) - - def mobileProfile = - def menuItem(item: MenuItem): HtmlElement = - a( - href := "#", - cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", - item.title - ) - - div( - cls := "pt-4 pb-3 border-t border-indigo-700", - div( - cls := "flex items-center px-5", - div( - cls := "flex-shrink-0", - child <-- avatarImage(10) - ), - div( - cls := "ml-3", - div( - cls := "text-base font-medium text-white", - child.text <-- $userInfo.map(_.name) - ), - child.maybe <-- $userInfo.map( - _.email.map(e => - div( - cls := "text-sm font-medium text-indigo-300", - e - ) - ) - ) - ), - notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) - ), - div( - cls := "mt-3 px-2 space-y-1", - children <-- $m.map(_.userMenu.map(menuItem)) - ) - ) - - def pageLink(page: Link): Anchor = - page - .a() - .amend( - cls := "text-white px-3 py-2 rounded-md text-sm font-medium", - cls := Seq( - "bg-indigo-700" -> page.active, - "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active - ), - ariaCurrent := (if page.active then "page" else "false") - ) - - def logoImg: Image = - img( - cls := "h-8 w-8", - src <-- $m.map(_.logo.img), - alt <-- $m.map(_.logo.name) - ) - - def mobileMenuButton = button( - tpe := "button", - cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - aria.controls := "mobile-menu", - aria.expanded <-- mobileMenuOpen.signal, - span(cls := "sr-only", "Open main menu"), - Icons.outline - .menu() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") - ), - Icons.outline - .x() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") - ), - onClick.preventDefault.mapTo( - !mobileMenuOpen.now() - ) --> mobileMenuOpen.writer - ) - - def navBarLeft = - div( - cls := "flex items-center", - div(cls := "flex-shrink-0", logoImg), - div( - desktopOnly, - div( - cls := "ml-10 flex items-baseline space-x-4", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ) - ) - ) - - def navBarRight = - div( - desktopOnly, - div( - cls := "ml-4 flex items-center md:ml-6", - notificationButton, - userProfile - ) - ) - - def navBarMobile = - div( - cls := "-mr-2 flex", - mobileOnly, - mobileMenuButton - ) - - def navBar = - div( - cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex items-center justify-between h-16", - navBarLeft, - navBarRight, - navBarMobile - ) - ) - - def mobileMenu = - div( - mobileOnly, - cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }, - idAttr := "mobile-menu", - div( - cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ), - mobileProfile - ) - - nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala deleted file mode 100644 index 75339df..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object PageHeader: - def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = - header( - cls := "bg-white shadow-sm", - div( - cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", - h1( - cls := "text-lg leading-6 font-semibold text-gray-900", - Breadcrumbs(actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala deleted file mode 100644 index 5096463..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ /dev/null @@ -1,30 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import fiftyforms.ui.components.tailwind.Loading -import io.laminext.syntax.core.* - -object PageLayout: - case class ViewModel( - navigation: NavigationBar.ViewModel, - content: Option[HtmlElement] - ) - def apply(actionBus: Observer[Action])( - $m: Signal[ViewModel], - mods: Modifier[HtmlElement]* - )(using router: Router[Page]): HtmlElement = - val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) - div( - cls := "h-full flex flex-col", - NavigationBar($m.map(_.navigation)), - child.maybe <-- router.$currentPage.map(_.isRoot) - .switch(None, Some(PageHeader(actionBus))), - main( - cls := "flex-grow-1 overflow-y-auto", - mods, - child <-- $maybeContent.map(_.getOrElse(Loading)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala deleted file mode 100644 index 62d820f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.LinkSupport.* -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo - -object PageLink: - type ViewModel = Page - def apply($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = - a(mods($m, actions), child.text <-- $m.map(_.title)) - - def apply(m: ViewModel, actions: Observer[Action])(using - router: Router[Page] - ): Anchor = apply(Val(m), actions) - - def container($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = a(mods($m, actions)) - - def container(m: ViewModel, actions: Observer[Action])(using - Router[Page] - ): Anchor = container(Val(m), actions) - - private def mods($m: Signal[Page], actions: Observer[Action])(using - router: Router[Page] - ): Modifier[Anchor] = - Seq( - href <-- $m.map(router.absoluteUrlForPage).recover { case _ => - Some("invalid url") - }, - composeEvents(onClick.noKeyMod.preventDefault)( - _.sample($m) - .map(NavigateTo.apply) - ) --> actions - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala deleted file mode 100644 index 87f825a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.pages.dashboard.DashboardPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -class DashboardPageConnector(actionBus: Observer[Action])(using - router: Router[Page] -): - def apply: HtmlElement = - AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala deleted file mode 100644 index e9b0f71..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala +++ /dev/null @@ -1,76 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailKriteriaPage -import pages.detail.components.DukazyKriteria -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -object DetailKriteriaPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailKriteriaPageConnector( - state: DetailKriteriaPageConnector.AppState -)( - $page: Signal[Page.DetailKriteria] -)(using Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => - (p.osobniCislo.value, p.parametr.value, p.kriterium.value) - )((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map( - FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) - ) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, s, $s) => - DetailKriteriaPage($s)(state.actionBus.contramap { - case DukazyKriteria.Add => - NavigateTo( - Page.UpravDukazKriteria( - Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), - Page.Titled(s.parametr.id, Some(s.parametr.nazev)), - Page.Titled(s.kriterium.id, Some(s.kriterium.id)) - ) - ) - }) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): DetailKriteriaPage.ViewModel = - DetailKriteriaPage.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala deleted file mode 100644 index 4d8024f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ /dev/null @@ -1,57 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import pages.detail.DetailPage -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import fiftyforms.ui.components.tailwind.Color - -object DetailPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailPageConnector(state: DetailPageConnector.AppState)( - $page: Signal[Page.Detail] -)(using router: Router[Page]): - val $oscChangeSignal = $page.splitOne(_.osobniCislo.value)((osc, _, _) => osc) - val $pageChangeSignal = - $oscChangeSignal.flatMap(osc => - EventStream.fromSeq(Seq(FetchUserDetails(osc), FetchParameters(osc))) - ) - // TODO: filter the value based on the current osc - // OSC change will fetch new data, but still - // - we need to be sure that what we got is really what we ought to display - // - we want to display stale data accordingly (at least with loading indicator) - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - def apply: HtmlElement = - AppPage(state.actionBus)( - $data.combineWithFn($params)(_ zip _) - .map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: List[Parameter] - ): DetailPage.ViewModel = - DetailPage.ViewModel( - o.toDetailOsoby, - p.map( - _.toParametr(param => - PageLink.container(Page.DetailParametru(o, param), state.actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala deleted file mode 100644 index fdaa7f8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailParametruPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink - -object DetailParametruPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailParametruPageConnector( - state: DetailParametruPageConnector.AppState -)( - $page: Signal[Page.DetailParametru] -)(using router: Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => (p.osobniCislo.value, p.parametr.value))((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map(FetchParameter(_, _)) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - } yield (da, pb) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailParametruPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter - ): DetailParametruPage.ViewModel = - DetailParametruPage.ViewModel( - o.toDetailOsoby, - p.toParametr(p => - PageLink.container(Page.DetailParametru(o, p), state.actionBus) - ), - p.criteria.map( - _.toKriterium { c => - PageLink.container( - Page.DetailKriteria(o, p, c), - state.actionBus - ) - } - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala deleted file mode 100644 index 049aa22..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -case class DirectoryPageConnector( - $input: EventStream[List[UserInfo]], - actionBus: Observer[Action] -)(using router: Router[Page]): - val $data = $input.startWithNone - val $actionSignal = EventStream.fromValue(FetchDirectory) - - def apply: HtmlElement = - AppPage(actionBus)( - $data.split(_ => ())((_, _, s) => - pages.directory.DirectoryPage( - s.map( - _.map( - _.toUserRow(u => - PageLink.container( - Page.Detail(Page.Titled(u.personalNumber)), - actionBus - ) - ) - ) - ) - ) - ), - $actionSignal --> actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala deleted file mode 100644 index 88e7450..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ /dev/null @@ -1,68 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii -import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria -import fiftyforms.ui.components.tailwind.Color - -extension (o: UserInfo) - def toDetailOsoby: DetailOsoby.ViewModel = - DetailOsoby.ViewModel( - o.personalNumber, - o.name, - o.email, - o.phone, - o.img, - o.mainFunction.map(f => - DetailOsoby.Funkce.ViewModel(f.name, f.dept, f.ou) - ), - o.userContracts.headOption.map(c => - DetailOsoby.PracovniPomer.ViewModel(c.rel, c.startDate, c.endDate) - ) - ) - -extension (param: Parameter) - def toParametr(container: Parameter => Anchor): DetailParametru.ViewModel = - DetailParametru.ViewModel( - id = param.id, - nazev = param.name, - popis = param.description, - status = "Nesplněno", - statusColor = Color.red, - a = container(param) - ) - -extension (crit: ParameterCriteria) - def toKriterium( - container: ParameterCriteria => Anchor - ): DetailKriteria.ViewModel = - DetailKriteria.ViewModel( - nazev = crit.criteriumText, - kapitola = crit.chapterId, - bod = crit.itemId, - status = "Nesplněno", - statusColor = Color.red, - splneno = false, - dukazy = Nil, - container = container(crit) - ) - -extension (user: UserInfo) - def toUserRow( - container: UserInfo => HtmlElement = (_: UserInfo) => div() - ): UserRow.ViewModel = - UserRow.ViewModel( - osobniCislo = user.personalNumber.toString, - celeJmeno = user.name, - prijmeni = user.surname, - hlavniFunkce = user.mainFunction.map(_.name), - img = user.img, - container = () => container(user) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala deleted file mode 100644 index 7d5b2c1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.dashboard - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.Page - -object DashboardPage: - - def render: HtmlElement = - div("Dashboard page") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala deleted file mode 100644 index 9ef0698..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala +++ /dev/null @@ -1,31 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailKriteriaPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply($m: Signal[ViewModel])( - action: Observer[DukazyKriteria.Action] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - DukazyKriteria($m.map(_.kriterium.dukazy))(action) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala deleted file mode 100644 index 3b316e1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object DetailPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametry: SeznamParametru.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby($m.map(_.osoba)), - SeznamParametru($m.map(_.parametry)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala deleted file mode 100644 index 17224cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala +++ /dev/null @@ -1,24 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailParametruPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriteria: SeznamKriterii.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby.header($m.map(_.osoba)), - DetailParametru($m.map(_.parametr)), - SeznamKriterii($m.map(_.kriteria)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala deleted file mode 100644 index dc912fa..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ /dev/null @@ -1,107 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb -package app -package pages.detail - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.UpravDukazForm -import fiftyforms.services.files.File - -object UpravDukaz: - - type ThisPage = Page.UpravDukazKriteria - type PageKey = (OsobniCislo, String, String) - - trait State { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def availableFiles: EventStream[List[File]] - def actionBus: Observer[Action] - } - - def keyOfPage(page: ThisPage): PageKey = - (page.osobniCislo.value, page.parametr.value, page.kriterium.value) - - def onChangeAction(key: PageKey): Action = - FetchParameterCriteria( - key._1, - key._2, - key._3, - Page.UpravDukazKriteria(_, _, _) - ) - - class Connector(state: State)($page: Signal[ThisPage])(using Router[Page]): - val $paramChangeSignal = $page.splitOne(keyOfPage)((x, _, _) => x) - val $pageChangeSignal = $paramChangeSignal.map(onChangeAction) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.split(_ => ())((_, s, $s) => - PageComponent( - $s.map(buildModel), - state.availableFiles, - state.actionBus.contramap { - case UpravDukazForm.Cancelled => - NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) - case UpravDukazForm.AvailableFilesRequested => - FetchAvailableFiles(s._1.personalNumber) - } - ) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): PageComponent.ViewModel = - import connectors.* - PageComponent.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) - - object PageComponent: - import components.* - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply( - $m: Signal[ViewModel], - availableFilesStream: EventStream[List[File]], - events: Observer[UpravDukazForm.Event] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - UpravDukazForm(availableFilesStream)(events) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala deleted file mode 100644 index ca311b0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.Icons -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Color - -object DetailKriteria: - case class ViewModel( - nazev: String, - kapitola: String, - bod: String, - status: String, - statusColor: Color, - splneno: Boolean, - dukazy: List[DukazKriteria.ViewModel], - container: HtmlElement = div() - ) { - val id = s"${kapitola}${bod}" - } - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h3( - cls := "text-xl leading-6 font-bold text-gray-900", - child.text <-- $m.map(_.nazev) - ), - h3( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - "Kapitola ", - child.text <-- $m.map(_.kapitola), - " bod ", - child <-- $m.map(_.bod) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala deleted file mode 100644 index ae90290..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala +++ /dev/null @@ -1,100 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Avatar - -object DetailOsoby: - - object Funkce: - case class ViewModel( - nazev: String, - stredisko: String, - voj: String - ) - - def render($m: Signal[ViewModel]): HtmlElement = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.nazev), - span( - cls := "hidden md:inline", - " @ ", - child.text <-- $m.map(_.stredisko), - ", ", - child.text <-- $m.map(_.voj) - ) - ) - - object PracovniPomer: - case class ViewModel( - druh: String, - pocatek: LocalDate, - konec: Option[LocalDate] - ) - - def render($m: Signal[ViewModel]): HtmlElement = - import fiftyforms.ui.components.tailwind.CustomAttrs.datetime - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.druh), - " od ", - time( - datetime <-- $m.map(_.pocatek.toString), - child.text <-- $m.map(_.pocatek.toString) - ) - ) - - case class ViewModel( - osobniCislo: OsobniCislo, - jmeno: String, - email: Option[String], - telefon: Option[String], - img: Option[String], - hlavniFunkce: Option[Funkce.ViewModel], - pracovniPomer: Option[PracovniPomer.ViewModel] - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-4", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(16) - ), - div( - h1( - cls := "text-2xl font-bold text-gray-900", - child.text <-- $m.map(_.jmeno) - ), - child.maybe <-- $m.map(_.hlavniFunkce).split(_ => ())((_, _, d) => - Funkce.render(d) - ), - child.maybe <-- $m.map(_.pracovniPomer).split(_ => ())((_, _, d) => - PracovniPomer.render(d) - ) - ) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-5", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(8) - ), - div( - h1( - cls := "text-lg font-medium text-gray-800", - child.text <-- $m.map(_.jmeno) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala deleted file mode 100644 index e11cb62..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color - -object DetailParametru: - case class ViewModel( - id: String, - nazev: String, - popis: String, - status: String, - statusColor: Color, - a: Anchor - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h2( - cls := "text-2xl leading-6 font-medium text-gray-900", - child.text <-- $m.map(_.nazev) - ), - p( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - child.text <-- $m.map(_.popis) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - h2( - cls := "text-lg leading-6 font-medium text-gray-600", - child.text <-- $m.map(_.nazev) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala deleted file mode 100644 index a00ede8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala +++ /dev/null @@ -1,177 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.CustomAttrs - -object DukazKriteria: - case class Osoba(osobniCislo: String, jmeno: String) - case class Udalost(osoba: Osoba, datum: LocalDate) - case class Dokument( - url: String, - nazev: String, - pridal: Udalost, - odebral: Option[Udalost] - ) - case class ViewModel( - dokumenty: List[Dokument], - autorizoval: Option[Udalost], - platiDo: Option[LocalDate], - poznámka: Option[String] - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-lg", - div( - cls := "px-4 py-5 sm:px-6", - h3( - cls := "text-lg leading-6 font-medium text-gray-900", - """Applicant Information""" - ), - p( - cls := "mt-1 max-w-2xl text-sm text-gray-500", - """Personal details and application.""" - ) - ), - div( - cls := "border-t border-gray-200 px-4 py-5 sm:px-6", - dl( - cls := "grid grid-cols-1 gap-x-4 gap-y-8 sm:grid-cols-2", - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Full name""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Margot Foster""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Application for""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Backend Developer""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Email address""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """margotfoster@example.com""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Salary expectation""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """$120,000""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """About""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Fugiat ipsum ipsum deserunt culpa aute sint do nostrud anim incididunt cillum culpa consequat. Excepteur qui ipsum aliquip consequat sint. Sit id mollit nulla mollit nostrud in ea officia proident. Irure nostrud pariatur mollit ad adipisicing reprehenderit deserunt qui eu.""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """Attachments""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - ul( - role := "list", - cls := "border border-gray-200 rounded-md divide-y divide-gray-200", - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """resume_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ), - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """coverletter_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala deleted file mode 100644 index 22f134f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala +++ /dev/null @@ -1,35 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object DukazyKriteria: - sealed trait Action - case object Add extends Action - - type ViewModel = List[DukazKriteria.ViewModel] - - def apply($m: Signal[ViewModel])(actions: Observer[Action]): HtmlElement = - div( - child.maybe <-- $m.map(m => - if (m.isEmpty) then Some(prazdnyDukaz(actions)) - else None - ), - children <-- $m.map(_.zipWithIndex).split(_._2)((_, _, $s) => - DukazKriteria($s.map(_._1)) - ) - ) - - private def prazdnyDukaz(actions: Observer[Action]): HtmlElement = - button( - tpe := "button", - onClick.preventDefault.mapTo(Add) --> actions, - cls := "relative block w-full border-2 border-gray-300 border-dashed rounded-lg p-12 text-center hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - Icons.outline - .`document-add`(12) - .amend(svg.cls := "mx-auto text-gray-400"), - span( - cls := "mt-2 block text-sm font-medium text-gray-900", - "Přidat důkaz" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala deleted file mode 100644 index f628697..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.list.StackedList -import fiftyforms.ui.components.tailwind.list.ListRow -import fiftyforms.ui.components.tailwind.list.RowTag -import fiftyforms.ui.components.tailwind.list.RowNext -import java.time.LocalDate - -object SeznamKriterii: - type ViewModel = List[DetailKriteria.ViewModel] - - private val kritList = new StackedList[DetailKriteria.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - kritList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = - p(cls := "text-sm text-gray-500", s"${i.kapitola}${i.bod}"), - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.container - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala deleted file mode 100644 index 2bfe4bc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.list.{ - StackedList, - ListRow, - RowTag, - PropList, - IconText, - RowNext -} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.LinkSupport.* - -object SeznamParametru: - type ViewModel = List[DetailParametru.ViewModel] - - private val parametrList = new StackedList[DetailParametru.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - parametrList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = emptyNode, - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.a - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala deleted file mode 100644 index f048e9d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ /dev/null @@ -1,101 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.form.* -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement -import fiftyforms.services.files.components.tailwind.FilePicker -import cz.e_bs.cmi.mdr.pdb.frontend.AutorizujDukaz -import cz.e_bs.cmi.mdr.pdb.frontend.DocumentRef -import fiftyforms.services.files.File - -object UpravDukazForm: - sealed trait Event - case object Cancelled extends Event - case object AvailableFilesRequested extends Event - def apply(availableFilesStream: EventStream[List[File]])( - updates: Observer[Event] - ): HtmlElement = - val (filesStream, filesObserver) = - EventStream.withObserver[FilePicker.Event] - val files = Var[List[File]](Nil) - def submitButtons: HtmlElement = - div( - cls := "pt-5", - div( - cls := "flex justify-end", - button( - tpe := "button", - cls := "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zrušit", - onClick.mapTo(Cancelled) --> updates - ), - button( - tpe := "submit", - cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - disabled <-- files.signal.map(_.isEmpty), - "Autorizovat" - ) - ) - ) - - div( - filesStream.collect { case FilePicker.SelectionUpdated(files) => - files.to(List) - } --> files.writer, - filesStream.collect { case FilePicker.AvailableFilesRequested => - AvailableFilesRequested - } --> updates, - cls := "bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6", - Form( - Form.Body( - Form.Section( - FormHeader( - FormHeader.ViewModel( - "Doložení kritéria", - "Sestavte doklady poskytující důkaz kritéria a potvrďte odesláním formuláře. Případné limitace či jiné relevantní údaje vepište do pole Komentář." - ) - ), - FormFields( - FormRow( - "dokumenty", - "Dokumenty", - FilePicker(files.signal, availableFilesStream)(filesObserver) - .amend(idAttr := "dokumenty", cls("max-w-lg")) - ).toHtml, - FormRow( - "platnost", - "Platnost", - input( - idAttr := "platnost", - name := "platnost", - tpe := "date", - autoComplete := "date", - cls := "block max-w-lg w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" - ) - ).toHtml, - FormRow( - "komentar", - "Komentář", - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - textArea( - idAttr := "komentar", - name := "about", - rows := 3, - cls := "max-w-lg shadow-sm block w-full focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border border-gray-300 rounded-md" - ), - p( - cls := "mt-2 text-sm text-gray-500", - "Doplňte prosím potřebné informace související s doložením kritéria, včetně případných limitací." - ) - ) - ).toHtml - ) - ) - ), - submitButtons - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala deleted file mode 100644 index a3b00af..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala +++ /dev/null @@ -1,103 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import scala.scalajs.js.annotation.JSExportTopLevel -import scala.scalajs.js.annotation.JSExport -import scala.scalajs.js.annotation.JSImport -import scala.scalajs.js -import org.scalajs.dom -import com.raquo.laminar.api.L.{*, given} -import scala.scalajs.js.Date -import com.raquo.waypoint.Router -import com.raquo.waypoint.SplitRender -import cz.e_bs.cmi.mdr.pdb.app.services.DataFetcher -import scala.scalajs.js.JSON -import zio.json._ -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.state.AppState - -@js.native -@JSImport("stylesheets/main.css", JSImport.Namespace) -object Css extends js.Any - -@js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native -@JSImport("params/pdb-params.json", JSImport.Default) -object pdbParams extends js.Object - -@JSExportTopLevel("app") -object Main: - - @JSExport - def main(args: Array[String]): Unit = - import Routes.given - onLoad { - setupAirstream() - val appContainer = dom.document.querySelector("#app") - val _ = - render( - appContainer, - renderPage(state.MockAppState(using unsafeWindowOwner, router)) - ) - } - - private def onLoad(f: => Unit): Unit = - documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) - - private def setupAirstream()(using router: Router[Page]): Unit = - AirstreamError.registerUnhandledErrorCallback(err => - router.forcePage( - Page.UnhandledError( - Some(err.getClass.getName), // TODO: Fill only in dev mode - Some(err.getMessage) - ) - ) - ) - - def renderPage(state: AppState)(using - router: Router[Page] - ): HtmlElement = - val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) - .collectSignal[Page.Detail]( - connectors - .DetailPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailParametru]( - connectors - .DetailParametruPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailKriteria]( - connectors - .DetailKriteriaPageConnector(state)(_) - .apply - ) - .collectSignal[Page.UpravDukazKriteria]( - pages.detail.UpravDukaz.Connector(state)(_).apply - ) - .collectStatic(Page.Dashboard)( - connectors.DashboardPageConnector(state.actionBus).apply - ) - .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) - ) - .collect[Page.UnhandledError](pg => - pages.errors - .UnhandledErrorPage( - pages.errors.UnhandledErrorPage - .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), - state.actionBus - ) - ) - .collectStatic(Page.Directory)( - connectors - .DirectoryPageConnector(state.users, state.actionBus) - .apply - ) - div(cls := "h-full", child <-- pageSplitter.$view) - - // Pull in the stylesheet - val css: Css.type = Css diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala deleted file mode 100644 index 3bbc8c0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ /dev/null @@ -1,198 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.* -import org.scalajs.dom -import zio.json.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import scala.scalajs.js -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.Page.Titled - -// enum is not working with Waypoints' SplitRender collectStatic -sealed abstract class Page( - val id: String, - val title: String, - val parent: Option[Page] -) { - val path: Vector[Page] = - parent match - case None => Vector(this) - case Some(p) => p.path :+ this - - val isRoot: Boolean = parent.isEmpty -} - -object Page: - - case class Titled[V](value: V, title: Option[String] = None): - val show: String = title.getOrElse(value.toString) - - case object Directory extends Page("directory", "Adresář", None) - - case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) - - case class Detail(osobniCislo: Titled[OsobniCislo]) - extends Page("user", osobniCislo.show, Some(Directory)) - - object Detail { - def apply(o: UserInfo): Detail = Detail( - Titled(o.personalNumber, Some(o.name)) - ) - } - - case class DetailParametru( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String] - ) extends Page( - "parameter", - parametr.show, - Some(Detail(osobniCislo)) - ) - - object DetailParametru { - def apply(o: UserInfo, p: Parameter): DetailParametru = - DetailParametru( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)) - ) - } - - case class DetailKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "criteria", - kriterium.show, - Some(DetailParametru(osobniCislo, parametr)) - ) - - object DetailKriteria { - def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = - DetailKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class UpravDukazKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "addProof", - "Důkaz", - Some(DetailKriteria(osobniCislo, parametr, kriterium)) - ) - - object UpravDukazKriteria { - def apply( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): UpravDukazKriteria = - UpravDukazKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class NotFound(url: String) extends Page("404", "404", Some(Directory)) - - case class UnhandledError( - errorName: Option[String], - errorMessage: Option[String] - ) extends Page("500", "Unexpected error", Some(Directory)) - -object Routes: - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) - given [V: JsonEncoder]: JsonEncoder[Titled[V]] = - DeriveJsonEncoder.gen[Titled[V]] - given [V: JsonDecoder]: JsonDecoder[Titled[V]] = - DeriveJsonDecoder.gen[Titled[V]] - given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] - given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] - - val base = - js.`import`.meta.env.BASE_URL - .asInstanceOf[String] + "app" - - val homePage: Page = Page.Directory - - given router: Router[Page] = Router[Page]( - routes = List( - Route.static(homePage, root / endOfSegments, basePath = base), - Route.static( - Page.Dashboard, - root / "dashboard" / endOfSegments, - basePath = base - ), - Route[Page.Detail, String]( - encode = _.osobniCislo.value.toString, - decode = osc => Page.Detail(Titled(OsobniCislo(osc))), - root / "osoba" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.DetailParametru, (String, String)]( - encode = p => (p.osobniCislo.value.toString, p.parametr.value), - decode = - p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / endOfSegments, - basePath = base - ), - Route[Page.DetailKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.DetailKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.UpravDukazKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.UpravDukazKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / "edit" / endOfSegments, - basePath = base - ) - ), - serializePage = _.toJson, - deserializePage = _.fromJson[Page] - .fold(s => throw IllegalStateException(s), identity), - getPageTitle = _.title, - routeFallback = url => Page.NotFound(url), - deserializeFallback = _ => Page.Dashboard - )( - $popStateEvent = windowEvents.onPopState, - owner = unsafeWindowOwner - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala deleted file mode 100644 index b24b82b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -sealed trait Action - -case object FetchDirectory extends Action -case class FetchUserDetails(osc: OsobniCislo) extends Action -case class FetchParameters(osc: OsobniCislo) extends Action -case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action -case class FetchParameterCriteria( - osc: OsobniCislo, - paramId: String, - critId: String, - page: (UserInfo, Parameter, ParameterCriteria) => Page -) extends Action -case class FetchAvailableFiles(osc: OsobniCislo) extends Action -case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala deleted file mode 100644 index c637d35..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala +++ /dev/null @@ -1,82 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.{UserProfile, UserInfo, OsobniCislo} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo -import cz.e_bs.cmi.mdr.pdb.UserFunction - -object AppPage: - // TODO: pages by logged in user - val pages: List[Page] = List(Page.Directory, Page.Dashboard) - - import NavigationBar.{Logo, MenuItem} - - val logo = Logo( - "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", - "Workflow" - ) - - // TODO: menu items by user profile - val userMenu = - List( - MenuItem("Your Profile"), - MenuItem("Settings"), - MenuItem("Sign out") - ) - - // TODO: load user profile - val $userProfile = Var( - UserProfile( - "tom", - UserInfo( - OsobniCislo("1031"), - "tom", - "Tom", - "Cook", - None, - None, - Some("tom@example.com"), - Some("+420 222 866 180"), - Some( - UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") - ), - Nil, - None - ) - ) - ) - - val $userInfo = $userProfile.signal.map(_.userInfo) - - type ViewModel = Option[HtmlElement] - def apply( - actionBus: Observer[Action] - )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using - router: Router[Page] - ): HtmlElement = - PageLayout(actionBus)( - $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => - PageLayout.ViewModel( - NavigationBar.ViewModel( - u, - pages.map(p => - NavigationBar.Link( - () => - PageLink( - p, - actionBus - ), - p == cp - ) - ), - userMenu, - logo - ), - c - ) - ), - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala deleted file mode 100644 index bf473c6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ /dev/null @@ -1,143 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden -import fiftyforms.ui.components.tailwind.list.IconText.ViewModel -import fiftyforms.ui.components.tailwind.Icons -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Action - -object Breadcrumbs: - - private def slash = - import svg.{*, given} - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-300", - xmlns := "http://www.w3.org/2000/svg", - fill := "currentColor", - viewBox := "0 0 20 20", - ariaHidden := true, - path( - d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" - ) - ) - - def backIcon = - Icons.solid - .`arrow-narrow-left`() - .amend( - svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" - ) - - object Link: - case class ViewModel( - page: Page, - icon: Option[SvgElement], - extraClasses: String, - text: String, - textClasses: Option[String] = None - ) - - val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" - - def shortHome(p: Page) = ViewModel( - p, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - "Zpět na úvodní stránku", - None - ) - - def fullHome(p: Page) = ViewModel( - p, - Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), - "text-gray-400 hover:text-gray-500", - "Domů", - Some("sr-only") - ) - - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - PageLink - .container($m.map(_.page), actionBus) - .amend( - cls <-- $m.map(_.extraClasses), - child.maybe <-- $m.map(_.icon), - span( - cls <-- $m.map(_.textClasses.getOrElse("")), - child.text <-- $m.map(_.text) - ) - ) - - object FullBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - ol( - role := "list", - cls := "flex items-center space-x-4", - children <-- $m.map(_.path).split(_.id)((_, _, $p) => - li( - div( - cls := "flex items-center", - child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), - Link( - $p.map(p => - if (p.isRoot) then Link.fullHome(p) - else - Link.ViewModel( - p, - None, - s"ml-4 max-w-xs truncate ${Link.baseClasses}", - p.title - ) - ), - actionBus - ) - ) - ) - ) - ) - - object ShortBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - Link( - $m.map { p => - val target = p.parent.getOrElse(p) - if target.isRoot then Link.shortHome(target) - else - Link.ViewModel( - target, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - s"Zpět na ${target.title}" - ) - }, - actionBus - ) - - def apply(actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - nav( - cls := "flex", - aria.label := "Breadcrumb", - div( - cls := "flex sm:hidden", - ShortBreadcrumbs(router.$currentPage, actionBus) - ), - div( - cls := "hidden sm:block", - FullBreadcrumbs(router.$currentPage, actionBus) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala deleted file mode 100644 index 0db2047..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ /dev/null @@ -1,242 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent -import fiftyforms.ui.components.tailwind.Icons -import fiftyforms.ui.components.tailwind.Avatar -import cz.e_bs.cmi.mdr.pdb.UserInfo -import io.laminext.syntax.core.* - -object NavigationBar: - - case class Logo(img: String, name: String) - case class Link(a: () => Anchor, active: Boolean) - case class MenuItem(title: String) - - case class ViewModel( - userInfo: UserInfo, - pages: List[Link], - userMenu: List[MenuItem], - logo: Logo - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - val $userInfo = $m.map(_.userInfo) - val mobileMenuOpen = Var(false) - - val desktopOnly = cls("hidden md:block") - val mobileOnly = cls("md:hidden") - - inline def avatarImage(size: Int = 8) = - Avatar($userInfo.map(_.img)).avatarImage(size) - - def notificationButton = button( - tpe := "button", - cls := List( - "bg-indigo-600", - "focus:outline-none", - "focus:ring-2", - "focus:ring-offset-2", - "focus:ring-offset-indigo-600", - "focus:ring-white", - "hover:text-white", - "p-1", - "rounded-full", - "text-indigo-200" - ), - span(cls := "sr-only", "View notifications"), - Icons.outline.bell() - ) - - def userProfile: HtmlElement = - val menuOpen = Var(false) - - def menuItem(item: MenuItem, idx: Int): HtmlElement = - a( - href := "#", - cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", - role := "menuitem", - tabIndex := -1, - idAttr := s"user-menu-item-$idx", - item.title - ) - - div( - cls := "ml-3 relative", - div( - button( - tpe := "button", - cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - idAttr := "user-menu-button", - aria.expanded <-- menuOpen.signal, - aria.hasPopup := true, - span(cls := "sr-only", "Open user menu"), - child <-- avatarImage(), - onClick.preventDefault.mapTo( - !menuOpen.now() - ) --> menuOpen.writer - ) - ), - /* - * */ - div( - cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", - cls <-- menuOpen.signal.map { o => - if (o) "md:block" else "md:hidden" - }, - role := "menu", - aria.orientation := "vertical", - aria.labelledBy := "user-menu-button", - tabIndex := -1, - // : keyboard navigation - children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) - ) - ) - - def mobileProfile = - def menuItem(item: MenuItem): HtmlElement = - a( - href := "#", - cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", - item.title - ) - - div( - cls := "pt-4 pb-3 border-t border-indigo-700", - div( - cls := "flex items-center px-5", - div( - cls := "flex-shrink-0", - child <-- avatarImage(10) - ), - div( - cls := "ml-3", - div( - cls := "text-base font-medium text-white", - child.text <-- $userInfo.map(_.name) - ), - child.maybe <-- $userInfo.map( - _.email.map(e => - div( - cls := "text-sm font-medium text-indigo-300", - e - ) - ) - ) - ), - notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) - ), - div( - cls := "mt-3 px-2 space-y-1", - children <-- $m.map(_.userMenu.map(menuItem)) - ) - ) - - def pageLink(page: Link): Anchor = - page - .a() - .amend( - cls := "text-white px-3 py-2 rounded-md text-sm font-medium", - cls := Seq( - "bg-indigo-700" -> page.active, - "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active - ), - ariaCurrent := (if page.active then "page" else "false") - ) - - def logoImg: Image = - img( - cls := "h-8 w-8", - src <-- $m.map(_.logo.img), - alt <-- $m.map(_.logo.name) - ) - - def mobileMenuButton = button( - tpe := "button", - cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - aria.controls := "mobile-menu", - aria.expanded <-- mobileMenuOpen.signal, - span(cls := "sr-only", "Open main menu"), - Icons.outline - .menu() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") - ), - Icons.outline - .x() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") - ), - onClick.preventDefault.mapTo( - !mobileMenuOpen.now() - ) --> mobileMenuOpen.writer - ) - - def navBarLeft = - div( - cls := "flex items-center", - div(cls := "flex-shrink-0", logoImg), - div( - desktopOnly, - div( - cls := "ml-10 flex items-baseline space-x-4", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ) - ) - ) - - def navBarRight = - div( - desktopOnly, - div( - cls := "ml-4 flex items-center md:ml-6", - notificationButton, - userProfile - ) - ) - - def navBarMobile = - div( - cls := "-mr-2 flex", - mobileOnly, - mobileMenuButton - ) - - def navBar = - div( - cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex items-center justify-between h-16", - navBarLeft, - navBarRight, - navBarMobile - ) - ) - - def mobileMenu = - div( - mobileOnly, - cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }, - idAttr := "mobile-menu", - div( - cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ), - mobileProfile - ) - - nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala deleted file mode 100644 index 75339df..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object PageHeader: - def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = - header( - cls := "bg-white shadow-sm", - div( - cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", - h1( - cls := "text-lg leading-6 font-semibold text-gray-900", - Breadcrumbs(actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala deleted file mode 100644 index 5096463..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ /dev/null @@ -1,30 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import fiftyforms.ui.components.tailwind.Loading -import io.laminext.syntax.core.* - -object PageLayout: - case class ViewModel( - navigation: NavigationBar.ViewModel, - content: Option[HtmlElement] - ) - def apply(actionBus: Observer[Action])( - $m: Signal[ViewModel], - mods: Modifier[HtmlElement]* - )(using router: Router[Page]): HtmlElement = - val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) - div( - cls := "h-full flex flex-col", - NavigationBar($m.map(_.navigation)), - child.maybe <-- router.$currentPage.map(_.isRoot) - .switch(None, Some(PageHeader(actionBus))), - main( - cls := "flex-grow-1 overflow-y-auto", - mods, - child <-- $maybeContent.map(_.getOrElse(Loading)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala deleted file mode 100644 index 62d820f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.LinkSupport.* -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo - -object PageLink: - type ViewModel = Page - def apply($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = - a(mods($m, actions), child.text <-- $m.map(_.title)) - - def apply(m: ViewModel, actions: Observer[Action])(using - router: Router[Page] - ): Anchor = apply(Val(m), actions) - - def container($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = a(mods($m, actions)) - - def container(m: ViewModel, actions: Observer[Action])(using - Router[Page] - ): Anchor = container(Val(m), actions) - - private def mods($m: Signal[Page], actions: Observer[Action])(using - router: Router[Page] - ): Modifier[Anchor] = - Seq( - href <-- $m.map(router.absoluteUrlForPage).recover { case _ => - Some("invalid url") - }, - composeEvents(onClick.noKeyMod.preventDefault)( - _.sample($m) - .map(NavigateTo.apply) - ) --> actions - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala deleted file mode 100644 index 87f825a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.pages.dashboard.DashboardPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -class DashboardPageConnector(actionBus: Observer[Action])(using - router: Router[Page] -): - def apply: HtmlElement = - AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala deleted file mode 100644 index e9b0f71..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala +++ /dev/null @@ -1,76 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailKriteriaPage -import pages.detail.components.DukazyKriteria -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -object DetailKriteriaPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailKriteriaPageConnector( - state: DetailKriteriaPageConnector.AppState -)( - $page: Signal[Page.DetailKriteria] -)(using Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => - (p.osobniCislo.value, p.parametr.value, p.kriterium.value) - )((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map( - FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) - ) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, s, $s) => - DetailKriteriaPage($s)(state.actionBus.contramap { - case DukazyKriteria.Add => - NavigateTo( - Page.UpravDukazKriteria( - Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), - Page.Titled(s.parametr.id, Some(s.parametr.nazev)), - Page.Titled(s.kriterium.id, Some(s.kriterium.id)) - ) - ) - }) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): DetailKriteriaPage.ViewModel = - DetailKriteriaPage.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala deleted file mode 100644 index 4d8024f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ /dev/null @@ -1,57 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import pages.detail.DetailPage -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import fiftyforms.ui.components.tailwind.Color - -object DetailPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailPageConnector(state: DetailPageConnector.AppState)( - $page: Signal[Page.Detail] -)(using router: Router[Page]): - val $oscChangeSignal = $page.splitOne(_.osobniCislo.value)((osc, _, _) => osc) - val $pageChangeSignal = - $oscChangeSignal.flatMap(osc => - EventStream.fromSeq(Seq(FetchUserDetails(osc), FetchParameters(osc))) - ) - // TODO: filter the value based on the current osc - // OSC change will fetch new data, but still - // - we need to be sure that what we got is really what we ought to display - // - we want to display stale data accordingly (at least with loading indicator) - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - def apply: HtmlElement = - AppPage(state.actionBus)( - $data.combineWithFn($params)(_ zip _) - .map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: List[Parameter] - ): DetailPage.ViewModel = - DetailPage.ViewModel( - o.toDetailOsoby, - p.map( - _.toParametr(param => - PageLink.container(Page.DetailParametru(o, param), state.actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala deleted file mode 100644 index fdaa7f8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailParametruPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink - -object DetailParametruPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailParametruPageConnector( - state: DetailParametruPageConnector.AppState -)( - $page: Signal[Page.DetailParametru] -)(using router: Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => (p.osobniCislo.value, p.parametr.value))((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map(FetchParameter(_, _)) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - } yield (da, pb) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailParametruPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter - ): DetailParametruPage.ViewModel = - DetailParametruPage.ViewModel( - o.toDetailOsoby, - p.toParametr(p => - PageLink.container(Page.DetailParametru(o, p), state.actionBus) - ), - p.criteria.map( - _.toKriterium { c => - PageLink.container( - Page.DetailKriteria(o, p, c), - state.actionBus - ) - } - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala deleted file mode 100644 index 049aa22..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -case class DirectoryPageConnector( - $input: EventStream[List[UserInfo]], - actionBus: Observer[Action] -)(using router: Router[Page]): - val $data = $input.startWithNone - val $actionSignal = EventStream.fromValue(FetchDirectory) - - def apply: HtmlElement = - AppPage(actionBus)( - $data.split(_ => ())((_, _, s) => - pages.directory.DirectoryPage( - s.map( - _.map( - _.toUserRow(u => - PageLink.container( - Page.Detail(Page.Titled(u.personalNumber)), - actionBus - ) - ) - ) - ) - ) - ), - $actionSignal --> actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala deleted file mode 100644 index 88e7450..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ /dev/null @@ -1,68 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii -import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria -import fiftyforms.ui.components.tailwind.Color - -extension (o: UserInfo) - def toDetailOsoby: DetailOsoby.ViewModel = - DetailOsoby.ViewModel( - o.personalNumber, - o.name, - o.email, - o.phone, - o.img, - o.mainFunction.map(f => - DetailOsoby.Funkce.ViewModel(f.name, f.dept, f.ou) - ), - o.userContracts.headOption.map(c => - DetailOsoby.PracovniPomer.ViewModel(c.rel, c.startDate, c.endDate) - ) - ) - -extension (param: Parameter) - def toParametr(container: Parameter => Anchor): DetailParametru.ViewModel = - DetailParametru.ViewModel( - id = param.id, - nazev = param.name, - popis = param.description, - status = "Nesplněno", - statusColor = Color.red, - a = container(param) - ) - -extension (crit: ParameterCriteria) - def toKriterium( - container: ParameterCriteria => Anchor - ): DetailKriteria.ViewModel = - DetailKriteria.ViewModel( - nazev = crit.criteriumText, - kapitola = crit.chapterId, - bod = crit.itemId, - status = "Nesplněno", - statusColor = Color.red, - splneno = false, - dukazy = Nil, - container = container(crit) - ) - -extension (user: UserInfo) - def toUserRow( - container: UserInfo => HtmlElement = (_: UserInfo) => div() - ): UserRow.ViewModel = - UserRow.ViewModel( - osobniCislo = user.personalNumber.toString, - celeJmeno = user.name, - prijmeni = user.surname, - hlavniFunkce = user.mainFunction.map(_.name), - img = user.img, - container = () => container(user) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala deleted file mode 100644 index 7d5b2c1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.dashboard - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.Page - -object DashboardPage: - - def render: HtmlElement = - div("Dashboard page") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala deleted file mode 100644 index 9ef0698..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala +++ /dev/null @@ -1,31 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailKriteriaPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply($m: Signal[ViewModel])( - action: Observer[DukazyKriteria.Action] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - DukazyKriteria($m.map(_.kriterium.dukazy))(action) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala deleted file mode 100644 index 3b316e1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object DetailPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametry: SeznamParametru.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby($m.map(_.osoba)), - SeznamParametru($m.map(_.parametry)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala deleted file mode 100644 index 17224cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala +++ /dev/null @@ -1,24 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailParametruPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriteria: SeznamKriterii.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby.header($m.map(_.osoba)), - DetailParametru($m.map(_.parametr)), - SeznamKriterii($m.map(_.kriteria)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala deleted file mode 100644 index dc912fa..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ /dev/null @@ -1,107 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb -package app -package pages.detail - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.UpravDukazForm -import fiftyforms.services.files.File - -object UpravDukaz: - - type ThisPage = Page.UpravDukazKriteria - type PageKey = (OsobniCislo, String, String) - - trait State { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def availableFiles: EventStream[List[File]] - def actionBus: Observer[Action] - } - - def keyOfPage(page: ThisPage): PageKey = - (page.osobniCislo.value, page.parametr.value, page.kriterium.value) - - def onChangeAction(key: PageKey): Action = - FetchParameterCriteria( - key._1, - key._2, - key._3, - Page.UpravDukazKriteria(_, _, _) - ) - - class Connector(state: State)($page: Signal[ThisPage])(using Router[Page]): - val $paramChangeSignal = $page.splitOne(keyOfPage)((x, _, _) => x) - val $pageChangeSignal = $paramChangeSignal.map(onChangeAction) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.split(_ => ())((_, s, $s) => - PageComponent( - $s.map(buildModel), - state.availableFiles, - state.actionBus.contramap { - case UpravDukazForm.Cancelled => - NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) - case UpravDukazForm.AvailableFilesRequested => - FetchAvailableFiles(s._1.personalNumber) - } - ) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): PageComponent.ViewModel = - import connectors.* - PageComponent.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) - - object PageComponent: - import components.* - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply( - $m: Signal[ViewModel], - availableFilesStream: EventStream[List[File]], - events: Observer[UpravDukazForm.Event] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - UpravDukazForm(availableFilesStream)(events) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala deleted file mode 100644 index ca311b0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.Icons -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Color - -object DetailKriteria: - case class ViewModel( - nazev: String, - kapitola: String, - bod: String, - status: String, - statusColor: Color, - splneno: Boolean, - dukazy: List[DukazKriteria.ViewModel], - container: HtmlElement = div() - ) { - val id = s"${kapitola}${bod}" - } - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h3( - cls := "text-xl leading-6 font-bold text-gray-900", - child.text <-- $m.map(_.nazev) - ), - h3( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - "Kapitola ", - child.text <-- $m.map(_.kapitola), - " bod ", - child <-- $m.map(_.bod) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala deleted file mode 100644 index ae90290..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala +++ /dev/null @@ -1,100 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Avatar - -object DetailOsoby: - - object Funkce: - case class ViewModel( - nazev: String, - stredisko: String, - voj: String - ) - - def render($m: Signal[ViewModel]): HtmlElement = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.nazev), - span( - cls := "hidden md:inline", - " @ ", - child.text <-- $m.map(_.stredisko), - ", ", - child.text <-- $m.map(_.voj) - ) - ) - - object PracovniPomer: - case class ViewModel( - druh: String, - pocatek: LocalDate, - konec: Option[LocalDate] - ) - - def render($m: Signal[ViewModel]): HtmlElement = - import fiftyforms.ui.components.tailwind.CustomAttrs.datetime - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.druh), - " od ", - time( - datetime <-- $m.map(_.pocatek.toString), - child.text <-- $m.map(_.pocatek.toString) - ) - ) - - case class ViewModel( - osobniCislo: OsobniCislo, - jmeno: String, - email: Option[String], - telefon: Option[String], - img: Option[String], - hlavniFunkce: Option[Funkce.ViewModel], - pracovniPomer: Option[PracovniPomer.ViewModel] - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-4", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(16) - ), - div( - h1( - cls := "text-2xl font-bold text-gray-900", - child.text <-- $m.map(_.jmeno) - ), - child.maybe <-- $m.map(_.hlavniFunkce).split(_ => ())((_, _, d) => - Funkce.render(d) - ), - child.maybe <-- $m.map(_.pracovniPomer).split(_ => ())((_, _, d) => - PracovniPomer.render(d) - ) - ) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-5", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(8) - ), - div( - h1( - cls := "text-lg font-medium text-gray-800", - child.text <-- $m.map(_.jmeno) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala deleted file mode 100644 index e11cb62..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color - -object DetailParametru: - case class ViewModel( - id: String, - nazev: String, - popis: String, - status: String, - statusColor: Color, - a: Anchor - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h2( - cls := "text-2xl leading-6 font-medium text-gray-900", - child.text <-- $m.map(_.nazev) - ), - p( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - child.text <-- $m.map(_.popis) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - h2( - cls := "text-lg leading-6 font-medium text-gray-600", - child.text <-- $m.map(_.nazev) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala deleted file mode 100644 index a00ede8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala +++ /dev/null @@ -1,177 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.CustomAttrs - -object DukazKriteria: - case class Osoba(osobniCislo: String, jmeno: String) - case class Udalost(osoba: Osoba, datum: LocalDate) - case class Dokument( - url: String, - nazev: String, - pridal: Udalost, - odebral: Option[Udalost] - ) - case class ViewModel( - dokumenty: List[Dokument], - autorizoval: Option[Udalost], - platiDo: Option[LocalDate], - poznámka: Option[String] - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-lg", - div( - cls := "px-4 py-5 sm:px-6", - h3( - cls := "text-lg leading-6 font-medium text-gray-900", - """Applicant Information""" - ), - p( - cls := "mt-1 max-w-2xl text-sm text-gray-500", - """Personal details and application.""" - ) - ), - div( - cls := "border-t border-gray-200 px-4 py-5 sm:px-6", - dl( - cls := "grid grid-cols-1 gap-x-4 gap-y-8 sm:grid-cols-2", - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Full name""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Margot Foster""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Application for""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Backend Developer""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Email address""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """margotfoster@example.com""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Salary expectation""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """$120,000""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """About""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Fugiat ipsum ipsum deserunt culpa aute sint do nostrud anim incididunt cillum culpa consequat. Excepteur qui ipsum aliquip consequat sint. Sit id mollit nulla mollit nostrud in ea officia proident. Irure nostrud pariatur mollit ad adipisicing reprehenderit deserunt qui eu.""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """Attachments""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - ul( - role := "list", - cls := "border border-gray-200 rounded-md divide-y divide-gray-200", - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """resume_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ), - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """coverletter_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala deleted file mode 100644 index 22f134f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala +++ /dev/null @@ -1,35 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object DukazyKriteria: - sealed trait Action - case object Add extends Action - - type ViewModel = List[DukazKriteria.ViewModel] - - def apply($m: Signal[ViewModel])(actions: Observer[Action]): HtmlElement = - div( - child.maybe <-- $m.map(m => - if (m.isEmpty) then Some(prazdnyDukaz(actions)) - else None - ), - children <-- $m.map(_.zipWithIndex).split(_._2)((_, _, $s) => - DukazKriteria($s.map(_._1)) - ) - ) - - private def prazdnyDukaz(actions: Observer[Action]): HtmlElement = - button( - tpe := "button", - onClick.preventDefault.mapTo(Add) --> actions, - cls := "relative block w-full border-2 border-gray-300 border-dashed rounded-lg p-12 text-center hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - Icons.outline - .`document-add`(12) - .amend(svg.cls := "mx-auto text-gray-400"), - span( - cls := "mt-2 block text-sm font-medium text-gray-900", - "Přidat důkaz" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala deleted file mode 100644 index f628697..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.list.StackedList -import fiftyforms.ui.components.tailwind.list.ListRow -import fiftyforms.ui.components.tailwind.list.RowTag -import fiftyforms.ui.components.tailwind.list.RowNext -import java.time.LocalDate - -object SeznamKriterii: - type ViewModel = List[DetailKriteria.ViewModel] - - private val kritList = new StackedList[DetailKriteria.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - kritList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = - p(cls := "text-sm text-gray-500", s"${i.kapitola}${i.bod}"), - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.container - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala deleted file mode 100644 index 2bfe4bc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.list.{ - StackedList, - ListRow, - RowTag, - PropList, - IconText, - RowNext -} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.LinkSupport.* - -object SeznamParametru: - type ViewModel = List[DetailParametru.ViewModel] - - private val parametrList = new StackedList[DetailParametru.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - parametrList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = emptyNode, - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.a - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala deleted file mode 100644 index f048e9d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ /dev/null @@ -1,101 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.form.* -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement -import fiftyforms.services.files.components.tailwind.FilePicker -import cz.e_bs.cmi.mdr.pdb.frontend.AutorizujDukaz -import cz.e_bs.cmi.mdr.pdb.frontend.DocumentRef -import fiftyforms.services.files.File - -object UpravDukazForm: - sealed trait Event - case object Cancelled extends Event - case object AvailableFilesRequested extends Event - def apply(availableFilesStream: EventStream[List[File]])( - updates: Observer[Event] - ): HtmlElement = - val (filesStream, filesObserver) = - EventStream.withObserver[FilePicker.Event] - val files = Var[List[File]](Nil) - def submitButtons: HtmlElement = - div( - cls := "pt-5", - div( - cls := "flex justify-end", - button( - tpe := "button", - cls := "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zrušit", - onClick.mapTo(Cancelled) --> updates - ), - button( - tpe := "submit", - cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - disabled <-- files.signal.map(_.isEmpty), - "Autorizovat" - ) - ) - ) - - div( - filesStream.collect { case FilePicker.SelectionUpdated(files) => - files.to(List) - } --> files.writer, - filesStream.collect { case FilePicker.AvailableFilesRequested => - AvailableFilesRequested - } --> updates, - cls := "bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6", - Form( - Form.Body( - Form.Section( - FormHeader( - FormHeader.ViewModel( - "Doložení kritéria", - "Sestavte doklady poskytující důkaz kritéria a potvrďte odesláním formuláře. Případné limitace či jiné relevantní údaje vepište do pole Komentář." - ) - ), - FormFields( - FormRow( - "dokumenty", - "Dokumenty", - FilePicker(files.signal, availableFilesStream)(filesObserver) - .amend(idAttr := "dokumenty", cls("max-w-lg")) - ).toHtml, - FormRow( - "platnost", - "Platnost", - input( - idAttr := "platnost", - name := "platnost", - tpe := "date", - autoComplete := "date", - cls := "block max-w-lg w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" - ) - ).toHtml, - FormRow( - "komentar", - "Komentář", - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - textArea( - idAttr := "komentar", - name := "about", - rows := 3, - cls := "max-w-lg shadow-sm block w-full focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border border-gray-300 rounded-md" - ), - p( - cls := "mt-2 text-sm text-gray-500", - "Doplňte prosím potřebné informace související s doložením kritéria, včetně případných limitací." - ) - ) - ).toHtml - ) - ) - ), - submitButtons - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala deleted file mode 100644 index 41033f6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -import components._ - -object DirectoryPage: - - type ViewModel = List[UserRow.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - val (actionsStream, actionObserver) = - EventStream.withObserver[SearchForm.Action] - val $filter = actionsStream - .collect { - case SearchForm.ClearFilter => None - case SearchForm.SetFilter(t) => Some(t) - } - .startWith(None) - val byLetter = for { - d <- $m - f <- $filter - } yield for { - (letter, users) <- d - .filter { user => - f match - case None => true - case Some(t) => user.search.contains(t) - } - .groupBy(_.prijmeni.head) - .to(List) - .sortBy(_._1) - } yield (letter.toString, users.sortBy(_.prijmeni)) - - div( - cls := "h-full max-w-7xl mx-auto order-first flex flex-col flex-shrink-0", - SearchForm(actionObserver), - Directory(byLetter) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala deleted file mode 100644 index a3b00af..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala +++ /dev/null @@ -1,103 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import scala.scalajs.js.annotation.JSExportTopLevel -import scala.scalajs.js.annotation.JSExport -import scala.scalajs.js.annotation.JSImport -import scala.scalajs.js -import org.scalajs.dom -import com.raquo.laminar.api.L.{*, given} -import scala.scalajs.js.Date -import com.raquo.waypoint.Router -import com.raquo.waypoint.SplitRender -import cz.e_bs.cmi.mdr.pdb.app.services.DataFetcher -import scala.scalajs.js.JSON -import zio.json._ -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.state.AppState - -@js.native -@JSImport("stylesheets/main.css", JSImport.Namespace) -object Css extends js.Any - -@js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native -@JSImport("params/pdb-params.json", JSImport.Default) -object pdbParams extends js.Object - -@JSExportTopLevel("app") -object Main: - - @JSExport - def main(args: Array[String]): Unit = - import Routes.given - onLoad { - setupAirstream() - val appContainer = dom.document.querySelector("#app") - val _ = - render( - appContainer, - renderPage(state.MockAppState(using unsafeWindowOwner, router)) - ) - } - - private def onLoad(f: => Unit): Unit = - documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) - - private def setupAirstream()(using router: Router[Page]): Unit = - AirstreamError.registerUnhandledErrorCallback(err => - router.forcePage( - Page.UnhandledError( - Some(err.getClass.getName), // TODO: Fill only in dev mode - Some(err.getMessage) - ) - ) - ) - - def renderPage(state: AppState)(using - router: Router[Page] - ): HtmlElement = - val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) - .collectSignal[Page.Detail]( - connectors - .DetailPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailParametru]( - connectors - .DetailParametruPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailKriteria]( - connectors - .DetailKriteriaPageConnector(state)(_) - .apply - ) - .collectSignal[Page.UpravDukazKriteria]( - pages.detail.UpravDukaz.Connector(state)(_).apply - ) - .collectStatic(Page.Dashboard)( - connectors.DashboardPageConnector(state.actionBus).apply - ) - .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) - ) - .collect[Page.UnhandledError](pg => - pages.errors - .UnhandledErrorPage( - pages.errors.UnhandledErrorPage - .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), - state.actionBus - ) - ) - .collectStatic(Page.Directory)( - connectors - .DirectoryPageConnector(state.users, state.actionBus) - .apply - ) - div(cls := "h-full", child <-- pageSplitter.$view) - - // Pull in the stylesheet - val css: Css.type = Css diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala deleted file mode 100644 index 3bbc8c0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ /dev/null @@ -1,198 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.* -import org.scalajs.dom -import zio.json.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import scala.scalajs.js -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.Page.Titled - -// enum is not working with Waypoints' SplitRender collectStatic -sealed abstract class Page( - val id: String, - val title: String, - val parent: Option[Page] -) { - val path: Vector[Page] = - parent match - case None => Vector(this) - case Some(p) => p.path :+ this - - val isRoot: Boolean = parent.isEmpty -} - -object Page: - - case class Titled[V](value: V, title: Option[String] = None): - val show: String = title.getOrElse(value.toString) - - case object Directory extends Page("directory", "Adresář", None) - - case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) - - case class Detail(osobniCislo: Titled[OsobniCislo]) - extends Page("user", osobniCislo.show, Some(Directory)) - - object Detail { - def apply(o: UserInfo): Detail = Detail( - Titled(o.personalNumber, Some(o.name)) - ) - } - - case class DetailParametru( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String] - ) extends Page( - "parameter", - parametr.show, - Some(Detail(osobniCislo)) - ) - - object DetailParametru { - def apply(o: UserInfo, p: Parameter): DetailParametru = - DetailParametru( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)) - ) - } - - case class DetailKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "criteria", - kriterium.show, - Some(DetailParametru(osobniCislo, parametr)) - ) - - object DetailKriteria { - def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = - DetailKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class UpravDukazKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "addProof", - "Důkaz", - Some(DetailKriteria(osobniCislo, parametr, kriterium)) - ) - - object UpravDukazKriteria { - def apply( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): UpravDukazKriteria = - UpravDukazKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class NotFound(url: String) extends Page("404", "404", Some(Directory)) - - case class UnhandledError( - errorName: Option[String], - errorMessage: Option[String] - ) extends Page("500", "Unexpected error", Some(Directory)) - -object Routes: - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) - given [V: JsonEncoder]: JsonEncoder[Titled[V]] = - DeriveJsonEncoder.gen[Titled[V]] - given [V: JsonDecoder]: JsonDecoder[Titled[V]] = - DeriveJsonDecoder.gen[Titled[V]] - given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] - given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] - - val base = - js.`import`.meta.env.BASE_URL - .asInstanceOf[String] + "app" - - val homePage: Page = Page.Directory - - given router: Router[Page] = Router[Page]( - routes = List( - Route.static(homePage, root / endOfSegments, basePath = base), - Route.static( - Page.Dashboard, - root / "dashboard" / endOfSegments, - basePath = base - ), - Route[Page.Detail, String]( - encode = _.osobniCislo.value.toString, - decode = osc => Page.Detail(Titled(OsobniCislo(osc))), - root / "osoba" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.DetailParametru, (String, String)]( - encode = p => (p.osobniCislo.value.toString, p.parametr.value), - decode = - p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / endOfSegments, - basePath = base - ), - Route[Page.DetailKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.DetailKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.UpravDukazKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.UpravDukazKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / "edit" / endOfSegments, - basePath = base - ) - ), - serializePage = _.toJson, - deserializePage = _.fromJson[Page] - .fold(s => throw IllegalStateException(s), identity), - getPageTitle = _.title, - routeFallback = url => Page.NotFound(url), - deserializeFallback = _ => Page.Dashboard - )( - $popStateEvent = windowEvents.onPopState, - owner = unsafeWindowOwner - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala deleted file mode 100644 index b24b82b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -sealed trait Action - -case object FetchDirectory extends Action -case class FetchUserDetails(osc: OsobniCislo) extends Action -case class FetchParameters(osc: OsobniCislo) extends Action -case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action -case class FetchParameterCriteria( - osc: OsobniCislo, - paramId: String, - critId: String, - page: (UserInfo, Parameter, ParameterCriteria) => Page -) extends Action -case class FetchAvailableFiles(osc: OsobniCislo) extends Action -case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala deleted file mode 100644 index c637d35..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala +++ /dev/null @@ -1,82 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.{UserProfile, UserInfo, OsobniCislo} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo -import cz.e_bs.cmi.mdr.pdb.UserFunction - -object AppPage: - // TODO: pages by logged in user - val pages: List[Page] = List(Page.Directory, Page.Dashboard) - - import NavigationBar.{Logo, MenuItem} - - val logo = Logo( - "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", - "Workflow" - ) - - // TODO: menu items by user profile - val userMenu = - List( - MenuItem("Your Profile"), - MenuItem("Settings"), - MenuItem("Sign out") - ) - - // TODO: load user profile - val $userProfile = Var( - UserProfile( - "tom", - UserInfo( - OsobniCislo("1031"), - "tom", - "Tom", - "Cook", - None, - None, - Some("tom@example.com"), - Some("+420 222 866 180"), - Some( - UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") - ), - Nil, - None - ) - ) - ) - - val $userInfo = $userProfile.signal.map(_.userInfo) - - type ViewModel = Option[HtmlElement] - def apply( - actionBus: Observer[Action] - )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using - router: Router[Page] - ): HtmlElement = - PageLayout(actionBus)( - $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => - PageLayout.ViewModel( - NavigationBar.ViewModel( - u, - pages.map(p => - NavigationBar.Link( - () => - PageLink( - p, - actionBus - ), - p == cp - ) - ), - userMenu, - logo - ), - c - ) - ), - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala deleted file mode 100644 index bf473c6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ /dev/null @@ -1,143 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden -import fiftyforms.ui.components.tailwind.list.IconText.ViewModel -import fiftyforms.ui.components.tailwind.Icons -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Action - -object Breadcrumbs: - - private def slash = - import svg.{*, given} - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-300", - xmlns := "http://www.w3.org/2000/svg", - fill := "currentColor", - viewBox := "0 0 20 20", - ariaHidden := true, - path( - d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" - ) - ) - - def backIcon = - Icons.solid - .`arrow-narrow-left`() - .amend( - svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" - ) - - object Link: - case class ViewModel( - page: Page, - icon: Option[SvgElement], - extraClasses: String, - text: String, - textClasses: Option[String] = None - ) - - val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" - - def shortHome(p: Page) = ViewModel( - p, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - "Zpět na úvodní stránku", - None - ) - - def fullHome(p: Page) = ViewModel( - p, - Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), - "text-gray-400 hover:text-gray-500", - "Domů", - Some("sr-only") - ) - - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - PageLink - .container($m.map(_.page), actionBus) - .amend( - cls <-- $m.map(_.extraClasses), - child.maybe <-- $m.map(_.icon), - span( - cls <-- $m.map(_.textClasses.getOrElse("")), - child.text <-- $m.map(_.text) - ) - ) - - object FullBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - ol( - role := "list", - cls := "flex items-center space-x-4", - children <-- $m.map(_.path).split(_.id)((_, _, $p) => - li( - div( - cls := "flex items-center", - child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), - Link( - $p.map(p => - if (p.isRoot) then Link.fullHome(p) - else - Link.ViewModel( - p, - None, - s"ml-4 max-w-xs truncate ${Link.baseClasses}", - p.title - ) - ), - actionBus - ) - ) - ) - ) - ) - - object ShortBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - Link( - $m.map { p => - val target = p.parent.getOrElse(p) - if target.isRoot then Link.shortHome(target) - else - Link.ViewModel( - target, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - s"Zpět na ${target.title}" - ) - }, - actionBus - ) - - def apply(actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - nav( - cls := "flex", - aria.label := "Breadcrumb", - div( - cls := "flex sm:hidden", - ShortBreadcrumbs(router.$currentPage, actionBus) - ), - div( - cls := "hidden sm:block", - FullBreadcrumbs(router.$currentPage, actionBus) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala deleted file mode 100644 index 0db2047..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ /dev/null @@ -1,242 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent -import fiftyforms.ui.components.tailwind.Icons -import fiftyforms.ui.components.tailwind.Avatar -import cz.e_bs.cmi.mdr.pdb.UserInfo -import io.laminext.syntax.core.* - -object NavigationBar: - - case class Logo(img: String, name: String) - case class Link(a: () => Anchor, active: Boolean) - case class MenuItem(title: String) - - case class ViewModel( - userInfo: UserInfo, - pages: List[Link], - userMenu: List[MenuItem], - logo: Logo - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - val $userInfo = $m.map(_.userInfo) - val mobileMenuOpen = Var(false) - - val desktopOnly = cls("hidden md:block") - val mobileOnly = cls("md:hidden") - - inline def avatarImage(size: Int = 8) = - Avatar($userInfo.map(_.img)).avatarImage(size) - - def notificationButton = button( - tpe := "button", - cls := List( - "bg-indigo-600", - "focus:outline-none", - "focus:ring-2", - "focus:ring-offset-2", - "focus:ring-offset-indigo-600", - "focus:ring-white", - "hover:text-white", - "p-1", - "rounded-full", - "text-indigo-200" - ), - span(cls := "sr-only", "View notifications"), - Icons.outline.bell() - ) - - def userProfile: HtmlElement = - val menuOpen = Var(false) - - def menuItem(item: MenuItem, idx: Int): HtmlElement = - a( - href := "#", - cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", - role := "menuitem", - tabIndex := -1, - idAttr := s"user-menu-item-$idx", - item.title - ) - - div( - cls := "ml-3 relative", - div( - button( - tpe := "button", - cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - idAttr := "user-menu-button", - aria.expanded <-- menuOpen.signal, - aria.hasPopup := true, - span(cls := "sr-only", "Open user menu"), - child <-- avatarImage(), - onClick.preventDefault.mapTo( - !menuOpen.now() - ) --> menuOpen.writer - ) - ), - /* - * */ - div( - cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", - cls <-- menuOpen.signal.map { o => - if (o) "md:block" else "md:hidden" - }, - role := "menu", - aria.orientation := "vertical", - aria.labelledBy := "user-menu-button", - tabIndex := -1, - // : keyboard navigation - children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) - ) - ) - - def mobileProfile = - def menuItem(item: MenuItem): HtmlElement = - a( - href := "#", - cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", - item.title - ) - - div( - cls := "pt-4 pb-3 border-t border-indigo-700", - div( - cls := "flex items-center px-5", - div( - cls := "flex-shrink-0", - child <-- avatarImage(10) - ), - div( - cls := "ml-3", - div( - cls := "text-base font-medium text-white", - child.text <-- $userInfo.map(_.name) - ), - child.maybe <-- $userInfo.map( - _.email.map(e => - div( - cls := "text-sm font-medium text-indigo-300", - e - ) - ) - ) - ), - notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) - ), - div( - cls := "mt-3 px-2 space-y-1", - children <-- $m.map(_.userMenu.map(menuItem)) - ) - ) - - def pageLink(page: Link): Anchor = - page - .a() - .amend( - cls := "text-white px-3 py-2 rounded-md text-sm font-medium", - cls := Seq( - "bg-indigo-700" -> page.active, - "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active - ), - ariaCurrent := (if page.active then "page" else "false") - ) - - def logoImg: Image = - img( - cls := "h-8 w-8", - src <-- $m.map(_.logo.img), - alt <-- $m.map(_.logo.name) - ) - - def mobileMenuButton = button( - tpe := "button", - cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - aria.controls := "mobile-menu", - aria.expanded <-- mobileMenuOpen.signal, - span(cls := "sr-only", "Open main menu"), - Icons.outline - .menu() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") - ), - Icons.outline - .x() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") - ), - onClick.preventDefault.mapTo( - !mobileMenuOpen.now() - ) --> mobileMenuOpen.writer - ) - - def navBarLeft = - div( - cls := "flex items-center", - div(cls := "flex-shrink-0", logoImg), - div( - desktopOnly, - div( - cls := "ml-10 flex items-baseline space-x-4", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ) - ) - ) - - def navBarRight = - div( - desktopOnly, - div( - cls := "ml-4 flex items-center md:ml-6", - notificationButton, - userProfile - ) - ) - - def navBarMobile = - div( - cls := "-mr-2 flex", - mobileOnly, - mobileMenuButton - ) - - def navBar = - div( - cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex items-center justify-between h-16", - navBarLeft, - navBarRight, - navBarMobile - ) - ) - - def mobileMenu = - div( - mobileOnly, - cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }, - idAttr := "mobile-menu", - div( - cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ), - mobileProfile - ) - - nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala deleted file mode 100644 index 75339df..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object PageHeader: - def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = - header( - cls := "bg-white shadow-sm", - div( - cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", - h1( - cls := "text-lg leading-6 font-semibold text-gray-900", - Breadcrumbs(actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala deleted file mode 100644 index 5096463..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ /dev/null @@ -1,30 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import fiftyforms.ui.components.tailwind.Loading -import io.laminext.syntax.core.* - -object PageLayout: - case class ViewModel( - navigation: NavigationBar.ViewModel, - content: Option[HtmlElement] - ) - def apply(actionBus: Observer[Action])( - $m: Signal[ViewModel], - mods: Modifier[HtmlElement]* - )(using router: Router[Page]): HtmlElement = - val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) - div( - cls := "h-full flex flex-col", - NavigationBar($m.map(_.navigation)), - child.maybe <-- router.$currentPage.map(_.isRoot) - .switch(None, Some(PageHeader(actionBus))), - main( - cls := "flex-grow-1 overflow-y-auto", - mods, - child <-- $maybeContent.map(_.getOrElse(Loading)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala deleted file mode 100644 index 62d820f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.LinkSupport.* -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo - -object PageLink: - type ViewModel = Page - def apply($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = - a(mods($m, actions), child.text <-- $m.map(_.title)) - - def apply(m: ViewModel, actions: Observer[Action])(using - router: Router[Page] - ): Anchor = apply(Val(m), actions) - - def container($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = a(mods($m, actions)) - - def container(m: ViewModel, actions: Observer[Action])(using - Router[Page] - ): Anchor = container(Val(m), actions) - - private def mods($m: Signal[Page], actions: Observer[Action])(using - router: Router[Page] - ): Modifier[Anchor] = - Seq( - href <-- $m.map(router.absoluteUrlForPage).recover { case _ => - Some("invalid url") - }, - composeEvents(onClick.noKeyMod.preventDefault)( - _.sample($m) - .map(NavigateTo.apply) - ) --> actions - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala deleted file mode 100644 index 87f825a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.pages.dashboard.DashboardPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -class DashboardPageConnector(actionBus: Observer[Action])(using - router: Router[Page] -): - def apply: HtmlElement = - AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala deleted file mode 100644 index e9b0f71..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala +++ /dev/null @@ -1,76 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailKriteriaPage -import pages.detail.components.DukazyKriteria -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -object DetailKriteriaPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailKriteriaPageConnector( - state: DetailKriteriaPageConnector.AppState -)( - $page: Signal[Page.DetailKriteria] -)(using Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => - (p.osobniCislo.value, p.parametr.value, p.kriterium.value) - )((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map( - FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) - ) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, s, $s) => - DetailKriteriaPage($s)(state.actionBus.contramap { - case DukazyKriteria.Add => - NavigateTo( - Page.UpravDukazKriteria( - Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), - Page.Titled(s.parametr.id, Some(s.parametr.nazev)), - Page.Titled(s.kriterium.id, Some(s.kriterium.id)) - ) - ) - }) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): DetailKriteriaPage.ViewModel = - DetailKriteriaPage.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala deleted file mode 100644 index 4d8024f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ /dev/null @@ -1,57 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import pages.detail.DetailPage -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import fiftyforms.ui.components.tailwind.Color - -object DetailPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailPageConnector(state: DetailPageConnector.AppState)( - $page: Signal[Page.Detail] -)(using router: Router[Page]): - val $oscChangeSignal = $page.splitOne(_.osobniCislo.value)((osc, _, _) => osc) - val $pageChangeSignal = - $oscChangeSignal.flatMap(osc => - EventStream.fromSeq(Seq(FetchUserDetails(osc), FetchParameters(osc))) - ) - // TODO: filter the value based on the current osc - // OSC change will fetch new data, but still - // - we need to be sure that what we got is really what we ought to display - // - we want to display stale data accordingly (at least with loading indicator) - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - def apply: HtmlElement = - AppPage(state.actionBus)( - $data.combineWithFn($params)(_ zip _) - .map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: List[Parameter] - ): DetailPage.ViewModel = - DetailPage.ViewModel( - o.toDetailOsoby, - p.map( - _.toParametr(param => - PageLink.container(Page.DetailParametru(o, param), state.actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala deleted file mode 100644 index fdaa7f8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailParametruPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink - -object DetailParametruPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailParametruPageConnector( - state: DetailParametruPageConnector.AppState -)( - $page: Signal[Page.DetailParametru] -)(using router: Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => (p.osobniCislo.value, p.parametr.value))((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map(FetchParameter(_, _)) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - } yield (da, pb) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailParametruPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter - ): DetailParametruPage.ViewModel = - DetailParametruPage.ViewModel( - o.toDetailOsoby, - p.toParametr(p => - PageLink.container(Page.DetailParametru(o, p), state.actionBus) - ), - p.criteria.map( - _.toKriterium { c => - PageLink.container( - Page.DetailKriteria(o, p, c), - state.actionBus - ) - } - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala deleted file mode 100644 index 049aa22..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -case class DirectoryPageConnector( - $input: EventStream[List[UserInfo]], - actionBus: Observer[Action] -)(using router: Router[Page]): - val $data = $input.startWithNone - val $actionSignal = EventStream.fromValue(FetchDirectory) - - def apply: HtmlElement = - AppPage(actionBus)( - $data.split(_ => ())((_, _, s) => - pages.directory.DirectoryPage( - s.map( - _.map( - _.toUserRow(u => - PageLink.container( - Page.Detail(Page.Titled(u.personalNumber)), - actionBus - ) - ) - ) - ) - ) - ), - $actionSignal --> actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala deleted file mode 100644 index 88e7450..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ /dev/null @@ -1,68 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii -import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria -import fiftyforms.ui.components.tailwind.Color - -extension (o: UserInfo) - def toDetailOsoby: DetailOsoby.ViewModel = - DetailOsoby.ViewModel( - o.personalNumber, - o.name, - o.email, - o.phone, - o.img, - o.mainFunction.map(f => - DetailOsoby.Funkce.ViewModel(f.name, f.dept, f.ou) - ), - o.userContracts.headOption.map(c => - DetailOsoby.PracovniPomer.ViewModel(c.rel, c.startDate, c.endDate) - ) - ) - -extension (param: Parameter) - def toParametr(container: Parameter => Anchor): DetailParametru.ViewModel = - DetailParametru.ViewModel( - id = param.id, - nazev = param.name, - popis = param.description, - status = "Nesplněno", - statusColor = Color.red, - a = container(param) - ) - -extension (crit: ParameterCriteria) - def toKriterium( - container: ParameterCriteria => Anchor - ): DetailKriteria.ViewModel = - DetailKriteria.ViewModel( - nazev = crit.criteriumText, - kapitola = crit.chapterId, - bod = crit.itemId, - status = "Nesplněno", - statusColor = Color.red, - splneno = false, - dukazy = Nil, - container = container(crit) - ) - -extension (user: UserInfo) - def toUserRow( - container: UserInfo => HtmlElement = (_: UserInfo) => div() - ): UserRow.ViewModel = - UserRow.ViewModel( - osobniCislo = user.personalNumber.toString, - celeJmeno = user.name, - prijmeni = user.surname, - hlavniFunkce = user.mainFunction.map(_.name), - img = user.img, - container = () => container(user) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala deleted file mode 100644 index 7d5b2c1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.dashboard - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.Page - -object DashboardPage: - - def render: HtmlElement = - div("Dashboard page") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala deleted file mode 100644 index 9ef0698..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala +++ /dev/null @@ -1,31 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailKriteriaPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply($m: Signal[ViewModel])( - action: Observer[DukazyKriteria.Action] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - DukazyKriteria($m.map(_.kriterium.dukazy))(action) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala deleted file mode 100644 index 3b316e1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object DetailPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametry: SeznamParametru.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby($m.map(_.osoba)), - SeznamParametru($m.map(_.parametry)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala deleted file mode 100644 index 17224cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala +++ /dev/null @@ -1,24 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailParametruPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriteria: SeznamKriterii.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby.header($m.map(_.osoba)), - DetailParametru($m.map(_.parametr)), - SeznamKriterii($m.map(_.kriteria)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala deleted file mode 100644 index dc912fa..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ /dev/null @@ -1,107 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb -package app -package pages.detail - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.UpravDukazForm -import fiftyforms.services.files.File - -object UpravDukaz: - - type ThisPage = Page.UpravDukazKriteria - type PageKey = (OsobniCislo, String, String) - - trait State { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def availableFiles: EventStream[List[File]] - def actionBus: Observer[Action] - } - - def keyOfPage(page: ThisPage): PageKey = - (page.osobniCislo.value, page.parametr.value, page.kriterium.value) - - def onChangeAction(key: PageKey): Action = - FetchParameterCriteria( - key._1, - key._2, - key._3, - Page.UpravDukazKriteria(_, _, _) - ) - - class Connector(state: State)($page: Signal[ThisPage])(using Router[Page]): - val $paramChangeSignal = $page.splitOne(keyOfPage)((x, _, _) => x) - val $pageChangeSignal = $paramChangeSignal.map(onChangeAction) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.split(_ => ())((_, s, $s) => - PageComponent( - $s.map(buildModel), - state.availableFiles, - state.actionBus.contramap { - case UpravDukazForm.Cancelled => - NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) - case UpravDukazForm.AvailableFilesRequested => - FetchAvailableFiles(s._1.personalNumber) - } - ) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): PageComponent.ViewModel = - import connectors.* - PageComponent.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) - - object PageComponent: - import components.* - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply( - $m: Signal[ViewModel], - availableFilesStream: EventStream[List[File]], - events: Observer[UpravDukazForm.Event] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - UpravDukazForm(availableFilesStream)(events) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala deleted file mode 100644 index ca311b0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.Icons -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Color - -object DetailKriteria: - case class ViewModel( - nazev: String, - kapitola: String, - bod: String, - status: String, - statusColor: Color, - splneno: Boolean, - dukazy: List[DukazKriteria.ViewModel], - container: HtmlElement = div() - ) { - val id = s"${kapitola}${bod}" - } - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h3( - cls := "text-xl leading-6 font-bold text-gray-900", - child.text <-- $m.map(_.nazev) - ), - h3( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - "Kapitola ", - child.text <-- $m.map(_.kapitola), - " bod ", - child <-- $m.map(_.bod) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala deleted file mode 100644 index ae90290..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala +++ /dev/null @@ -1,100 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Avatar - -object DetailOsoby: - - object Funkce: - case class ViewModel( - nazev: String, - stredisko: String, - voj: String - ) - - def render($m: Signal[ViewModel]): HtmlElement = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.nazev), - span( - cls := "hidden md:inline", - " @ ", - child.text <-- $m.map(_.stredisko), - ", ", - child.text <-- $m.map(_.voj) - ) - ) - - object PracovniPomer: - case class ViewModel( - druh: String, - pocatek: LocalDate, - konec: Option[LocalDate] - ) - - def render($m: Signal[ViewModel]): HtmlElement = - import fiftyforms.ui.components.tailwind.CustomAttrs.datetime - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.druh), - " od ", - time( - datetime <-- $m.map(_.pocatek.toString), - child.text <-- $m.map(_.pocatek.toString) - ) - ) - - case class ViewModel( - osobniCislo: OsobniCislo, - jmeno: String, - email: Option[String], - telefon: Option[String], - img: Option[String], - hlavniFunkce: Option[Funkce.ViewModel], - pracovniPomer: Option[PracovniPomer.ViewModel] - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-4", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(16) - ), - div( - h1( - cls := "text-2xl font-bold text-gray-900", - child.text <-- $m.map(_.jmeno) - ), - child.maybe <-- $m.map(_.hlavniFunkce).split(_ => ())((_, _, d) => - Funkce.render(d) - ), - child.maybe <-- $m.map(_.pracovniPomer).split(_ => ())((_, _, d) => - PracovniPomer.render(d) - ) - ) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-5", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(8) - ), - div( - h1( - cls := "text-lg font-medium text-gray-800", - child.text <-- $m.map(_.jmeno) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala deleted file mode 100644 index e11cb62..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color - -object DetailParametru: - case class ViewModel( - id: String, - nazev: String, - popis: String, - status: String, - statusColor: Color, - a: Anchor - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h2( - cls := "text-2xl leading-6 font-medium text-gray-900", - child.text <-- $m.map(_.nazev) - ), - p( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - child.text <-- $m.map(_.popis) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - h2( - cls := "text-lg leading-6 font-medium text-gray-600", - child.text <-- $m.map(_.nazev) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala deleted file mode 100644 index a00ede8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala +++ /dev/null @@ -1,177 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.CustomAttrs - -object DukazKriteria: - case class Osoba(osobniCislo: String, jmeno: String) - case class Udalost(osoba: Osoba, datum: LocalDate) - case class Dokument( - url: String, - nazev: String, - pridal: Udalost, - odebral: Option[Udalost] - ) - case class ViewModel( - dokumenty: List[Dokument], - autorizoval: Option[Udalost], - platiDo: Option[LocalDate], - poznámka: Option[String] - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-lg", - div( - cls := "px-4 py-5 sm:px-6", - h3( - cls := "text-lg leading-6 font-medium text-gray-900", - """Applicant Information""" - ), - p( - cls := "mt-1 max-w-2xl text-sm text-gray-500", - """Personal details and application.""" - ) - ), - div( - cls := "border-t border-gray-200 px-4 py-5 sm:px-6", - dl( - cls := "grid grid-cols-1 gap-x-4 gap-y-8 sm:grid-cols-2", - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Full name""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Margot Foster""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Application for""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Backend Developer""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Email address""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """margotfoster@example.com""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Salary expectation""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """$120,000""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """About""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Fugiat ipsum ipsum deserunt culpa aute sint do nostrud anim incididunt cillum culpa consequat. Excepteur qui ipsum aliquip consequat sint. Sit id mollit nulla mollit nostrud in ea officia proident. Irure nostrud pariatur mollit ad adipisicing reprehenderit deserunt qui eu.""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """Attachments""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - ul( - role := "list", - cls := "border border-gray-200 rounded-md divide-y divide-gray-200", - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """resume_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ), - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """coverletter_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala deleted file mode 100644 index 22f134f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala +++ /dev/null @@ -1,35 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object DukazyKriteria: - sealed trait Action - case object Add extends Action - - type ViewModel = List[DukazKriteria.ViewModel] - - def apply($m: Signal[ViewModel])(actions: Observer[Action]): HtmlElement = - div( - child.maybe <-- $m.map(m => - if (m.isEmpty) then Some(prazdnyDukaz(actions)) - else None - ), - children <-- $m.map(_.zipWithIndex).split(_._2)((_, _, $s) => - DukazKriteria($s.map(_._1)) - ) - ) - - private def prazdnyDukaz(actions: Observer[Action]): HtmlElement = - button( - tpe := "button", - onClick.preventDefault.mapTo(Add) --> actions, - cls := "relative block w-full border-2 border-gray-300 border-dashed rounded-lg p-12 text-center hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - Icons.outline - .`document-add`(12) - .amend(svg.cls := "mx-auto text-gray-400"), - span( - cls := "mt-2 block text-sm font-medium text-gray-900", - "Přidat důkaz" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala deleted file mode 100644 index f628697..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.list.StackedList -import fiftyforms.ui.components.tailwind.list.ListRow -import fiftyforms.ui.components.tailwind.list.RowTag -import fiftyforms.ui.components.tailwind.list.RowNext -import java.time.LocalDate - -object SeznamKriterii: - type ViewModel = List[DetailKriteria.ViewModel] - - private val kritList = new StackedList[DetailKriteria.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - kritList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = - p(cls := "text-sm text-gray-500", s"${i.kapitola}${i.bod}"), - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.container - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala deleted file mode 100644 index 2bfe4bc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.list.{ - StackedList, - ListRow, - RowTag, - PropList, - IconText, - RowNext -} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.LinkSupport.* - -object SeznamParametru: - type ViewModel = List[DetailParametru.ViewModel] - - private val parametrList = new StackedList[DetailParametru.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - parametrList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = emptyNode, - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.a - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala deleted file mode 100644 index f048e9d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ /dev/null @@ -1,101 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.form.* -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement -import fiftyforms.services.files.components.tailwind.FilePicker -import cz.e_bs.cmi.mdr.pdb.frontend.AutorizujDukaz -import cz.e_bs.cmi.mdr.pdb.frontend.DocumentRef -import fiftyforms.services.files.File - -object UpravDukazForm: - sealed trait Event - case object Cancelled extends Event - case object AvailableFilesRequested extends Event - def apply(availableFilesStream: EventStream[List[File]])( - updates: Observer[Event] - ): HtmlElement = - val (filesStream, filesObserver) = - EventStream.withObserver[FilePicker.Event] - val files = Var[List[File]](Nil) - def submitButtons: HtmlElement = - div( - cls := "pt-5", - div( - cls := "flex justify-end", - button( - tpe := "button", - cls := "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zrušit", - onClick.mapTo(Cancelled) --> updates - ), - button( - tpe := "submit", - cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - disabled <-- files.signal.map(_.isEmpty), - "Autorizovat" - ) - ) - ) - - div( - filesStream.collect { case FilePicker.SelectionUpdated(files) => - files.to(List) - } --> files.writer, - filesStream.collect { case FilePicker.AvailableFilesRequested => - AvailableFilesRequested - } --> updates, - cls := "bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6", - Form( - Form.Body( - Form.Section( - FormHeader( - FormHeader.ViewModel( - "Doložení kritéria", - "Sestavte doklady poskytující důkaz kritéria a potvrďte odesláním formuláře. Případné limitace či jiné relevantní údaje vepište do pole Komentář." - ) - ), - FormFields( - FormRow( - "dokumenty", - "Dokumenty", - FilePicker(files.signal, availableFilesStream)(filesObserver) - .amend(idAttr := "dokumenty", cls("max-w-lg")) - ).toHtml, - FormRow( - "platnost", - "Platnost", - input( - idAttr := "platnost", - name := "platnost", - tpe := "date", - autoComplete := "date", - cls := "block max-w-lg w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" - ) - ).toHtml, - FormRow( - "komentar", - "Komentář", - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - textArea( - idAttr := "komentar", - name := "about", - rows := 3, - cls := "max-w-lg shadow-sm block w-full focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border border-gray-300 rounded-md" - ), - p( - cls := "mt-2 text-sm text-gray-500", - "Doplňte prosím potřebné informace související s doložením kritéria, včetně případných limitací." - ) - ) - ).toHtml - ) - ) - ), - submitButtons - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala deleted file mode 100644 index 41033f6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -import components._ - -object DirectoryPage: - - type ViewModel = List[UserRow.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - val (actionsStream, actionObserver) = - EventStream.withObserver[SearchForm.Action] - val $filter = actionsStream - .collect { - case SearchForm.ClearFilter => None - case SearchForm.SetFilter(t) => Some(t) - } - .startWith(None) - val byLetter = for { - d <- $m - f <- $filter - } yield for { - (letter, users) <- d - .filter { user => - f match - case None => true - case Some(t) => user.search.contains(t) - } - .groupBy(_.prijmeni.head) - .to(List) - .sortBy(_._1) - } yield (letter.toString, users.sortBy(_.prijmeni)) - - div( - cls := "h-full max-w-7xl mx-auto order-first flex flex-col flex-shrink-0", - SearchForm(actionObserver), - Directory(byLetter) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala deleted file mode 100644 index 73f915d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -object Directory: - - object Header: - type ViewModel = String - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500", - h3(child.text <-- $m) - ) - - object UserList: - type ViewModel = List[UserRow.ViewModel] - def apply($m: Signal[ViewModel]): HtmlElement = - ul( - role := "list", - cls := "relative z-0 divide-y divide-gray-200", - children <-- $m.split(_.osobniCislo)((_, _, s) => UserRow(s)) - ) - - type ViewModel = List[(String, List[UserRow.ViewModel])] - def apply($m: Signal[ViewModel]): HtmlElement = - nav( - cls := "flex-1 min-h-0 overflow-y-auto", - aria.label := "Directory", - children <-- $m.split(_._1)((_, _, s) => - div( - cls := "relative", - Header(s.map(_._1)), - UserList(s.map(_._2)) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala deleted file mode 100644 index a3b00af..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala +++ /dev/null @@ -1,103 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import scala.scalajs.js.annotation.JSExportTopLevel -import scala.scalajs.js.annotation.JSExport -import scala.scalajs.js.annotation.JSImport -import scala.scalajs.js -import org.scalajs.dom -import com.raquo.laminar.api.L.{*, given} -import scala.scalajs.js.Date -import com.raquo.waypoint.Router -import com.raquo.waypoint.SplitRender -import cz.e_bs.cmi.mdr.pdb.app.services.DataFetcher -import scala.scalajs.js.JSON -import zio.json._ -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.state.AppState - -@js.native -@JSImport("stylesheets/main.css", JSImport.Namespace) -object Css extends js.Any - -@js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native -@JSImport("params/pdb-params.json", JSImport.Default) -object pdbParams extends js.Object - -@JSExportTopLevel("app") -object Main: - - @JSExport - def main(args: Array[String]): Unit = - import Routes.given - onLoad { - setupAirstream() - val appContainer = dom.document.querySelector("#app") - val _ = - render( - appContainer, - renderPage(state.MockAppState(using unsafeWindowOwner, router)) - ) - } - - private def onLoad(f: => Unit): Unit = - documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) - - private def setupAirstream()(using router: Router[Page]): Unit = - AirstreamError.registerUnhandledErrorCallback(err => - router.forcePage( - Page.UnhandledError( - Some(err.getClass.getName), // TODO: Fill only in dev mode - Some(err.getMessage) - ) - ) - ) - - def renderPage(state: AppState)(using - router: Router[Page] - ): HtmlElement = - val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) - .collectSignal[Page.Detail]( - connectors - .DetailPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailParametru]( - connectors - .DetailParametruPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailKriteria]( - connectors - .DetailKriteriaPageConnector(state)(_) - .apply - ) - .collectSignal[Page.UpravDukazKriteria]( - pages.detail.UpravDukaz.Connector(state)(_).apply - ) - .collectStatic(Page.Dashboard)( - connectors.DashboardPageConnector(state.actionBus).apply - ) - .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) - ) - .collect[Page.UnhandledError](pg => - pages.errors - .UnhandledErrorPage( - pages.errors.UnhandledErrorPage - .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), - state.actionBus - ) - ) - .collectStatic(Page.Directory)( - connectors - .DirectoryPageConnector(state.users, state.actionBus) - .apply - ) - div(cls := "h-full", child <-- pageSplitter.$view) - - // Pull in the stylesheet - val css: Css.type = Css diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala deleted file mode 100644 index 3bbc8c0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ /dev/null @@ -1,198 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.* -import org.scalajs.dom -import zio.json.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import scala.scalajs.js -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.Page.Titled - -// enum is not working with Waypoints' SplitRender collectStatic -sealed abstract class Page( - val id: String, - val title: String, - val parent: Option[Page] -) { - val path: Vector[Page] = - parent match - case None => Vector(this) - case Some(p) => p.path :+ this - - val isRoot: Boolean = parent.isEmpty -} - -object Page: - - case class Titled[V](value: V, title: Option[String] = None): - val show: String = title.getOrElse(value.toString) - - case object Directory extends Page("directory", "Adresář", None) - - case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) - - case class Detail(osobniCislo: Titled[OsobniCislo]) - extends Page("user", osobniCislo.show, Some(Directory)) - - object Detail { - def apply(o: UserInfo): Detail = Detail( - Titled(o.personalNumber, Some(o.name)) - ) - } - - case class DetailParametru( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String] - ) extends Page( - "parameter", - parametr.show, - Some(Detail(osobniCislo)) - ) - - object DetailParametru { - def apply(o: UserInfo, p: Parameter): DetailParametru = - DetailParametru( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)) - ) - } - - case class DetailKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "criteria", - kriterium.show, - Some(DetailParametru(osobniCislo, parametr)) - ) - - object DetailKriteria { - def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = - DetailKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class UpravDukazKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "addProof", - "Důkaz", - Some(DetailKriteria(osobniCislo, parametr, kriterium)) - ) - - object UpravDukazKriteria { - def apply( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): UpravDukazKriteria = - UpravDukazKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class NotFound(url: String) extends Page("404", "404", Some(Directory)) - - case class UnhandledError( - errorName: Option[String], - errorMessage: Option[String] - ) extends Page("500", "Unexpected error", Some(Directory)) - -object Routes: - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) - given [V: JsonEncoder]: JsonEncoder[Titled[V]] = - DeriveJsonEncoder.gen[Titled[V]] - given [V: JsonDecoder]: JsonDecoder[Titled[V]] = - DeriveJsonDecoder.gen[Titled[V]] - given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] - given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] - - val base = - js.`import`.meta.env.BASE_URL - .asInstanceOf[String] + "app" - - val homePage: Page = Page.Directory - - given router: Router[Page] = Router[Page]( - routes = List( - Route.static(homePage, root / endOfSegments, basePath = base), - Route.static( - Page.Dashboard, - root / "dashboard" / endOfSegments, - basePath = base - ), - Route[Page.Detail, String]( - encode = _.osobniCislo.value.toString, - decode = osc => Page.Detail(Titled(OsobniCislo(osc))), - root / "osoba" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.DetailParametru, (String, String)]( - encode = p => (p.osobniCislo.value.toString, p.parametr.value), - decode = - p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / endOfSegments, - basePath = base - ), - Route[Page.DetailKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.DetailKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.UpravDukazKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.UpravDukazKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / "edit" / endOfSegments, - basePath = base - ) - ), - serializePage = _.toJson, - deserializePage = _.fromJson[Page] - .fold(s => throw IllegalStateException(s), identity), - getPageTitle = _.title, - routeFallback = url => Page.NotFound(url), - deserializeFallback = _ => Page.Dashboard - )( - $popStateEvent = windowEvents.onPopState, - owner = unsafeWindowOwner - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala deleted file mode 100644 index b24b82b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -sealed trait Action - -case object FetchDirectory extends Action -case class FetchUserDetails(osc: OsobniCislo) extends Action -case class FetchParameters(osc: OsobniCislo) extends Action -case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action -case class FetchParameterCriteria( - osc: OsobniCislo, - paramId: String, - critId: String, - page: (UserInfo, Parameter, ParameterCriteria) => Page -) extends Action -case class FetchAvailableFiles(osc: OsobniCislo) extends Action -case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala deleted file mode 100644 index c637d35..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala +++ /dev/null @@ -1,82 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.{UserProfile, UserInfo, OsobniCislo} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo -import cz.e_bs.cmi.mdr.pdb.UserFunction - -object AppPage: - // TODO: pages by logged in user - val pages: List[Page] = List(Page.Directory, Page.Dashboard) - - import NavigationBar.{Logo, MenuItem} - - val logo = Logo( - "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", - "Workflow" - ) - - // TODO: menu items by user profile - val userMenu = - List( - MenuItem("Your Profile"), - MenuItem("Settings"), - MenuItem("Sign out") - ) - - // TODO: load user profile - val $userProfile = Var( - UserProfile( - "tom", - UserInfo( - OsobniCislo("1031"), - "tom", - "Tom", - "Cook", - None, - None, - Some("tom@example.com"), - Some("+420 222 866 180"), - Some( - UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") - ), - Nil, - None - ) - ) - ) - - val $userInfo = $userProfile.signal.map(_.userInfo) - - type ViewModel = Option[HtmlElement] - def apply( - actionBus: Observer[Action] - )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using - router: Router[Page] - ): HtmlElement = - PageLayout(actionBus)( - $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => - PageLayout.ViewModel( - NavigationBar.ViewModel( - u, - pages.map(p => - NavigationBar.Link( - () => - PageLink( - p, - actionBus - ), - p == cp - ) - ), - userMenu, - logo - ), - c - ) - ), - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala deleted file mode 100644 index bf473c6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ /dev/null @@ -1,143 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden -import fiftyforms.ui.components.tailwind.list.IconText.ViewModel -import fiftyforms.ui.components.tailwind.Icons -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Action - -object Breadcrumbs: - - private def slash = - import svg.{*, given} - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-300", - xmlns := "http://www.w3.org/2000/svg", - fill := "currentColor", - viewBox := "0 0 20 20", - ariaHidden := true, - path( - d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" - ) - ) - - def backIcon = - Icons.solid - .`arrow-narrow-left`() - .amend( - svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" - ) - - object Link: - case class ViewModel( - page: Page, - icon: Option[SvgElement], - extraClasses: String, - text: String, - textClasses: Option[String] = None - ) - - val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" - - def shortHome(p: Page) = ViewModel( - p, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - "Zpět na úvodní stránku", - None - ) - - def fullHome(p: Page) = ViewModel( - p, - Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), - "text-gray-400 hover:text-gray-500", - "Domů", - Some("sr-only") - ) - - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - PageLink - .container($m.map(_.page), actionBus) - .amend( - cls <-- $m.map(_.extraClasses), - child.maybe <-- $m.map(_.icon), - span( - cls <-- $m.map(_.textClasses.getOrElse("")), - child.text <-- $m.map(_.text) - ) - ) - - object FullBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - ol( - role := "list", - cls := "flex items-center space-x-4", - children <-- $m.map(_.path).split(_.id)((_, _, $p) => - li( - div( - cls := "flex items-center", - child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), - Link( - $p.map(p => - if (p.isRoot) then Link.fullHome(p) - else - Link.ViewModel( - p, - None, - s"ml-4 max-w-xs truncate ${Link.baseClasses}", - p.title - ) - ), - actionBus - ) - ) - ) - ) - ) - - object ShortBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - Link( - $m.map { p => - val target = p.parent.getOrElse(p) - if target.isRoot then Link.shortHome(target) - else - Link.ViewModel( - target, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - s"Zpět na ${target.title}" - ) - }, - actionBus - ) - - def apply(actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - nav( - cls := "flex", - aria.label := "Breadcrumb", - div( - cls := "flex sm:hidden", - ShortBreadcrumbs(router.$currentPage, actionBus) - ), - div( - cls := "hidden sm:block", - FullBreadcrumbs(router.$currentPage, actionBus) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala deleted file mode 100644 index 0db2047..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ /dev/null @@ -1,242 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent -import fiftyforms.ui.components.tailwind.Icons -import fiftyforms.ui.components.tailwind.Avatar -import cz.e_bs.cmi.mdr.pdb.UserInfo -import io.laminext.syntax.core.* - -object NavigationBar: - - case class Logo(img: String, name: String) - case class Link(a: () => Anchor, active: Boolean) - case class MenuItem(title: String) - - case class ViewModel( - userInfo: UserInfo, - pages: List[Link], - userMenu: List[MenuItem], - logo: Logo - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - val $userInfo = $m.map(_.userInfo) - val mobileMenuOpen = Var(false) - - val desktopOnly = cls("hidden md:block") - val mobileOnly = cls("md:hidden") - - inline def avatarImage(size: Int = 8) = - Avatar($userInfo.map(_.img)).avatarImage(size) - - def notificationButton = button( - tpe := "button", - cls := List( - "bg-indigo-600", - "focus:outline-none", - "focus:ring-2", - "focus:ring-offset-2", - "focus:ring-offset-indigo-600", - "focus:ring-white", - "hover:text-white", - "p-1", - "rounded-full", - "text-indigo-200" - ), - span(cls := "sr-only", "View notifications"), - Icons.outline.bell() - ) - - def userProfile: HtmlElement = - val menuOpen = Var(false) - - def menuItem(item: MenuItem, idx: Int): HtmlElement = - a( - href := "#", - cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", - role := "menuitem", - tabIndex := -1, - idAttr := s"user-menu-item-$idx", - item.title - ) - - div( - cls := "ml-3 relative", - div( - button( - tpe := "button", - cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - idAttr := "user-menu-button", - aria.expanded <-- menuOpen.signal, - aria.hasPopup := true, - span(cls := "sr-only", "Open user menu"), - child <-- avatarImage(), - onClick.preventDefault.mapTo( - !menuOpen.now() - ) --> menuOpen.writer - ) - ), - /* - * */ - div( - cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", - cls <-- menuOpen.signal.map { o => - if (o) "md:block" else "md:hidden" - }, - role := "menu", - aria.orientation := "vertical", - aria.labelledBy := "user-menu-button", - tabIndex := -1, - // : keyboard navigation - children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) - ) - ) - - def mobileProfile = - def menuItem(item: MenuItem): HtmlElement = - a( - href := "#", - cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", - item.title - ) - - div( - cls := "pt-4 pb-3 border-t border-indigo-700", - div( - cls := "flex items-center px-5", - div( - cls := "flex-shrink-0", - child <-- avatarImage(10) - ), - div( - cls := "ml-3", - div( - cls := "text-base font-medium text-white", - child.text <-- $userInfo.map(_.name) - ), - child.maybe <-- $userInfo.map( - _.email.map(e => - div( - cls := "text-sm font-medium text-indigo-300", - e - ) - ) - ) - ), - notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) - ), - div( - cls := "mt-3 px-2 space-y-1", - children <-- $m.map(_.userMenu.map(menuItem)) - ) - ) - - def pageLink(page: Link): Anchor = - page - .a() - .amend( - cls := "text-white px-3 py-2 rounded-md text-sm font-medium", - cls := Seq( - "bg-indigo-700" -> page.active, - "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active - ), - ariaCurrent := (if page.active then "page" else "false") - ) - - def logoImg: Image = - img( - cls := "h-8 w-8", - src <-- $m.map(_.logo.img), - alt <-- $m.map(_.logo.name) - ) - - def mobileMenuButton = button( - tpe := "button", - cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - aria.controls := "mobile-menu", - aria.expanded <-- mobileMenuOpen.signal, - span(cls := "sr-only", "Open main menu"), - Icons.outline - .menu() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") - ), - Icons.outline - .x() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") - ), - onClick.preventDefault.mapTo( - !mobileMenuOpen.now() - ) --> mobileMenuOpen.writer - ) - - def navBarLeft = - div( - cls := "flex items-center", - div(cls := "flex-shrink-0", logoImg), - div( - desktopOnly, - div( - cls := "ml-10 flex items-baseline space-x-4", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ) - ) - ) - - def navBarRight = - div( - desktopOnly, - div( - cls := "ml-4 flex items-center md:ml-6", - notificationButton, - userProfile - ) - ) - - def navBarMobile = - div( - cls := "-mr-2 flex", - mobileOnly, - mobileMenuButton - ) - - def navBar = - div( - cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex items-center justify-between h-16", - navBarLeft, - navBarRight, - navBarMobile - ) - ) - - def mobileMenu = - div( - mobileOnly, - cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }, - idAttr := "mobile-menu", - div( - cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ), - mobileProfile - ) - - nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala deleted file mode 100644 index 75339df..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object PageHeader: - def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = - header( - cls := "bg-white shadow-sm", - div( - cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", - h1( - cls := "text-lg leading-6 font-semibold text-gray-900", - Breadcrumbs(actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala deleted file mode 100644 index 5096463..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ /dev/null @@ -1,30 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import fiftyforms.ui.components.tailwind.Loading -import io.laminext.syntax.core.* - -object PageLayout: - case class ViewModel( - navigation: NavigationBar.ViewModel, - content: Option[HtmlElement] - ) - def apply(actionBus: Observer[Action])( - $m: Signal[ViewModel], - mods: Modifier[HtmlElement]* - )(using router: Router[Page]): HtmlElement = - val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) - div( - cls := "h-full flex flex-col", - NavigationBar($m.map(_.navigation)), - child.maybe <-- router.$currentPage.map(_.isRoot) - .switch(None, Some(PageHeader(actionBus))), - main( - cls := "flex-grow-1 overflow-y-auto", - mods, - child <-- $maybeContent.map(_.getOrElse(Loading)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala deleted file mode 100644 index 62d820f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.LinkSupport.* -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo - -object PageLink: - type ViewModel = Page - def apply($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = - a(mods($m, actions), child.text <-- $m.map(_.title)) - - def apply(m: ViewModel, actions: Observer[Action])(using - router: Router[Page] - ): Anchor = apply(Val(m), actions) - - def container($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = a(mods($m, actions)) - - def container(m: ViewModel, actions: Observer[Action])(using - Router[Page] - ): Anchor = container(Val(m), actions) - - private def mods($m: Signal[Page], actions: Observer[Action])(using - router: Router[Page] - ): Modifier[Anchor] = - Seq( - href <-- $m.map(router.absoluteUrlForPage).recover { case _ => - Some("invalid url") - }, - composeEvents(onClick.noKeyMod.preventDefault)( - _.sample($m) - .map(NavigateTo.apply) - ) --> actions - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala deleted file mode 100644 index 87f825a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.pages.dashboard.DashboardPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -class DashboardPageConnector(actionBus: Observer[Action])(using - router: Router[Page] -): - def apply: HtmlElement = - AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala deleted file mode 100644 index e9b0f71..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala +++ /dev/null @@ -1,76 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailKriteriaPage -import pages.detail.components.DukazyKriteria -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -object DetailKriteriaPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailKriteriaPageConnector( - state: DetailKriteriaPageConnector.AppState -)( - $page: Signal[Page.DetailKriteria] -)(using Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => - (p.osobniCislo.value, p.parametr.value, p.kriterium.value) - )((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map( - FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) - ) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, s, $s) => - DetailKriteriaPage($s)(state.actionBus.contramap { - case DukazyKriteria.Add => - NavigateTo( - Page.UpravDukazKriteria( - Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), - Page.Titled(s.parametr.id, Some(s.parametr.nazev)), - Page.Titled(s.kriterium.id, Some(s.kriterium.id)) - ) - ) - }) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): DetailKriteriaPage.ViewModel = - DetailKriteriaPage.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala deleted file mode 100644 index 4d8024f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ /dev/null @@ -1,57 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import pages.detail.DetailPage -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import fiftyforms.ui.components.tailwind.Color - -object DetailPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailPageConnector(state: DetailPageConnector.AppState)( - $page: Signal[Page.Detail] -)(using router: Router[Page]): - val $oscChangeSignal = $page.splitOne(_.osobniCislo.value)((osc, _, _) => osc) - val $pageChangeSignal = - $oscChangeSignal.flatMap(osc => - EventStream.fromSeq(Seq(FetchUserDetails(osc), FetchParameters(osc))) - ) - // TODO: filter the value based on the current osc - // OSC change will fetch new data, but still - // - we need to be sure that what we got is really what we ought to display - // - we want to display stale data accordingly (at least with loading indicator) - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - def apply: HtmlElement = - AppPage(state.actionBus)( - $data.combineWithFn($params)(_ zip _) - .map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: List[Parameter] - ): DetailPage.ViewModel = - DetailPage.ViewModel( - o.toDetailOsoby, - p.map( - _.toParametr(param => - PageLink.container(Page.DetailParametru(o, param), state.actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala deleted file mode 100644 index fdaa7f8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailParametruPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink - -object DetailParametruPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailParametruPageConnector( - state: DetailParametruPageConnector.AppState -)( - $page: Signal[Page.DetailParametru] -)(using router: Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => (p.osobniCislo.value, p.parametr.value))((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map(FetchParameter(_, _)) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - } yield (da, pb) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailParametruPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter - ): DetailParametruPage.ViewModel = - DetailParametruPage.ViewModel( - o.toDetailOsoby, - p.toParametr(p => - PageLink.container(Page.DetailParametru(o, p), state.actionBus) - ), - p.criteria.map( - _.toKriterium { c => - PageLink.container( - Page.DetailKriteria(o, p, c), - state.actionBus - ) - } - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala deleted file mode 100644 index 049aa22..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -case class DirectoryPageConnector( - $input: EventStream[List[UserInfo]], - actionBus: Observer[Action] -)(using router: Router[Page]): - val $data = $input.startWithNone - val $actionSignal = EventStream.fromValue(FetchDirectory) - - def apply: HtmlElement = - AppPage(actionBus)( - $data.split(_ => ())((_, _, s) => - pages.directory.DirectoryPage( - s.map( - _.map( - _.toUserRow(u => - PageLink.container( - Page.Detail(Page.Titled(u.personalNumber)), - actionBus - ) - ) - ) - ) - ) - ), - $actionSignal --> actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala deleted file mode 100644 index 88e7450..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ /dev/null @@ -1,68 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii -import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria -import fiftyforms.ui.components.tailwind.Color - -extension (o: UserInfo) - def toDetailOsoby: DetailOsoby.ViewModel = - DetailOsoby.ViewModel( - o.personalNumber, - o.name, - o.email, - o.phone, - o.img, - o.mainFunction.map(f => - DetailOsoby.Funkce.ViewModel(f.name, f.dept, f.ou) - ), - o.userContracts.headOption.map(c => - DetailOsoby.PracovniPomer.ViewModel(c.rel, c.startDate, c.endDate) - ) - ) - -extension (param: Parameter) - def toParametr(container: Parameter => Anchor): DetailParametru.ViewModel = - DetailParametru.ViewModel( - id = param.id, - nazev = param.name, - popis = param.description, - status = "Nesplněno", - statusColor = Color.red, - a = container(param) - ) - -extension (crit: ParameterCriteria) - def toKriterium( - container: ParameterCriteria => Anchor - ): DetailKriteria.ViewModel = - DetailKriteria.ViewModel( - nazev = crit.criteriumText, - kapitola = crit.chapterId, - bod = crit.itemId, - status = "Nesplněno", - statusColor = Color.red, - splneno = false, - dukazy = Nil, - container = container(crit) - ) - -extension (user: UserInfo) - def toUserRow( - container: UserInfo => HtmlElement = (_: UserInfo) => div() - ): UserRow.ViewModel = - UserRow.ViewModel( - osobniCislo = user.personalNumber.toString, - celeJmeno = user.name, - prijmeni = user.surname, - hlavniFunkce = user.mainFunction.map(_.name), - img = user.img, - container = () => container(user) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala deleted file mode 100644 index 7d5b2c1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.dashboard - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.Page - -object DashboardPage: - - def render: HtmlElement = - div("Dashboard page") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala deleted file mode 100644 index 9ef0698..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala +++ /dev/null @@ -1,31 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailKriteriaPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply($m: Signal[ViewModel])( - action: Observer[DukazyKriteria.Action] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - DukazyKriteria($m.map(_.kriterium.dukazy))(action) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala deleted file mode 100644 index 3b316e1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object DetailPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametry: SeznamParametru.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby($m.map(_.osoba)), - SeznamParametru($m.map(_.parametry)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala deleted file mode 100644 index 17224cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala +++ /dev/null @@ -1,24 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailParametruPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriteria: SeznamKriterii.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby.header($m.map(_.osoba)), - DetailParametru($m.map(_.parametr)), - SeznamKriterii($m.map(_.kriteria)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala deleted file mode 100644 index dc912fa..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ /dev/null @@ -1,107 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb -package app -package pages.detail - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.UpravDukazForm -import fiftyforms.services.files.File - -object UpravDukaz: - - type ThisPage = Page.UpravDukazKriteria - type PageKey = (OsobniCislo, String, String) - - trait State { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def availableFiles: EventStream[List[File]] - def actionBus: Observer[Action] - } - - def keyOfPage(page: ThisPage): PageKey = - (page.osobniCislo.value, page.parametr.value, page.kriterium.value) - - def onChangeAction(key: PageKey): Action = - FetchParameterCriteria( - key._1, - key._2, - key._3, - Page.UpravDukazKriteria(_, _, _) - ) - - class Connector(state: State)($page: Signal[ThisPage])(using Router[Page]): - val $paramChangeSignal = $page.splitOne(keyOfPage)((x, _, _) => x) - val $pageChangeSignal = $paramChangeSignal.map(onChangeAction) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.split(_ => ())((_, s, $s) => - PageComponent( - $s.map(buildModel), - state.availableFiles, - state.actionBus.contramap { - case UpravDukazForm.Cancelled => - NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) - case UpravDukazForm.AvailableFilesRequested => - FetchAvailableFiles(s._1.personalNumber) - } - ) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): PageComponent.ViewModel = - import connectors.* - PageComponent.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) - - object PageComponent: - import components.* - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply( - $m: Signal[ViewModel], - availableFilesStream: EventStream[List[File]], - events: Observer[UpravDukazForm.Event] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - UpravDukazForm(availableFilesStream)(events) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala deleted file mode 100644 index ca311b0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.Icons -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Color - -object DetailKriteria: - case class ViewModel( - nazev: String, - kapitola: String, - bod: String, - status: String, - statusColor: Color, - splneno: Boolean, - dukazy: List[DukazKriteria.ViewModel], - container: HtmlElement = div() - ) { - val id = s"${kapitola}${bod}" - } - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h3( - cls := "text-xl leading-6 font-bold text-gray-900", - child.text <-- $m.map(_.nazev) - ), - h3( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - "Kapitola ", - child.text <-- $m.map(_.kapitola), - " bod ", - child <-- $m.map(_.bod) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala deleted file mode 100644 index ae90290..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala +++ /dev/null @@ -1,100 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Avatar - -object DetailOsoby: - - object Funkce: - case class ViewModel( - nazev: String, - stredisko: String, - voj: String - ) - - def render($m: Signal[ViewModel]): HtmlElement = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.nazev), - span( - cls := "hidden md:inline", - " @ ", - child.text <-- $m.map(_.stredisko), - ", ", - child.text <-- $m.map(_.voj) - ) - ) - - object PracovniPomer: - case class ViewModel( - druh: String, - pocatek: LocalDate, - konec: Option[LocalDate] - ) - - def render($m: Signal[ViewModel]): HtmlElement = - import fiftyforms.ui.components.tailwind.CustomAttrs.datetime - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.druh), - " od ", - time( - datetime <-- $m.map(_.pocatek.toString), - child.text <-- $m.map(_.pocatek.toString) - ) - ) - - case class ViewModel( - osobniCislo: OsobniCislo, - jmeno: String, - email: Option[String], - telefon: Option[String], - img: Option[String], - hlavniFunkce: Option[Funkce.ViewModel], - pracovniPomer: Option[PracovniPomer.ViewModel] - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-4", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(16) - ), - div( - h1( - cls := "text-2xl font-bold text-gray-900", - child.text <-- $m.map(_.jmeno) - ), - child.maybe <-- $m.map(_.hlavniFunkce).split(_ => ())((_, _, d) => - Funkce.render(d) - ), - child.maybe <-- $m.map(_.pracovniPomer).split(_ => ())((_, _, d) => - PracovniPomer.render(d) - ) - ) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-5", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(8) - ), - div( - h1( - cls := "text-lg font-medium text-gray-800", - child.text <-- $m.map(_.jmeno) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala deleted file mode 100644 index e11cb62..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color - -object DetailParametru: - case class ViewModel( - id: String, - nazev: String, - popis: String, - status: String, - statusColor: Color, - a: Anchor - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h2( - cls := "text-2xl leading-6 font-medium text-gray-900", - child.text <-- $m.map(_.nazev) - ), - p( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - child.text <-- $m.map(_.popis) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - h2( - cls := "text-lg leading-6 font-medium text-gray-600", - child.text <-- $m.map(_.nazev) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala deleted file mode 100644 index a00ede8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala +++ /dev/null @@ -1,177 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.CustomAttrs - -object DukazKriteria: - case class Osoba(osobniCislo: String, jmeno: String) - case class Udalost(osoba: Osoba, datum: LocalDate) - case class Dokument( - url: String, - nazev: String, - pridal: Udalost, - odebral: Option[Udalost] - ) - case class ViewModel( - dokumenty: List[Dokument], - autorizoval: Option[Udalost], - platiDo: Option[LocalDate], - poznámka: Option[String] - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-lg", - div( - cls := "px-4 py-5 sm:px-6", - h3( - cls := "text-lg leading-6 font-medium text-gray-900", - """Applicant Information""" - ), - p( - cls := "mt-1 max-w-2xl text-sm text-gray-500", - """Personal details and application.""" - ) - ), - div( - cls := "border-t border-gray-200 px-4 py-5 sm:px-6", - dl( - cls := "grid grid-cols-1 gap-x-4 gap-y-8 sm:grid-cols-2", - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Full name""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Margot Foster""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Application for""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Backend Developer""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Email address""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """margotfoster@example.com""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Salary expectation""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """$120,000""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """About""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Fugiat ipsum ipsum deserunt culpa aute sint do nostrud anim incididunt cillum culpa consequat. Excepteur qui ipsum aliquip consequat sint. Sit id mollit nulla mollit nostrud in ea officia proident. Irure nostrud pariatur mollit ad adipisicing reprehenderit deserunt qui eu.""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """Attachments""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - ul( - role := "list", - cls := "border border-gray-200 rounded-md divide-y divide-gray-200", - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """resume_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ), - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """coverletter_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala deleted file mode 100644 index 22f134f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala +++ /dev/null @@ -1,35 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object DukazyKriteria: - sealed trait Action - case object Add extends Action - - type ViewModel = List[DukazKriteria.ViewModel] - - def apply($m: Signal[ViewModel])(actions: Observer[Action]): HtmlElement = - div( - child.maybe <-- $m.map(m => - if (m.isEmpty) then Some(prazdnyDukaz(actions)) - else None - ), - children <-- $m.map(_.zipWithIndex).split(_._2)((_, _, $s) => - DukazKriteria($s.map(_._1)) - ) - ) - - private def prazdnyDukaz(actions: Observer[Action]): HtmlElement = - button( - tpe := "button", - onClick.preventDefault.mapTo(Add) --> actions, - cls := "relative block w-full border-2 border-gray-300 border-dashed rounded-lg p-12 text-center hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - Icons.outline - .`document-add`(12) - .amend(svg.cls := "mx-auto text-gray-400"), - span( - cls := "mt-2 block text-sm font-medium text-gray-900", - "Přidat důkaz" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala deleted file mode 100644 index f628697..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.list.StackedList -import fiftyforms.ui.components.tailwind.list.ListRow -import fiftyforms.ui.components.tailwind.list.RowTag -import fiftyforms.ui.components.tailwind.list.RowNext -import java.time.LocalDate - -object SeznamKriterii: - type ViewModel = List[DetailKriteria.ViewModel] - - private val kritList = new StackedList[DetailKriteria.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - kritList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = - p(cls := "text-sm text-gray-500", s"${i.kapitola}${i.bod}"), - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.container - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala deleted file mode 100644 index 2bfe4bc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.list.{ - StackedList, - ListRow, - RowTag, - PropList, - IconText, - RowNext -} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.LinkSupport.* - -object SeznamParametru: - type ViewModel = List[DetailParametru.ViewModel] - - private val parametrList = new StackedList[DetailParametru.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - parametrList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = emptyNode, - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.a - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala deleted file mode 100644 index f048e9d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ /dev/null @@ -1,101 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.form.* -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement -import fiftyforms.services.files.components.tailwind.FilePicker -import cz.e_bs.cmi.mdr.pdb.frontend.AutorizujDukaz -import cz.e_bs.cmi.mdr.pdb.frontend.DocumentRef -import fiftyforms.services.files.File - -object UpravDukazForm: - sealed trait Event - case object Cancelled extends Event - case object AvailableFilesRequested extends Event - def apply(availableFilesStream: EventStream[List[File]])( - updates: Observer[Event] - ): HtmlElement = - val (filesStream, filesObserver) = - EventStream.withObserver[FilePicker.Event] - val files = Var[List[File]](Nil) - def submitButtons: HtmlElement = - div( - cls := "pt-5", - div( - cls := "flex justify-end", - button( - tpe := "button", - cls := "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zrušit", - onClick.mapTo(Cancelled) --> updates - ), - button( - tpe := "submit", - cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - disabled <-- files.signal.map(_.isEmpty), - "Autorizovat" - ) - ) - ) - - div( - filesStream.collect { case FilePicker.SelectionUpdated(files) => - files.to(List) - } --> files.writer, - filesStream.collect { case FilePicker.AvailableFilesRequested => - AvailableFilesRequested - } --> updates, - cls := "bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6", - Form( - Form.Body( - Form.Section( - FormHeader( - FormHeader.ViewModel( - "Doložení kritéria", - "Sestavte doklady poskytující důkaz kritéria a potvrďte odesláním formuláře. Případné limitace či jiné relevantní údaje vepište do pole Komentář." - ) - ), - FormFields( - FormRow( - "dokumenty", - "Dokumenty", - FilePicker(files.signal, availableFilesStream)(filesObserver) - .amend(idAttr := "dokumenty", cls("max-w-lg")) - ).toHtml, - FormRow( - "platnost", - "Platnost", - input( - idAttr := "platnost", - name := "platnost", - tpe := "date", - autoComplete := "date", - cls := "block max-w-lg w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" - ) - ).toHtml, - FormRow( - "komentar", - "Komentář", - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - textArea( - idAttr := "komentar", - name := "about", - rows := 3, - cls := "max-w-lg shadow-sm block w-full focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border border-gray-300 rounded-md" - ), - p( - cls := "mt-2 text-sm text-gray-500", - "Doplňte prosím potřebné informace související s doložením kritéria, včetně případných limitací." - ) - ) - ).toHtml - ) - ) - ), - submitButtons - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala deleted file mode 100644 index 41033f6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -import components._ - -object DirectoryPage: - - type ViewModel = List[UserRow.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - val (actionsStream, actionObserver) = - EventStream.withObserver[SearchForm.Action] - val $filter = actionsStream - .collect { - case SearchForm.ClearFilter => None - case SearchForm.SetFilter(t) => Some(t) - } - .startWith(None) - val byLetter = for { - d <- $m - f <- $filter - } yield for { - (letter, users) <- d - .filter { user => - f match - case None => true - case Some(t) => user.search.contains(t) - } - .groupBy(_.prijmeni.head) - .to(List) - .sortBy(_._1) - } yield (letter.toString, users.sortBy(_.prijmeni)) - - div( - cls := "h-full max-w-7xl mx-auto order-first flex flex-col flex-shrink-0", - SearchForm(actionObserver), - Directory(byLetter) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala deleted file mode 100644 index 73f915d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -object Directory: - - object Header: - type ViewModel = String - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500", - h3(child.text <-- $m) - ) - - object UserList: - type ViewModel = List[UserRow.ViewModel] - def apply($m: Signal[ViewModel]): HtmlElement = - ul( - role := "list", - cls := "relative z-0 divide-y divide-gray-200", - children <-- $m.split(_.osobniCislo)((_, _, s) => UserRow(s)) - ) - - type ViewModel = List[(String, List[UserRow.ViewModel])] - def apply($m: Signal[ViewModel]): HtmlElement = - nav( - cls := "flex-1 min-h-0 overflow-y-auto", - aria.label := "Directory", - children <-- $m.split(_._1)((_, _, s) => - div( - cls := "relative", - Header(s.map(_._1)), - UserList(s.map(_._2)) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala deleted file mode 100644 index e182910..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala +++ /dev/null @@ -1,54 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object SearchForm: - sealed trait Action - case object SubmitFilter extends Action - sealed trait FilterAction extends Action - case object ClearFilter extends FilterAction - case class SetFilter(value: String) extends FilterAction - - def apply(actions: Observer[Action]): HtmlElement = - div( - cls := "px-6 pt-4 pb-4", - form( - cls := "flex space-x-4", - onSubmit.mapTo(SubmitFilter) --> actions, - div( - cls := "flex-1 min-w-0", - label( - forId := "search", - cls := "sr-only", - "Hledat" - ), - div( - cls := "relative rounded-md shadow-sm", - div( - cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", - Icons.solid.search().amend(svg.cls := "text-gray-400") - ), - input( - tpe := "search", - name := "search", - idAttr := "search", - cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", - placeholder := "Hledat", - composeEvents(onInput.mapToValue.setAsValue.map(SetFilter(_)))( - _.throttle(500) - ) --> actions - ) - ) - ), - button( - tpe := "submit", - cls := "inline-flex justify-center px-3.5 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500", - Icons.solid.filter().amend(svg.cls := "text-gray-400"), - span( - cls := "sr-only", - "Hledat" - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala deleted file mode 100644 index a3b00af..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala +++ /dev/null @@ -1,103 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import scala.scalajs.js.annotation.JSExportTopLevel -import scala.scalajs.js.annotation.JSExport -import scala.scalajs.js.annotation.JSImport -import scala.scalajs.js -import org.scalajs.dom -import com.raquo.laminar.api.L.{*, given} -import scala.scalajs.js.Date -import com.raquo.waypoint.Router -import com.raquo.waypoint.SplitRender -import cz.e_bs.cmi.mdr.pdb.app.services.DataFetcher -import scala.scalajs.js.JSON -import zio.json._ -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.state.AppState - -@js.native -@JSImport("stylesheets/main.css", JSImport.Namespace) -object Css extends js.Any - -@js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native -@JSImport("params/pdb-params.json", JSImport.Default) -object pdbParams extends js.Object - -@JSExportTopLevel("app") -object Main: - - @JSExport - def main(args: Array[String]): Unit = - import Routes.given - onLoad { - setupAirstream() - val appContainer = dom.document.querySelector("#app") - val _ = - render( - appContainer, - renderPage(state.MockAppState(using unsafeWindowOwner, router)) - ) - } - - private def onLoad(f: => Unit): Unit = - documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) - - private def setupAirstream()(using router: Router[Page]): Unit = - AirstreamError.registerUnhandledErrorCallback(err => - router.forcePage( - Page.UnhandledError( - Some(err.getClass.getName), // TODO: Fill only in dev mode - Some(err.getMessage) - ) - ) - ) - - def renderPage(state: AppState)(using - router: Router[Page] - ): HtmlElement = - val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) - .collectSignal[Page.Detail]( - connectors - .DetailPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailParametru]( - connectors - .DetailParametruPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailKriteria]( - connectors - .DetailKriteriaPageConnector(state)(_) - .apply - ) - .collectSignal[Page.UpravDukazKriteria]( - pages.detail.UpravDukaz.Connector(state)(_).apply - ) - .collectStatic(Page.Dashboard)( - connectors.DashboardPageConnector(state.actionBus).apply - ) - .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) - ) - .collect[Page.UnhandledError](pg => - pages.errors - .UnhandledErrorPage( - pages.errors.UnhandledErrorPage - .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), - state.actionBus - ) - ) - .collectStatic(Page.Directory)( - connectors - .DirectoryPageConnector(state.users, state.actionBus) - .apply - ) - div(cls := "h-full", child <-- pageSplitter.$view) - - // Pull in the stylesheet - val css: Css.type = Css diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala deleted file mode 100644 index 3bbc8c0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ /dev/null @@ -1,198 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.* -import org.scalajs.dom -import zio.json.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import scala.scalajs.js -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.Page.Titled - -// enum is not working with Waypoints' SplitRender collectStatic -sealed abstract class Page( - val id: String, - val title: String, - val parent: Option[Page] -) { - val path: Vector[Page] = - parent match - case None => Vector(this) - case Some(p) => p.path :+ this - - val isRoot: Boolean = parent.isEmpty -} - -object Page: - - case class Titled[V](value: V, title: Option[String] = None): - val show: String = title.getOrElse(value.toString) - - case object Directory extends Page("directory", "Adresář", None) - - case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) - - case class Detail(osobniCislo: Titled[OsobniCislo]) - extends Page("user", osobniCislo.show, Some(Directory)) - - object Detail { - def apply(o: UserInfo): Detail = Detail( - Titled(o.personalNumber, Some(o.name)) - ) - } - - case class DetailParametru( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String] - ) extends Page( - "parameter", - parametr.show, - Some(Detail(osobniCislo)) - ) - - object DetailParametru { - def apply(o: UserInfo, p: Parameter): DetailParametru = - DetailParametru( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)) - ) - } - - case class DetailKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "criteria", - kriterium.show, - Some(DetailParametru(osobniCislo, parametr)) - ) - - object DetailKriteria { - def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = - DetailKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class UpravDukazKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "addProof", - "Důkaz", - Some(DetailKriteria(osobniCislo, parametr, kriterium)) - ) - - object UpravDukazKriteria { - def apply( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): UpravDukazKriteria = - UpravDukazKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class NotFound(url: String) extends Page("404", "404", Some(Directory)) - - case class UnhandledError( - errorName: Option[String], - errorMessage: Option[String] - ) extends Page("500", "Unexpected error", Some(Directory)) - -object Routes: - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) - given [V: JsonEncoder]: JsonEncoder[Titled[V]] = - DeriveJsonEncoder.gen[Titled[V]] - given [V: JsonDecoder]: JsonDecoder[Titled[V]] = - DeriveJsonDecoder.gen[Titled[V]] - given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] - given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] - - val base = - js.`import`.meta.env.BASE_URL - .asInstanceOf[String] + "app" - - val homePage: Page = Page.Directory - - given router: Router[Page] = Router[Page]( - routes = List( - Route.static(homePage, root / endOfSegments, basePath = base), - Route.static( - Page.Dashboard, - root / "dashboard" / endOfSegments, - basePath = base - ), - Route[Page.Detail, String]( - encode = _.osobniCislo.value.toString, - decode = osc => Page.Detail(Titled(OsobniCislo(osc))), - root / "osoba" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.DetailParametru, (String, String)]( - encode = p => (p.osobniCislo.value.toString, p.parametr.value), - decode = - p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / endOfSegments, - basePath = base - ), - Route[Page.DetailKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.DetailKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.UpravDukazKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.UpravDukazKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / "edit" / endOfSegments, - basePath = base - ) - ), - serializePage = _.toJson, - deserializePage = _.fromJson[Page] - .fold(s => throw IllegalStateException(s), identity), - getPageTitle = _.title, - routeFallback = url => Page.NotFound(url), - deserializeFallback = _ => Page.Dashboard - )( - $popStateEvent = windowEvents.onPopState, - owner = unsafeWindowOwner - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala deleted file mode 100644 index b24b82b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -sealed trait Action - -case object FetchDirectory extends Action -case class FetchUserDetails(osc: OsobniCislo) extends Action -case class FetchParameters(osc: OsobniCislo) extends Action -case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action -case class FetchParameterCriteria( - osc: OsobniCislo, - paramId: String, - critId: String, - page: (UserInfo, Parameter, ParameterCriteria) => Page -) extends Action -case class FetchAvailableFiles(osc: OsobniCislo) extends Action -case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala deleted file mode 100644 index c637d35..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala +++ /dev/null @@ -1,82 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.{UserProfile, UserInfo, OsobniCislo} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo -import cz.e_bs.cmi.mdr.pdb.UserFunction - -object AppPage: - // TODO: pages by logged in user - val pages: List[Page] = List(Page.Directory, Page.Dashboard) - - import NavigationBar.{Logo, MenuItem} - - val logo = Logo( - "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", - "Workflow" - ) - - // TODO: menu items by user profile - val userMenu = - List( - MenuItem("Your Profile"), - MenuItem("Settings"), - MenuItem("Sign out") - ) - - // TODO: load user profile - val $userProfile = Var( - UserProfile( - "tom", - UserInfo( - OsobniCislo("1031"), - "tom", - "Tom", - "Cook", - None, - None, - Some("tom@example.com"), - Some("+420 222 866 180"), - Some( - UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") - ), - Nil, - None - ) - ) - ) - - val $userInfo = $userProfile.signal.map(_.userInfo) - - type ViewModel = Option[HtmlElement] - def apply( - actionBus: Observer[Action] - )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using - router: Router[Page] - ): HtmlElement = - PageLayout(actionBus)( - $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => - PageLayout.ViewModel( - NavigationBar.ViewModel( - u, - pages.map(p => - NavigationBar.Link( - () => - PageLink( - p, - actionBus - ), - p == cp - ) - ), - userMenu, - logo - ), - c - ) - ), - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala deleted file mode 100644 index bf473c6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ /dev/null @@ -1,143 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden -import fiftyforms.ui.components.tailwind.list.IconText.ViewModel -import fiftyforms.ui.components.tailwind.Icons -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Action - -object Breadcrumbs: - - private def slash = - import svg.{*, given} - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-300", - xmlns := "http://www.w3.org/2000/svg", - fill := "currentColor", - viewBox := "0 0 20 20", - ariaHidden := true, - path( - d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" - ) - ) - - def backIcon = - Icons.solid - .`arrow-narrow-left`() - .amend( - svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" - ) - - object Link: - case class ViewModel( - page: Page, - icon: Option[SvgElement], - extraClasses: String, - text: String, - textClasses: Option[String] = None - ) - - val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" - - def shortHome(p: Page) = ViewModel( - p, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - "Zpět na úvodní stránku", - None - ) - - def fullHome(p: Page) = ViewModel( - p, - Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), - "text-gray-400 hover:text-gray-500", - "Domů", - Some("sr-only") - ) - - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - PageLink - .container($m.map(_.page), actionBus) - .amend( - cls <-- $m.map(_.extraClasses), - child.maybe <-- $m.map(_.icon), - span( - cls <-- $m.map(_.textClasses.getOrElse("")), - child.text <-- $m.map(_.text) - ) - ) - - object FullBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - ol( - role := "list", - cls := "flex items-center space-x-4", - children <-- $m.map(_.path).split(_.id)((_, _, $p) => - li( - div( - cls := "flex items-center", - child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), - Link( - $p.map(p => - if (p.isRoot) then Link.fullHome(p) - else - Link.ViewModel( - p, - None, - s"ml-4 max-w-xs truncate ${Link.baseClasses}", - p.title - ) - ), - actionBus - ) - ) - ) - ) - ) - - object ShortBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - Link( - $m.map { p => - val target = p.parent.getOrElse(p) - if target.isRoot then Link.shortHome(target) - else - Link.ViewModel( - target, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - s"Zpět na ${target.title}" - ) - }, - actionBus - ) - - def apply(actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - nav( - cls := "flex", - aria.label := "Breadcrumb", - div( - cls := "flex sm:hidden", - ShortBreadcrumbs(router.$currentPage, actionBus) - ), - div( - cls := "hidden sm:block", - FullBreadcrumbs(router.$currentPage, actionBus) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala deleted file mode 100644 index 0db2047..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ /dev/null @@ -1,242 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent -import fiftyforms.ui.components.tailwind.Icons -import fiftyforms.ui.components.tailwind.Avatar -import cz.e_bs.cmi.mdr.pdb.UserInfo -import io.laminext.syntax.core.* - -object NavigationBar: - - case class Logo(img: String, name: String) - case class Link(a: () => Anchor, active: Boolean) - case class MenuItem(title: String) - - case class ViewModel( - userInfo: UserInfo, - pages: List[Link], - userMenu: List[MenuItem], - logo: Logo - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - val $userInfo = $m.map(_.userInfo) - val mobileMenuOpen = Var(false) - - val desktopOnly = cls("hidden md:block") - val mobileOnly = cls("md:hidden") - - inline def avatarImage(size: Int = 8) = - Avatar($userInfo.map(_.img)).avatarImage(size) - - def notificationButton = button( - tpe := "button", - cls := List( - "bg-indigo-600", - "focus:outline-none", - "focus:ring-2", - "focus:ring-offset-2", - "focus:ring-offset-indigo-600", - "focus:ring-white", - "hover:text-white", - "p-1", - "rounded-full", - "text-indigo-200" - ), - span(cls := "sr-only", "View notifications"), - Icons.outline.bell() - ) - - def userProfile: HtmlElement = - val menuOpen = Var(false) - - def menuItem(item: MenuItem, idx: Int): HtmlElement = - a( - href := "#", - cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", - role := "menuitem", - tabIndex := -1, - idAttr := s"user-menu-item-$idx", - item.title - ) - - div( - cls := "ml-3 relative", - div( - button( - tpe := "button", - cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - idAttr := "user-menu-button", - aria.expanded <-- menuOpen.signal, - aria.hasPopup := true, - span(cls := "sr-only", "Open user menu"), - child <-- avatarImage(), - onClick.preventDefault.mapTo( - !menuOpen.now() - ) --> menuOpen.writer - ) - ), - /* - * */ - div( - cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", - cls <-- menuOpen.signal.map { o => - if (o) "md:block" else "md:hidden" - }, - role := "menu", - aria.orientation := "vertical", - aria.labelledBy := "user-menu-button", - tabIndex := -1, - // : keyboard navigation - children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) - ) - ) - - def mobileProfile = - def menuItem(item: MenuItem): HtmlElement = - a( - href := "#", - cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", - item.title - ) - - div( - cls := "pt-4 pb-3 border-t border-indigo-700", - div( - cls := "flex items-center px-5", - div( - cls := "flex-shrink-0", - child <-- avatarImage(10) - ), - div( - cls := "ml-3", - div( - cls := "text-base font-medium text-white", - child.text <-- $userInfo.map(_.name) - ), - child.maybe <-- $userInfo.map( - _.email.map(e => - div( - cls := "text-sm font-medium text-indigo-300", - e - ) - ) - ) - ), - notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) - ), - div( - cls := "mt-3 px-2 space-y-1", - children <-- $m.map(_.userMenu.map(menuItem)) - ) - ) - - def pageLink(page: Link): Anchor = - page - .a() - .amend( - cls := "text-white px-3 py-2 rounded-md text-sm font-medium", - cls := Seq( - "bg-indigo-700" -> page.active, - "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active - ), - ariaCurrent := (if page.active then "page" else "false") - ) - - def logoImg: Image = - img( - cls := "h-8 w-8", - src <-- $m.map(_.logo.img), - alt <-- $m.map(_.logo.name) - ) - - def mobileMenuButton = button( - tpe := "button", - cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - aria.controls := "mobile-menu", - aria.expanded <-- mobileMenuOpen.signal, - span(cls := "sr-only", "Open main menu"), - Icons.outline - .menu() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") - ), - Icons.outline - .x() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") - ), - onClick.preventDefault.mapTo( - !mobileMenuOpen.now() - ) --> mobileMenuOpen.writer - ) - - def navBarLeft = - div( - cls := "flex items-center", - div(cls := "flex-shrink-0", logoImg), - div( - desktopOnly, - div( - cls := "ml-10 flex items-baseline space-x-4", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ) - ) - ) - - def navBarRight = - div( - desktopOnly, - div( - cls := "ml-4 flex items-center md:ml-6", - notificationButton, - userProfile - ) - ) - - def navBarMobile = - div( - cls := "-mr-2 flex", - mobileOnly, - mobileMenuButton - ) - - def navBar = - div( - cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex items-center justify-between h-16", - navBarLeft, - navBarRight, - navBarMobile - ) - ) - - def mobileMenu = - div( - mobileOnly, - cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }, - idAttr := "mobile-menu", - div( - cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ), - mobileProfile - ) - - nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala deleted file mode 100644 index 75339df..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object PageHeader: - def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = - header( - cls := "bg-white shadow-sm", - div( - cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", - h1( - cls := "text-lg leading-6 font-semibold text-gray-900", - Breadcrumbs(actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala deleted file mode 100644 index 5096463..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ /dev/null @@ -1,30 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import fiftyforms.ui.components.tailwind.Loading -import io.laminext.syntax.core.* - -object PageLayout: - case class ViewModel( - navigation: NavigationBar.ViewModel, - content: Option[HtmlElement] - ) - def apply(actionBus: Observer[Action])( - $m: Signal[ViewModel], - mods: Modifier[HtmlElement]* - )(using router: Router[Page]): HtmlElement = - val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) - div( - cls := "h-full flex flex-col", - NavigationBar($m.map(_.navigation)), - child.maybe <-- router.$currentPage.map(_.isRoot) - .switch(None, Some(PageHeader(actionBus))), - main( - cls := "flex-grow-1 overflow-y-auto", - mods, - child <-- $maybeContent.map(_.getOrElse(Loading)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala deleted file mode 100644 index 62d820f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.LinkSupport.* -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo - -object PageLink: - type ViewModel = Page - def apply($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = - a(mods($m, actions), child.text <-- $m.map(_.title)) - - def apply(m: ViewModel, actions: Observer[Action])(using - router: Router[Page] - ): Anchor = apply(Val(m), actions) - - def container($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = a(mods($m, actions)) - - def container(m: ViewModel, actions: Observer[Action])(using - Router[Page] - ): Anchor = container(Val(m), actions) - - private def mods($m: Signal[Page], actions: Observer[Action])(using - router: Router[Page] - ): Modifier[Anchor] = - Seq( - href <-- $m.map(router.absoluteUrlForPage).recover { case _ => - Some("invalid url") - }, - composeEvents(onClick.noKeyMod.preventDefault)( - _.sample($m) - .map(NavigateTo.apply) - ) --> actions - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala deleted file mode 100644 index 87f825a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.pages.dashboard.DashboardPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -class DashboardPageConnector(actionBus: Observer[Action])(using - router: Router[Page] -): - def apply: HtmlElement = - AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala deleted file mode 100644 index e9b0f71..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala +++ /dev/null @@ -1,76 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailKriteriaPage -import pages.detail.components.DukazyKriteria -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -object DetailKriteriaPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailKriteriaPageConnector( - state: DetailKriteriaPageConnector.AppState -)( - $page: Signal[Page.DetailKriteria] -)(using Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => - (p.osobniCislo.value, p.parametr.value, p.kriterium.value) - )((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map( - FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) - ) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, s, $s) => - DetailKriteriaPage($s)(state.actionBus.contramap { - case DukazyKriteria.Add => - NavigateTo( - Page.UpravDukazKriteria( - Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), - Page.Titled(s.parametr.id, Some(s.parametr.nazev)), - Page.Titled(s.kriterium.id, Some(s.kriterium.id)) - ) - ) - }) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): DetailKriteriaPage.ViewModel = - DetailKriteriaPage.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala deleted file mode 100644 index 4d8024f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ /dev/null @@ -1,57 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import pages.detail.DetailPage -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import fiftyforms.ui.components.tailwind.Color - -object DetailPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailPageConnector(state: DetailPageConnector.AppState)( - $page: Signal[Page.Detail] -)(using router: Router[Page]): - val $oscChangeSignal = $page.splitOne(_.osobniCislo.value)((osc, _, _) => osc) - val $pageChangeSignal = - $oscChangeSignal.flatMap(osc => - EventStream.fromSeq(Seq(FetchUserDetails(osc), FetchParameters(osc))) - ) - // TODO: filter the value based on the current osc - // OSC change will fetch new data, but still - // - we need to be sure that what we got is really what we ought to display - // - we want to display stale data accordingly (at least with loading indicator) - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - def apply: HtmlElement = - AppPage(state.actionBus)( - $data.combineWithFn($params)(_ zip _) - .map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: List[Parameter] - ): DetailPage.ViewModel = - DetailPage.ViewModel( - o.toDetailOsoby, - p.map( - _.toParametr(param => - PageLink.container(Page.DetailParametru(o, param), state.actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala deleted file mode 100644 index fdaa7f8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailParametruPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink - -object DetailParametruPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailParametruPageConnector( - state: DetailParametruPageConnector.AppState -)( - $page: Signal[Page.DetailParametru] -)(using router: Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => (p.osobniCislo.value, p.parametr.value))((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map(FetchParameter(_, _)) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - } yield (da, pb) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailParametruPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter - ): DetailParametruPage.ViewModel = - DetailParametruPage.ViewModel( - o.toDetailOsoby, - p.toParametr(p => - PageLink.container(Page.DetailParametru(o, p), state.actionBus) - ), - p.criteria.map( - _.toKriterium { c => - PageLink.container( - Page.DetailKriteria(o, p, c), - state.actionBus - ) - } - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala deleted file mode 100644 index 049aa22..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -case class DirectoryPageConnector( - $input: EventStream[List[UserInfo]], - actionBus: Observer[Action] -)(using router: Router[Page]): - val $data = $input.startWithNone - val $actionSignal = EventStream.fromValue(FetchDirectory) - - def apply: HtmlElement = - AppPage(actionBus)( - $data.split(_ => ())((_, _, s) => - pages.directory.DirectoryPage( - s.map( - _.map( - _.toUserRow(u => - PageLink.container( - Page.Detail(Page.Titled(u.personalNumber)), - actionBus - ) - ) - ) - ) - ) - ), - $actionSignal --> actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala deleted file mode 100644 index 88e7450..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ /dev/null @@ -1,68 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii -import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria -import fiftyforms.ui.components.tailwind.Color - -extension (o: UserInfo) - def toDetailOsoby: DetailOsoby.ViewModel = - DetailOsoby.ViewModel( - o.personalNumber, - o.name, - o.email, - o.phone, - o.img, - o.mainFunction.map(f => - DetailOsoby.Funkce.ViewModel(f.name, f.dept, f.ou) - ), - o.userContracts.headOption.map(c => - DetailOsoby.PracovniPomer.ViewModel(c.rel, c.startDate, c.endDate) - ) - ) - -extension (param: Parameter) - def toParametr(container: Parameter => Anchor): DetailParametru.ViewModel = - DetailParametru.ViewModel( - id = param.id, - nazev = param.name, - popis = param.description, - status = "Nesplněno", - statusColor = Color.red, - a = container(param) - ) - -extension (crit: ParameterCriteria) - def toKriterium( - container: ParameterCriteria => Anchor - ): DetailKriteria.ViewModel = - DetailKriteria.ViewModel( - nazev = crit.criteriumText, - kapitola = crit.chapterId, - bod = crit.itemId, - status = "Nesplněno", - statusColor = Color.red, - splneno = false, - dukazy = Nil, - container = container(crit) - ) - -extension (user: UserInfo) - def toUserRow( - container: UserInfo => HtmlElement = (_: UserInfo) => div() - ): UserRow.ViewModel = - UserRow.ViewModel( - osobniCislo = user.personalNumber.toString, - celeJmeno = user.name, - prijmeni = user.surname, - hlavniFunkce = user.mainFunction.map(_.name), - img = user.img, - container = () => container(user) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala deleted file mode 100644 index 7d5b2c1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.dashboard - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.Page - -object DashboardPage: - - def render: HtmlElement = - div("Dashboard page") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala deleted file mode 100644 index 9ef0698..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala +++ /dev/null @@ -1,31 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailKriteriaPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply($m: Signal[ViewModel])( - action: Observer[DukazyKriteria.Action] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - DukazyKriteria($m.map(_.kriterium.dukazy))(action) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala deleted file mode 100644 index 3b316e1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object DetailPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametry: SeznamParametru.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby($m.map(_.osoba)), - SeznamParametru($m.map(_.parametry)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala deleted file mode 100644 index 17224cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala +++ /dev/null @@ -1,24 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailParametruPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriteria: SeznamKriterii.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby.header($m.map(_.osoba)), - DetailParametru($m.map(_.parametr)), - SeznamKriterii($m.map(_.kriteria)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala deleted file mode 100644 index dc912fa..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ /dev/null @@ -1,107 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb -package app -package pages.detail - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.UpravDukazForm -import fiftyforms.services.files.File - -object UpravDukaz: - - type ThisPage = Page.UpravDukazKriteria - type PageKey = (OsobniCislo, String, String) - - trait State { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def availableFiles: EventStream[List[File]] - def actionBus: Observer[Action] - } - - def keyOfPage(page: ThisPage): PageKey = - (page.osobniCislo.value, page.parametr.value, page.kriterium.value) - - def onChangeAction(key: PageKey): Action = - FetchParameterCriteria( - key._1, - key._2, - key._3, - Page.UpravDukazKriteria(_, _, _) - ) - - class Connector(state: State)($page: Signal[ThisPage])(using Router[Page]): - val $paramChangeSignal = $page.splitOne(keyOfPage)((x, _, _) => x) - val $pageChangeSignal = $paramChangeSignal.map(onChangeAction) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.split(_ => ())((_, s, $s) => - PageComponent( - $s.map(buildModel), - state.availableFiles, - state.actionBus.contramap { - case UpravDukazForm.Cancelled => - NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) - case UpravDukazForm.AvailableFilesRequested => - FetchAvailableFiles(s._1.personalNumber) - } - ) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): PageComponent.ViewModel = - import connectors.* - PageComponent.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) - - object PageComponent: - import components.* - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply( - $m: Signal[ViewModel], - availableFilesStream: EventStream[List[File]], - events: Observer[UpravDukazForm.Event] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - UpravDukazForm(availableFilesStream)(events) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala deleted file mode 100644 index ca311b0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.Icons -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Color - -object DetailKriteria: - case class ViewModel( - nazev: String, - kapitola: String, - bod: String, - status: String, - statusColor: Color, - splneno: Boolean, - dukazy: List[DukazKriteria.ViewModel], - container: HtmlElement = div() - ) { - val id = s"${kapitola}${bod}" - } - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h3( - cls := "text-xl leading-6 font-bold text-gray-900", - child.text <-- $m.map(_.nazev) - ), - h3( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - "Kapitola ", - child.text <-- $m.map(_.kapitola), - " bod ", - child <-- $m.map(_.bod) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala deleted file mode 100644 index ae90290..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala +++ /dev/null @@ -1,100 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Avatar - -object DetailOsoby: - - object Funkce: - case class ViewModel( - nazev: String, - stredisko: String, - voj: String - ) - - def render($m: Signal[ViewModel]): HtmlElement = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.nazev), - span( - cls := "hidden md:inline", - " @ ", - child.text <-- $m.map(_.stredisko), - ", ", - child.text <-- $m.map(_.voj) - ) - ) - - object PracovniPomer: - case class ViewModel( - druh: String, - pocatek: LocalDate, - konec: Option[LocalDate] - ) - - def render($m: Signal[ViewModel]): HtmlElement = - import fiftyforms.ui.components.tailwind.CustomAttrs.datetime - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.druh), - " od ", - time( - datetime <-- $m.map(_.pocatek.toString), - child.text <-- $m.map(_.pocatek.toString) - ) - ) - - case class ViewModel( - osobniCislo: OsobniCislo, - jmeno: String, - email: Option[String], - telefon: Option[String], - img: Option[String], - hlavniFunkce: Option[Funkce.ViewModel], - pracovniPomer: Option[PracovniPomer.ViewModel] - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-4", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(16) - ), - div( - h1( - cls := "text-2xl font-bold text-gray-900", - child.text <-- $m.map(_.jmeno) - ), - child.maybe <-- $m.map(_.hlavniFunkce).split(_ => ())((_, _, d) => - Funkce.render(d) - ), - child.maybe <-- $m.map(_.pracovniPomer).split(_ => ())((_, _, d) => - PracovniPomer.render(d) - ) - ) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-5", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(8) - ), - div( - h1( - cls := "text-lg font-medium text-gray-800", - child.text <-- $m.map(_.jmeno) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala deleted file mode 100644 index e11cb62..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color - -object DetailParametru: - case class ViewModel( - id: String, - nazev: String, - popis: String, - status: String, - statusColor: Color, - a: Anchor - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h2( - cls := "text-2xl leading-6 font-medium text-gray-900", - child.text <-- $m.map(_.nazev) - ), - p( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - child.text <-- $m.map(_.popis) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - h2( - cls := "text-lg leading-6 font-medium text-gray-600", - child.text <-- $m.map(_.nazev) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala deleted file mode 100644 index a00ede8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala +++ /dev/null @@ -1,177 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.CustomAttrs - -object DukazKriteria: - case class Osoba(osobniCislo: String, jmeno: String) - case class Udalost(osoba: Osoba, datum: LocalDate) - case class Dokument( - url: String, - nazev: String, - pridal: Udalost, - odebral: Option[Udalost] - ) - case class ViewModel( - dokumenty: List[Dokument], - autorizoval: Option[Udalost], - platiDo: Option[LocalDate], - poznámka: Option[String] - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-lg", - div( - cls := "px-4 py-5 sm:px-6", - h3( - cls := "text-lg leading-6 font-medium text-gray-900", - """Applicant Information""" - ), - p( - cls := "mt-1 max-w-2xl text-sm text-gray-500", - """Personal details and application.""" - ) - ), - div( - cls := "border-t border-gray-200 px-4 py-5 sm:px-6", - dl( - cls := "grid grid-cols-1 gap-x-4 gap-y-8 sm:grid-cols-2", - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Full name""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Margot Foster""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Application for""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Backend Developer""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Email address""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """margotfoster@example.com""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Salary expectation""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """$120,000""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """About""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Fugiat ipsum ipsum deserunt culpa aute sint do nostrud anim incididunt cillum culpa consequat. Excepteur qui ipsum aliquip consequat sint. Sit id mollit nulla mollit nostrud in ea officia proident. Irure nostrud pariatur mollit ad adipisicing reprehenderit deserunt qui eu.""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """Attachments""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - ul( - role := "list", - cls := "border border-gray-200 rounded-md divide-y divide-gray-200", - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """resume_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ), - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """coverletter_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala deleted file mode 100644 index 22f134f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala +++ /dev/null @@ -1,35 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object DukazyKriteria: - sealed trait Action - case object Add extends Action - - type ViewModel = List[DukazKriteria.ViewModel] - - def apply($m: Signal[ViewModel])(actions: Observer[Action]): HtmlElement = - div( - child.maybe <-- $m.map(m => - if (m.isEmpty) then Some(prazdnyDukaz(actions)) - else None - ), - children <-- $m.map(_.zipWithIndex).split(_._2)((_, _, $s) => - DukazKriteria($s.map(_._1)) - ) - ) - - private def prazdnyDukaz(actions: Observer[Action]): HtmlElement = - button( - tpe := "button", - onClick.preventDefault.mapTo(Add) --> actions, - cls := "relative block w-full border-2 border-gray-300 border-dashed rounded-lg p-12 text-center hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - Icons.outline - .`document-add`(12) - .amend(svg.cls := "mx-auto text-gray-400"), - span( - cls := "mt-2 block text-sm font-medium text-gray-900", - "Přidat důkaz" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala deleted file mode 100644 index f628697..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.list.StackedList -import fiftyforms.ui.components.tailwind.list.ListRow -import fiftyforms.ui.components.tailwind.list.RowTag -import fiftyforms.ui.components.tailwind.list.RowNext -import java.time.LocalDate - -object SeznamKriterii: - type ViewModel = List[DetailKriteria.ViewModel] - - private val kritList = new StackedList[DetailKriteria.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - kritList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = - p(cls := "text-sm text-gray-500", s"${i.kapitola}${i.bod}"), - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.container - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala deleted file mode 100644 index 2bfe4bc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.list.{ - StackedList, - ListRow, - RowTag, - PropList, - IconText, - RowNext -} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.LinkSupport.* - -object SeznamParametru: - type ViewModel = List[DetailParametru.ViewModel] - - private val parametrList = new StackedList[DetailParametru.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - parametrList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = emptyNode, - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.a - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala deleted file mode 100644 index f048e9d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ /dev/null @@ -1,101 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.form.* -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement -import fiftyforms.services.files.components.tailwind.FilePicker -import cz.e_bs.cmi.mdr.pdb.frontend.AutorizujDukaz -import cz.e_bs.cmi.mdr.pdb.frontend.DocumentRef -import fiftyforms.services.files.File - -object UpravDukazForm: - sealed trait Event - case object Cancelled extends Event - case object AvailableFilesRequested extends Event - def apply(availableFilesStream: EventStream[List[File]])( - updates: Observer[Event] - ): HtmlElement = - val (filesStream, filesObserver) = - EventStream.withObserver[FilePicker.Event] - val files = Var[List[File]](Nil) - def submitButtons: HtmlElement = - div( - cls := "pt-5", - div( - cls := "flex justify-end", - button( - tpe := "button", - cls := "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zrušit", - onClick.mapTo(Cancelled) --> updates - ), - button( - tpe := "submit", - cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - disabled <-- files.signal.map(_.isEmpty), - "Autorizovat" - ) - ) - ) - - div( - filesStream.collect { case FilePicker.SelectionUpdated(files) => - files.to(List) - } --> files.writer, - filesStream.collect { case FilePicker.AvailableFilesRequested => - AvailableFilesRequested - } --> updates, - cls := "bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6", - Form( - Form.Body( - Form.Section( - FormHeader( - FormHeader.ViewModel( - "Doložení kritéria", - "Sestavte doklady poskytující důkaz kritéria a potvrďte odesláním formuláře. Případné limitace či jiné relevantní údaje vepište do pole Komentář." - ) - ), - FormFields( - FormRow( - "dokumenty", - "Dokumenty", - FilePicker(files.signal, availableFilesStream)(filesObserver) - .amend(idAttr := "dokumenty", cls("max-w-lg")) - ).toHtml, - FormRow( - "platnost", - "Platnost", - input( - idAttr := "platnost", - name := "platnost", - tpe := "date", - autoComplete := "date", - cls := "block max-w-lg w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" - ) - ).toHtml, - FormRow( - "komentar", - "Komentář", - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - textArea( - idAttr := "komentar", - name := "about", - rows := 3, - cls := "max-w-lg shadow-sm block w-full focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border border-gray-300 rounded-md" - ), - p( - cls := "mt-2 text-sm text-gray-500", - "Doplňte prosím potřebné informace související s doložením kritéria, včetně případných limitací." - ) - ) - ).toHtml - ) - ) - ), - submitButtons - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala deleted file mode 100644 index 41033f6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -import components._ - -object DirectoryPage: - - type ViewModel = List[UserRow.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - val (actionsStream, actionObserver) = - EventStream.withObserver[SearchForm.Action] - val $filter = actionsStream - .collect { - case SearchForm.ClearFilter => None - case SearchForm.SetFilter(t) => Some(t) - } - .startWith(None) - val byLetter = for { - d <- $m - f <- $filter - } yield for { - (letter, users) <- d - .filter { user => - f match - case None => true - case Some(t) => user.search.contains(t) - } - .groupBy(_.prijmeni.head) - .to(List) - .sortBy(_._1) - } yield (letter.toString, users.sortBy(_.prijmeni)) - - div( - cls := "h-full max-w-7xl mx-auto order-first flex flex-col flex-shrink-0", - SearchForm(actionObserver), - Directory(byLetter) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala deleted file mode 100644 index 73f915d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -object Directory: - - object Header: - type ViewModel = String - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500", - h3(child.text <-- $m) - ) - - object UserList: - type ViewModel = List[UserRow.ViewModel] - def apply($m: Signal[ViewModel]): HtmlElement = - ul( - role := "list", - cls := "relative z-0 divide-y divide-gray-200", - children <-- $m.split(_.osobniCislo)((_, _, s) => UserRow(s)) - ) - - type ViewModel = List[(String, List[UserRow.ViewModel])] - def apply($m: Signal[ViewModel]): HtmlElement = - nav( - cls := "flex-1 min-h-0 overflow-y-auto", - aria.label := "Directory", - children <-- $m.split(_._1)((_, _, s) => - div( - cls := "relative", - Header(s.map(_._1)), - UserList(s.map(_._2)) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala deleted file mode 100644 index e182910..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala +++ /dev/null @@ -1,54 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object SearchForm: - sealed trait Action - case object SubmitFilter extends Action - sealed trait FilterAction extends Action - case object ClearFilter extends FilterAction - case class SetFilter(value: String) extends FilterAction - - def apply(actions: Observer[Action]): HtmlElement = - div( - cls := "px-6 pt-4 pb-4", - form( - cls := "flex space-x-4", - onSubmit.mapTo(SubmitFilter) --> actions, - div( - cls := "flex-1 min-w-0", - label( - forId := "search", - cls := "sr-only", - "Hledat" - ), - div( - cls := "relative rounded-md shadow-sm", - div( - cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", - Icons.solid.search().amend(svg.cls := "text-gray-400") - ), - input( - tpe := "search", - name := "search", - idAttr := "search", - cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", - placeholder := "Hledat", - composeEvents(onInput.mapToValue.setAsValue.map(SetFilter(_)))( - _.throttle(500) - ) --> actions - ) - ) - ), - button( - tpe := "submit", - cls := "inline-flex justify-center px-3.5 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500", - Icons.solid.filter().amend(svg.cls := "text-gray-400"), - span( - cls := "sr-only", - "Hledat" - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala deleted file mode 100644 index 82f7723..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala +++ /dev/null @@ -1,52 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.airstream.core.Signal -import fiftyforms.ui.components.tailwind.Avatar - -object UserRow: - case class ViewModel( - osobniCislo: String, - celeJmeno: String, - prijmeni: String, - hlavniFunkce: Option[String], - img: Option[String], - container: () => HtmlElement = () => div() - ) { - val search = prijmeni.toLowerCase - } - - def apply($m: Signal[ViewModel]): HtmlElement = - inline def avatarImage = - Avatar($m.map(_.img)).avatarImage(10) - - li( - div( - cls := "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500", - div( - cls := "flex-shrink-0", - child <-- avatarImage - ), - div( - cls := "flex-1 min-w-0", - child <-- $m.map { o => - o.container() - .amend( - cls := "focus:outline-none", - span( - cls := "absolute inset-0", - aria.hidden := true - ), - p( - cls := "text-sm font-medium text-gray-900", - o.celeJmeno - ), - p( - cls := "text-sm text-gray-500 truncate", - o.hlavniFunkce - ) - ) - } - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala deleted file mode 100644 index a3b00af..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala +++ /dev/null @@ -1,103 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import scala.scalajs.js.annotation.JSExportTopLevel -import scala.scalajs.js.annotation.JSExport -import scala.scalajs.js.annotation.JSImport -import scala.scalajs.js -import org.scalajs.dom -import com.raquo.laminar.api.L.{*, given} -import scala.scalajs.js.Date -import com.raquo.waypoint.Router -import com.raquo.waypoint.SplitRender -import cz.e_bs.cmi.mdr.pdb.app.services.DataFetcher -import scala.scalajs.js.JSON -import zio.json._ -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.state.AppState - -@js.native -@JSImport("stylesheets/main.css", JSImport.Namespace) -object Css extends js.Any - -@js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native -@JSImport("params/pdb-params.json", JSImport.Default) -object pdbParams extends js.Object - -@JSExportTopLevel("app") -object Main: - - @JSExport - def main(args: Array[String]): Unit = - import Routes.given - onLoad { - setupAirstream() - val appContainer = dom.document.querySelector("#app") - val _ = - render( - appContainer, - renderPage(state.MockAppState(using unsafeWindowOwner, router)) - ) - } - - private def onLoad(f: => Unit): Unit = - documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) - - private def setupAirstream()(using router: Router[Page]): Unit = - AirstreamError.registerUnhandledErrorCallback(err => - router.forcePage( - Page.UnhandledError( - Some(err.getClass.getName), // TODO: Fill only in dev mode - Some(err.getMessage) - ) - ) - ) - - def renderPage(state: AppState)(using - router: Router[Page] - ): HtmlElement = - val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) - .collectSignal[Page.Detail]( - connectors - .DetailPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailParametru]( - connectors - .DetailParametruPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailKriteria]( - connectors - .DetailKriteriaPageConnector(state)(_) - .apply - ) - .collectSignal[Page.UpravDukazKriteria]( - pages.detail.UpravDukaz.Connector(state)(_).apply - ) - .collectStatic(Page.Dashboard)( - connectors.DashboardPageConnector(state.actionBus).apply - ) - .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) - ) - .collect[Page.UnhandledError](pg => - pages.errors - .UnhandledErrorPage( - pages.errors.UnhandledErrorPage - .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), - state.actionBus - ) - ) - .collectStatic(Page.Directory)( - connectors - .DirectoryPageConnector(state.users, state.actionBus) - .apply - ) - div(cls := "h-full", child <-- pageSplitter.$view) - - // Pull in the stylesheet - val css: Css.type = Css diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala deleted file mode 100644 index 3bbc8c0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ /dev/null @@ -1,198 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.* -import org.scalajs.dom -import zio.json.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import scala.scalajs.js -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.Page.Titled - -// enum is not working with Waypoints' SplitRender collectStatic -sealed abstract class Page( - val id: String, - val title: String, - val parent: Option[Page] -) { - val path: Vector[Page] = - parent match - case None => Vector(this) - case Some(p) => p.path :+ this - - val isRoot: Boolean = parent.isEmpty -} - -object Page: - - case class Titled[V](value: V, title: Option[String] = None): - val show: String = title.getOrElse(value.toString) - - case object Directory extends Page("directory", "Adresář", None) - - case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) - - case class Detail(osobniCislo: Titled[OsobniCislo]) - extends Page("user", osobniCislo.show, Some(Directory)) - - object Detail { - def apply(o: UserInfo): Detail = Detail( - Titled(o.personalNumber, Some(o.name)) - ) - } - - case class DetailParametru( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String] - ) extends Page( - "parameter", - parametr.show, - Some(Detail(osobniCislo)) - ) - - object DetailParametru { - def apply(o: UserInfo, p: Parameter): DetailParametru = - DetailParametru( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)) - ) - } - - case class DetailKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "criteria", - kriterium.show, - Some(DetailParametru(osobniCislo, parametr)) - ) - - object DetailKriteria { - def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = - DetailKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class UpravDukazKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "addProof", - "Důkaz", - Some(DetailKriteria(osobniCislo, parametr, kriterium)) - ) - - object UpravDukazKriteria { - def apply( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): UpravDukazKriteria = - UpravDukazKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class NotFound(url: String) extends Page("404", "404", Some(Directory)) - - case class UnhandledError( - errorName: Option[String], - errorMessage: Option[String] - ) extends Page("500", "Unexpected error", Some(Directory)) - -object Routes: - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) - given [V: JsonEncoder]: JsonEncoder[Titled[V]] = - DeriveJsonEncoder.gen[Titled[V]] - given [V: JsonDecoder]: JsonDecoder[Titled[V]] = - DeriveJsonDecoder.gen[Titled[V]] - given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] - given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] - - val base = - js.`import`.meta.env.BASE_URL - .asInstanceOf[String] + "app" - - val homePage: Page = Page.Directory - - given router: Router[Page] = Router[Page]( - routes = List( - Route.static(homePage, root / endOfSegments, basePath = base), - Route.static( - Page.Dashboard, - root / "dashboard" / endOfSegments, - basePath = base - ), - Route[Page.Detail, String]( - encode = _.osobniCislo.value.toString, - decode = osc => Page.Detail(Titled(OsobniCislo(osc))), - root / "osoba" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.DetailParametru, (String, String)]( - encode = p => (p.osobniCislo.value.toString, p.parametr.value), - decode = - p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / endOfSegments, - basePath = base - ), - Route[Page.DetailKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.DetailKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.UpravDukazKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.UpravDukazKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / "edit" / endOfSegments, - basePath = base - ) - ), - serializePage = _.toJson, - deserializePage = _.fromJson[Page] - .fold(s => throw IllegalStateException(s), identity), - getPageTitle = _.title, - routeFallback = url => Page.NotFound(url), - deserializeFallback = _ => Page.Dashboard - )( - $popStateEvent = windowEvents.onPopState, - owner = unsafeWindowOwner - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala deleted file mode 100644 index b24b82b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -sealed trait Action - -case object FetchDirectory extends Action -case class FetchUserDetails(osc: OsobniCislo) extends Action -case class FetchParameters(osc: OsobniCislo) extends Action -case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action -case class FetchParameterCriteria( - osc: OsobniCislo, - paramId: String, - critId: String, - page: (UserInfo, Parameter, ParameterCriteria) => Page -) extends Action -case class FetchAvailableFiles(osc: OsobniCislo) extends Action -case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala deleted file mode 100644 index c637d35..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala +++ /dev/null @@ -1,82 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.{UserProfile, UserInfo, OsobniCislo} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo -import cz.e_bs.cmi.mdr.pdb.UserFunction - -object AppPage: - // TODO: pages by logged in user - val pages: List[Page] = List(Page.Directory, Page.Dashboard) - - import NavigationBar.{Logo, MenuItem} - - val logo = Logo( - "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", - "Workflow" - ) - - // TODO: menu items by user profile - val userMenu = - List( - MenuItem("Your Profile"), - MenuItem("Settings"), - MenuItem("Sign out") - ) - - // TODO: load user profile - val $userProfile = Var( - UserProfile( - "tom", - UserInfo( - OsobniCislo("1031"), - "tom", - "Tom", - "Cook", - None, - None, - Some("tom@example.com"), - Some("+420 222 866 180"), - Some( - UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") - ), - Nil, - None - ) - ) - ) - - val $userInfo = $userProfile.signal.map(_.userInfo) - - type ViewModel = Option[HtmlElement] - def apply( - actionBus: Observer[Action] - )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using - router: Router[Page] - ): HtmlElement = - PageLayout(actionBus)( - $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => - PageLayout.ViewModel( - NavigationBar.ViewModel( - u, - pages.map(p => - NavigationBar.Link( - () => - PageLink( - p, - actionBus - ), - p == cp - ) - ), - userMenu, - logo - ), - c - ) - ), - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala deleted file mode 100644 index bf473c6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ /dev/null @@ -1,143 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden -import fiftyforms.ui.components.tailwind.list.IconText.ViewModel -import fiftyforms.ui.components.tailwind.Icons -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Action - -object Breadcrumbs: - - private def slash = - import svg.{*, given} - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-300", - xmlns := "http://www.w3.org/2000/svg", - fill := "currentColor", - viewBox := "0 0 20 20", - ariaHidden := true, - path( - d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" - ) - ) - - def backIcon = - Icons.solid - .`arrow-narrow-left`() - .amend( - svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" - ) - - object Link: - case class ViewModel( - page: Page, - icon: Option[SvgElement], - extraClasses: String, - text: String, - textClasses: Option[String] = None - ) - - val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" - - def shortHome(p: Page) = ViewModel( - p, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - "Zpět na úvodní stránku", - None - ) - - def fullHome(p: Page) = ViewModel( - p, - Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), - "text-gray-400 hover:text-gray-500", - "Domů", - Some("sr-only") - ) - - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - PageLink - .container($m.map(_.page), actionBus) - .amend( - cls <-- $m.map(_.extraClasses), - child.maybe <-- $m.map(_.icon), - span( - cls <-- $m.map(_.textClasses.getOrElse("")), - child.text <-- $m.map(_.text) - ) - ) - - object FullBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - ol( - role := "list", - cls := "flex items-center space-x-4", - children <-- $m.map(_.path).split(_.id)((_, _, $p) => - li( - div( - cls := "flex items-center", - child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), - Link( - $p.map(p => - if (p.isRoot) then Link.fullHome(p) - else - Link.ViewModel( - p, - None, - s"ml-4 max-w-xs truncate ${Link.baseClasses}", - p.title - ) - ), - actionBus - ) - ) - ) - ) - ) - - object ShortBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - Link( - $m.map { p => - val target = p.parent.getOrElse(p) - if target.isRoot then Link.shortHome(target) - else - Link.ViewModel( - target, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - s"Zpět na ${target.title}" - ) - }, - actionBus - ) - - def apply(actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - nav( - cls := "flex", - aria.label := "Breadcrumb", - div( - cls := "flex sm:hidden", - ShortBreadcrumbs(router.$currentPage, actionBus) - ), - div( - cls := "hidden sm:block", - FullBreadcrumbs(router.$currentPage, actionBus) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala deleted file mode 100644 index 0db2047..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ /dev/null @@ -1,242 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent -import fiftyforms.ui.components.tailwind.Icons -import fiftyforms.ui.components.tailwind.Avatar -import cz.e_bs.cmi.mdr.pdb.UserInfo -import io.laminext.syntax.core.* - -object NavigationBar: - - case class Logo(img: String, name: String) - case class Link(a: () => Anchor, active: Boolean) - case class MenuItem(title: String) - - case class ViewModel( - userInfo: UserInfo, - pages: List[Link], - userMenu: List[MenuItem], - logo: Logo - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - val $userInfo = $m.map(_.userInfo) - val mobileMenuOpen = Var(false) - - val desktopOnly = cls("hidden md:block") - val mobileOnly = cls("md:hidden") - - inline def avatarImage(size: Int = 8) = - Avatar($userInfo.map(_.img)).avatarImage(size) - - def notificationButton = button( - tpe := "button", - cls := List( - "bg-indigo-600", - "focus:outline-none", - "focus:ring-2", - "focus:ring-offset-2", - "focus:ring-offset-indigo-600", - "focus:ring-white", - "hover:text-white", - "p-1", - "rounded-full", - "text-indigo-200" - ), - span(cls := "sr-only", "View notifications"), - Icons.outline.bell() - ) - - def userProfile: HtmlElement = - val menuOpen = Var(false) - - def menuItem(item: MenuItem, idx: Int): HtmlElement = - a( - href := "#", - cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", - role := "menuitem", - tabIndex := -1, - idAttr := s"user-menu-item-$idx", - item.title - ) - - div( - cls := "ml-3 relative", - div( - button( - tpe := "button", - cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - idAttr := "user-menu-button", - aria.expanded <-- menuOpen.signal, - aria.hasPopup := true, - span(cls := "sr-only", "Open user menu"), - child <-- avatarImage(), - onClick.preventDefault.mapTo( - !menuOpen.now() - ) --> menuOpen.writer - ) - ), - /* - * */ - div( - cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", - cls <-- menuOpen.signal.map { o => - if (o) "md:block" else "md:hidden" - }, - role := "menu", - aria.orientation := "vertical", - aria.labelledBy := "user-menu-button", - tabIndex := -1, - // : keyboard navigation - children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) - ) - ) - - def mobileProfile = - def menuItem(item: MenuItem): HtmlElement = - a( - href := "#", - cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", - item.title - ) - - div( - cls := "pt-4 pb-3 border-t border-indigo-700", - div( - cls := "flex items-center px-5", - div( - cls := "flex-shrink-0", - child <-- avatarImage(10) - ), - div( - cls := "ml-3", - div( - cls := "text-base font-medium text-white", - child.text <-- $userInfo.map(_.name) - ), - child.maybe <-- $userInfo.map( - _.email.map(e => - div( - cls := "text-sm font-medium text-indigo-300", - e - ) - ) - ) - ), - notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) - ), - div( - cls := "mt-3 px-2 space-y-1", - children <-- $m.map(_.userMenu.map(menuItem)) - ) - ) - - def pageLink(page: Link): Anchor = - page - .a() - .amend( - cls := "text-white px-3 py-2 rounded-md text-sm font-medium", - cls := Seq( - "bg-indigo-700" -> page.active, - "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active - ), - ariaCurrent := (if page.active then "page" else "false") - ) - - def logoImg: Image = - img( - cls := "h-8 w-8", - src <-- $m.map(_.logo.img), - alt <-- $m.map(_.logo.name) - ) - - def mobileMenuButton = button( - tpe := "button", - cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - aria.controls := "mobile-menu", - aria.expanded <-- mobileMenuOpen.signal, - span(cls := "sr-only", "Open main menu"), - Icons.outline - .menu() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") - ), - Icons.outline - .x() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") - ), - onClick.preventDefault.mapTo( - !mobileMenuOpen.now() - ) --> mobileMenuOpen.writer - ) - - def navBarLeft = - div( - cls := "flex items-center", - div(cls := "flex-shrink-0", logoImg), - div( - desktopOnly, - div( - cls := "ml-10 flex items-baseline space-x-4", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ) - ) - ) - - def navBarRight = - div( - desktopOnly, - div( - cls := "ml-4 flex items-center md:ml-6", - notificationButton, - userProfile - ) - ) - - def navBarMobile = - div( - cls := "-mr-2 flex", - mobileOnly, - mobileMenuButton - ) - - def navBar = - div( - cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex items-center justify-between h-16", - navBarLeft, - navBarRight, - navBarMobile - ) - ) - - def mobileMenu = - div( - mobileOnly, - cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }, - idAttr := "mobile-menu", - div( - cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ), - mobileProfile - ) - - nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala deleted file mode 100644 index 75339df..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object PageHeader: - def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = - header( - cls := "bg-white shadow-sm", - div( - cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", - h1( - cls := "text-lg leading-6 font-semibold text-gray-900", - Breadcrumbs(actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala deleted file mode 100644 index 5096463..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ /dev/null @@ -1,30 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import fiftyforms.ui.components.tailwind.Loading -import io.laminext.syntax.core.* - -object PageLayout: - case class ViewModel( - navigation: NavigationBar.ViewModel, - content: Option[HtmlElement] - ) - def apply(actionBus: Observer[Action])( - $m: Signal[ViewModel], - mods: Modifier[HtmlElement]* - )(using router: Router[Page]): HtmlElement = - val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) - div( - cls := "h-full flex flex-col", - NavigationBar($m.map(_.navigation)), - child.maybe <-- router.$currentPage.map(_.isRoot) - .switch(None, Some(PageHeader(actionBus))), - main( - cls := "flex-grow-1 overflow-y-auto", - mods, - child <-- $maybeContent.map(_.getOrElse(Loading)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala deleted file mode 100644 index 62d820f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.LinkSupport.* -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo - -object PageLink: - type ViewModel = Page - def apply($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = - a(mods($m, actions), child.text <-- $m.map(_.title)) - - def apply(m: ViewModel, actions: Observer[Action])(using - router: Router[Page] - ): Anchor = apply(Val(m), actions) - - def container($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = a(mods($m, actions)) - - def container(m: ViewModel, actions: Observer[Action])(using - Router[Page] - ): Anchor = container(Val(m), actions) - - private def mods($m: Signal[Page], actions: Observer[Action])(using - router: Router[Page] - ): Modifier[Anchor] = - Seq( - href <-- $m.map(router.absoluteUrlForPage).recover { case _ => - Some("invalid url") - }, - composeEvents(onClick.noKeyMod.preventDefault)( - _.sample($m) - .map(NavigateTo.apply) - ) --> actions - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala deleted file mode 100644 index 87f825a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.pages.dashboard.DashboardPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -class DashboardPageConnector(actionBus: Observer[Action])(using - router: Router[Page] -): - def apply: HtmlElement = - AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala deleted file mode 100644 index e9b0f71..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala +++ /dev/null @@ -1,76 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailKriteriaPage -import pages.detail.components.DukazyKriteria -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -object DetailKriteriaPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailKriteriaPageConnector( - state: DetailKriteriaPageConnector.AppState -)( - $page: Signal[Page.DetailKriteria] -)(using Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => - (p.osobniCislo.value, p.parametr.value, p.kriterium.value) - )((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map( - FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) - ) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, s, $s) => - DetailKriteriaPage($s)(state.actionBus.contramap { - case DukazyKriteria.Add => - NavigateTo( - Page.UpravDukazKriteria( - Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), - Page.Titled(s.parametr.id, Some(s.parametr.nazev)), - Page.Titled(s.kriterium.id, Some(s.kriterium.id)) - ) - ) - }) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): DetailKriteriaPage.ViewModel = - DetailKriteriaPage.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala deleted file mode 100644 index 4d8024f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ /dev/null @@ -1,57 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import pages.detail.DetailPage -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import fiftyforms.ui.components.tailwind.Color - -object DetailPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailPageConnector(state: DetailPageConnector.AppState)( - $page: Signal[Page.Detail] -)(using router: Router[Page]): - val $oscChangeSignal = $page.splitOne(_.osobniCislo.value)((osc, _, _) => osc) - val $pageChangeSignal = - $oscChangeSignal.flatMap(osc => - EventStream.fromSeq(Seq(FetchUserDetails(osc), FetchParameters(osc))) - ) - // TODO: filter the value based on the current osc - // OSC change will fetch new data, but still - // - we need to be sure that what we got is really what we ought to display - // - we want to display stale data accordingly (at least with loading indicator) - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - def apply: HtmlElement = - AppPage(state.actionBus)( - $data.combineWithFn($params)(_ zip _) - .map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: List[Parameter] - ): DetailPage.ViewModel = - DetailPage.ViewModel( - o.toDetailOsoby, - p.map( - _.toParametr(param => - PageLink.container(Page.DetailParametru(o, param), state.actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala deleted file mode 100644 index fdaa7f8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailParametruPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink - -object DetailParametruPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailParametruPageConnector( - state: DetailParametruPageConnector.AppState -)( - $page: Signal[Page.DetailParametru] -)(using router: Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => (p.osobniCislo.value, p.parametr.value))((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map(FetchParameter(_, _)) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - } yield (da, pb) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailParametruPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter - ): DetailParametruPage.ViewModel = - DetailParametruPage.ViewModel( - o.toDetailOsoby, - p.toParametr(p => - PageLink.container(Page.DetailParametru(o, p), state.actionBus) - ), - p.criteria.map( - _.toKriterium { c => - PageLink.container( - Page.DetailKriteria(o, p, c), - state.actionBus - ) - } - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala deleted file mode 100644 index 049aa22..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -case class DirectoryPageConnector( - $input: EventStream[List[UserInfo]], - actionBus: Observer[Action] -)(using router: Router[Page]): - val $data = $input.startWithNone - val $actionSignal = EventStream.fromValue(FetchDirectory) - - def apply: HtmlElement = - AppPage(actionBus)( - $data.split(_ => ())((_, _, s) => - pages.directory.DirectoryPage( - s.map( - _.map( - _.toUserRow(u => - PageLink.container( - Page.Detail(Page.Titled(u.personalNumber)), - actionBus - ) - ) - ) - ) - ) - ), - $actionSignal --> actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala deleted file mode 100644 index 88e7450..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ /dev/null @@ -1,68 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii -import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria -import fiftyforms.ui.components.tailwind.Color - -extension (o: UserInfo) - def toDetailOsoby: DetailOsoby.ViewModel = - DetailOsoby.ViewModel( - o.personalNumber, - o.name, - o.email, - o.phone, - o.img, - o.mainFunction.map(f => - DetailOsoby.Funkce.ViewModel(f.name, f.dept, f.ou) - ), - o.userContracts.headOption.map(c => - DetailOsoby.PracovniPomer.ViewModel(c.rel, c.startDate, c.endDate) - ) - ) - -extension (param: Parameter) - def toParametr(container: Parameter => Anchor): DetailParametru.ViewModel = - DetailParametru.ViewModel( - id = param.id, - nazev = param.name, - popis = param.description, - status = "Nesplněno", - statusColor = Color.red, - a = container(param) - ) - -extension (crit: ParameterCriteria) - def toKriterium( - container: ParameterCriteria => Anchor - ): DetailKriteria.ViewModel = - DetailKriteria.ViewModel( - nazev = crit.criteriumText, - kapitola = crit.chapterId, - bod = crit.itemId, - status = "Nesplněno", - statusColor = Color.red, - splneno = false, - dukazy = Nil, - container = container(crit) - ) - -extension (user: UserInfo) - def toUserRow( - container: UserInfo => HtmlElement = (_: UserInfo) => div() - ): UserRow.ViewModel = - UserRow.ViewModel( - osobniCislo = user.personalNumber.toString, - celeJmeno = user.name, - prijmeni = user.surname, - hlavniFunkce = user.mainFunction.map(_.name), - img = user.img, - container = () => container(user) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala deleted file mode 100644 index 7d5b2c1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.dashboard - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.Page - -object DashboardPage: - - def render: HtmlElement = - div("Dashboard page") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala deleted file mode 100644 index 9ef0698..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala +++ /dev/null @@ -1,31 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailKriteriaPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply($m: Signal[ViewModel])( - action: Observer[DukazyKriteria.Action] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - DukazyKriteria($m.map(_.kriterium.dukazy))(action) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala deleted file mode 100644 index 3b316e1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object DetailPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametry: SeznamParametru.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby($m.map(_.osoba)), - SeznamParametru($m.map(_.parametry)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala deleted file mode 100644 index 17224cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala +++ /dev/null @@ -1,24 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailParametruPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriteria: SeznamKriterii.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby.header($m.map(_.osoba)), - DetailParametru($m.map(_.parametr)), - SeznamKriterii($m.map(_.kriteria)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala deleted file mode 100644 index dc912fa..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ /dev/null @@ -1,107 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb -package app -package pages.detail - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.UpravDukazForm -import fiftyforms.services.files.File - -object UpravDukaz: - - type ThisPage = Page.UpravDukazKriteria - type PageKey = (OsobniCislo, String, String) - - trait State { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def availableFiles: EventStream[List[File]] - def actionBus: Observer[Action] - } - - def keyOfPage(page: ThisPage): PageKey = - (page.osobniCislo.value, page.parametr.value, page.kriterium.value) - - def onChangeAction(key: PageKey): Action = - FetchParameterCriteria( - key._1, - key._2, - key._3, - Page.UpravDukazKriteria(_, _, _) - ) - - class Connector(state: State)($page: Signal[ThisPage])(using Router[Page]): - val $paramChangeSignal = $page.splitOne(keyOfPage)((x, _, _) => x) - val $pageChangeSignal = $paramChangeSignal.map(onChangeAction) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.split(_ => ())((_, s, $s) => - PageComponent( - $s.map(buildModel), - state.availableFiles, - state.actionBus.contramap { - case UpravDukazForm.Cancelled => - NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) - case UpravDukazForm.AvailableFilesRequested => - FetchAvailableFiles(s._1.personalNumber) - } - ) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): PageComponent.ViewModel = - import connectors.* - PageComponent.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) - - object PageComponent: - import components.* - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply( - $m: Signal[ViewModel], - availableFilesStream: EventStream[List[File]], - events: Observer[UpravDukazForm.Event] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - UpravDukazForm(availableFilesStream)(events) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala deleted file mode 100644 index ca311b0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.Icons -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Color - -object DetailKriteria: - case class ViewModel( - nazev: String, - kapitola: String, - bod: String, - status: String, - statusColor: Color, - splneno: Boolean, - dukazy: List[DukazKriteria.ViewModel], - container: HtmlElement = div() - ) { - val id = s"${kapitola}${bod}" - } - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h3( - cls := "text-xl leading-6 font-bold text-gray-900", - child.text <-- $m.map(_.nazev) - ), - h3( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - "Kapitola ", - child.text <-- $m.map(_.kapitola), - " bod ", - child <-- $m.map(_.bod) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala deleted file mode 100644 index ae90290..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala +++ /dev/null @@ -1,100 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Avatar - -object DetailOsoby: - - object Funkce: - case class ViewModel( - nazev: String, - stredisko: String, - voj: String - ) - - def render($m: Signal[ViewModel]): HtmlElement = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.nazev), - span( - cls := "hidden md:inline", - " @ ", - child.text <-- $m.map(_.stredisko), - ", ", - child.text <-- $m.map(_.voj) - ) - ) - - object PracovniPomer: - case class ViewModel( - druh: String, - pocatek: LocalDate, - konec: Option[LocalDate] - ) - - def render($m: Signal[ViewModel]): HtmlElement = - import fiftyforms.ui.components.tailwind.CustomAttrs.datetime - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.druh), - " od ", - time( - datetime <-- $m.map(_.pocatek.toString), - child.text <-- $m.map(_.pocatek.toString) - ) - ) - - case class ViewModel( - osobniCislo: OsobniCislo, - jmeno: String, - email: Option[String], - telefon: Option[String], - img: Option[String], - hlavniFunkce: Option[Funkce.ViewModel], - pracovniPomer: Option[PracovniPomer.ViewModel] - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-4", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(16) - ), - div( - h1( - cls := "text-2xl font-bold text-gray-900", - child.text <-- $m.map(_.jmeno) - ), - child.maybe <-- $m.map(_.hlavniFunkce).split(_ => ())((_, _, d) => - Funkce.render(d) - ), - child.maybe <-- $m.map(_.pracovniPomer).split(_ => ())((_, _, d) => - PracovniPomer.render(d) - ) - ) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-5", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(8) - ), - div( - h1( - cls := "text-lg font-medium text-gray-800", - child.text <-- $m.map(_.jmeno) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala deleted file mode 100644 index e11cb62..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color - -object DetailParametru: - case class ViewModel( - id: String, - nazev: String, - popis: String, - status: String, - statusColor: Color, - a: Anchor - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h2( - cls := "text-2xl leading-6 font-medium text-gray-900", - child.text <-- $m.map(_.nazev) - ), - p( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - child.text <-- $m.map(_.popis) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - h2( - cls := "text-lg leading-6 font-medium text-gray-600", - child.text <-- $m.map(_.nazev) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala deleted file mode 100644 index a00ede8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala +++ /dev/null @@ -1,177 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.CustomAttrs - -object DukazKriteria: - case class Osoba(osobniCislo: String, jmeno: String) - case class Udalost(osoba: Osoba, datum: LocalDate) - case class Dokument( - url: String, - nazev: String, - pridal: Udalost, - odebral: Option[Udalost] - ) - case class ViewModel( - dokumenty: List[Dokument], - autorizoval: Option[Udalost], - platiDo: Option[LocalDate], - poznámka: Option[String] - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-lg", - div( - cls := "px-4 py-5 sm:px-6", - h3( - cls := "text-lg leading-6 font-medium text-gray-900", - """Applicant Information""" - ), - p( - cls := "mt-1 max-w-2xl text-sm text-gray-500", - """Personal details and application.""" - ) - ), - div( - cls := "border-t border-gray-200 px-4 py-5 sm:px-6", - dl( - cls := "grid grid-cols-1 gap-x-4 gap-y-8 sm:grid-cols-2", - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Full name""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Margot Foster""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Application for""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Backend Developer""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Email address""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """margotfoster@example.com""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Salary expectation""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """$120,000""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """About""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Fugiat ipsum ipsum deserunt culpa aute sint do nostrud anim incididunt cillum culpa consequat. Excepteur qui ipsum aliquip consequat sint. Sit id mollit nulla mollit nostrud in ea officia proident. Irure nostrud pariatur mollit ad adipisicing reprehenderit deserunt qui eu.""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """Attachments""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - ul( - role := "list", - cls := "border border-gray-200 rounded-md divide-y divide-gray-200", - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """resume_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ), - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """coverletter_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala deleted file mode 100644 index 22f134f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala +++ /dev/null @@ -1,35 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object DukazyKriteria: - sealed trait Action - case object Add extends Action - - type ViewModel = List[DukazKriteria.ViewModel] - - def apply($m: Signal[ViewModel])(actions: Observer[Action]): HtmlElement = - div( - child.maybe <-- $m.map(m => - if (m.isEmpty) then Some(prazdnyDukaz(actions)) - else None - ), - children <-- $m.map(_.zipWithIndex).split(_._2)((_, _, $s) => - DukazKriteria($s.map(_._1)) - ) - ) - - private def prazdnyDukaz(actions: Observer[Action]): HtmlElement = - button( - tpe := "button", - onClick.preventDefault.mapTo(Add) --> actions, - cls := "relative block w-full border-2 border-gray-300 border-dashed rounded-lg p-12 text-center hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - Icons.outline - .`document-add`(12) - .amend(svg.cls := "mx-auto text-gray-400"), - span( - cls := "mt-2 block text-sm font-medium text-gray-900", - "Přidat důkaz" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala deleted file mode 100644 index f628697..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.list.StackedList -import fiftyforms.ui.components.tailwind.list.ListRow -import fiftyforms.ui.components.tailwind.list.RowTag -import fiftyforms.ui.components.tailwind.list.RowNext -import java.time.LocalDate - -object SeznamKriterii: - type ViewModel = List[DetailKriteria.ViewModel] - - private val kritList = new StackedList[DetailKriteria.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - kritList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = - p(cls := "text-sm text-gray-500", s"${i.kapitola}${i.bod}"), - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.container - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala deleted file mode 100644 index 2bfe4bc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.list.{ - StackedList, - ListRow, - RowTag, - PropList, - IconText, - RowNext -} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.LinkSupport.* - -object SeznamParametru: - type ViewModel = List[DetailParametru.ViewModel] - - private val parametrList = new StackedList[DetailParametru.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - parametrList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = emptyNode, - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.a - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala deleted file mode 100644 index f048e9d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ /dev/null @@ -1,101 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.form.* -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement -import fiftyforms.services.files.components.tailwind.FilePicker -import cz.e_bs.cmi.mdr.pdb.frontend.AutorizujDukaz -import cz.e_bs.cmi.mdr.pdb.frontend.DocumentRef -import fiftyforms.services.files.File - -object UpravDukazForm: - sealed trait Event - case object Cancelled extends Event - case object AvailableFilesRequested extends Event - def apply(availableFilesStream: EventStream[List[File]])( - updates: Observer[Event] - ): HtmlElement = - val (filesStream, filesObserver) = - EventStream.withObserver[FilePicker.Event] - val files = Var[List[File]](Nil) - def submitButtons: HtmlElement = - div( - cls := "pt-5", - div( - cls := "flex justify-end", - button( - tpe := "button", - cls := "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zrušit", - onClick.mapTo(Cancelled) --> updates - ), - button( - tpe := "submit", - cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - disabled <-- files.signal.map(_.isEmpty), - "Autorizovat" - ) - ) - ) - - div( - filesStream.collect { case FilePicker.SelectionUpdated(files) => - files.to(List) - } --> files.writer, - filesStream.collect { case FilePicker.AvailableFilesRequested => - AvailableFilesRequested - } --> updates, - cls := "bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6", - Form( - Form.Body( - Form.Section( - FormHeader( - FormHeader.ViewModel( - "Doložení kritéria", - "Sestavte doklady poskytující důkaz kritéria a potvrďte odesláním formuláře. Případné limitace či jiné relevantní údaje vepište do pole Komentář." - ) - ), - FormFields( - FormRow( - "dokumenty", - "Dokumenty", - FilePicker(files.signal, availableFilesStream)(filesObserver) - .amend(idAttr := "dokumenty", cls("max-w-lg")) - ).toHtml, - FormRow( - "platnost", - "Platnost", - input( - idAttr := "platnost", - name := "platnost", - tpe := "date", - autoComplete := "date", - cls := "block max-w-lg w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" - ) - ).toHtml, - FormRow( - "komentar", - "Komentář", - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - textArea( - idAttr := "komentar", - name := "about", - rows := 3, - cls := "max-w-lg shadow-sm block w-full focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border border-gray-300 rounded-md" - ), - p( - cls := "mt-2 text-sm text-gray-500", - "Doplňte prosím potřebné informace související s doložením kritéria, včetně případných limitací." - ) - ) - ).toHtml - ) - ) - ), - submitButtons - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala deleted file mode 100644 index 41033f6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -import components._ - -object DirectoryPage: - - type ViewModel = List[UserRow.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - val (actionsStream, actionObserver) = - EventStream.withObserver[SearchForm.Action] - val $filter = actionsStream - .collect { - case SearchForm.ClearFilter => None - case SearchForm.SetFilter(t) => Some(t) - } - .startWith(None) - val byLetter = for { - d <- $m - f <- $filter - } yield for { - (letter, users) <- d - .filter { user => - f match - case None => true - case Some(t) => user.search.contains(t) - } - .groupBy(_.prijmeni.head) - .to(List) - .sortBy(_._1) - } yield (letter.toString, users.sortBy(_.prijmeni)) - - div( - cls := "h-full max-w-7xl mx-auto order-first flex flex-col flex-shrink-0", - SearchForm(actionObserver), - Directory(byLetter) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala deleted file mode 100644 index 73f915d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -object Directory: - - object Header: - type ViewModel = String - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500", - h3(child.text <-- $m) - ) - - object UserList: - type ViewModel = List[UserRow.ViewModel] - def apply($m: Signal[ViewModel]): HtmlElement = - ul( - role := "list", - cls := "relative z-0 divide-y divide-gray-200", - children <-- $m.split(_.osobniCislo)((_, _, s) => UserRow(s)) - ) - - type ViewModel = List[(String, List[UserRow.ViewModel])] - def apply($m: Signal[ViewModel]): HtmlElement = - nav( - cls := "flex-1 min-h-0 overflow-y-auto", - aria.label := "Directory", - children <-- $m.split(_._1)((_, _, s) => - div( - cls := "relative", - Header(s.map(_._1)), - UserList(s.map(_._2)) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala deleted file mode 100644 index e182910..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala +++ /dev/null @@ -1,54 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object SearchForm: - sealed trait Action - case object SubmitFilter extends Action - sealed trait FilterAction extends Action - case object ClearFilter extends FilterAction - case class SetFilter(value: String) extends FilterAction - - def apply(actions: Observer[Action]): HtmlElement = - div( - cls := "px-6 pt-4 pb-4", - form( - cls := "flex space-x-4", - onSubmit.mapTo(SubmitFilter) --> actions, - div( - cls := "flex-1 min-w-0", - label( - forId := "search", - cls := "sr-only", - "Hledat" - ), - div( - cls := "relative rounded-md shadow-sm", - div( - cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", - Icons.solid.search().amend(svg.cls := "text-gray-400") - ), - input( - tpe := "search", - name := "search", - idAttr := "search", - cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", - placeholder := "Hledat", - composeEvents(onInput.mapToValue.setAsValue.map(SetFilter(_)))( - _.throttle(500) - ) --> actions - ) - ) - ), - button( - tpe := "submit", - cls := "inline-flex justify-center px-3.5 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500", - Icons.solid.filter().amend(svg.cls := "text-gray-400"), - span( - cls := "sr-only", - "Hledat" - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala deleted file mode 100644 index 82f7723..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala +++ /dev/null @@ -1,52 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.airstream.core.Signal -import fiftyforms.ui.components.tailwind.Avatar - -object UserRow: - case class ViewModel( - osobniCislo: String, - celeJmeno: String, - prijmeni: String, - hlavniFunkce: Option[String], - img: Option[String], - container: () => HtmlElement = () => div() - ) { - val search = prijmeni.toLowerCase - } - - def apply($m: Signal[ViewModel]): HtmlElement = - inline def avatarImage = - Avatar($m.map(_.img)).avatarImage(10) - - li( - div( - cls := "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500", - div( - cls := "flex-shrink-0", - child <-- avatarImage - ), - div( - cls := "flex-1 min-w-0", - child <-- $m.map { o => - o.container() - .amend( - cls := "focus:outline-none", - span( - cls := "absolute inset-0", - aria.hidden := true - ), - p( - cls := "text-sm font-medium text-gray-900", - o.celeJmeno - ), - p( - cls := "text-sm text-gray-500 truncate", - o.hlavniFunkce - ) - ) - } - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala deleted file mode 100644 index 467c5f5..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala +++ /dev/null @@ -1,99 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router - -object ErrorPage: - case class ViewModel( - homePage: Page, - errorName: String, - title: String, - subTitle: String - ) - - def apply(m: ViewModel, actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - val ViewModel(homePage, errorName, title, subTitle) = m - div( - cls := "min-h-full pt-16 pb-12 flex flex-col bg-white", - main( - cls := "flex-grow flex flex-col justify-center max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex-shrink-0 flex justify-center", - a( - href := "/", - cls := "inline-flex", - span( - cls := "sr-only", - """Workflow""" - ), - img( - cls := "h-12 w-auto", - src := "https://tailwindui.com/img/logos/workflow-mark.svg?color=indigo&shade=600", - alt := "" - ) - ) - ), - div( - cls := "py-16", - div( - cls := "text-center", - p( - cls := "text-sm font-semibold text-indigo-600 uppercase tracking-wide", - errorName - ), - h1( - cls := "mt-2 text-4xl font-extrabold text-gray-900 tracking-tight sm:text-5xl", - title - ), - p( - cls := "mt-2 text-base text-gray-500", - subTitle - ), - div( - cls := "mt-6", - PageLink - .container(homePage, actionBus) - .amend( - cls := "text-base font-medium text-indigo-600 hover:text-indigo-500", - """Go back home""" - ) - ) - ) - ) - ), - footer( - cls := "flex-shrink-0 max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", - nav( - cls := "flex justify-center space-x-4", - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Contact Support""" - ), - span( - cls := "inline-block border-l border-gray-300", - aria.hidden := true - ), - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Status""" - ), - span( - cls := "inline-block border-l border-gray-300", - aria.hidden := true - ), - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Twitter""" - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala deleted file mode 100644 index a3b00af..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala +++ /dev/null @@ -1,103 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import scala.scalajs.js.annotation.JSExportTopLevel -import scala.scalajs.js.annotation.JSExport -import scala.scalajs.js.annotation.JSImport -import scala.scalajs.js -import org.scalajs.dom -import com.raquo.laminar.api.L.{*, given} -import scala.scalajs.js.Date -import com.raquo.waypoint.Router -import com.raquo.waypoint.SplitRender -import cz.e_bs.cmi.mdr.pdb.app.services.DataFetcher -import scala.scalajs.js.JSON -import zio.json._ -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.state.AppState - -@js.native -@JSImport("stylesheets/main.css", JSImport.Namespace) -object Css extends js.Any - -@js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native -@JSImport("params/pdb-params.json", JSImport.Default) -object pdbParams extends js.Object - -@JSExportTopLevel("app") -object Main: - - @JSExport - def main(args: Array[String]): Unit = - import Routes.given - onLoad { - setupAirstream() - val appContainer = dom.document.querySelector("#app") - val _ = - render( - appContainer, - renderPage(state.MockAppState(using unsafeWindowOwner, router)) - ) - } - - private def onLoad(f: => Unit): Unit = - documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) - - private def setupAirstream()(using router: Router[Page]): Unit = - AirstreamError.registerUnhandledErrorCallback(err => - router.forcePage( - Page.UnhandledError( - Some(err.getClass.getName), // TODO: Fill only in dev mode - Some(err.getMessage) - ) - ) - ) - - def renderPage(state: AppState)(using - router: Router[Page] - ): HtmlElement = - val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) - .collectSignal[Page.Detail]( - connectors - .DetailPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailParametru]( - connectors - .DetailParametruPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailKriteria]( - connectors - .DetailKriteriaPageConnector(state)(_) - .apply - ) - .collectSignal[Page.UpravDukazKriteria]( - pages.detail.UpravDukaz.Connector(state)(_).apply - ) - .collectStatic(Page.Dashboard)( - connectors.DashboardPageConnector(state.actionBus).apply - ) - .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) - ) - .collect[Page.UnhandledError](pg => - pages.errors - .UnhandledErrorPage( - pages.errors.UnhandledErrorPage - .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), - state.actionBus - ) - ) - .collectStatic(Page.Directory)( - connectors - .DirectoryPageConnector(state.users, state.actionBus) - .apply - ) - div(cls := "h-full", child <-- pageSplitter.$view) - - // Pull in the stylesheet - val css: Css.type = Css diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala deleted file mode 100644 index 3bbc8c0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ /dev/null @@ -1,198 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.* -import org.scalajs.dom -import zio.json.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import scala.scalajs.js -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.Page.Titled - -// enum is not working with Waypoints' SplitRender collectStatic -sealed abstract class Page( - val id: String, - val title: String, - val parent: Option[Page] -) { - val path: Vector[Page] = - parent match - case None => Vector(this) - case Some(p) => p.path :+ this - - val isRoot: Boolean = parent.isEmpty -} - -object Page: - - case class Titled[V](value: V, title: Option[String] = None): - val show: String = title.getOrElse(value.toString) - - case object Directory extends Page("directory", "Adresář", None) - - case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) - - case class Detail(osobniCislo: Titled[OsobniCislo]) - extends Page("user", osobniCislo.show, Some(Directory)) - - object Detail { - def apply(o: UserInfo): Detail = Detail( - Titled(o.personalNumber, Some(o.name)) - ) - } - - case class DetailParametru( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String] - ) extends Page( - "parameter", - parametr.show, - Some(Detail(osobniCislo)) - ) - - object DetailParametru { - def apply(o: UserInfo, p: Parameter): DetailParametru = - DetailParametru( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)) - ) - } - - case class DetailKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "criteria", - kriterium.show, - Some(DetailParametru(osobniCislo, parametr)) - ) - - object DetailKriteria { - def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = - DetailKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class UpravDukazKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "addProof", - "Důkaz", - Some(DetailKriteria(osobniCislo, parametr, kriterium)) - ) - - object UpravDukazKriteria { - def apply( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): UpravDukazKriteria = - UpravDukazKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class NotFound(url: String) extends Page("404", "404", Some(Directory)) - - case class UnhandledError( - errorName: Option[String], - errorMessage: Option[String] - ) extends Page("500", "Unexpected error", Some(Directory)) - -object Routes: - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) - given [V: JsonEncoder]: JsonEncoder[Titled[V]] = - DeriveJsonEncoder.gen[Titled[V]] - given [V: JsonDecoder]: JsonDecoder[Titled[V]] = - DeriveJsonDecoder.gen[Titled[V]] - given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] - given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] - - val base = - js.`import`.meta.env.BASE_URL - .asInstanceOf[String] + "app" - - val homePage: Page = Page.Directory - - given router: Router[Page] = Router[Page]( - routes = List( - Route.static(homePage, root / endOfSegments, basePath = base), - Route.static( - Page.Dashboard, - root / "dashboard" / endOfSegments, - basePath = base - ), - Route[Page.Detail, String]( - encode = _.osobniCislo.value.toString, - decode = osc => Page.Detail(Titled(OsobniCislo(osc))), - root / "osoba" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.DetailParametru, (String, String)]( - encode = p => (p.osobniCislo.value.toString, p.parametr.value), - decode = - p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / endOfSegments, - basePath = base - ), - Route[Page.DetailKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.DetailKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.UpravDukazKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.UpravDukazKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / "edit" / endOfSegments, - basePath = base - ) - ), - serializePage = _.toJson, - deserializePage = _.fromJson[Page] - .fold(s => throw IllegalStateException(s), identity), - getPageTitle = _.title, - routeFallback = url => Page.NotFound(url), - deserializeFallback = _ => Page.Dashboard - )( - $popStateEvent = windowEvents.onPopState, - owner = unsafeWindowOwner - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala deleted file mode 100644 index b24b82b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -sealed trait Action - -case object FetchDirectory extends Action -case class FetchUserDetails(osc: OsobniCislo) extends Action -case class FetchParameters(osc: OsobniCislo) extends Action -case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action -case class FetchParameterCriteria( - osc: OsobniCislo, - paramId: String, - critId: String, - page: (UserInfo, Parameter, ParameterCriteria) => Page -) extends Action -case class FetchAvailableFiles(osc: OsobniCislo) extends Action -case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala deleted file mode 100644 index c637d35..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala +++ /dev/null @@ -1,82 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.{UserProfile, UserInfo, OsobniCislo} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo -import cz.e_bs.cmi.mdr.pdb.UserFunction - -object AppPage: - // TODO: pages by logged in user - val pages: List[Page] = List(Page.Directory, Page.Dashboard) - - import NavigationBar.{Logo, MenuItem} - - val logo = Logo( - "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", - "Workflow" - ) - - // TODO: menu items by user profile - val userMenu = - List( - MenuItem("Your Profile"), - MenuItem("Settings"), - MenuItem("Sign out") - ) - - // TODO: load user profile - val $userProfile = Var( - UserProfile( - "tom", - UserInfo( - OsobniCislo("1031"), - "tom", - "Tom", - "Cook", - None, - None, - Some("tom@example.com"), - Some("+420 222 866 180"), - Some( - UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") - ), - Nil, - None - ) - ) - ) - - val $userInfo = $userProfile.signal.map(_.userInfo) - - type ViewModel = Option[HtmlElement] - def apply( - actionBus: Observer[Action] - )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using - router: Router[Page] - ): HtmlElement = - PageLayout(actionBus)( - $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => - PageLayout.ViewModel( - NavigationBar.ViewModel( - u, - pages.map(p => - NavigationBar.Link( - () => - PageLink( - p, - actionBus - ), - p == cp - ) - ), - userMenu, - logo - ), - c - ) - ), - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala deleted file mode 100644 index bf473c6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ /dev/null @@ -1,143 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden -import fiftyforms.ui.components.tailwind.list.IconText.ViewModel -import fiftyforms.ui.components.tailwind.Icons -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Action - -object Breadcrumbs: - - private def slash = - import svg.{*, given} - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-300", - xmlns := "http://www.w3.org/2000/svg", - fill := "currentColor", - viewBox := "0 0 20 20", - ariaHidden := true, - path( - d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" - ) - ) - - def backIcon = - Icons.solid - .`arrow-narrow-left`() - .amend( - svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" - ) - - object Link: - case class ViewModel( - page: Page, - icon: Option[SvgElement], - extraClasses: String, - text: String, - textClasses: Option[String] = None - ) - - val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" - - def shortHome(p: Page) = ViewModel( - p, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - "Zpět na úvodní stránku", - None - ) - - def fullHome(p: Page) = ViewModel( - p, - Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), - "text-gray-400 hover:text-gray-500", - "Domů", - Some("sr-only") - ) - - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - PageLink - .container($m.map(_.page), actionBus) - .amend( - cls <-- $m.map(_.extraClasses), - child.maybe <-- $m.map(_.icon), - span( - cls <-- $m.map(_.textClasses.getOrElse("")), - child.text <-- $m.map(_.text) - ) - ) - - object FullBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - ol( - role := "list", - cls := "flex items-center space-x-4", - children <-- $m.map(_.path).split(_.id)((_, _, $p) => - li( - div( - cls := "flex items-center", - child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), - Link( - $p.map(p => - if (p.isRoot) then Link.fullHome(p) - else - Link.ViewModel( - p, - None, - s"ml-4 max-w-xs truncate ${Link.baseClasses}", - p.title - ) - ), - actionBus - ) - ) - ) - ) - ) - - object ShortBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - Link( - $m.map { p => - val target = p.parent.getOrElse(p) - if target.isRoot then Link.shortHome(target) - else - Link.ViewModel( - target, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - s"Zpět na ${target.title}" - ) - }, - actionBus - ) - - def apply(actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - nav( - cls := "flex", - aria.label := "Breadcrumb", - div( - cls := "flex sm:hidden", - ShortBreadcrumbs(router.$currentPage, actionBus) - ), - div( - cls := "hidden sm:block", - FullBreadcrumbs(router.$currentPage, actionBus) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala deleted file mode 100644 index 0db2047..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ /dev/null @@ -1,242 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent -import fiftyforms.ui.components.tailwind.Icons -import fiftyforms.ui.components.tailwind.Avatar -import cz.e_bs.cmi.mdr.pdb.UserInfo -import io.laminext.syntax.core.* - -object NavigationBar: - - case class Logo(img: String, name: String) - case class Link(a: () => Anchor, active: Boolean) - case class MenuItem(title: String) - - case class ViewModel( - userInfo: UserInfo, - pages: List[Link], - userMenu: List[MenuItem], - logo: Logo - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - val $userInfo = $m.map(_.userInfo) - val mobileMenuOpen = Var(false) - - val desktopOnly = cls("hidden md:block") - val mobileOnly = cls("md:hidden") - - inline def avatarImage(size: Int = 8) = - Avatar($userInfo.map(_.img)).avatarImage(size) - - def notificationButton = button( - tpe := "button", - cls := List( - "bg-indigo-600", - "focus:outline-none", - "focus:ring-2", - "focus:ring-offset-2", - "focus:ring-offset-indigo-600", - "focus:ring-white", - "hover:text-white", - "p-1", - "rounded-full", - "text-indigo-200" - ), - span(cls := "sr-only", "View notifications"), - Icons.outline.bell() - ) - - def userProfile: HtmlElement = - val menuOpen = Var(false) - - def menuItem(item: MenuItem, idx: Int): HtmlElement = - a( - href := "#", - cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", - role := "menuitem", - tabIndex := -1, - idAttr := s"user-menu-item-$idx", - item.title - ) - - div( - cls := "ml-3 relative", - div( - button( - tpe := "button", - cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - idAttr := "user-menu-button", - aria.expanded <-- menuOpen.signal, - aria.hasPopup := true, - span(cls := "sr-only", "Open user menu"), - child <-- avatarImage(), - onClick.preventDefault.mapTo( - !menuOpen.now() - ) --> menuOpen.writer - ) - ), - /* - * */ - div( - cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", - cls <-- menuOpen.signal.map { o => - if (o) "md:block" else "md:hidden" - }, - role := "menu", - aria.orientation := "vertical", - aria.labelledBy := "user-menu-button", - tabIndex := -1, - // : keyboard navigation - children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) - ) - ) - - def mobileProfile = - def menuItem(item: MenuItem): HtmlElement = - a( - href := "#", - cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", - item.title - ) - - div( - cls := "pt-4 pb-3 border-t border-indigo-700", - div( - cls := "flex items-center px-5", - div( - cls := "flex-shrink-0", - child <-- avatarImage(10) - ), - div( - cls := "ml-3", - div( - cls := "text-base font-medium text-white", - child.text <-- $userInfo.map(_.name) - ), - child.maybe <-- $userInfo.map( - _.email.map(e => - div( - cls := "text-sm font-medium text-indigo-300", - e - ) - ) - ) - ), - notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) - ), - div( - cls := "mt-3 px-2 space-y-1", - children <-- $m.map(_.userMenu.map(menuItem)) - ) - ) - - def pageLink(page: Link): Anchor = - page - .a() - .amend( - cls := "text-white px-3 py-2 rounded-md text-sm font-medium", - cls := Seq( - "bg-indigo-700" -> page.active, - "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active - ), - ariaCurrent := (if page.active then "page" else "false") - ) - - def logoImg: Image = - img( - cls := "h-8 w-8", - src <-- $m.map(_.logo.img), - alt <-- $m.map(_.logo.name) - ) - - def mobileMenuButton = button( - tpe := "button", - cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - aria.controls := "mobile-menu", - aria.expanded <-- mobileMenuOpen.signal, - span(cls := "sr-only", "Open main menu"), - Icons.outline - .menu() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") - ), - Icons.outline - .x() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") - ), - onClick.preventDefault.mapTo( - !mobileMenuOpen.now() - ) --> mobileMenuOpen.writer - ) - - def navBarLeft = - div( - cls := "flex items-center", - div(cls := "flex-shrink-0", logoImg), - div( - desktopOnly, - div( - cls := "ml-10 flex items-baseline space-x-4", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ) - ) - ) - - def navBarRight = - div( - desktopOnly, - div( - cls := "ml-4 flex items-center md:ml-6", - notificationButton, - userProfile - ) - ) - - def navBarMobile = - div( - cls := "-mr-2 flex", - mobileOnly, - mobileMenuButton - ) - - def navBar = - div( - cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex items-center justify-between h-16", - navBarLeft, - navBarRight, - navBarMobile - ) - ) - - def mobileMenu = - div( - mobileOnly, - cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }, - idAttr := "mobile-menu", - div( - cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ), - mobileProfile - ) - - nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala deleted file mode 100644 index 75339df..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object PageHeader: - def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = - header( - cls := "bg-white shadow-sm", - div( - cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", - h1( - cls := "text-lg leading-6 font-semibold text-gray-900", - Breadcrumbs(actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala deleted file mode 100644 index 5096463..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ /dev/null @@ -1,30 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import fiftyforms.ui.components.tailwind.Loading -import io.laminext.syntax.core.* - -object PageLayout: - case class ViewModel( - navigation: NavigationBar.ViewModel, - content: Option[HtmlElement] - ) - def apply(actionBus: Observer[Action])( - $m: Signal[ViewModel], - mods: Modifier[HtmlElement]* - )(using router: Router[Page]): HtmlElement = - val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) - div( - cls := "h-full flex flex-col", - NavigationBar($m.map(_.navigation)), - child.maybe <-- router.$currentPage.map(_.isRoot) - .switch(None, Some(PageHeader(actionBus))), - main( - cls := "flex-grow-1 overflow-y-auto", - mods, - child <-- $maybeContent.map(_.getOrElse(Loading)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala deleted file mode 100644 index 62d820f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.LinkSupport.* -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo - -object PageLink: - type ViewModel = Page - def apply($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = - a(mods($m, actions), child.text <-- $m.map(_.title)) - - def apply(m: ViewModel, actions: Observer[Action])(using - router: Router[Page] - ): Anchor = apply(Val(m), actions) - - def container($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = a(mods($m, actions)) - - def container(m: ViewModel, actions: Observer[Action])(using - Router[Page] - ): Anchor = container(Val(m), actions) - - private def mods($m: Signal[Page], actions: Observer[Action])(using - router: Router[Page] - ): Modifier[Anchor] = - Seq( - href <-- $m.map(router.absoluteUrlForPage).recover { case _ => - Some("invalid url") - }, - composeEvents(onClick.noKeyMod.preventDefault)( - _.sample($m) - .map(NavigateTo.apply) - ) --> actions - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala deleted file mode 100644 index 87f825a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.pages.dashboard.DashboardPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -class DashboardPageConnector(actionBus: Observer[Action])(using - router: Router[Page] -): - def apply: HtmlElement = - AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala deleted file mode 100644 index e9b0f71..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala +++ /dev/null @@ -1,76 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailKriteriaPage -import pages.detail.components.DukazyKriteria -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -object DetailKriteriaPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailKriteriaPageConnector( - state: DetailKriteriaPageConnector.AppState -)( - $page: Signal[Page.DetailKriteria] -)(using Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => - (p.osobniCislo.value, p.parametr.value, p.kriterium.value) - )((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map( - FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) - ) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, s, $s) => - DetailKriteriaPage($s)(state.actionBus.contramap { - case DukazyKriteria.Add => - NavigateTo( - Page.UpravDukazKriteria( - Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), - Page.Titled(s.parametr.id, Some(s.parametr.nazev)), - Page.Titled(s.kriterium.id, Some(s.kriterium.id)) - ) - ) - }) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): DetailKriteriaPage.ViewModel = - DetailKriteriaPage.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala deleted file mode 100644 index 4d8024f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ /dev/null @@ -1,57 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import pages.detail.DetailPage -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import fiftyforms.ui.components.tailwind.Color - -object DetailPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailPageConnector(state: DetailPageConnector.AppState)( - $page: Signal[Page.Detail] -)(using router: Router[Page]): - val $oscChangeSignal = $page.splitOne(_.osobniCislo.value)((osc, _, _) => osc) - val $pageChangeSignal = - $oscChangeSignal.flatMap(osc => - EventStream.fromSeq(Seq(FetchUserDetails(osc), FetchParameters(osc))) - ) - // TODO: filter the value based on the current osc - // OSC change will fetch new data, but still - // - we need to be sure that what we got is really what we ought to display - // - we want to display stale data accordingly (at least with loading indicator) - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - def apply: HtmlElement = - AppPage(state.actionBus)( - $data.combineWithFn($params)(_ zip _) - .map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: List[Parameter] - ): DetailPage.ViewModel = - DetailPage.ViewModel( - o.toDetailOsoby, - p.map( - _.toParametr(param => - PageLink.container(Page.DetailParametru(o, param), state.actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala deleted file mode 100644 index fdaa7f8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailParametruPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink - -object DetailParametruPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailParametruPageConnector( - state: DetailParametruPageConnector.AppState -)( - $page: Signal[Page.DetailParametru] -)(using router: Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => (p.osobniCislo.value, p.parametr.value))((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map(FetchParameter(_, _)) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - } yield (da, pb) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailParametruPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter - ): DetailParametruPage.ViewModel = - DetailParametruPage.ViewModel( - o.toDetailOsoby, - p.toParametr(p => - PageLink.container(Page.DetailParametru(o, p), state.actionBus) - ), - p.criteria.map( - _.toKriterium { c => - PageLink.container( - Page.DetailKriteria(o, p, c), - state.actionBus - ) - } - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala deleted file mode 100644 index 049aa22..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -case class DirectoryPageConnector( - $input: EventStream[List[UserInfo]], - actionBus: Observer[Action] -)(using router: Router[Page]): - val $data = $input.startWithNone - val $actionSignal = EventStream.fromValue(FetchDirectory) - - def apply: HtmlElement = - AppPage(actionBus)( - $data.split(_ => ())((_, _, s) => - pages.directory.DirectoryPage( - s.map( - _.map( - _.toUserRow(u => - PageLink.container( - Page.Detail(Page.Titled(u.personalNumber)), - actionBus - ) - ) - ) - ) - ) - ), - $actionSignal --> actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala deleted file mode 100644 index 88e7450..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ /dev/null @@ -1,68 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii -import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria -import fiftyforms.ui.components.tailwind.Color - -extension (o: UserInfo) - def toDetailOsoby: DetailOsoby.ViewModel = - DetailOsoby.ViewModel( - o.personalNumber, - o.name, - o.email, - o.phone, - o.img, - o.mainFunction.map(f => - DetailOsoby.Funkce.ViewModel(f.name, f.dept, f.ou) - ), - o.userContracts.headOption.map(c => - DetailOsoby.PracovniPomer.ViewModel(c.rel, c.startDate, c.endDate) - ) - ) - -extension (param: Parameter) - def toParametr(container: Parameter => Anchor): DetailParametru.ViewModel = - DetailParametru.ViewModel( - id = param.id, - nazev = param.name, - popis = param.description, - status = "Nesplněno", - statusColor = Color.red, - a = container(param) - ) - -extension (crit: ParameterCriteria) - def toKriterium( - container: ParameterCriteria => Anchor - ): DetailKriteria.ViewModel = - DetailKriteria.ViewModel( - nazev = crit.criteriumText, - kapitola = crit.chapterId, - bod = crit.itemId, - status = "Nesplněno", - statusColor = Color.red, - splneno = false, - dukazy = Nil, - container = container(crit) - ) - -extension (user: UserInfo) - def toUserRow( - container: UserInfo => HtmlElement = (_: UserInfo) => div() - ): UserRow.ViewModel = - UserRow.ViewModel( - osobniCislo = user.personalNumber.toString, - celeJmeno = user.name, - prijmeni = user.surname, - hlavniFunkce = user.mainFunction.map(_.name), - img = user.img, - container = () => container(user) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala deleted file mode 100644 index 7d5b2c1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.dashboard - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.Page - -object DashboardPage: - - def render: HtmlElement = - div("Dashboard page") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala deleted file mode 100644 index 9ef0698..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala +++ /dev/null @@ -1,31 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailKriteriaPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply($m: Signal[ViewModel])( - action: Observer[DukazyKriteria.Action] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - DukazyKriteria($m.map(_.kriterium.dukazy))(action) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala deleted file mode 100644 index 3b316e1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object DetailPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametry: SeznamParametru.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby($m.map(_.osoba)), - SeznamParametru($m.map(_.parametry)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala deleted file mode 100644 index 17224cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala +++ /dev/null @@ -1,24 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailParametruPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriteria: SeznamKriterii.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby.header($m.map(_.osoba)), - DetailParametru($m.map(_.parametr)), - SeznamKriterii($m.map(_.kriteria)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala deleted file mode 100644 index dc912fa..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ /dev/null @@ -1,107 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb -package app -package pages.detail - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.UpravDukazForm -import fiftyforms.services.files.File - -object UpravDukaz: - - type ThisPage = Page.UpravDukazKriteria - type PageKey = (OsobniCislo, String, String) - - trait State { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def availableFiles: EventStream[List[File]] - def actionBus: Observer[Action] - } - - def keyOfPage(page: ThisPage): PageKey = - (page.osobniCislo.value, page.parametr.value, page.kriterium.value) - - def onChangeAction(key: PageKey): Action = - FetchParameterCriteria( - key._1, - key._2, - key._3, - Page.UpravDukazKriteria(_, _, _) - ) - - class Connector(state: State)($page: Signal[ThisPage])(using Router[Page]): - val $paramChangeSignal = $page.splitOne(keyOfPage)((x, _, _) => x) - val $pageChangeSignal = $paramChangeSignal.map(onChangeAction) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.split(_ => ())((_, s, $s) => - PageComponent( - $s.map(buildModel), - state.availableFiles, - state.actionBus.contramap { - case UpravDukazForm.Cancelled => - NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) - case UpravDukazForm.AvailableFilesRequested => - FetchAvailableFiles(s._1.personalNumber) - } - ) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): PageComponent.ViewModel = - import connectors.* - PageComponent.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) - - object PageComponent: - import components.* - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply( - $m: Signal[ViewModel], - availableFilesStream: EventStream[List[File]], - events: Observer[UpravDukazForm.Event] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - UpravDukazForm(availableFilesStream)(events) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala deleted file mode 100644 index ca311b0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.Icons -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Color - -object DetailKriteria: - case class ViewModel( - nazev: String, - kapitola: String, - bod: String, - status: String, - statusColor: Color, - splneno: Boolean, - dukazy: List[DukazKriteria.ViewModel], - container: HtmlElement = div() - ) { - val id = s"${kapitola}${bod}" - } - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h3( - cls := "text-xl leading-6 font-bold text-gray-900", - child.text <-- $m.map(_.nazev) - ), - h3( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - "Kapitola ", - child.text <-- $m.map(_.kapitola), - " bod ", - child <-- $m.map(_.bod) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala deleted file mode 100644 index ae90290..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala +++ /dev/null @@ -1,100 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Avatar - -object DetailOsoby: - - object Funkce: - case class ViewModel( - nazev: String, - stredisko: String, - voj: String - ) - - def render($m: Signal[ViewModel]): HtmlElement = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.nazev), - span( - cls := "hidden md:inline", - " @ ", - child.text <-- $m.map(_.stredisko), - ", ", - child.text <-- $m.map(_.voj) - ) - ) - - object PracovniPomer: - case class ViewModel( - druh: String, - pocatek: LocalDate, - konec: Option[LocalDate] - ) - - def render($m: Signal[ViewModel]): HtmlElement = - import fiftyforms.ui.components.tailwind.CustomAttrs.datetime - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.druh), - " od ", - time( - datetime <-- $m.map(_.pocatek.toString), - child.text <-- $m.map(_.pocatek.toString) - ) - ) - - case class ViewModel( - osobniCislo: OsobniCislo, - jmeno: String, - email: Option[String], - telefon: Option[String], - img: Option[String], - hlavniFunkce: Option[Funkce.ViewModel], - pracovniPomer: Option[PracovniPomer.ViewModel] - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-4", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(16) - ), - div( - h1( - cls := "text-2xl font-bold text-gray-900", - child.text <-- $m.map(_.jmeno) - ), - child.maybe <-- $m.map(_.hlavniFunkce).split(_ => ())((_, _, d) => - Funkce.render(d) - ), - child.maybe <-- $m.map(_.pracovniPomer).split(_ => ())((_, _, d) => - PracovniPomer.render(d) - ) - ) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-5", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(8) - ), - div( - h1( - cls := "text-lg font-medium text-gray-800", - child.text <-- $m.map(_.jmeno) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala deleted file mode 100644 index e11cb62..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color - -object DetailParametru: - case class ViewModel( - id: String, - nazev: String, - popis: String, - status: String, - statusColor: Color, - a: Anchor - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h2( - cls := "text-2xl leading-6 font-medium text-gray-900", - child.text <-- $m.map(_.nazev) - ), - p( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - child.text <-- $m.map(_.popis) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - h2( - cls := "text-lg leading-6 font-medium text-gray-600", - child.text <-- $m.map(_.nazev) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala deleted file mode 100644 index a00ede8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala +++ /dev/null @@ -1,177 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.CustomAttrs - -object DukazKriteria: - case class Osoba(osobniCislo: String, jmeno: String) - case class Udalost(osoba: Osoba, datum: LocalDate) - case class Dokument( - url: String, - nazev: String, - pridal: Udalost, - odebral: Option[Udalost] - ) - case class ViewModel( - dokumenty: List[Dokument], - autorizoval: Option[Udalost], - platiDo: Option[LocalDate], - poznámka: Option[String] - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-lg", - div( - cls := "px-4 py-5 sm:px-6", - h3( - cls := "text-lg leading-6 font-medium text-gray-900", - """Applicant Information""" - ), - p( - cls := "mt-1 max-w-2xl text-sm text-gray-500", - """Personal details and application.""" - ) - ), - div( - cls := "border-t border-gray-200 px-4 py-5 sm:px-6", - dl( - cls := "grid grid-cols-1 gap-x-4 gap-y-8 sm:grid-cols-2", - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Full name""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Margot Foster""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Application for""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Backend Developer""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Email address""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """margotfoster@example.com""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Salary expectation""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """$120,000""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """About""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Fugiat ipsum ipsum deserunt culpa aute sint do nostrud anim incididunt cillum culpa consequat. Excepteur qui ipsum aliquip consequat sint. Sit id mollit nulla mollit nostrud in ea officia proident. Irure nostrud pariatur mollit ad adipisicing reprehenderit deserunt qui eu.""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """Attachments""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - ul( - role := "list", - cls := "border border-gray-200 rounded-md divide-y divide-gray-200", - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """resume_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ), - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """coverletter_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala deleted file mode 100644 index 22f134f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala +++ /dev/null @@ -1,35 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object DukazyKriteria: - sealed trait Action - case object Add extends Action - - type ViewModel = List[DukazKriteria.ViewModel] - - def apply($m: Signal[ViewModel])(actions: Observer[Action]): HtmlElement = - div( - child.maybe <-- $m.map(m => - if (m.isEmpty) then Some(prazdnyDukaz(actions)) - else None - ), - children <-- $m.map(_.zipWithIndex).split(_._2)((_, _, $s) => - DukazKriteria($s.map(_._1)) - ) - ) - - private def prazdnyDukaz(actions: Observer[Action]): HtmlElement = - button( - tpe := "button", - onClick.preventDefault.mapTo(Add) --> actions, - cls := "relative block w-full border-2 border-gray-300 border-dashed rounded-lg p-12 text-center hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - Icons.outline - .`document-add`(12) - .amend(svg.cls := "mx-auto text-gray-400"), - span( - cls := "mt-2 block text-sm font-medium text-gray-900", - "Přidat důkaz" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala deleted file mode 100644 index f628697..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.list.StackedList -import fiftyforms.ui.components.tailwind.list.ListRow -import fiftyforms.ui.components.tailwind.list.RowTag -import fiftyforms.ui.components.tailwind.list.RowNext -import java.time.LocalDate - -object SeznamKriterii: - type ViewModel = List[DetailKriteria.ViewModel] - - private val kritList = new StackedList[DetailKriteria.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - kritList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = - p(cls := "text-sm text-gray-500", s"${i.kapitola}${i.bod}"), - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.container - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala deleted file mode 100644 index 2bfe4bc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.list.{ - StackedList, - ListRow, - RowTag, - PropList, - IconText, - RowNext -} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.LinkSupport.* - -object SeznamParametru: - type ViewModel = List[DetailParametru.ViewModel] - - private val parametrList = new StackedList[DetailParametru.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - parametrList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = emptyNode, - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.a - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala deleted file mode 100644 index f048e9d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ /dev/null @@ -1,101 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.form.* -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement -import fiftyforms.services.files.components.tailwind.FilePicker -import cz.e_bs.cmi.mdr.pdb.frontend.AutorizujDukaz -import cz.e_bs.cmi.mdr.pdb.frontend.DocumentRef -import fiftyforms.services.files.File - -object UpravDukazForm: - sealed trait Event - case object Cancelled extends Event - case object AvailableFilesRequested extends Event - def apply(availableFilesStream: EventStream[List[File]])( - updates: Observer[Event] - ): HtmlElement = - val (filesStream, filesObserver) = - EventStream.withObserver[FilePicker.Event] - val files = Var[List[File]](Nil) - def submitButtons: HtmlElement = - div( - cls := "pt-5", - div( - cls := "flex justify-end", - button( - tpe := "button", - cls := "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zrušit", - onClick.mapTo(Cancelled) --> updates - ), - button( - tpe := "submit", - cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - disabled <-- files.signal.map(_.isEmpty), - "Autorizovat" - ) - ) - ) - - div( - filesStream.collect { case FilePicker.SelectionUpdated(files) => - files.to(List) - } --> files.writer, - filesStream.collect { case FilePicker.AvailableFilesRequested => - AvailableFilesRequested - } --> updates, - cls := "bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6", - Form( - Form.Body( - Form.Section( - FormHeader( - FormHeader.ViewModel( - "Doložení kritéria", - "Sestavte doklady poskytující důkaz kritéria a potvrďte odesláním formuláře. Případné limitace či jiné relevantní údaje vepište do pole Komentář." - ) - ), - FormFields( - FormRow( - "dokumenty", - "Dokumenty", - FilePicker(files.signal, availableFilesStream)(filesObserver) - .amend(idAttr := "dokumenty", cls("max-w-lg")) - ).toHtml, - FormRow( - "platnost", - "Platnost", - input( - idAttr := "platnost", - name := "platnost", - tpe := "date", - autoComplete := "date", - cls := "block max-w-lg w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" - ) - ).toHtml, - FormRow( - "komentar", - "Komentář", - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - textArea( - idAttr := "komentar", - name := "about", - rows := 3, - cls := "max-w-lg shadow-sm block w-full focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border border-gray-300 rounded-md" - ), - p( - cls := "mt-2 text-sm text-gray-500", - "Doplňte prosím potřebné informace související s doložením kritéria, včetně případných limitací." - ) - ) - ).toHtml - ) - ) - ), - submitButtons - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala deleted file mode 100644 index 41033f6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -import components._ - -object DirectoryPage: - - type ViewModel = List[UserRow.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - val (actionsStream, actionObserver) = - EventStream.withObserver[SearchForm.Action] - val $filter = actionsStream - .collect { - case SearchForm.ClearFilter => None - case SearchForm.SetFilter(t) => Some(t) - } - .startWith(None) - val byLetter = for { - d <- $m - f <- $filter - } yield for { - (letter, users) <- d - .filter { user => - f match - case None => true - case Some(t) => user.search.contains(t) - } - .groupBy(_.prijmeni.head) - .to(List) - .sortBy(_._1) - } yield (letter.toString, users.sortBy(_.prijmeni)) - - div( - cls := "h-full max-w-7xl mx-auto order-first flex flex-col flex-shrink-0", - SearchForm(actionObserver), - Directory(byLetter) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala deleted file mode 100644 index 73f915d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -object Directory: - - object Header: - type ViewModel = String - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500", - h3(child.text <-- $m) - ) - - object UserList: - type ViewModel = List[UserRow.ViewModel] - def apply($m: Signal[ViewModel]): HtmlElement = - ul( - role := "list", - cls := "relative z-0 divide-y divide-gray-200", - children <-- $m.split(_.osobniCislo)((_, _, s) => UserRow(s)) - ) - - type ViewModel = List[(String, List[UserRow.ViewModel])] - def apply($m: Signal[ViewModel]): HtmlElement = - nav( - cls := "flex-1 min-h-0 overflow-y-auto", - aria.label := "Directory", - children <-- $m.split(_._1)((_, _, s) => - div( - cls := "relative", - Header(s.map(_._1)), - UserList(s.map(_._2)) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala deleted file mode 100644 index e182910..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala +++ /dev/null @@ -1,54 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object SearchForm: - sealed trait Action - case object SubmitFilter extends Action - sealed trait FilterAction extends Action - case object ClearFilter extends FilterAction - case class SetFilter(value: String) extends FilterAction - - def apply(actions: Observer[Action]): HtmlElement = - div( - cls := "px-6 pt-4 pb-4", - form( - cls := "flex space-x-4", - onSubmit.mapTo(SubmitFilter) --> actions, - div( - cls := "flex-1 min-w-0", - label( - forId := "search", - cls := "sr-only", - "Hledat" - ), - div( - cls := "relative rounded-md shadow-sm", - div( - cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", - Icons.solid.search().amend(svg.cls := "text-gray-400") - ), - input( - tpe := "search", - name := "search", - idAttr := "search", - cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", - placeholder := "Hledat", - composeEvents(onInput.mapToValue.setAsValue.map(SetFilter(_)))( - _.throttle(500) - ) --> actions - ) - ) - ), - button( - tpe := "submit", - cls := "inline-flex justify-center px-3.5 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500", - Icons.solid.filter().amend(svg.cls := "text-gray-400"), - span( - cls := "sr-only", - "Hledat" - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala deleted file mode 100644 index 82f7723..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala +++ /dev/null @@ -1,52 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.airstream.core.Signal -import fiftyforms.ui.components.tailwind.Avatar - -object UserRow: - case class ViewModel( - osobniCislo: String, - celeJmeno: String, - prijmeni: String, - hlavniFunkce: Option[String], - img: Option[String], - container: () => HtmlElement = () => div() - ) { - val search = prijmeni.toLowerCase - } - - def apply($m: Signal[ViewModel]): HtmlElement = - inline def avatarImage = - Avatar($m.map(_.img)).avatarImage(10) - - li( - div( - cls := "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500", - div( - cls := "flex-shrink-0", - child <-- avatarImage - ), - div( - cls := "flex-1 min-w-0", - child <-- $m.map { o => - o.container() - .amend( - cls := "focus:outline-none", - span( - cls := "absolute inset-0", - aria.hidden := true - ), - p( - cls := "text-sm font-medium text-gray-900", - o.celeJmeno - ), - p( - cls := "text-sm text-gray-500 truncate", - o.hlavniFunkce - ) - ) - } - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala deleted file mode 100644 index 467c5f5..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala +++ /dev/null @@ -1,99 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router - -object ErrorPage: - case class ViewModel( - homePage: Page, - errorName: String, - title: String, - subTitle: String - ) - - def apply(m: ViewModel, actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - val ViewModel(homePage, errorName, title, subTitle) = m - div( - cls := "min-h-full pt-16 pb-12 flex flex-col bg-white", - main( - cls := "flex-grow flex flex-col justify-center max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex-shrink-0 flex justify-center", - a( - href := "/", - cls := "inline-flex", - span( - cls := "sr-only", - """Workflow""" - ), - img( - cls := "h-12 w-auto", - src := "https://tailwindui.com/img/logos/workflow-mark.svg?color=indigo&shade=600", - alt := "" - ) - ) - ), - div( - cls := "py-16", - div( - cls := "text-center", - p( - cls := "text-sm font-semibold text-indigo-600 uppercase tracking-wide", - errorName - ), - h1( - cls := "mt-2 text-4xl font-extrabold text-gray-900 tracking-tight sm:text-5xl", - title - ), - p( - cls := "mt-2 text-base text-gray-500", - subTitle - ), - div( - cls := "mt-6", - PageLink - .container(homePage, actionBus) - .amend( - cls := "text-base font-medium text-indigo-600 hover:text-indigo-500", - """Go back home""" - ) - ) - ) - ) - ), - footer( - cls := "flex-shrink-0 max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", - nav( - cls := "flex justify-center space-x-4", - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Contact Support""" - ), - span( - cls := "inline-block border-l border-gray-300", - aria.hidden := true - ), - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Status""" - ), - span( - cls := "inline-block border-l border-gray-300", - aria.hidden := true - ), - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Twitter""" - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala deleted file mode 100644 index bd9baec..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action - -object NotFoundPage: - def apply(homePage: Page, url: String, actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - ErrorPage( - ErrorPage.ViewModel( - homePage, - "404 error", - "Page not found.", - s"Sorry, but page $url doesn't exist." - ), - actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala deleted file mode 100644 index a3b00af..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala +++ /dev/null @@ -1,103 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import scala.scalajs.js.annotation.JSExportTopLevel -import scala.scalajs.js.annotation.JSExport -import scala.scalajs.js.annotation.JSImport -import scala.scalajs.js -import org.scalajs.dom -import com.raquo.laminar.api.L.{*, given} -import scala.scalajs.js.Date -import com.raquo.waypoint.Router -import com.raquo.waypoint.SplitRender -import cz.e_bs.cmi.mdr.pdb.app.services.DataFetcher -import scala.scalajs.js.JSON -import zio.json._ -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.state.AppState - -@js.native -@JSImport("stylesheets/main.css", JSImport.Namespace) -object Css extends js.Any - -@js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native -@JSImport("params/pdb-params.json", JSImport.Default) -object pdbParams extends js.Object - -@JSExportTopLevel("app") -object Main: - - @JSExport - def main(args: Array[String]): Unit = - import Routes.given - onLoad { - setupAirstream() - val appContainer = dom.document.querySelector("#app") - val _ = - render( - appContainer, - renderPage(state.MockAppState(using unsafeWindowOwner, router)) - ) - } - - private def onLoad(f: => Unit): Unit = - documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) - - private def setupAirstream()(using router: Router[Page]): Unit = - AirstreamError.registerUnhandledErrorCallback(err => - router.forcePage( - Page.UnhandledError( - Some(err.getClass.getName), // TODO: Fill only in dev mode - Some(err.getMessage) - ) - ) - ) - - def renderPage(state: AppState)(using - router: Router[Page] - ): HtmlElement = - val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) - .collectSignal[Page.Detail]( - connectors - .DetailPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailParametru]( - connectors - .DetailParametruPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailKriteria]( - connectors - .DetailKriteriaPageConnector(state)(_) - .apply - ) - .collectSignal[Page.UpravDukazKriteria]( - pages.detail.UpravDukaz.Connector(state)(_).apply - ) - .collectStatic(Page.Dashboard)( - connectors.DashboardPageConnector(state.actionBus).apply - ) - .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) - ) - .collect[Page.UnhandledError](pg => - pages.errors - .UnhandledErrorPage( - pages.errors.UnhandledErrorPage - .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), - state.actionBus - ) - ) - .collectStatic(Page.Directory)( - connectors - .DirectoryPageConnector(state.users, state.actionBus) - .apply - ) - div(cls := "h-full", child <-- pageSplitter.$view) - - // Pull in the stylesheet - val css: Css.type = Css diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala deleted file mode 100644 index 3bbc8c0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ /dev/null @@ -1,198 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.* -import org.scalajs.dom -import zio.json.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import scala.scalajs.js -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.Page.Titled - -// enum is not working with Waypoints' SplitRender collectStatic -sealed abstract class Page( - val id: String, - val title: String, - val parent: Option[Page] -) { - val path: Vector[Page] = - parent match - case None => Vector(this) - case Some(p) => p.path :+ this - - val isRoot: Boolean = parent.isEmpty -} - -object Page: - - case class Titled[V](value: V, title: Option[String] = None): - val show: String = title.getOrElse(value.toString) - - case object Directory extends Page("directory", "Adresář", None) - - case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) - - case class Detail(osobniCislo: Titled[OsobniCislo]) - extends Page("user", osobniCislo.show, Some(Directory)) - - object Detail { - def apply(o: UserInfo): Detail = Detail( - Titled(o.personalNumber, Some(o.name)) - ) - } - - case class DetailParametru( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String] - ) extends Page( - "parameter", - parametr.show, - Some(Detail(osobniCislo)) - ) - - object DetailParametru { - def apply(o: UserInfo, p: Parameter): DetailParametru = - DetailParametru( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)) - ) - } - - case class DetailKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "criteria", - kriterium.show, - Some(DetailParametru(osobniCislo, parametr)) - ) - - object DetailKriteria { - def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = - DetailKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class UpravDukazKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "addProof", - "Důkaz", - Some(DetailKriteria(osobniCislo, parametr, kriterium)) - ) - - object UpravDukazKriteria { - def apply( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): UpravDukazKriteria = - UpravDukazKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class NotFound(url: String) extends Page("404", "404", Some(Directory)) - - case class UnhandledError( - errorName: Option[String], - errorMessage: Option[String] - ) extends Page("500", "Unexpected error", Some(Directory)) - -object Routes: - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) - given [V: JsonEncoder]: JsonEncoder[Titled[V]] = - DeriveJsonEncoder.gen[Titled[V]] - given [V: JsonDecoder]: JsonDecoder[Titled[V]] = - DeriveJsonDecoder.gen[Titled[V]] - given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] - given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] - - val base = - js.`import`.meta.env.BASE_URL - .asInstanceOf[String] + "app" - - val homePage: Page = Page.Directory - - given router: Router[Page] = Router[Page]( - routes = List( - Route.static(homePage, root / endOfSegments, basePath = base), - Route.static( - Page.Dashboard, - root / "dashboard" / endOfSegments, - basePath = base - ), - Route[Page.Detail, String]( - encode = _.osobniCislo.value.toString, - decode = osc => Page.Detail(Titled(OsobniCislo(osc))), - root / "osoba" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.DetailParametru, (String, String)]( - encode = p => (p.osobniCislo.value.toString, p.parametr.value), - decode = - p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / endOfSegments, - basePath = base - ), - Route[Page.DetailKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.DetailKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.UpravDukazKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.UpravDukazKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / "edit" / endOfSegments, - basePath = base - ) - ), - serializePage = _.toJson, - deserializePage = _.fromJson[Page] - .fold(s => throw IllegalStateException(s), identity), - getPageTitle = _.title, - routeFallback = url => Page.NotFound(url), - deserializeFallback = _ => Page.Dashboard - )( - $popStateEvent = windowEvents.onPopState, - owner = unsafeWindowOwner - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala deleted file mode 100644 index b24b82b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -sealed trait Action - -case object FetchDirectory extends Action -case class FetchUserDetails(osc: OsobniCislo) extends Action -case class FetchParameters(osc: OsobniCislo) extends Action -case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action -case class FetchParameterCriteria( - osc: OsobniCislo, - paramId: String, - critId: String, - page: (UserInfo, Parameter, ParameterCriteria) => Page -) extends Action -case class FetchAvailableFiles(osc: OsobniCislo) extends Action -case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala deleted file mode 100644 index c637d35..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala +++ /dev/null @@ -1,82 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.{UserProfile, UserInfo, OsobniCislo} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo -import cz.e_bs.cmi.mdr.pdb.UserFunction - -object AppPage: - // TODO: pages by logged in user - val pages: List[Page] = List(Page.Directory, Page.Dashboard) - - import NavigationBar.{Logo, MenuItem} - - val logo = Logo( - "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", - "Workflow" - ) - - // TODO: menu items by user profile - val userMenu = - List( - MenuItem("Your Profile"), - MenuItem("Settings"), - MenuItem("Sign out") - ) - - // TODO: load user profile - val $userProfile = Var( - UserProfile( - "tom", - UserInfo( - OsobniCislo("1031"), - "tom", - "Tom", - "Cook", - None, - None, - Some("tom@example.com"), - Some("+420 222 866 180"), - Some( - UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") - ), - Nil, - None - ) - ) - ) - - val $userInfo = $userProfile.signal.map(_.userInfo) - - type ViewModel = Option[HtmlElement] - def apply( - actionBus: Observer[Action] - )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using - router: Router[Page] - ): HtmlElement = - PageLayout(actionBus)( - $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => - PageLayout.ViewModel( - NavigationBar.ViewModel( - u, - pages.map(p => - NavigationBar.Link( - () => - PageLink( - p, - actionBus - ), - p == cp - ) - ), - userMenu, - logo - ), - c - ) - ), - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala deleted file mode 100644 index bf473c6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ /dev/null @@ -1,143 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden -import fiftyforms.ui.components.tailwind.list.IconText.ViewModel -import fiftyforms.ui.components.tailwind.Icons -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Action - -object Breadcrumbs: - - private def slash = - import svg.{*, given} - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-300", - xmlns := "http://www.w3.org/2000/svg", - fill := "currentColor", - viewBox := "0 0 20 20", - ariaHidden := true, - path( - d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" - ) - ) - - def backIcon = - Icons.solid - .`arrow-narrow-left`() - .amend( - svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" - ) - - object Link: - case class ViewModel( - page: Page, - icon: Option[SvgElement], - extraClasses: String, - text: String, - textClasses: Option[String] = None - ) - - val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" - - def shortHome(p: Page) = ViewModel( - p, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - "Zpět na úvodní stránku", - None - ) - - def fullHome(p: Page) = ViewModel( - p, - Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), - "text-gray-400 hover:text-gray-500", - "Domů", - Some("sr-only") - ) - - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - PageLink - .container($m.map(_.page), actionBus) - .amend( - cls <-- $m.map(_.extraClasses), - child.maybe <-- $m.map(_.icon), - span( - cls <-- $m.map(_.textClasses.getOrElse("")), - child.text <-- $m.map(_.text) - ) - ) - - object FullBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - ol( - role := "list", - cls := "flex items-center space-x-4", - children <-- $m.map(_.path).split(_.id)((_, _, $p) => - li( - div( - cls := "flex items-center", - child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), - Link( - $p.map(p => - if (p.isRoot) then Link.fullHome(p) - else - Link.ViewModel( - p, - None, - s"ml-4 max-w-xs truncate ${Link.baseClasses}", - p.title - ) - ), - actionBus - ) - ) - ) - ) - ) - - object ShortBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - Link( - $m.map { p => - val target = p.parent.getOrElse(p) - if target.isRoot then Link.shortHome(target) - else - Link.ViewModel( - target, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - s"Zpět na ${target.title}" - ) - }, - actionBus - ) - - def apply(actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - nav( - cls := "flex", - aria.label := "Breadcrumb", - div( - cls := "flex sm:hidden", - ShortBreadcrumbs(router.$currentPage, actionBus) - ), - div( - cls := "hidden sm:block", - FullBreadcrumbs(router.$currentPage, actionBus) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala deleted file mode 100644 index 0db2047..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ /dev/null @@ -1,242 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent -import fiftyforms.ui.components.tailwind.Icons -import fiftyforms.ui.components.tailwind.Avatar -import cz.e_bs.cmi.mdr.pdb.UserInfo -import io.laminext.syntax.core.* - -object NavigationBar: - - case class Logo(img: String, name: String) - case class Link(a: () => Anchor, active: Boolean) - case class MenuItem(title: String) - - case class ViewModel( - userInfo: UserInfo, - pages: List[Link], - userMenu: List[MenuItem], - logo: Logo - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - val $userInfo = $m.map(_.userInfo) - val mobileMenuOpen = Var(false) - - val desktopOnly = cls("hidden md:block") - val mobileOnly = cls("md:hidden") - - inline def avatarImage(size: Int = 8) = - Avatar($userInfo.map(_.img)).avatarImage(size) - - def notificationButton = button( - tpe := "button", - cls := List( - "bg-indigo-600", - "focus:outline-none", - "focus:ring-2", - "focus:ring-offset-2", - "focus:ring-offset-indigo-600", - "focus:ring-white", - "hover:text-white", - "p-1", - "rounded-full", - "text-indigo-200" - ), - span(cls := "sr-only", "View notifications"), - Icons.outline.bell() - ) - - def userProfile: HtmlElement = - val menuOpen = Var(false) - - def menuItem(item: MenuItem, idx: Int): HtmlElement = - a( - href := "#", - cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", - role := "menuitem", - tabIndex := -1, - idAttr := s"user-menu-item-$idx", - item.title - ) - - div( - cls := "ml-3 relative", - div( - button( - tpe := "button", - cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - idAttr := "user-menu-button", - aria.expanded <-- menuOpen.signal, - aria.hasPopup := true, - span(cls := "sr-only", "Open user menu"), - child <-- avatarImage(), - onClick.preventDefault.mapTo( - !menuOpen.now() - ) --> menuOpen.writer - ) - ), - /* - * */ - div( - cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", - cls <-- menuOpen.signal.map { o => - if (o) "md:block" else "md:hidden" - }, - role := "menu", - aria.orientation := "vertical", - aria.labelledBy := "user-menu-button", - tabIndex := -1, - // : keyboard navigation - children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) - ) - ) - - def mobileProfile = - def menuItem(item: MenuItem): HtmlElement = - a( - href := "#", - cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", - item.title - ) - - div( - cls := "pt-4 pb-3 border-t border-indigo-700", - div( - cls := "flex items-center px-5", - div( - cls := "flex-shrink-0", - child <-- avatarImage(10) - ), - div( - cls := "ml-3", - div( - cls := "text-base font-medium text-white", - child.text <-- $userInfo.map(_.name) - ), - child.maybe <-- $userInfo.map( - _.email.map(e => - div( - cls := "text-sm font-medium text-indigo-300", - e - ) - ) - ) - ), - notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) - ), - div( - cls := "mt-3 px-2 space-y-1", - children <-- $m.map(_.userMenu.map(menuItem)) - ) - ) - - def pageLink(page: Link): Anchor = - page - .a() - .amend( - cls := "text-white px-3 py-2 rounded-md text-sm font-medium", - cls := Seq( - "bg-indigo-700" -> page.active, - "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active - ), - ariaCurrent := (if page.active then "page" else "false") - ) - - def logoImg: Image = - img( - cls := "h-8 w-8", - src <-- $m.map(_.logo.img), - alt <-- $m.map(_.logo.name) - ) - - def mobileMenuButton = button( - tpe := "button", - cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - aria.controls := "mobile-menu", - aria.expanded <-- mobileMenuOpen.signal, - span(cls := "sr-only", "Open main menu"), - Icons.outline - .menu() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") - ), - Icons.outline - .x() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") - ), - onClick.preventDefault.mapTo( - !mobileMenuOpen.now() - ) --> mobileMenuOpen.writer - ) - - def navBarLeft = - div( - cls := "flex items-center", - div(cls := "flex-shrink-0", logoImg), - div( - desktopOnly, - div( - cls := "ml-10 flex items-baseline space-x-4", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ) - ) - ) - - def navBarRight = - div( - desktopOnly, - div( - cls := "ml-4 flex items-center md:ml-6", - notificationButton, - userProfile - ) - ) - - def navBarMobile = - div( - cls := "-mr-2 flex", - mobileOnly, - mobileMenuButton - ) - - def navBar = - div( - cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex items-center justify-between h-16", - navBarLeft, - navBarRight, - navBarMobile - ) - ) - - def mobileMenu = - div( - mobileOnly, - cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }, - idAttr := "mobile-menu", - div( - cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ), - mobileProfile - ) - - nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala deleted file mode 100644 index 75339df..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object PageHeader: - def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = - header( - cls := "bg-white shadow-sm", - div( - cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", - h1( - cls := "text-lg leading-6 font-semibold text-gray-900", - Breadcrumbs(actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala deleted file mode 100644 index 5096463..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ /dev/null @@ -1,30 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import fiftyforms.ui.components.tailwind.Loading -import io.laminext.syntax.core.* - -object PageLayout: - case class ViewModel( - navigation: NavigationBar.ViewModel, - content: Option[HtmlElement] - ) - def apply(actionBus: Observer[Action])( - $m: Signal[ViewModel], - mods: Modifier[HtmlElement]* - )(using router: Router[Page]): HtmlElement = - val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) - div( - cls := "h-full flex flex-col", - NavigationBar($m.map(_.navigation)), - child.maybe <-- router.$currentPage.map(_.isRoot) - .switch(None, Some(PageHeader(actionBus))), - main( - cls := "flex-grow-1 overflow-y-auto", - mods, - child <-- $maybeContent.map(_.getOrElse(Loading)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala deleted file mode 100644 index 62d820f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.LinkSupport.* -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo - -object PageLink: - type ViewModel = Page - def apply($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = - a(mods($m, actions), child.text <-- $m.map(_.title)) - - def apply(m: ViewModel, actions: Observer[Action])(using - router: Router[Page] - ): Anchor = apply(Val(m), actions) - - def container($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = a(mods($m, actions)) - - def container(m: ViewModel, actions: Observer[Action])(using - Router[Page] - ): Anchor = container(Val(m), actions) - - private def mods($m: Signal[Page], actions: Observer[Action])(using - router: Router[Page] - ): Modifier[Anchor] = - Seq( - href <-- $m.map(router.absoluteUrlForPage).recover { case _ => - Some("invalid url") - }, - composeEvents(onClick.noKeyMod.preventDefault)( - _.sample($m) - .map(NavigateTo.apply) - ) --> actions - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala deleted file mode 100644 index 87f825a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.pages.dashboard.DashboardPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -class DashboardPageConnector(actionBus: Observer[Action])(using - router: Router[Page] -): - def apply: HtmlElement = - AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala deleted file mode 100644 index e9b0f71..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala +++ /dev/null @@ -1,76 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailKriteriaPage -import pages.detail.components.DukazyKriteria -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -object DetailKriteriaPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailKriteriaPageConnector( - state: DetailKriteriaPageConnector.AppState -)( - $page: Signal[Page.DetailKriteria] -)(using Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => - (p.osobniCislo.value, p.parametr.value, p.kriterium.value) - )((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map( - FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) - ) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, s, $s) => - DetailKriteriaPage($s)(state.actionBus.contramap { - case DukazyKriteria.Add => - NavigateTo( - Page.UpravDukazKriteria( - Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), - Page.Titled(s.parametr.id, Some(s.parametr.nazev)), - Page.Titled(s.kriterium.id, Some(s.kriterium.id)) - ) - ) - }) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): DetailKriteriaPage.ViewModel = - DetailKriteriaPage.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala deleted file mode 100644 index 4d8024f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ /dev/null @@ -1,57 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import pages.detail.DetailPage -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import fiftyforms.ui.components.tailwind.Color - -object DetailPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailPageConnector(state: DetailPageConnector.AppState)( - $page: Signal[Page.Detail] -)(using router: Router[Page]): - val $oscChangeSignal = $page.splitOne(_.osobniCislo.value)((osc, _, _) => osc) - val $pageChangeSignal = - $oscChangeSignal.flatMap(osc => - EventStream.fromSeq(Seq(FetchUserDetails(osc), FetchParameters(osc))) - ) - // TODO: filter the value based on the current osc - // OSC change will fetch new data, but still - // - we need to be sure that what we got is really what we ought to display - // - we want to display stale data accordingly (at least with loading indicator) - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - def apply: HtmlElement = - AppPage(state.actionBus)( - $data.combineWithFn($params)(_ zip _) - .map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: List[Parameter] - ): DetailPage.ViewModel = - DetailPage.ViewModel( - o.toDetailOsoby, - p.map( - _.toParametr(param => - PageLink.container(Page.DetailParametru(o, param), state.actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala deleted file mode 100644 index fdaa7f8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailParametruPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink - -object DetailParametruPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailParametruPageConnector( - state: DetailParametruPageConnector.AppState -)( - $page: Signal[Page.DetailParametru] -)(using router: Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => (p.osobniCislo.value, p.parametr.value))((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map(FetchParameter(_, _)) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - } yield (da, pb) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailParametruPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter - ): DetailParametruPage.ViewModel = - DetailParametruPage.ViewModel( - o.toDetailOsoby, - p.toParametr(p => - PageLink.container(Page.DetailParametru(o, p), state.actionBus) - ), - p.criteria.map( - _.toKriterium { c => - PageLink.container( - Page.DetailKriteria(o, p, c), - state.actionBus - ) - } - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala deleted file mode 100644 index 049aa22..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -case class DirectoryPageConnector( - $input: EventStream[List[UserInfo]], - actionBus: Observer[Action] -)(using router: Router[Page]): - val $data = $input.startWithNone - val $actionSignal = EventStream.fromValue(FetchDirectory) - - def apply: HtmlElement = - AppPage(actionBus)( - $data.split(_ => ())((_, _, s) => - pages.directory.DirectoryPage( - s.map( - _.map( - _.toUserRow(u => - PageLink.container( - Page.Detail(Page.Titled(u.personalNumber)), - actionBus - ) - ) - ) - ) - ) - ), - $actionSignal --> actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala deleted file mode 100644 index 88e7450..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ /dev/null @@ -1,68 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii -import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria -import fiftyforms.ui.components.tailwind.Color - -extension (o: UserInfo) - def toDetailOsoby: DetailOsoby.ViewModel = - DetailOsoby.ViewModel( - o.personalNumber, - o.name, - o.email, - o.phone, - o.img, - o.mainFunction.map(f => - DetailOsoby.Funkce.ViewModel(f.name, f.dept, f.ou) - ), - o.userContracts.headOption.map(c => - DetailOsoby.PracovniPomer.ViewModel(c.rel, c.startDate, c.endDate) - ) - ) - -extension (param: Parameter) - def toParametr(container: Parameter => Anchor): DetailParametru.ViewModel = - DetailParametru.ViewModel( - id = param.id, - nazev = param.name, - popis = param.description, - status = "Nesplněno", - statusColor = Color.red, - a = container(param) - ) - -extension (crit: ParameterCriteria) - def toKriterium( - container: ParameterCriteria => Anchor - ): DetailKriteria.ViewModel = - DetailKriteria.ViewModel( - nazev = crit.criteriumText, - kapitola = crit.chapterId, - bod = crit.itemId, - status = "Nesplněno", - statusColor = Color.red, - splneno = false, - dukazy = Nil, - container = container(crit) - ) - -extension (user: UserInfo) - def toUserRow( - container: UserInfo => HtmlElement = (_: UserInfo) => div() - ): UserRow.ViewModel = - UserRow.ViewModel( - osobniCislo = user.personalNumber.toString, - celeJmeno = user.name, - prijmeni = user.surname, - hlavniFunkce = user.mainFunction.map(_.name), - img = user.img, - container = () => container(user) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala deleted file mode 100644 index 7d5b2c1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.dashboard - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.Page - -object DashboardPage: - - def render: HtmlElement = - div("Dashboard page") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala deleted file mode 100644 index 9ef0698..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala +++ /dev/null @@ -1,31 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailKriteriaPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply($m: Signal[ViewModel])( - action: Observer[DukazyKriteria.Action] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - DukazyKriteria($m.map(_.kriterium.dukazy))(action) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala deleted file mode 100644 index 3b316e1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object DetailPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametry: SeznamParametru.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby($m.map(_.osoba)), - SeznamParametru($m.map(_.parametry)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala deleted file mode 100644 index 17224cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala +++ /dev/null @@ -1,24 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailParametruPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriteria: SeznamKriterii.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby.header($m.map(_.osoba)), - DetailParametru($m.map(_.parametr)), - SeznamKriterii($m.map(_.kriteria)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala deleted file mode 100644 index dc912fa..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ /dev/null @@ -1,107 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb -package app -package pages.detail - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.UpravDukazForm -import fiftyforms.services.files.File - -object UpravDukaz: - - type ThisPage = Page.UpravDukazKriteria - type PageKey = (OsobniCislo, String, String) - - trait State { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def availableFiles: EventStream[List[File]] - def actionBus: Observer[Action] - } - - def keyOfPage(page: ThisPage): PageKey = - (page.osobniCislo.value, page.parametr.value, page.kriterium.value) - - def onChangeAction(key: PageKey): Action = - FetchParameterCriteria( - key._1, - key._2, - key._3, - Page.UpravDukazKriteria(_, _, _) - ) - - class Connector(state: State)($page: Signal[ThisPage])(using Router[Page]): - val $paramChangeSignal = $page.splitOne(keyOfPage)((x, _, _) => x) - val $pageChangeSignal = $paramChangeSignal.map(onChangeAction) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.split(_ => ())((_, s, $s) => - PageComponent( - $s.map(buildModel), - state.availableFiles, - state.actionBus.contramap { - case UpravDukazForm.Cancelled => - NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) - case UpravDukazForm.AvailableFilesRequested => - FetchAvailableFiles(s._1.personalNumber) - } - ) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): PageComponent.ViewModel = - import connectors.* - PageComponent.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) - - object PageComponent: - import components.* - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply( - $m: Signal[ViewModel], - availableFilesStream: EventStream[List[File]], - events: Observer[UpravDukazForm.Event] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - UpravDukazForm(availableFilesStream)(events) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala deleted file mode 100644 index ca311b0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.Icons -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Color - -object DetailKriteria: - case class ViewModel( - nazev: String, - kapitola: String, - bod: String, - status: String, - statusColor: Color, - splneno: Boolean, - dukazy: List[DukazKriteria.ViewModel], - container: HtmlElement = div() - ) { - val id = s"${kapitola}${bod}" - } - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h3( - cls := "text-xl leading-6 font-bold text-gray-900", - child.text <-- $m.map(_.nazev) - ), - h3( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - "Kapitola ", - child.text <-- $m.map(_.kapitola), - " bod ", - child <-- $m.map(_.bod) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala deleted file mode 100644 index ae90290..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala +++ /dev/null @@ -1,100 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Avatar - -object DetailOsoby: - - object Funkce: - case class ViewModel( - nazev: String, - stredisko: String, - voj: String - ) - - def render($m: Signal[ViewModel]): HtmlElement = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.nazev), - span( - cls := "hidden md:inline", - " @ ", - child.text <-- $m.map(_.stredisko), - ", ", - child.text <-- $m.map(_.voj) - ) - ) - - object PracovniPomer: - case class ViewModel( - druh: String, - pocatek: LocalDate, - konec: Option[LocalDate] - ) - - def render($m: Signal[ViewModel]): HtmlElement = - import fiftyforms.ui.components.tailwind.CustomAttrs.datetime - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.druh), - " od ", - time( - datetime <-- $m.map(_.pocatek.toString), - child.text <-- $m.map(_.pocatek.toString) - ) - ) - - case class ViewModel( - osobniCislo: OsobniCislo, - jmeno: String, - email: Option[String], - telefon: Option[String], - img: Option[String], - hlavniFunkce: Option[Funkce.ViewModel], - pracovniPomer: Option[PracovniPomer.ViewModel] - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-4", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(16) - ), - div( - h1( - cls := "text-2xl font-bold text-gray-900", - child.text <-- $m.map(_.jmeno) - ), - child.maybe <-- $m.map(_.hlavniFunkce).split(_ => ())((_, _, d) => - Funkce.render(d) - ), - child.maybe <-- $m.map(_.pracovniPomer).split(_ => ())((_, _, d) => - PracovniPomer.render(d) - ) - ) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-5", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(8) - ), - div( - h1( - cls := "text-lg font-medium text-gray-800", - child.text <-- $m.map(_.jmeno) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala deleted file mode 100644 index e11cb62..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color - -object DetailParametru: - case class ViewModel( - id: String, - nazev: String, - popis: String, - status: String, - statusColor: Color, - a: Anchor - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h2( - cls := "text-2xl leading-6 font-medium text-gray-900", - child.text <-- $m.map(_.nazev) - ), - p( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - child.text <-- $m.map(_.popis) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - h2( - cls := "text-lg leading-6 font-medium text-gray-600", - child.text <-- $m.map(_.nazev) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala deleted file mode 100644 index a00ede8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala +++ /dev/null @@ -1,177 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.CustomAttrs - -object DukazKriteria: - case class Osoba(osobniCislo: String, jmeno: String) - case class Udalost(osoba: Osoba, datum: LocalDate) - case class Dokument( - url: String, - nazev: String, - pridal: Udalost, - odebral: Option[Udalost] - ) - case class ViewModel( - dokumenty: List[Dokument], - autorizoval: Option[Udalost], - platiDo: Option[LocalDate], - poznámka: Option[String] - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-lg", - div( - cls := "px-4 py-5 sm:px-6", - h3( - cls := "text-lg leading-6 font-medium text-gray-900", - """Applicant Information""" - ), - p( - cls := "mt-1 max-w-2xl text-sm text-gray-500", - """Personal details and application.""" - ) - ), - div( - cls := "border-t border-gray-200 px-4 py-5 sm:px-6", - dl( - cls := "grid grid-cols-1 gap-x-4 gap-y-8 sm:grid-cols-2", - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Full name""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Margot Foster""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Application for""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Backend Developer""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Email address""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """margotfoster@example.com""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Salary expectation""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """$120,000""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """About""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Fugiat ipsum ipsum deserunt culpa aute sint do nostrud anim incididunt cillum culpa consequat. Excepteur qui ipsum aliquip consequat sint. Sit id mollit nulla mollit nostrud in ea officia proident. Irure nostrud pariatur mollit ad adipisicing reprehenderit deserunt qui eu.""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """Attachments""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - ul( - role := "list", - cls := "border border-gray-200 rounded-md divide-y divide-gray-200", - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """resume_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ), - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """coverletter_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala deleted file mode 100644 index 22f134f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala +++ /dev/null @@ -1,35 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object DukazyKriteria: - sealed trait Action - case object Add extends Action - - type ViewModel = List[DukazKriteria.ViewModel] - - def apply($m: Signal[ViewModel])(actions: Observer[Action]): HtmlElement = - div( - child.maybe <-- $m.map(m => - if (m.isEmpty) then Some(prazdnyDukaz(actions)) - else None - ), - children <-- $m.map(_.zipWithIndex).split(_._2)((_, _, $s) => - DukazKriteria($s.map(_._1)) - ) - ) - - private def prazdnyDukaz(actions: Observer[Action]): HtmlElement = - button( - tpe := "button", - onClick.preventDefault.mapTo(Add) --> actions, - cls := "relative block w-full border-2 border-gray-300 border-dashed rounded-lg p-12 text-center hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - Icons.outline - .`document-add`(12) - .amend(svg.cls := "mx-auto text-gray-400"), - span( - cls := "mt-2 block text-sm font-medium text-gray-900", - "Přidat důkaz" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala deleted file mode 100644 index f628697..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.list.StackedList -import fiftyforms.ui.components.tailwind.list.ListRow -import fiftyforms.ui.components.tailwind.list.RowTag -import fiftyforms.ui.components.tailwind.list.RowNext -import java.time.LocalDate - -object SeznamKriterii: - type ViewModel = List[DetailKriteria.ViewModel] - - private val kritList = new StackedList[DetailKriteria.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - kritList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = - p(cls := "text-sm text-gray-500", s"${i.kapitola}${i.bod}"), - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.container - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala deleted file mode 100644 index 2bfe4bc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.list.{ - StackedList, - ListRow, - RowTag, - PropList, - IconText, - RowNext -} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.LinkSupport.* - -object SeznamParametru: - type ViewModel = List[DetailParametru.ViewModel] - - private val parametrList = new StackedList[DetailParametru.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - parametrList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = emptyNode, - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.a - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala deleted file mode 100644 index f048e9d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ /dev/null @@ -1,101 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.form.* -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement -import fiftyforms.services.files.components.tailwind.FilePicker -import cz.e_bs.cmi.mdr.pdb.frontend.AutorizujDukaz -import cz.e_bs.cmi.mdr.pdb.frontend.DocumentRef -import fiftyforms.services.files.File - -object UpravDukazForm: - sealed trait Event - case object Cancelled extends Event - case object AvailableFilesRequested extends Event - def apply(availableFilesStream: EventStream[List[File]])( - updates: Observer[Event] - ): HtmlElement = - val (filesStream, filesObserver) = - EventStream.withObserver[FilePicker.Event] - val files = Var[List[File]](Nil) - def submitButtons: HtmlElement = - div( - cls := "pt-5", - div( - cls := "flex justify-end", - button( - tpe := "button", - cls := "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zrušit", - onClick.mapTo(Cancelled) --> updates - ), - button( - tpe := "submit", - cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - disabled <-- files.signal.map(_.isEmpty), - "Autorizovat" - ) - ) - ) - - div( - filesStream.collect { case FilePicker.SelectionUpdated(files) => - files.to(List) - } --> files.writer, - filesStream.collect { case FilePicker.AvailableFilesRequested => - AvailableFilesRequested - } --> updates, - cls := "bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6", - Form( - Form.Body( - Form.Section( - FormHeader( - FormHeader.ViewModel( - "Doložení kritéria", - "Sestavte doklady poskytující důkaz kritéria a potvrďte odesláním formuláře. Případné limitace či jiné relevantní údaje vepište do pole Komentář." - ) - ), - FormFields( - FormRow( - "dokumenty", - "Dokumenty", - FilePicker(files.signal, availableFilesStream)(filesObserver) - .amend(idAttr := "dokumenty", cls("max-w-lg")) - ).toHtml, - FormRow( - "platnost", - "Platnost", - input( - idAttr := "platnost", - name := "platnost", - tpe := "date", - autoComplete := "date", - cls := "block max-w-lg w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" - ) - ).toHtml, - FormRow( - "komentar", - "Komentář", - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - textArea( - idAttr := "komentar", - name := "about", - rows := 3, - cls := "max-w-lg shadow-sm block w-full focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border border-gray-300 rounded-md" - ), - p( - cls := "mt-2 text-sm text-gray-500", - "Doplňte prosím potřebné informace související s doložením kritéria, včetně případných limitací." - ) - ) - ).toHtml - ) - ) - ), - submitButtons - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala deleted file mode 100644 index 41033f6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -import components._ - -object DirectoryPage: - - type ViewModel = List[UserRow.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - val (actionsStream, actionObserver) = - EventStream.withObserver[SearchForm.Action] - val $filter = actionsStream - .collect { - case SearchForm.ClearFilter => None - case SearchForm.SetFilter(t) => Some(t) - } - .startWith(None) - val byLetter = for { - d <- $m - f <- $filter - } yield for { - (letter, users) <- d - .filter { user => - f match - case None => true - case Some(t) => user.search.contains(t) - } - .groupBy(_.prijmeni.head) - .to(List) - .sortBy(_._1) - } yield (letter.toString, users.sortBy(_.prijmeni)) - - div( - cls := "h-full max-w-7xl mx-auto order-first flex flex-col flex-shrink-0", - SearchForm(actionObserver), - Directory(byLetter) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala deleted file mode 100644 index 73f915d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -object Directory: - - object Header: - type ViewModel = String - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500", - h3(child.text <-- $m) - ) - - object UserList: - type ViewModel = List[UserRow.ViewModel] - def apply($m: Signal[ViewModel]): HtmlElement = - ul( - role := "list", - cls := "relative z-0 divide-y divide-gray-200", - children <-- $m.split(_.osobniCislo)((_, _, s) => UserRow(s)) - ) - - type ViewModel = List[(String, List[UserRow.ViewModel])] - def apply($m: Signal[ViewModel]): HtmlElement = - nav( - cls := "flex-1 min-h-0 overflow-y-auto", - aria.label := "Directory", - children <-- $m.split(_._1)((_, _, s) => - div( - cls := "relative", - Header(s.map(_._1)), - UserList(s.map(_._2)) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala deleted file mode 100644 index e182910..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala +++ /dev/null @@ -1,54 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object SearchForm: - sealed trait Action - case object SubmitFilter extends Action - sealed trait FilterAction extends Action - case object ClearFilter extends FilterAction - case class SetFilter(value: String) extends FilterAction - - def apply(actions: Observer[Action]): HtmlElement = - div( - cls := "px-6 pt-4 pb-4", - form( - cls := "flex space-x-4", - onSubmit.mapTo(SubmitFilter) --> actions, - div( - cls := "flex-1 min-w-0", - label( - forId := "search", - cls := "sr-only", - "Hledat" - ), - div( - cls := "relative rounded-md shadow-sm", - div( - cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", - Icons.solid.search().amend(svg.cls := "text-gray-400") - ), - input( - tpe := "search", - name := "search", - idAttr := "search", - cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", - placeholder := "Hledat", - composeEvents(onInput.mapToValue.setAsValue.map(SetFilter(_)))( - _.throttle(500) - ) --> actions - ) - ) - ), - button( - tpe := "submit", - cls := "inline-flex justify-center px-3.5 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500", - Icons.solid.filter().amend(svg.cls := "text-gray-400"), - span( - cls := "sr-only", - "Hledat" - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala deleted file mode 100644 index 82f7723..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala +++ /dev/null @@ -1,52 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.airstream.core.Signal -import fiftyforms.ui.components.tailwind.Avatar - -object UserRow: - case class ViewModel( - osobniCislo: String, - celeJmeno: String, - prijmeni: String, - hlavniFunkce: Option[String], - img: Option[String], - container: () => HtmlElement = () => div() - ) { - val search = prijmeni.toLowerCase - } - - def apply($m: Signal[ViewModel]): HtmlElement = - inline def avatarImage = - Avatar($m.map(_.img)).avatarImage(10) - - li( - div( - cls := "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500", - div( - cls := "flex-shrink-0", - child <-- avatarImage - ), - div( - cls := "flex-1 min-w-0", - child <-- $m.map { o => - o.container() - .amend( - cls := "focus:outline-none", - span( - cls := "absolute inset-0", - aria.hidden := true - ), - p( - cls := "text-sm font-medium text-gray-900", - o.celeJmeno - ), - p( - cls := "text-sm text-gray-500 truncate", - o.hlavniFunkce - ) - ) - } - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala deleted file mode 100644 index 467c5f5..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala +++ /dev/null @@ -1,99 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router - -object ErrorPage: - case class ViewModel( - homePage: Page, - errorName: String, - title: String, - subTitle: String - ) - - def apply(m: ViewModel, actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - val ViewModel(homePage, errorName, title, subTitle) = m - div( - cls := "min-h-full pt-16 pb-12 flex flex-col bg-white", - main( - cls := "flex-grow flex flex-col justify-center max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex-shrink-0 flex justify-center", - a( - href := "/", - cls := "inline-flex", - span( - cls := "sr-only", - """Workflow""" - ), - img( - cls := "h-12 w-auto", - src := "https://tailwindui.com/img/logos/workflow-mark.svg?color=indigo&shade=600", - alt := "" - ) - ) - ), - div( - cls := "py-16", - div( - cls := "text-center", - p( - cls := "text-sm font-semibold text-indigo-600 uppercase tracking-wide", - errorName - ), - h1( - cls := "mt-2 text-4xl font-extrabold text-gray-900 tracking-tight sm:text-5xl", - title - ), - p( - cls := "mt-2 text-base text-gray-500", - subTitle - ), - div( - cls := "mt-6", - PageLink - .container(homePage, actionBus) - .amend( - cls := "text-base font-medium text-indigo-600 hover:text-indigo-500", - """Go back home""" - ) - ) - ) - ) - ), - footer( - cls := "flex-shrink-0 max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", - nav( - cls := "flex justify-center space-x-4", - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Contact Support""" - ), - span( - cls := "inline-block border-l border-gray-300", - aria.hidden := true - ), - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Status""" - ), - span( - cls := "inline-block border-l border-gray-300", - aria.hidden := true - ), - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Twitter""" - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala deleted file mode 100644 index bd9baec..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action - -object NotFoundPage: - def apply(homePage: Page, url: String, actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - ErrorPage( - ErrorPage.ViewModel( - homePage, - "404 error", - "Page not found.", - s"Sorry, but page $url doesn't exist." - ), - actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala deleted file mode 100644 index 476fe3c..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action - -object UnhandledErrorPage: - - case class ViewModel( - homePage: Page, - errorName: Option[String], - errorMessage: Option[String] - ) - - def apply(m: ViewModel, actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - ErrorPage( - ErrorPage.ViewModel( - m.homePage, - "Unexpected error occurred", - m.errorName.getOrElse( - "Uh oh!" - ), // TODO: translations, better text than uh oh - m.errorMessage.getOrElse( - "This wasn't supposed to happen! Please try again." - ) - ), - actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala deleted file mode 100644 index a3b00af..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala +++ /dev/null @@ -1,103 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import scala.scalajs.js.annotation.JSExportTopLevel -import scala.scalajs.js.annotation.JSExport -import scala.scalajs.js.annotation.JSImport -import scala.scalajs.js -import org.scalajs.dom -import com.raquo.laminar.api.L.{*, given} -import scala.scalajs.js.Date -import com.raquo.waypoint.Router -import com.raquo.waypoint.SplitRender -import cz.e_bs.cmi.mdr.pdb.app.services.DataFetcher -import scala.scalajs.js.JSON -import zio.json._ -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.state.AppState - -@js.native -@JSImport("stylesheets/main.css", JSImport.Namespace) -object Css extends js.Any - -@js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native -@JSImport("params/pdb-params.json", JSImport.Default) -object pdbParams extends js.Object - -@JSExportTopLevel("app") -object Main: - - @JSExport - def main(args: Array[String]): Unit = - import Routes.given - onLoad { - setupAirstream() - val appContainer = dom.document.querySelector("#app") - val _ = - render( - appContainer, - renderPage(state.MockAppState(using unsafeWindowOwner, router)) - ) - } - - private def onLoad(f: => Unit): Unit = - documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) - - private def setupAirstream()(using router: Router[Page]): Unit = - AirstreamError.registerUnhandledErrorCallback(err => - router.forcePage( - Page.UnhandledError( - Some(err.getClass.getName), // TODO: Fill only in dev mode - Some(err.getMessage) - ) - ) - ) - - def renderPage(state: AppState)(using - router: Router[Page] - ): HtmlElement = - val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) - .collectSignal[Page.Detail]( - connectors - .DetailPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailParametru]( - connectors - .DetailParametruPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailKriteria]( - connectors - .DetailKriteriaPageConnector(state)(_) - .apply - ) - .collectSignal[Page.UpravDukazKriteria]( - pages.detail.UpravDukaz.Connector(state)(_).apply - ) - .collectStatic(Page.Dashboard)( - connectors.DashboardPageConnector(state.actionBus).apply - ) - .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) - ) - .collect[Page.UnhandledError](pg => - pages.errors - .UnhandledErrorPage( - pages.errors.UnhandledErrorPage - .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), - state.actionBus - ) - ) - .collectStatic(Page.Directory)( - connectors - .DirectoryPageConnector(state.users, state.actionBus) - .apply - ) - div(cls := "h-full", child <-- pageSplitter.$view) - - // Pull in the stylesheet - val css: Css.type = Css diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala deleted file mode 100644 index 3bbc8c0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ /dev/null @@ -1,198 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.* -import org.scalajs.dom -import zio.json.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import scala.scalajs.js -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.Page.Titled - -// enum is not working with Waypoints' SplitRender collectStatic -sealed abstract class Page( - val id: String, - val title: String, - val parent: Option[Page] -) { - val path: Vector[Page] = - parent match - case None => Vector(this) - case Some(p) => p.path :+ this - - val isRoot: Boolean = parent.isEmpty -} - -object Page: - - case class Titled[V](value: V, title: Option[String] = None): - val show: String = title.getOrElse(value.toString) - - case object Directory extends Page("directory", "Adresář", None) - - case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) - - case class Detail(osobniCislo: Titled[OsobniCislo]) - extends Page("user", osobniCislo.show, Some(Directory)) - - object Detail { - def apply(o: UserInfo): Detail = Detail( - Titled(o.personalNumber, Some(o.name)) - ) - } - - case class DetailParametru( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String] - ) extends Page( - "parameter", - parametr.show, - Some(Detail(osobniCislo)) - ) - - object DetailParametru { - def apply(o: UserInfo, p: Parameter): DetailParametru = - DetailParametru( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)) - ) - } - - case class DetailKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "criteria", - kriterium.show, - Some(DetailParametru(osobniCislo, parametr)) - ) - - object DetailKriteria { - def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = - DetailKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class UpravDukazKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "addProof", - "Důkaz", - Some(DetailKriteria(osobniCislo, parametr, kriterium)) - ) - - object UpravDukazKriteria { - def apply( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): UpravDukazKriteria = - UpravDukazKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class NotFound(url: String) extends Page("404", "404", Some(Directory)) - - case class UnhandledError( - errorName: Option[String], - errorMessage: Option[String] - ) extends Page("500", "Unexpected error", Some(Directory)) - -object Routes: - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) - given [V: JsonEncoder]: JsonEncoder[Titled[V]] = - DeriveJsonEncoder.gen[Titled[V]] - given [V: JsonDecoder]: JsonDecoder[Titled[V]] = - DeriveJsonDecoder.gen[Titled[V]] - given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] - given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] - - val base = - js.`import`.meta.env.BASE_URL - .asInstanceOf[String] + "app" - - val homePage: Page = Page.Directory - - given router: Router[Page] = Router[Page]( - routes = List( - Route.static(homePage, root / endOfSegments, basePath = base), - Route.static( - Page.Dashboard, - root / "dashboard" / endOfSegments, - basePath = base - ), - Route[Page.Detail, String]( - encode = _.osobniCislo.value.toString, - decode = osc => Page.Detail(Titled(OsobniCislo(osc))), - root / "osoba" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.DetailParametru, (String, String)]( - encode = p => (p.osobniCislo.value.toString, p.parametr.value), - decode = - p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / endOfSegments, - basePath = base - ), - Route[Page.DetailKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.DetailKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.UpravDukazKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.UpravDukazKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / "edit" / endOfSegments, - basePath = base - ) - ), - serializePage = _.toJson, - deserializePage = _.fromJson[Page] - .fold(s => throw IllegalStateException(s), identity), - getPageTitle = _.title, - routeFallback = url => Page.NotFound(url), - deserializeFallback = _ => Page.Dashboard - )( - $popStateEvent = windowEvents.onPopState, - owner = unsafeWindowOwner - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala deleted file mode 100644 index b24b82b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -sealed trait Action - -case object FetchDirectory extends Action -case class FetchUserDetails(osc: OsobniCislo) extends Action -case class FetchParameters(osc: OsobniCislo) extends Action -case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action -case class FetchParameterCriteria( - osc: OsobniCislo, - paramId: String, - critId: String, - page: (UserInfo, Parameter, ParameterCriteria) => Page -) extends Action -case class FetchAvailableFiles(osc: OsobniCislo) extends Action -case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala deleted file mode 100644 index c637d35..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala +++ /dev/null @@ -1,82 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.{UserProfile, UserInfo, OsobniCislo} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo -import cz.e_bs.cmi.mdr.pdb.UserFunction - -object AppPage: - // TODO: pages by logged in user - val pages: List[Page] = List(Page.Directory, Page.Dashboard) - - import NavigationBar.{Logo, MenuItem} - - val logo = Logo( - "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", - "Workflow" - ) - - // TODO: menu items by user profile - val userMenu = - List( - MenuItem("Your Profile"), - MenuItem("Settings"), - MenuItem("Sign out") - ) - - // TODO: load user profile - val $userProfile = Var( - UserProfile( - "tom", - UserInfo( - OsobniCislo("1031"), - "tom", - "Tom", - "Cook", - None, - None, - Some("tom@example.com"), - Some("+420 222 866 180"), - Some( - UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") - ), - Nil, - None - ) - ) - ) - - val $userInfo = $userProfile.signal.map(_.userInfo) - - type ViewModel = Option[HtmlElement] - def apply( - actionBus: Observer[Action] - )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using - router: Router[Page] - ): HtmlElement = - PageLayout(actionBus)( - $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => - PageLayout.ViewModel( - NavigationBar.ViewModel( - u, - pages.map(p => - NavigationBar.Link( - () => - PageLink( - p, - actionBus - ), - p == cp - ) - ), - userMenu, - logo - ), - c - ) - ), - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala deleted file mode 100644 index bf473c6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ /dev/null @@ -1,143 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden -import fiftyforms.ui.components.tailwind.list.IconText.ViewModel -import fiftyforms.ui.components.tailwind.Icons -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Action - -object Breadcrumbs: - - private def slash = - import svg.{*, given} - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-300", - xmlns := "http://www.w3.org/2000/svg", - fill := "currentColor", - viewBox := "0 0 20 20", - ariaHidden := true, - path( - d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" - ) - ) - - def backIcon = - Icons.solid - .`arrow-narrow-left`() - .amend( - svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" - ) - - object Link: - case class ViewModel( - page: Page, - icon: Option[SvgElement], - extraClasses: String, - text: String, - textClasses: Option[String] = None - ) - - val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" - - def shortHome(p: Page) = ViewModel( - p, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - "Zpět na úvodní stránku", - None - ) - - def fullHome(p: Page) = ViewModel( - p, - Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), - "text-gray-400 hover:text-gray-500", - "Domů", - Some("sr-only") - ) - - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - PageLink - .container($m.map(_.page), actionBus) - .amend( - cls <-- $m.map(_.extraClasses), - child.maybe <-- $m.map(_.icon), - span( - cls <-- $m.map(_.textClasses.getOrElse("")), - child.text <-- $m.map(_.text) - ) - ) - - object FullBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - ol( - role := "list", - cls := "flex items-center space-x-4", - children <-- $m.map(_.path).split(_.id)((_, _, $p) => - li( - div( - cls := "flex items-center", - child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), - Link( - $p.map(p => - if (p.isRoot) then Link.fullHome(p) - else - Link.ViewModel( - p, - None, - s"ml-4 max-w-xs truncate ${Link.baseClasses}", - p.title - ) - ), - actionBus - ) - ) - ) - ) - ) - - object ShortBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - Link( - $m.map { p => - val target = p.parent.getOrElse(p) - if target.isRoot then Link.shortHome(target) - else - Link.ViewModel( - target, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - s"Zpět na ${target.title}" - ) - }, - actionBus - ) - - def apply(actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - nav( - cls := "flex", - aria.label := "Breadcrumb", - div( - cls := "flex sm:hidden", - ShortBreadcrumbs(router.$currentPage, actionBus) - ), - div( - cls := "hidden sm:block", - FullBreadcrumbs(router.$currentPage, actionBus) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala deleted file mode 100644 index 0db2047..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ /dev/null @@ -1,242 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent -import fiftyforms.ui.components.tailwind.Icons -import fiftyforms.ui.components.tailwind.Avatar -import cz.e_bs.cmi.mdr.pdb.UserInfo -import io.laminext.syntax.core.* - -object NavigationBar: - - case class Logo(img: String, name: String) - case class Link(a: () => Anchor, active: Boolean) - case class MenuItem(title: String) - - case class ViewModel( - userInfo: UserInfo, - pages: List[Link], - userMenu: List[MenuItem], - logo: Logo - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - val $userInfo = $m.map(_.userInfo) - val mobileMenuOpen = Var(false) - - val desktopOnly = cls("hidden md:block") - val mobileOnly = cls("md:hidden") - - inline def avatarImage(size: Int = 8) = - Avatar($userInfo.map(_.img)).avatarImage(size) - - def notificationButton = button( - tpe := "button", - cls := List( - "bg-indigo-600", - "focus:outline-none", - "focus:ring-2", - "focus:ring-offset-2", - "focus:ring-offset-indigo-600", - "focus:ring-white", - "hover:text-white", - "p-1", - "rounded-full", - "text-indigo-200" - ), - span(cls := "sr-only", "View notifications"), - Icons.outline.bell() - ) - - def userProfile: HtmlElement = - val menuOpen = Var(false) - - def menuItem(item: MenuItem, idx: Int): HtmlElement = - a( - href := "#", - cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", - role := "menuitem", - tabIndex := -1, - idAttr := s"user-menu-item-$idx", - item.title - ) - - div( - cls := "ml-3 relative", - div( - button( - tpe := "button", - cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - idAttr := "user-menu-button", - aria.expanded <-- menuOpen.signal, - aria.hasPopup := true, - span(cls := "sr-only", "Open user menu"), - child <-- avatarImage(), - onClick.preventDefault.mapTo( - !menuOpen.now() - ) --> menuOpen.writer - ) - ), - /* - * */ - div( - cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", - cls <-- menuOpen.signal.map { o => - if (o) "md:block" else "md:hidden" - }, - role := "menu", - aria.orientation := "vertical", - aria.labelledBy := "user-menu-button", - tabIndex := -1, - // : keyboard navigation - children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) - ) - ) - - def mobileProfile = - def menuItem(item: MenuItem): HtmlElement = - a( - href := "#", - cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", - item.title - ) - - div( - cls := "pt-4 pb-3 border-t border-indigo-700", - div( - cls := "flex items-center px-5", - div( - cls := "flex-shrink-0", - child <-- avatarImage(10) - ), - div( - cls := "ml-3", - div( - cls := "text-base font-medium text-white", - child.text <-- $userInfo.map(_.name) - ), - child.maybe <-- $userInfo.map( - _.email.map(e => - div( - cls := "text-sm font-medium text-indigo-300", - e - ) - ) - ) - ), - notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) - ), - div( - cls := "mt-3 px-2 space-y-1", - children <-- $m.map(_.userMenu.map(menuItem)) - ) - ) - - def pageLink(page: Link): Anchor = - page - .a() - .amend( - cls := "text-white px-3 py-2 rounded-md text-sm font-medium", - cls := Seq( - "bg-indigo-700" -> page.active, - "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active - ), - ariaCurrent := (if page.active then "page" else "false") - ) - - def logoImg: Image = - img( - cls := "h-8 w-8", - src <-- $m.map(_.logo.img), - alt <-- $m.map(_.logo.name) - ) - - def mobileMenuButton = button( - tpe := "button", - cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - aria.controls := "mobile-menu", - aria.expanded <-- mobileMenuOpen.signal, - span(cls := "sr-only", "Open main menu"), - Icons.outline - .menu() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") - ), - Icons.outline - .x() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") - ), - onClick.preventDefault.mapTo( - !mobileMenuOpen.now() - ) --> mobileMenuOpen.writer - ) - - def navBarLeft = - div( - cls := "flex items-center", - div(cls := "flex-shrink-0", logoImg), - div( - desktopOnly, - div( - cls := "ml-10 flex items-baseline space-x-4", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ) - ) - ) - - def navBarRight = - div( - desktopOnly, - div( - cls := "ml-4 flex items-center md:ml-6", - notificationButton, - userProfile - ) - ) - - def navBarMobile = - div( - cls := "-mr-2 flex", - mobileOnly, - mobileMenuButton - ) - - def navBar = - div( - cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex items-center justify-between h-16", - navBarLeft, - navBarRight, - navBarMobile - ) - ) - - def mobileMenu = - div( - mobileOnly, - cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }, - idAttr := "mobile-menu", - div( - cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ), - mobileProfile - ) - - nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala deleted file mode 100644 index 75339df..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object PageHeader: - def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = - header( - cls := "bg-white shadow-sm", - div( - cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", - h1( - cls := "text-lg leading-6 font-semibold text-gray-900", - Breadcrumbs(actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala deleted file mode 100644 index 5096463..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ /dev/null @@ -1,30 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import fiftyforms.ui.components.tailwind.Loading -import io.laminext.syntax.core.* - -object PageLayout: - case class ViewModel( - navigation: NavigationBar.ViewModel, - content: Option[HtmlElement] - ) - def apply(actionBus: Observer[Action])( - $m: Signal[ViewModel], - mods: Modifier[HtmlElement]* - )(using router: Router[Page]): HtmlElement = - val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) - div( - cls := "h-full flex flex-col", - NavigationBar($m.map(_.navigation)), - child.maybe <-- router.$currentPage.map(_.isRoot) - .switch(None, Some(PageHeader(actionBus))), - main( - cls := "flex-grow-1 overflow-y-auto", - mods, - child <-- $maybeContent.map(_.getOrElse(Loading)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala deleted file mode 100644 index 62d820f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.LinkSupport.* -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo - -object PageLink: - type ViewModel = Page - def apply($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = - a(mods($m, actions), child.text <-- $m.map(_.title)) - - def apply(m: ViewModel, actions: Observer[Action])(using - router: Router[Page] - ): Anchor = apply(Val(m), actions) - - def container($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = a(mods($m, actions)) - - def container(m: ViewModel, actions: Observer[Action])(using - Router[Page] - ): Anchor = container(Val(m), actions) - - private def mods($m: Signal[Page], actions: Observer[Action])(using - router: Router[Page] - ): Modifier[Anchor] = - Seq( - href <-- $m.map(router.absoluteUrlForPage).recover { case _ => - Some("invalid url") - }, - composeEvents(onClick.noKeyMod.preventDefault)( - _.sample($m) - .map(NavigateTo.apply) - ) --> actions - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala deleted file mode 100644 index 87f825a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.pages.dashboard.DashboardPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -class DashboardPageConnector(actionBus: Observer[Action])(using - router: Router[Page] -): - def apply: HtmlElement = - AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala deleted file mode 100644 index e9b0f71..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala +++ /dev/null @@ -1,76 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailKriteriaPage -import pages.detail.components.DukazyKriteria -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -object DetailKriteriaPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailKriteriaPageConnector( - state: DetailKriteriaPageConnector.AppState -)( - $page: Signal[Page.DetailKriteria] -)(using Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => - (p.osobniCislo.value, p.parametr.value, p.kriterium.value) - )((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map( - FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) - ) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, s, $s) => - DetailKriteriaPage($s)(state.actionBus.contramap { - case DukazyKriteria.Add => - NavigateTo( - Page.UpravDukazKriteria( - Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), - Page.Titled(s.parametr.id, Some(s.parametr.nazev)), - Page.Titled(s.kriterium.id, Some(s.kriterium.id)) - ) - ) - }) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): DetailKriteriaPage.ViewModel = - DetailKriteriaPage.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala deleted file mode 100644 index 4d8024f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ /dev/null @@ -1,57 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import pages.detail.DetailPage -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import fiftyforms.ui.components.tailwind.Color - -object DetailPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailPageConnector(state: DetailPageConnector.AppState)( - $page: Signal[Page.Detail] -)(using router: Router[Page]): - val $oscChangeSignal = $page.splitOne(_.osobniCislo.value)((osc, _, _) => osc) - val $pageChangeSignal = - $oscChangeSignal.flatMap(osc => - EventStream.fromSeq(Seq(FetchUserDetails(osc), FetchParameters(osc))) - ) - // TODO: filter the value based on the current osc - // OSC change will fetch new data, but still - // - we need to be sure that what we got is really what we ought to display - // - we want to display stale data accordingly (at least with loading indicator) - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - def apply: HtmlElement = - AppPage(state.actionBus)( - $data.combineWithFn($params)(_ zip _) - .map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: List[Parameter] - ): DetailPage.ViewModel = - DetailPage.ViewModel( - o.toDetailOsoby, - p.map( - _.toParametr(param => - PageLink.container(Page.DetailParametru(o, param), state.actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala deleted file mode 100644 index fdaa7f8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailParametruPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink - -object DetailParametruPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailParametruPageConnector( - state: DetailParametruPageConnector.AppState -)( - $page: Signal[Page.DetailParametru] -)(using router: Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => (p.osobniCislo.value, p.parametr.value))((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map(FetchParameter(_, _)) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - } yield (da, pb) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailParametruPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter - ): DetailParametruPage.ViewModel = - DetailParametruPage.ViewModel( - o.toDetailOsoby, - p.toParametr(p => - PageLink.container(Page.DetailParametru(o, p), state.actionBus) - ), - p.criteria.map( - _.toKriterium { c => - PageLink.container( - Page.DetailKriteria(o, p, c), - state.actionBus - ) - } - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala deleted file mode 100644 index 049aa22..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -case class DirectoryPageConnector( - $input: EventStream[List[UserInfo]], - actionBus: Observer[Action] -)(using router: Router[Page]): - val $data = $input.startWithNone - val $actionSignal = EventStream.fromValue(FetchDirectory) - - def apply: HtmlElement = - AppPage(actionBus)( - $data.split(_ => ())((_, _, s) => - pages.directory.DirectoryPage( - s.map( - _.map( - _.toUserRow(u => - PageLink.container( - Page.Detail(Page.Titled(u.personalNumber)), - actionBus - ) - ) - ) - ) - ) - ), - $actionSignal --> actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala deleted file mode 100644 index 88e7450..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ /dev/null @@ -1,68 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii -import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria -import fiftyforms.ui.components.tailwind.Color - -extension (o: UserInfo) - def toDetailOsoby: DetailOsoby.ViewModel = - DetailOsoby.ViewModel( - o.personalNumber, - o.name, - o.email, - o.phone, - o.img, - o.mainFunction.map(f => - DetailOsoby.Funkce.ViewModel(f.name, f.dept, f.ou) - ), - o.userContracts.headOption.map(c => - DetailOsoby.PracovniPomer.ViewModel(c.rel, c.startDate, c.endDate) - ) - ) - -extension (param: Parameter) - def toParametr(container: Parameter => Anchor): DetailParametru.ViewModel = - DetailParametru.ViewModel( - id = param.id, - nazev = param.name, - popis = param.description, - status = "Nesplněno", - statusColor = Color.red, - a = container(param) - ) - -extension (crit: ParameterCriteria) - def toKriterium( - container: ParameterCriteria => Anchor - ): DetailKriteria.ViewModel = - DetailKriteria.ViewModel( - nazev = crit.criteriumText, - kapitola = crit.chapterId, - bod = crit.itemId, - status = "Nesplněno", - statusColor = Color.red, - splneno = false, - dukazy = Nil, - container = container(crit) - ) - -extension (user: UserInfo) - def toUserRow( - container: UserInfo => HtmlElement = (_: UserInfo) => div() - ): UserRow.ViewModel = - UserRow.ViewModel( - osobniCislo = user.personalNumber.toString, - celeJmeno = user.name, - prijmeni = user.surname, - hlavniFunkce = user.mainFunction.map(_.name), - img = user.img, - container = () => container(user) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala deleted file mode 100644 index 7d5b2c1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.dashboard - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.Page - -object DashboardPage: - - def render: HtmlElement = - div("Dashboard page") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala deleted file mode 100644 index 9ef0698..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala +++ /dev/null @@ -1,31 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailKriteriaPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply($m: Signal[ViewModel])( - action: Observer[DukazyKriteria.Action] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - DukazyKriteria($m.map(_.kriterium.dukazy))(action) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala deleted file mode 100644 index 3b316e1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object DetailPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametry: SeznamParametru.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby($m.map(_.osoba)), - SeznamParametru($m.map(_.parametry)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala deleted file mode 100644 index 17224cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala +++ /dev/null @@ -1,24 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailParametruPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriteria: SeznamKriterii.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby.header($m.map(_.osoba)), - DetailParametru($m.map(_.parametr)), - SeznamKriterii($m.map(_.kriteria)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala deleted file mode 100644 index dc912fa..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ /dev/null @@ -1,107 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb -package app -package pages.detail - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.UpravDukazForm -import fiftyforms.services.files.File - -object UpravDukaz: - - type ThisPage = Page.UpravDukazKriteria - type PageKey = (OsobniCislo, String, String) - - trait State { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def availableFiles: EventStream[List[File]] - def actionBus: Observer[Action] - } - - def keyOfPage(page: ThisPage): PageKey = - (page.osobniCislo.value, page.parametr.value, page.kriterium.value) - - def onChangeAction(key: PageKey): Action = - FetchParameterCriteria( - key._1, - key._2, - key._3, - Page.UpravDukazKriteria(_, _, _) - ) - - class Connector(state: State)($page: Signal[ThisPage])(using Router[Page]): - val $paramChangeSignal = $page.splitOne(keyOfPage)((x, _, _) => x) - val $pageChangeSignal = $paramChangeSignal.map(onChangeAction) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.split(_ => ())((_, s, $s) => - PageComponent( - $s.map(buildModel), - state.availableFiles, - state.actionBus.contramap { - case UpravDukazForm.Cancelled => - NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) - case UpravDukazForm.AvailableFilesRequested => - FetchAvailableFiles(s._1.personalNumber) - } - ) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): PageComponent.ViewModel = - import connectors.* - PageComponent.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) - - object PageComponent: - import components.* - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply( - $m: Signal[ViewModel], - availableFilesStream: EventStream[List[File]], - events: Observer[UpravDukazForm.Event] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - UpravDukazForm(availableFilesStream)(events) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala deleted file mode 100644 index ca311b0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.Icons -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Color - -object DetailKriteria: - case class ViewModel( - nazev: String, - kapitola: String, - bod: String, - status: String, - statusColor: Color, - splneno: Boolean, - dukazy: List[DukazKriteria.ViewModel], - container: HtmlElement = div() - ) { - val id = s"${kapitola}${bod}" - } - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h3( - cls := "text-xl leading-6 font-bold text-gray-900", - child.text <-- $m.map(_.nazev) - ), - h3( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - "Kapitola ", - child.text <-- $m.map(_.kapitola), - " bod ", - child <-- $m.map(_.bod) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala deleted file mode 100644 index ae90290..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala +++ /dev/null @@ -1,100 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Avatar - -object DetailOsoby: - - object Funkce: - case class ViewModel( - nazev: String, - stredisko: String, - voj: String - ) - - def render($m: Signal[ViewModel]): HtmlElement = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.nazev), - span( - cls := "hidden md:inline", - " @ ", - child.text <-- $m.map(_.stredisko), - ", ", - child.text <-- $m.map(_.voj) - ) - ) - - object PracovniPomer: - case class ViewModel( - druh: String, - pocatek: LocalDate, - konec: Option[LocalDate] - ) - - def render($m: Signal[ViewModel]): HtmlElement = - import fiftyforms.ui.components.tailwind.CustomAttrs.datetime - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.druh), - " od ", - time( - datetime <-- $m.map(_.pocatek.toString), - child.text <-- $m.map(_.pocatek.toString) - ) - ) - - case class ViewModel( - osobniCislo: OsobniCislo, - jmeno: String, - email: Option[String], - telefon: Option[String], - img: Option[String], - hlavniFunkce: Option[Funkce.ViewModel], - pracovniPomer: Option[PracovniPomer.ViewModel] - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-4", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(16) - ), - div( - h1( - cls := "text-2xl font-bold text-gray-900", - child.text <-- $m.map(_.jmeno) - ), - child.maybe <-- $m.map(_.hlavniFunkce).split(_ => ())((_, _, d) => - Funkce.render(d) - ), - child.maybe <-- $m.map(_.pracovniPomer).split(_ => ())((_, _, d) => - PracovniPomer.render(d) - ) - ) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-5", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(8) - ), - div( - h1( - cls := "text-lg font-medium text-gray-800", - child.text <-- $m.map(_.jmeno) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala deleted file mode 100644 index e11cb62..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color - -object DetailParametru: - case class ViewModel( - id: String, - nazev: String, - popis: String, - status: String, - statusColor: Color, - a: Anchor - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h2( - cls := "text-2xl leading-6 font-medium text-gray-900", - child.text <-- $m.map(_.nazev) - ), - p( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - child.text <-- $m.map(_.popis) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - h2( - cls := "text-lg leading-6 font-medium text-gray-600", - child.text <-- $m.map(_.nazev) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala deleted file mode 100644 index a00ede8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala +++ /dev/null @@ -1,177 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.CustomAttrs - -object DukazKriteria: - case class Osoba(osobniCislo: String, jmeno: String) - case class Udalost(osoba: Osoba, datum: LocalDate) - case class Dokument( - url: String, - nazev: String, - pridal: Udalost, - odebral: Option[Udalost] - ) - case class ViewModel( - dokumenty: List[Dokument], - autorizoval: Option[Udalost], - platiDo: Option[LocalDate], - poznámka: Option[String] - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-lg", - div( - cls := "px-4 py-5 sm:px-6", - h3( - cls := "text-lg leading-6 font-medium text-gray-900", - """Applicant Information""" - ), - p( - cls := "mt-1 max-w-2xl text-sm text-gray-500", - """Personal details and application.""" - ) - ), - div( - cls := "border-t border-gray-200 px-4 py-5 sm:px-6", - dl( - cls := "grid grid-cols-1 gap-x-4 gap-y-8 sm:grid-cols-2", - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Full name""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Margot Foster""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Application for""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Backend Developer""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Email address""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """margotfoster@example.com""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Salary expectation""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """$120,000""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """About""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Fugiat ipsum ipsum deserunt culpa aute sint do nostrud anim incididunt cillum culpa consequat. Excepteur qui ipsum aliquip consequat sint. Sit id mollit nulla mollit nostrud in ea officia proident. Irure nostrud pariatur mollit ad adipisicing reprehenderit deserunt qui eu.""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """Attachments""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - ul( - role := "list", - cls := "border border-gray-200 rounded-md divide-y divide-gray-200", - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """resume_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ), - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """coverletter_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala deleted file mode 100644 index 22f134f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala +++ /dev/null @@ -1,35 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object DukazyKriteria: - sealed trait Action - case object Add extends Action - - type ViewModel = List[DukazKriteria.ViewModel] - - def apply($m: Signal[ViewModel])(actions: Observer[Action]): HtmlElement = - div( - child.maybe <-- $m.map(m => - if (m.isEmpty) then Some(prazdnyDukaz(actions)) - else None - ), - children <-- $m.map(_.zipWithIndex).split(_._2)((_, _, $s) => - DukazKriteria($s.map(_._1)) - ) - ) - - private def prazdnyDukaz(actions: Observer[Action]): HtmlElement = - button( - tpe := "button", - onClick.preventDefault.mapTo(Add) --> actions, - cls := "relative block w-full border-2 border-gray-300 border-dashed rounded-lg p-12 text-center hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - Icons.outline - .`document-add`(12) - .amend(svg.cls := "mx-auto text-gray-400"), - span( - cls := "mt-2 block text-sm font-medium text-gray-900", - "Přidat důkaz" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala deleted file mode 100644 index f628697..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.list.StackedList -import fiftyforms.ui.components.tailwind.list.ListRow -import fiftyforms.ui.components.tailwind.list.RowTag -import fiftyforms.ui.components.tailwind.list.RowNext -import java.time.LocalDate - -object SeznamKriterii: - type ViewModel = List[DetailKriteria.ViewModel] - - private val kritList = new StackedList[DetailKriteria.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - kritList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = - p(cls := "text-sm text-gray-500", s"${i.kapitola}${i.bod}"), - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.container - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala deleted file mode 100644 index 2bfe4bc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.list.{ - StackedList, - ListRow, - RowTag, - PropList, - IconText, - RowNext -} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.LinkSupport.* - -object SeznamParametru: - type ViewModel = List[DetailParametru.ViewModel] - - private val parametrList = new StackedList[DetailParametru.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - parametrList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = emptyNode, - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.a - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala deleted file mode 100644 index f048e9d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ /dev/null @@ -1,101 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.form.* -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement -import fiftyforms.services.files.components.tailwind.FilePicker -import cz.e_bs.cmi.mdr.pdb.frontend.AutorizujDukaz -import cz.e_bs.cmi.mdr.pdb.frontend.DocumentRef -import fiftyforms.services.files.File - -object UpravDukazForm: - sealed trait Event - case object Cancelled extends Event - case object AvailableFilesRequested extends Event - def apply(availableFilesStream: EventStream[List[File]])( - updates: Observer[Event] - ): HtmlElement = - val (filesStream, filesObserver) = - EventStream.withObserver[FilePicker.Event] - val files = Var[List[File]](Nil) - def submitButtons: HtmlElement = - div( - cls := "pt-5", - div( - cls := "flex justify-end", - button( - tpe := "button", - cls := "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zrušit", - onClick.mapTo(Cancelled) --> updates - ), - button( - tpe := "submit", - cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - disabled <-- files.signal.map(_.isEmpty), - "Autorizovat" - ) - ) - ) - - div( - filesStream.collect { case FilePicker.SelectionUpdated(files) => - files.to(List) - } --> files.writer, - filesStream.collect { case FilePicker.AvailableFilesRequested => - AvailableFilesRequested - } --> updates, - cls := "bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6", - Form( - Form.Body( - Form.Section( - FormHeader( - FormHeader.ViewModel( - "Doložení kritéria", - "Sestavte doklady poskytující důkaz kritéria a potvrďte odesláním formuláře. Případné limitace či jiné relevantní údaje vepište do pole Komentář." - ) - ), - FormFields( - FormRow( - "dokumenty", - "Dokumenty", - FilePicker(files.signal, availableFilesStream)(filesObserver) - .amend(idAttr := "dokumenty", cls("max-w-lg")) - ).toHtml, - FormRow( - "platnost", - "Platnost", - input( - idAttr := "platnost", - name := "platnost", - tpe := "date", - autoComplete := "date", - cls := "block max-w-lg w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" - ) - ).toHtml, - FormRow( - "komentar", - "Komentář", - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - textArea( - idAttr := "komentar", - name := "about", - rows := 3, - cls := "max-w-lg shadow-sm block w-full focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border border-gray-300 rounded-md" - ), - p( - cls := "mt-2 text-sm text-gray-500", - "Doplňte prosím potřebné informace související s doložením kritéria, včetně případných limitací." - ) - ) - ).toHtml - ) - ) - ), - submitButtons - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala deleted file mode 100644 index 41033f6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -import components._ - -object DirectoryPage: - - type ViewModel = List[UserRow.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - val (actionsStream, actionObserver) = - EventStream.withObserver[SearchForm.Action] - val $filter = actionsStream - .collect { - case SearchForm.ClearFilter => None - case SearchForm.SetFilter(t) => Some(t) - } - .startWith(None) - val byLetter = for { - d <- $m - f <- $filter - } yield for { - (letter, users) <- d - .filter { user => - f match - case None => true - case Some(t) => user.search.contains(t) - } - .groupBy(_.prijmeni.head) - .to(List) - .sortBy(_._1) - } yield (letter.toString, users.sortBy(_.prijmeni)) - - div( - cls := "h-full max-w-7xl mx-auto order-first flex flex-col flex-shrink-0", - SearchForm(actionObserver), - Directory(byLetter) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala deleted file mode 100644 index 73f915d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -object Directory: - - object Header: - type ViewModel = String - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500", - h3(child.text <-- $m) - ) - - object UserList: - type ViewModel = List[UserRow.ViewModel] - def apply($m: Signal[ViewModel]): HtmlElement = - ul( - role := "list", - cls := "relative z-0 divide-y divide-gray-200", - children <-- $m.split(_.osobniCislo)((_, _, s) => UserRow(s)) - ) - - type ViewModel = List[(String, List[UserRow.ViewModel])] - def apply($m: Signal[ViewModel]): HtmlElement = - nav( - cls := "flex-1 min-h-0 overflow-y-auto", - aria.label := "Directory", - children <-- $m.split(_._1)((_, _, s) => - div( - cls := "relative", - Header(s.map(_._1)), - UserList(s.map(_._2)) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala deleted file mode 100644 index e182910..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala +++ /dev/null @@ -1,54 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object SearchForm: - sealed trait Action - case object SubmitFilter extends Action - sealed trait FilterAction extends Action - case object ClearFilter extends FilterAction - case class SetFilter(value: String) extends FilterAction - - def apply(actions: Observer[Action]): HtmlElement = - div( - cls := "px-6 pt-4 pb-4", - form( - cls := "flex space-x-4", - onSubmit.mapTo(SubmitFilter) --> actions, - div( - cls := "flex-1 min-w-0", - label( - forId := "search", - cls := "sr-only", - "Hledat" - ), - div( - cls := "relative rounded-md shadow-sm", - div( - cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", - Icons.solid.search().amend(svg.cls := "text-gray-400") - ), - input( - tpe := "search", - name := "search", - idAttr := "search", - cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", - placeholder := "Hledat", - composeEvents(onInput.mapToValue.setAsValue.map(SetFilter(_)))( - _.throttle(500) - ) --> actions - ) - ) - ), - button( - tpe := "submit", - cls := "inline-flex justify-center px-3.5 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500", - Icons.solid.filter().amend(svg.cls := "text-gray-400"), - span( - cls := "sr-only", - "Hledat" - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala deleted file mode 100644 index 82f7723..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala +++ /dev/null @@ -1,52 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.airstream.core.Signal -import fiftyforms.ui.components.tailwind.Avatar - -object UserRow: - case class ViewModel( - osobniCislo: String, - celeJmeno: String, - prijmeni: String, - hlavniFunkce: Option[String], - img: Option[String], - container: () => HtmlElement = () => div() - ) { - val search = prijmeni.toLowerCase - } - - def apply($m: Signal[ViewModel]): HtmlElement = - inline def avatarImage = - Avatar($m.map(_.img)).avatarImage(10) - - li( - div( - cls := "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500", - div( - cls := "flex-shrink-0", - child <-- avatarImage - ), - div( - cls := "flex-1 min-w-0", - child <-- $m.map { o => - o.container() - .amend( - cls := "focus:outline-none", - span( - cls := "absolute inset-0", - aria.hidden := true - ), - p( - cls := "text-sm font-medium text-gray-900", - o.celeJmeno - ), - p( - cls := "text-sm text-gray-500 truncate", - o.hlavniFunkce - ) - ) - } - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala deleted file mode 100644 index 467c5f5..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala +++ /dev/null @@ -1,99 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router - -object ErrorPage: - case class ViewModel( - homePage: Page, - errorName: String, - title: String, - subTitle: String - ) - - def apply(m: ViewModel, actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - val ViewModel(homePage, errorName, title, subTitle) = m - div( - cls := "min-h-full pt-16 pb-12 flex flex-col bg-white", - main( - cls := "flex-grow flex flex-col justify-center max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex-shrink-0 flex justify-center", - a( - href := "/", - cls := "inline-flex", - span( - cls := "sr-only", - """Workflow""" - ), - img( - cls := "h-12 w-auto", - src := "https://tailwindui.com/img/logos/workflow-mark.svg?color=indigo&shade=600", - alt := "" - ) - ) - ), - div( - cls := "py-16", - div( - cls := "text-center", - p( - cls := "text-sm font-semibold text-indigo-600 uppercase tracking-wide", - errorName - ), - h1( - cls := "mt-2 text-4xl font-extrabold text-gray-900 tracking-tight sm:text-5xl", - title - ), - p( - cls := "mt-2 text-base text-gray-500", - subTitle - ), - div( - cls := "mt-6", - PageLink - .container(homePage, actionBus) - .amend( - cls := "text-base font-medium text-indigo-600 hover:text-indigo-500", - """Go back home""" - ) - ) - ) - ) - ), - footer( - cls := "flex-shrink-0 max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", - nav( - cls := "flex justify-center space-x-4", - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Contact Support""" - ), - span( - cls := "inline-block border-l border-gray-300", - aria.hidden := true - ), - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Status""" - ), - span( - cls := "inline-block border-l border-gray-300", - aria.hidden := true - ), - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Twitter""" - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala deleted file mode 100644 index bd9baec..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action - -object NotFoundPage: - def apply(homePage: Page, url: String, actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - ErrorPage( - ErrorPage.ViewModel( - homePage, - "404 error", - "Page not found.", - s"Sorry, but page $url doesn't exist." - ), - actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala deleted file mode 100644 index 476fe3c..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action - -object UnhandledErrorPage: - - case class ViewModel( - homePage: Page, - errorName: Option[String], - errorMessage: Option[String] - ) - - def apply(m: ViewModel, actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - ErrorPage( - ErrorPage.ViewModel( - m.homePage, - "Unexpected error occurred", - m.errorName.getOrElse( - "Uh oh!" - ), // TODO: translations, better text than uh oh - m.errorMessage.getOrElse( - "This wasn't supposed to happen! Please try again." - ) - ), - actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala deleted file mode 100644 index 25f059f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala +++ /dev/null @@ -1,6 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.services - -import com.raquo.airstream.core.Observer - -trait DataFetcher[K, A]: - def fetch(id: K, sink: Observer[A]): Unit diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala deleted file mode 100644 index a3b00af..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala +++ /dev/null @@ -1,103 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import scala.scalajs.js.annotation.JSExportTopLevel -import scala.scalajs.js.annotation.JSExport -import scala.scalajs.js.annotation.JSImport -import scala.scalajs.js -import org.scalajs.dom -import com.raquo.laminar.api.L.{*, given} -import scala.scalajs.js.Date -import com.raquo.waypoint.Router -import com.raquo.waypoint.SplitRender -import cz.e_bs.cmi.mdr.pdb.app.services.DataFetcher -import scala.scalajs.js.JSON -import zio.json._ -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.state.AppState - -@js.native -@JSImport("stylesheets/main.css", JSImport.Namespace) -object Css extends js.Any - -@js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native -@JSImport("params/pdb-params.json", JSImport.Default) -object pdbParams extends js.Object - -@JSExportTopLevel("app") -object Main: - - @JSExport - def main(args: Array[String]): Unit = - import Routes.given - onLoad { - setupAirstream() - val appContainer = dom.document.querySelector("#app") - val _ = - render( - appContainer, - renderPage(state.MockAppState(using unsafeWindowOwner, router)) - ) - } - - private def onLoad(f: => Unit): Unit = - documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) - - private def setupAirstream()(using router: Router[Page]): Unit = - AirstreamError.registerUnhandledErrorCallback(err => - router.forcePage( - Page.UnhandledError( - Some(err.getClass.getName), // TODO: Fill only in dev mode - Some(err.getMessage) - ) - ) - ) - - def renderPage(state: AppState)(using - router: Router[Page] - ): HtmlElement = - val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) - .collectSignal[Page.Detail]( - connectors - .DetailPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailParametru]( - connectors - .DetailParametruPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailKriteria]( - connectors - .DetailKriteriaPageConnector(state)(_) - .apply - ) - .collectSignal[Page.UpravDukazKriteria]( - pages.detail.UpravDukaz.Connector(state)(_).apply - ) - .collectStatic(Page.Dashboard)( - connectors.DashboardPageConnector(state.actionBus).apply - ) - .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) - ) - .collect[Page.UnhandledError](pg => - pages.errors - .UnhandledErrorPage( - pages.errors.UnhandledErrorPage - .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), - state.actionBus - ) - ) - .collectStatic(Page.Directory)( - connectors - .DirectoryPageConnector(state.users, state.actionBus) - .apply - ) - div(cls := "h-full", child <-- pageSplitter.$view) - - // Pull in the stylesheet - val css: Css.type = Css diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala deleted file mode 100644 index 3bbc8c0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ /dev/null @@ -1,198 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.* -import org.scalajs.dom -import zio.json.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import scala.scalajs.js -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.Page.Titled - -// enum is not working with Waypoints' SplitRender collectStatic -sealed abstract class Page( - val id: String, - val title: String, - val parent: Option[Page] -) { - val path: Vector[Page] = - parent match - case None => Vector(this) - case Some(p) => p.path :+ this - - val isRoot: Boolean = parent.isEmpty -} - -object Page: - - case class Titled[V](value: V, title: Option[String] = None): - val show: String = title.getOrElse(value.toString) - - case object Directory extends Page("directory", "Adresář", None) - - case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) - - case class Detail(osobniCislo: Titled[OsobniCislo]) - extends Page("user", osobniCislo.show, Some(Directory)) - - object Detail { - def apply(o: UserInfo): Detail = Detail( - Titled(o.personalNumber, Some(o.name)) - ) - } - - case class DetailParametru( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String] - ) extends Page( - "parameter", - parametr.show, - Some(Detail(osobniCislo)) - ) - - object DetailParametru { - def apply(o: UserInfo, p: Parameter): DetailParametru = - DetailParametru( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)) - ) - } - - case class DetailKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "criteria", - kriterium.show, - Some(DetailParametru(osobniCislo, parametr)) - ) - - object DetailKriteria { - def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = - DetailKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class UpravDukazKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "addProof", - "Důkaz", - Some(DetailKriteria(osobniCislo, parametr, kriterium)) - ) - - object UpravDukazKriteria { - def apply( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): UpravDukazKriteria = - UpravDukazKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class NotFound(url: String) extends Page("404", "404", Some(Directory)) - - case class UnhandledError( - errorName: Option[String], - errorMessage: Option[String] - ) extends Page("500", "Unexpected error", Some(Directory)) - -object Routes: - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) - given [V: JsonEncoder]: JsonEncoder[Titled[V]] = - DeriveJsonEncoder.gen[Titled[V]] - given [V: JsonDecoder]: JsonDecoder[Titled[V]] = - DeriveJsonDecoder.gen[Titled[V]] - given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] - given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] - - val base = - js.`import`.meta.env.BASE_URL - .asInstanceOf[String] + "app" - - val homePage: Page = Page.Directory - - given router: Router[Page] = Router[Page]( - routes = List( - Route.static(homePage, root / endOfSegments, basePath = base), - Route.static( - Page.Dashboard, - root / "dashboard" / endOfSegments, - basePath = base - ), - Route[Page.Detail, String]( - encode = _.osobniCislo.value.toString, - decode = osc => Page.Detail(Titled(OsobniCislo(osc))), - root / "osoba" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.DetailParametru, (String, String)]( - encode = p => (p.osobniCislo.value.toString, p.parametr.value), - decode = - p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / endOfSegments, - basePath = base - ), - Route[Page.DetailKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.DetailKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.UpravDukazKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.UpravDukazKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / "edit" / endOfSegments, - basePath = base - ) - ), - serializePage = _.toJson, - deserializePage = _.fromJson[Page] - .fold(s => throw IllegalStateException(s), identity), - getPageTitle = _.title, - routeFallback = url => Page.NotFound(url), - deserializeFallback = _ => Page.Dashboard - )( - $popStateEvent = windowEvents.onPopState, - owner = unsafeWindowOwner - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala deleted file mode 100644 index b24b82b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -sealed trait Action - -case object FetchDirectory extends Action -case class FetchUserDetails(osc: OsobniCislo) extends Action -case class FetchParameters(osc: OsobniCislo) extends Action -case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action -case class FetchParameterCriteria( - osc: OsobniCislo, - paramId: String, - critId: String, - page: (UserInfo, Parameter, ParameterCriteria) => Page -) extends Action -case class FetchAvailableFiles(osc: OsobniCislo) extends Action -case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala deleted file mode 100644 index c637d35..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala +++ /dev/null @@ -1,82 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.{UserProfile, UserInfo, OsobniCislo} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo -import cz.e_bs.cmi.mdr.pdb.UserFunction - -object AppPage: - // TODO: pages by logged in user - val pages: List[Page] = List(Page.Directory, Page.Dashboard) - - import NavigationBar.{Logo, MenuItem} - - val logo = Logo( - "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", - "Workflow" - ) - - // TODO: menu items by user profile - val userMenu = - List( - MenuItem("Your Profile"), - MenuItem("Settings"), - MenuItem("Sign out") - ) - - // TODO: load user profile - val $userProfile = Var( - UserProfile( - "tom", - UserInfo( - OsobniCislo("1031"), - "tom", - "Tom", - "Cook", - None, - None, - Some("tom@example.com"), - Some("+420 222 866 180"), - Some( - UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") - ), - Nil, - None - ) - ) - ) - - val $userInfo = $userProfile.signal.map(_.userInfo) - - type ViewModel = Option[HtmlElement] - def apply( - actionBus: Observer[Action] - )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using - router: Router[Page] - ): HtmlElement = - PageLayout(actionBus)( - $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => - PageLayout.ViewModel( - NavigationBar.ViewModel( - u, - pages.map(p => - NavigationBar.Link( - () => - PageLink( - p, - actionBus - ), - p == cp - ) - ), - userMenu, - logo - ), - c - ) - ), - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala deleted file mode 100644 index bf473c6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ /dev/null @@ -1,143 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden -import fiftyforms.ui.components.tailwind.list.IconText.ViewModel -import fiftyforms.ui.components.tailwind.Icons -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Action - -object Breadcrumbs: - - private def slash = - import svg.{*, given} - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-300", - xmlns := "http://www.w3.org/2000/svg", - fill := "currentColor", - viewBox := "0 0 20 20", - ariaHidden := true, - path( - d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" - ) - ) - - def backIcon = - Icons.solid - .`arrow-narrow-left`() - .amend( - svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" - ) - - object Link: - case class ViewModel( - page: Page, - icon: Option[SvgElement], - extraClasses: String, - text: String, - textClasses: Option[String] = None - ) - - val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" - - def shortHome(p: Page) = ViewModel( - p, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - "Zpět na úvodní stránku", - None - ) - - def fullHome(p: Page) = ViewModel( - p, - Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), - "text-gray-400 hover:text-gray-500", - "Domů", - Some("sr-only") - ) - - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - PageLink - .container($m.map(_.page), actionBus) - .amend( - cls <-- $m.map(_.extraClasses), - child.maybe <-- $m.map(_.icon), - span( - cls <-- $m.map(_.textClasses.getOrElse("")), - child.text <-- $m.map(_.text) - ) - ) - - object FullBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - ol( - role := "list", - cls := "flex items-center space-x-4", - children <-- $m.map(_.path).split(_.id)((_, _, $p) => - li( - div( - cls := "flex items-center", - child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), - Link( - $p.map(p => - if (p.isRoot) then Link.fullHome(p) - else - Link.ViewModel( - p, - None, - s"ml-4 max-w-xs truncate ${Link.baseClasses}", - p.title - ) - ), - actionBus - ) - ) - ) - ) - ) - - object ShortBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - Link( - $m.map { p => - val target = p.parent.getOrElse(p) - if target.isRoot then Link.shortHome(target) - else - Link.ViewModel( - target, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - s"Zpět na ${target.title}" - ) - }, - actionBus - ) - - def apply(actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - nav( - cls := "flex", - aria.label := "Breadcrumb", - div( - cls := "flex sm:hidden", - ShortBreadcrumbs(router.$currentPage, actionBus) - ), - div( - cls := "hidden sm:block", - FullBreadcrumbs(router.$currentPage, actionBus) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala deleted file mode 100644 index 0db2047..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ /dev/null @@ -1,242 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent -import fiftyforms.ui.components.tailwind.Icons -import fiftyforms.ui.components.tailwind.Avatar -import cz.e_bs.cmi.mdr.pdb.UserInfo -import io.laminext.syntax.core.* - -object NavigationBar: - - case class Logo(img: String, name: String) - case class Link(a: () => Anchor, active: Boolean) - case class MenuItem(title: String) - - case class ViewModel( - userInfo: UserInfo, - pages: List[Link], - userMenu: List[MenuItem], - logo: Logo - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - val $userInfo = $m.map(_.userInfo) - val mobileMenuOpen = Var(false) - - val desktopOnly = cls("hidden md:block") - val mobileOnly = cls("md:hidden") - - inline def avatarImage(size: Int = 8) = - Avatar($userInfo.map(_.img)).avatarImage(size) - - def notificationButton = button( - tpe := "button", - cls := List( - "bg-indigo-600", - "focus:outline-none", - "focus:ring-2", - "focus:ring-offset-2", - "focus:ring-offset-indigo-600", - "focus:ring-white", - "hover:text-white", - "p-1", - "rounded-full", - "text-indigo-200" - ), - span(cls := "sr-only", "View notifications"), - Icons.outline.bell() - ) - - def userProfile: HtmlElement = - val menuOpen = Var(false) - - def menuItem(item: MenuItem, idx: Int): HtmlElement = - a( - href := "#", - cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", - role := "menuitem", - tabIndex := -1, - idAttr := s"user-menu-item-$idx", - item.title - ) - - div( - cls := "ml-3 relative", - div( - button( - tpe := "button", - cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - idAttr := "user-menu-button", - aria.expanded <-- menuOpen.signal, - aria.hasPopup := true, - span(cls := "sr-only", "Open user menu"), - child <-- avatarImage(), - onClick.preventDefault.mapTo( - !menuOpen.now() - ) --> menuOpen.writer - ) - ), - /* - * */ - div( - cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", - cls <-- menuOpen.signal.map { o => - if (o) "md:block" else "md:hidden" - }, - role := "menu", - aria.orientation := "vertical", - aria.labelledBy := "user-menu-button", - tabIndex := -1, - // : keyboard navigation - children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) - ) - ) - - def mobileProfile = - def menuItem(item: MenuItem): HtmlElement = - a( - href := "#", - cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", - item.title - ) - - div( - cls := "pt-4 pb-3 border-t border-indigo-700", - div( - cls := "flex items-center px-5", - div( - cls := "flex-shrink-0", - child <-- avatarImage(10) - ), - div( - cls := "ml-3", - div( - cls := "text-base font-medium text-white", - child.text <-- $userInfo.map(_.name) - ), - child.maybe <-- $userInfo.map( - _.email.map(e => - div( - cls := "text-sm font-medium text-indigo-300", - e - ) - ) - ) - ), - notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) - ), - div( - cls := "mt-3 px-2 space-y-1", - children <-- $m.map(_.userMenu.map(menuItem)) - ) - ) - - def pageLink(page: Link): Anchor = - page - .a() - .amend( - cls := "text-white px-3 py-2 rounded-md text-sm font-medium", - cls := Seq( - "bg-indigo-700" -> page.active, - "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active - ), - ariaCurrent := (if page.active then "page" else "false") - ) - - def logoImg: Image = - img( - cls := "h-8 w-8", - src <-- $m.map(_.logo.img), - alt <-- $m.map(_.logo.name) - ) - - def mobileMenuButton = button( - tpe := "button", - cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - aria.controls := "mobile-menu", - aria.expanded <-- mobileMenuOpen.signal, - span(cls := "sr-only", "Open main menu"), - Icons.outline - .menu() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") - ), - Icons.outline - .x() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") - ), - onClick.preventDefault.mapTo( - !mobileMenuOpen.now() - ) --> mobileMenuOpen.writer - ) - - def navBarLeft = - div( - cls := "flex items-center", - div(cls := "flex-shrink-0", logoImg), - div( - desktopOnly, - div( - cls := "ml-10 flex items-baseline space-x-4", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ) - ) - ) - - def navBarRight = - div( - desktopOnly, - div( - cls := "ml-4 flex items-center md:ml-6", - notificationButton, - userProfile - ) - ) - - def navBarMobile = - div( - cls := "-mr-2 flex", - mobileOnly, - mobileMenuButton - ) - - def navBar = - div( - cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex items-center justify-between h-16", - navBarLeft, - navBarRight, - navBarMobile - ) - ) - - def mobileMenu = - div( - mobileOnly, - cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }, - idAttr := "mobile-menu", - div( - cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ), - mobileProfile - ) - - nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala deleted file mode 100644 index 75339df..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object PageHeader: - def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = - header( - cls := "bg-white shadow-sm", - div( - cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", - h1( - cls := "text-lg leading-6 font-semibold text-gray-900", - Breadcrumbs(actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala deleted file mode 100644 index 5096463..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ /dev/null @@ -1,30 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import fiftyforms.ui.components.tailwind.Loading -import io.laminext.syntax.core.* - -object PageLayout: - case class ViewModel( - navigation: NavigationBar.ViewModel, - content: Option[HtmlElement] - ) - def apply(actionBus: Observer[Action])( - $m: Signal[ViewModel], - mods: Modifier[HtmlElement]* - )(using router: Router[Page]): HtmlElement = - val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) - div( - cls := "h-full flex flex-col", - NavigationBar($m.map(_.navigation)), - child.maybe <-- router.$currentPage.map(_.isRoot) - .switch(None, Some(PageHeader(actionBus))), - main( - cls := "flex-grow-1 overflow-y-auto", - mods, - child <-- $maybeContent.map(_.getOrElse(Loading)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala deleted file mode 100644 index 62d820f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.LinkSupport.* -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo - -object PageLink: - type ViewModel = Page - def apply($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = - a(mods($m, actions), child.text <-- $m.map(_.title)) - - def apply(m: ViewModel, actions: Observer[Action])(using - router: Router[Page] - ): Anchor = apply(Val(m), actions) - - def container($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = a(mods($m, actions)) - - def container(m: ViewModel, actions: Observer[Action])(using - Router[Page] - ): Anchor = container(Val(m), actions) - - private def mods($m: Signal[Page], actions: Observer[Action])(using - router: Router[Page] - ): Modifier[Anchor] = - Seq( - href <-- $m.map(router.absoluteUrlForPage).recover { case _ => - Some("invalid url") - }, - composeEvents(onClick.noKeyMod.preventDefault)( - _.sample($m) - .map(NavigateTo.apply) - ) --> actions - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala deleted file mode 100644 index 87f825a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.pages.dashboard.DashboardPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -class DashboardPageConnector(actionBus: Observer[Action])(using - router: Router[Page] -): - def apply: HtmlElement = - AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala deleted file mode 100644 index e9b0f71..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala +++ /dev/null @@ -1,76 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailKriteriaPage -import pages.detail.components.DukazyKriteria -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -object DetailKriteriaPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailKriteriaPageConnector( - state: DetailKriteriaPageConnector.AppState -)( - $page: Signal[Page.DetailKriteria] -)(using Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => - (p.osobniCislo.value, p.parametr.value, p.kriterium.value) - )((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map( - FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) - ) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, s, $s) => - DetailKriteriaPage($s)(state.actionBus.contramap { - case DukazyKriteria.Add => - NavigateTo( - Page.UpravDukazKriteria( - Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), - Page.Titled(s.parametr.id, Some(s.parametr.nazev)), - Page.Titled(s.kriterium.id, Some(s.kriterium.id)) - ) - ) - }) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): DetailKriteriaPage.ViewModel = - DetailKriteriaPage.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala deleted file mode 100644 index 4d8024f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ /dev/null @@ -1,57 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import pages.detail.DetailPage -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import fiftyforms.ui.components.tailwind.Color - -object DetailPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailPageConnector(state: DetailPageConnector.AppState)( - $page: Signal[Page.Detail] -)(using router: Router[Page]): - val $oscChangeSignal = $page.splitOne(_.osobniCislo.value)((osc, _, _) => osc) - val $pageChangeSignal = - $oscChangeSignal.flatMap(osc => - EventStream.fromSeq(Seq(FetchUserDetails(osc), FetchParameters(osc))) - ) - // TODO: filter the value based on the current osc - // OSC change will fetch new data, but still - // - we need to be sure that what we got is really what we ought to display - // - we want to display stale data accordingly (at least with loading indicator) - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - def apply: HtmlElement = - AppPage(state.actionBus)( - $data.combineWithFn($params)(_ zip _) - .map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: List[Parameter] - ): DetailPage.ViewModel = - DetailPage.ViewModel( - o.toDetailOsoby, - p.map( - _.toParametr(param => - PageLink.container(Page.DetailParametru(o, param), state.actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala deleted file mode 100644 index fdaa7f8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailParametruPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink - -object DetailParametruPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailParametruPageConnector( - state: DetailParametruPageConnector.AppState -)( - $page: Signal[Page.DetailParametru] -)(using router: Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => (p.osobniCislo.value, p.parametr.value))((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map(FetchParameter(_, _)) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - } yield (da, pb) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailParametruPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter - ): DetailParametruPage.ViewModel = - DetailParametruPage.ViewModel( - o.toDetailOsoby, - p.toParametr(p => - PageLink.container(Page.DetailParametru(o, p), state.actionBus) - ), - p.criteria.map( - _.toKriterium { c => - PageLink.container( - Page.DetailKriteria(o, p, c), - state.actionBus - ) - } - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala deleted file mode 100644 index 049aa22..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -case class DirectoryPageConnector( - $input: EventStream[List[UserInfo]], - actionBus: Observer[Action] -)(using router: Router[Page]): - val $data = $input.startWithNone - val $actionSignal = EventStream.fromValue(FetchDirectory) - - def apply: HtmlElement = - AppPage(actionBus)( - $data.split(_ => ())((_, _, s) => - pages.directory.DirectoryPage( - s.map( - _.map( - _.toUserRow(u => - PageLink.container( - Page.Detail(Page.Titled(u.personalNumber)), - actionBus - ) - ) - ) - ) - ) - ), - $actionSignal --> actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala deleted file mode 100644 index 88e7450..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ /dev/null @@ -1,68 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii -import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria -import fiftyforms.ui.components.tailwind.Color - -extension (o: UserInfo) - def toDetailOsoby: DetailOsoby.ViewModel = - DetailOsoby.ViewModel( - o.personalNumber, - o.name, - o.email, - o.phone, - o.img, - o.mainFunction.map(f => - DetailOsoby.Funkce.ViewModel(f.name, f.dept, f.ou) - ), - o.userContracts.headOption.map(c => - DetailOsoby.PracovniPomer.ViewModel(c.rel, c.startDate, c.endDate) - ) - ) - -extension (param: Parameter) - def toParametr(container: Parameter => Anchor): DetailParametru.ViewModel = - DetailParametru.ViewModel( - id = param.id, - nazev = param.name, - popis = param.description, - status = "Nesplněno", - statusColor = Color.red, - a = container(param) - ) - -extension (crit: ParameterCriteria) - def toKriterium( - container: ParameterCriteria => Anchor - ): DetailKriteria.ViewModel = - DetailKriteria.ViewModel( - nazev = crit.criteriumText, - kapitola = crit.chapterId, - bod = crit.itemId, - status = "Nesplněno", - statusColor = Color.red, - splneno = false, - dukazy = Nil, - container = container(crit) - ) - -extension (user: UserInfo) - def toUserRow( - container: UserInfo => HtmlElement = (_: UserInfo) => div() - ): UserRow.ViewModel = - UserRow.ViewModel( - osobniCislo = user.personalNumber.toString, - celeJmeno = user.name, - prijmeni = user.surname, - hlavniFunkce = user.mainFunction.map(_.name), - img = user.img, - container = () => container(user) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala deleted file mode 100644 index 7d5b2c1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.dashboard - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.Page - -object DashboardPage: - - def render: HtmlElement = - div("Dashboard page") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala deleted file mode 100644 index 9ef0698..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala +++ /dev/null @@ -1,31 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailKriteriaPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply($m: Signal[ViewModel])( - action: Observer[DukazyKriteria.Action] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - DukazyKriteria($m.map(_.kriterium.dukazy))(action) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala deleted file mode 100644 index 3b316e1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object DetailPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametry: SeznamParametru.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby($m.map(_.osoba)), - SeznamParametru($m.map(_.parametry)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala deleted file mode 100644 index 17224cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala +++ /dev/null @@ -1,24 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailParametruPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriteria: SeznamKriterii.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby.header($m.map(_.osoba)), - DetailParametru($m.map(_.parametr)), - SeznamKriterii($m.map(_.kriteria)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala deleted file mode 100644 index dc912fa..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ /dev/null @@ -1,107 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb -package app -package pages.detail - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.UpravDukazForm -import fiftyforms.services.files.File - -object UpravDukaz: - - type ThisPage = Page.UpravDukazKriteria - type PageKey = (OsobniCislo, String, String) - - trait State { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def availableFiles: EventStream[List[File]] - def actionBus: Observer[Action] - } - - def keyOfPage(page: ThisPage): PageKey = - (page.osobniCislo.value, page.parametr.value, page.kriterium.value) - - def onChangeAction(key: PageKey): Action = - FetchParameterCriteria( - key._1, - key._2, - key._3, - Page.UpravDukazKriteria(_, _, _) - ) - - class Connector(state: State)($page: Signal[ThisPage])(using Router[Page]): - val $paramChangeSignal = $page.splitOne(keyOfPage)((x, _, _) => x) - val $pageChangeSignal = $paramChangeSignal.map(onChangeAction) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.split(_ => ())((_, s, $s) => - PageComponent( - $s.map(buildModel), - state.availableFiles, - state.actionBus.contramap { - case UpravDukazForm.Cancelled => - NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) - case UpravDukazForm.AvailableFilesRequested => - FetchAvailableFiles(s._1.personalNumber) - } - ) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): PageComponent.ViewModel = - import connectors.* - PageComponent.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) - - object PageComponent: - import components.* - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply( - $m: Signal[ViewModel], - availableFilesStream: EventStream[List[File]], - events: Observer[UpravDukazForm.Event] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - UpravDukazForm(availableFilesStream)(events) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala deleted file mode 100644 index ca311b0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.Icons -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Color - -object DetailKriteria: - case class ViewModel( - nazev: String, - kapitola: String, - bod: String, - status: String, - statusColor: Color, - splneno: Boolean, - dukazy: List[DukazKriteria.ViewModel], - container: HtmlElement = div() - ) { - val id = s"${kapitola}${bod}" - } - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h3( - cls := "text-xl leading-6 font-bold text-gray-900", - child.text <-- $m.map(_.nazev) - ), - h3( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - "Kapitola ", - child.text <-- $m.map(_.kapitola), - " bod ", - child <-- $m.map(_.bod) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala deleted file mode 100644 index ae90290..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala +++ /dev/null @@ -1,100 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Avatar - -object DetailOsoby: - - object Funkce: - case class ViewModel( - nazev: String, - stredisko: String, - voj: String - ) - - def render($m: Signal[ViewModel]): HtmlElement = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.nazev), - span( - cls := "hidden md:inline", - " @ ", - child.text <-- $m.map(_.stredisko), - ", ", - child.text <-- $m.map(_.voj) - ) - ) - - object PracovniPomer: - case class ViewModel( - druh: String, - pocatek: LocalDate, - konec: Option[LocalDate] - ) - - def render($m: Signal[ViewModel]): HtmlElement = - import fiftyforms.ui.components.tailwind.CustomAttrs.datetime - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.druh), - " od ", - time( - datetime <-- $m.map(_.pocatek.toString), - child.text <-- $m.map(_.pocatek.toString) - ) - ) - - case class ViewModel( - osobniCislo: OsobniCislo, - jmeno: String, - email: Option[String], - telefon: Option[String], - img: Option[String], - hlavniFunkce: Option[Funkce.ViewModel], - pracovniPomer: Option[PracovniPomer.ViewModel] - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-4", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(16) - ), - div( - h1( - cls := "text-2xl font-bold text-gray-900", - child.text <-- $m.map(_.jmeno) - ), - child.maybe <-- $m.map(_.hlavniFunkce).split(_ => ())((_, _, d) => - Funkce.render(d) - ), - child.maybe <-- $m.map(_.pracovniPomer).split(_ => ())((_, _, d) => - PracovniPomer.render(d) - ) - ) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-5", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(8) - ), - div( - h1( - cls := "text-lg font-medium text-gray-800", - child.text <-- $m.map(_.jmeno) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala deleted file mode 100644 index e11cb62..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color - -object DetailParametru: - case class ViewModel( - id: String, - nazev: String, - popis: String, - status: String, - statusColor: Color, - a: Anchor - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h2( - cls := "text-2xl leading-6 font-medium text-gray-900", - child.text <-- $m.map(_.nazev) - ), - p( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - child.text <-- $m.map(_.popis) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - h2( - cls := "text-lg leading-6 font-medium text-gray-600", - child.text <-- $m.map(_.nazev) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala deleted file mode 100644 index a00ede8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala +++ /dev/null @@ -1,177 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.CustomAttrs - -object DukazKriteria: - case class Osoba(osobniCislo: String, jmeno: String) - case class Udalost(osoba: Osoba, datum: LocalDate) - case class Dokument( - url: String, - nazev: String, - pridal: Udalost, - odebral: Option[Udalost] - ) - case class ViewModel( - dokumenty: List[Dokument], - autorizoval: Option[Udalost], - platiDo: Option[LocalDate], - poznámka: Option[String] - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-lg", - div( - cls := "px-4 py-5 sm:px-6", - h3( - cls := "text-lg leading-6 font-medium text-gray-900", - """Applicant Information""" - ), - p( - cls := "mt-1 max-w-2xl text-sm text-gray-500", - """Personal details and application.""" - ) - ), - div( - cls := "border-t border-gray-200 px-4 py-5 sm:px-6", - dl( - cls := "grid grid-cols-1 gap-x-4 gap-y-8 sm:grid-cols-2", - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Full name""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Margot Foster""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Application for""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Backend Developer""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Email address""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """margotfoster@example.com""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Salary expectation""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """$120,000""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """About""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Fugiat ipsum ipsum deserunt culpa aute sint do nostrud anim incididunt cillum culpa consequat. Excepteur qui ipsum aliquip consequat sint. Sit id mollit nulla mollit nostrud in ea officia proident. Irure nostrud pariatur mollit ad adipisicing reprehenderit deserunt qui eu.""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """Attachments""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - ul( - role := "list", - cls := "border border-gray-200 rounded-md divide-y divide-gray-200", - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """resume_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ), - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """coverletter_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala deleted file mode 100644 index 22f134f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala +++ /dev/null @@ -1,35 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object DukazyKriteria: - sealed trait Action - case object Add extends Action - - type ViewModel = List[DukazKriteria.ViewModel] - - def apply($m: Signal[ViewModel])(actions: Observer[Action]): HtmlElement = - div( - child.maybe <-- $m.map(m => - if (m.isEmpty) then Some(prazdnyDukaz(actions)) - else None - ), - children <-- $m.map(_.zipWithIndex).split(_._2)((_, _, $s) => - DukazKriteria($s.map(_._1)) - ) - ) - - private def prazdnyDukaz(actions: Observer[Action]): HtmlElement = - button( - tpe := "button", - onClick.preventDefault.mapTo(Add) --> actions, - cls := "relative block w-full border-2 border-gray-300 border-dashed rounded-lg p-12 text-center hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - Icons.outline - .`document-add`(12) - .amend(svg.cls := "mx-auto text-gray-400"), - span( - cls := "mt-2 block text-sm font-medium text-gray-900", - "Přidat důkaz" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala deleted file mode 100644 index f628697..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.list.StackedList -import fiftyforms.ui.components.tailwind.list.ListRow -import fiftyforms.ui.components.tailwind.list.RowTag -import fiftyforms.ui.components.tailwind.list.RowNext -import java.time.LocalDate - -object SeznamKriterii: - type ViewModel = List[DetailKriteria.ViewModel] - - private val kritList = new StackedList[DetailKriteria.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - kritList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = - p(cls := "text-sm text-gray-500", s"${i.kapitola}${i.bod}"), - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.container - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala deleted file mode 100644 index 2bfe4bc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.list.{ - StackedList, - ListRow, - RowTag, - PropList, - IconText, - RowNext -} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.LinkSupport.* - -object SeznamParametru: - type ViewModel = List[DetailParametru.ViewModel] - - private val parametrList = new StackedList[DetailParametru.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - parametrList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = emptyNode, - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.a - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala deleted file mode 100644 index f048e9d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ /dev/null @@ -1,101 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.form.* -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement -import fiftyforms.services.files.components.tailwind.FilePicker -import cz.e_bs.cmi.mdr.pdb.frontend.AutorizujDukaz -import cz.e_bs.cmi.mdr.pdb.frontend.DocumentRef -import fiftyforms.services.files.File - -object UpravDukazForm: - sealed trait Event - case object Cancelled extends Event - case object AvailableFilesRequested extends Event - def apply(availableFilesStream: EventStream[List[File]])( - updates: Observer[Event] - ): HtmlElement = - val (filesStream, filesObserver) = - EventStream.withObserver[FilePicker.Event] - val files = Var[List[File]](Nil) - def submitButtons: HtmlElement = - div( - cls := "pt-5", - div( - cls := "flex justify-end", - button( - tpe := "button", - cls := "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zrušit", - onClick.mapTo(Cancelled) --> updates - ), - button( - tpe := "submit", - cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - disabled <-- files.signal.map(_.isEmpty), - "Autorizovat" - ) - ) - ) - - div( - filesStream.collect { case FilePicker.SelectionUpdated(files) => - files.to(List) - } --> files.writer, - filesStream.collect { case FilePicker.AvailableFilesRequested => - AvailableFilesRequested - } --> updates, - cls := "bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6", - Form( - Form.Body( - Form.Section( - FormHeader( - FormHeader.ViewModel( - "Doložení kritéria", - "Sestavte doklady poskytující důkaz kritéria a potvrďte odesláním formuláře. Případné limitace či jiné relevantní údaje vepište do pole Komentář." - ) - ), - FormFields( - FormRow( - "dokumenty", - "Dokumenty", - FilePicker(files.signal, availableFilesStream)(filesObserver) - .amend(idAttr := "dokumenty", cls("max-w-lg")) - ).toHtml, - FormRow( - "platnost", - "Platnost", - input( - idAttr := "platnost", - name := "platnost", - tpe := "date", - autoComplete := "date", - cls := "block max-w-lg w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" - ) - ).toHtml, - FormRow( - "komentar", - "Komentář", - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - textArea( - idAttr := "komentar", - name := "about", - rows := 3, - cls := "max-w-lg shadow-sm block w-full focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border border-gray-300 rounded-md" - ), - p( - cls := "mt-2 text-sm text-gray-500", - "Doplňte prosím potřebné informace související s doložením kritéria, včetně případných limitací." - ) - ) - ).toHtml - ) - ) - ), - submitButtons - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala deleted file mode 100644 index 41033f6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -import components._ - -object DirectoryPage: - - type ViewModel = List[UserRow.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - val (actionsStream, actionObserver) = - EventStream.withObserver[SearchForm.Action] - val $filter = actionsStream - .collect { - case SearchForm.ClearFilter => None - case SearchForm.SetFilter(t) => Some(t) - } - .startWith(None) - val byLetter = for { - d <- $m - f <- $filter - } yield for { - (letter, users) <- d - .filter { user => - f match - case None => true - case Some(t) => user.search.contains(t) - } - .groupBy(_.prijmeni.head) - .to(List) - .sortBy(_._1) - } yield (letter.toString, users.sortBy(_.prijmeni)) - - div( - cls := "h-full max-w-7xl mx-auto order-first flex flex-col flex-shrink-0", - SearchForm(actionObserver), - Directory(byLetter) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala deleted file mode 100644 index 73f915d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -object Directory: - - object Header: - type ViewModel = String - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500", - h3(child.text <-- $m) - ) - - object UserList: - type ViewModel = List[UserRow.ViewModel] - def apply($m: Signal[ViewModel]): HtmlElement = - ul( - role := "list", - cls := "relative z-0 divide-y divide-gray-200", - children <-- $m.split(_.osobniCislo)((_, _, s) => UserRow(s)) - ) - - type ViewModel = List[(String, List[UserRow.ViewModel])] - def apply($m: Signal[ViewModel]): HtmlElement = - nav( - cls := "flex-1 min-h-0 overflow-y-auto", - aria.label := "Directory", - children <-- $m.split(_._1)((_, _, s) => - div( - cls := "relative", - Header(s.map(_._1)), - UserList(s.map(_._2)) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala deleted file mode 100644 index e182910..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala +++ /dev/null @@ -1,54 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object SearchForm: - sealed trait Action - case object SubmitFilter extends Action - sealed trait FilterAction extends Action - case object ClearFilter extends FilterAction - case class SetFilter(value: String) extends FilterAction - - def apply(actions: Observer[Action]): HtmlElement = - div( - cls := "px-6 pt-4 pb-4", - form( - cls := "flex space-x-4", - onSubmit.mapTo(SubmitFilter) --> actions, - div( - cls := "flex-1 min-w-0", - label( - forId := "search", - cls := "sr-only", - "Hledat" - ), - div( - cls := "relative rounded-md shadow-sm", - div( - cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", - Icons.solid.search().amend(svg.cls := "text-gray-400") - ), - input( - tpe := "search", - name := "search", - idAttr := "search", - cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", - placeholder := "Hledat", - composeEvents(onInput.mapToValue.setAsValue.map(SetFilter(_)))( - _.throttle(500) - ) --> actions - ) - ) - ), - button( - tpe := "submit", - cls := "inline-flex justify-center px-3.5 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500", - Icons.solid.filter().amend(svg.cls := "text-gray-400"), - span( - cls := "sr-only", - "Hledat" - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala deleted file mode 100644 index 82f7723..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala +++ /dev/null @@ -1,52 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.airstream.core.Signal -import fiftyforms.ui.components.tailwind.Avatar - -object UserRow: - case class ViewModel( - osobniCislo: String, - celeJmeno: String, - prijmeni: String, - hlavniFunkce: Option[String], - img: Option[String], - container: () => HtmlElement = () => div() - ) { - val search = prijmeni.toLowerCase - } - - def apply($m: Signal[ViewModel]): HtmlElement = - inline def avatarImage = - Avatar($m.map(_.img)).avatarImage(10) - - li( - div( - cls := "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500", - div( - cls := "flex-shrink-0", - child <-- avatarImage - ), - div( - cls := "flex-1 min-w-0", - child <-- $m.map { o => - o.container() - .amend( - cls := "focus:outline-none", - span( - cls := "absolute inset-0", - aria.hidden := true - ), - p( - cls := "text-sm font-medium text-gray-900", - o.celeJmeno - ), - p( - cls := "text-sm text-gray-500 truncate", - o.hlavniFunkce - ) - ) - } - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala deleted file mode 100644 index 467c5f5..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala +++ /dev/null @@ -1,99 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router - -object ErrorPage: - case class ViewModel( - homePage: Page, - errorName: String, - title: String, - subTitle: String - ) - - def apply(m: ViewModel, actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - val ViewModel(homePage, errorName, title, subTitle) = m - div( - cls := "min-h-full pt-16 pb-12 flex flex-col bg-white", - main( - cls := "flex-grow flex flex-col justify-center max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex-shrink-0 flex justify-center", - a( - href := "/", - cls := "inline-flex", - span( - cls := "sr-only", - """Workflow""" - ), - img( - cls := "h-12 w-auto", - src := "https://tailwindui.com/img/logos/workflow-mark.svg?color=indigo&shade=600", - alt := "" - ) - ) - ), - div( - cls := "py-16", - div( - cls := "text-center", - p( - cls := "text-sm font-semibold text-indigo-600 uppercase tracking-wide", - errorName - ), - h1( - cls := "mt-2 text-4xl font-extrabold text-gray-900 tracking-tight sm:text-5xl", - title - ), - p( - cls := "mt-2 text-base text-gray-500", - subTitle - ), - div( - cls := "mt-6", - PageLink - .container(homePage, actionBus) - .amend( - cls := "text-base font-medium text-indigo-600 hover:text-indigo-500", - """Go back home""" - ) - ) - ) - ) - ), - footer( - cls := "flex-shrink-0 max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", - nav( - cls := "flex justify-center space-x-4", - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Contact Support""" - ), - span( - cls := "inline-block border-l border-gray-300", - aria.hidden := true - ), - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Status""" - ), - span( - cls := "inline-block border-l border-gray-300", - aria.hidden := true - ), - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Twitter""" - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala deleted file mode 100644 index bd9baec..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action - -object NotFoundPage: - def apply(homePage: Page, url: String, actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - ErrorPage( - ErrorPage.ViewModel( - homePage, - "404 error", - "Page not found.", - s"Sorry, but page $url doesn't exist." - ), - actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala deleted file mode 100644 index 476fe3c..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action - -object UnhandledErrorPage: - - case class ViewModel( - homePage: Page, - errorName: Option[String], - errorMessage: Option[String] - ) - - def apply(m: ViewModel, actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - ErrorPage( - ErrorPage.ViewModel( - m.homePage, - "Unexpected error occurred", - m.errorName.getOrElse( - "Uh oh!" - ), // TODO: translations, better text than uh oh - m.errorMessage.getOrElse( - "This wasn't supposed to happen! Please try again." - ) - ), - actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala deleted file mode 100644 index 25f059f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala +++ /dev/null @@ -1,6 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.services - -import com.raquo.airstream.core.Observer - -trait DataFetcher[K, A]: - def fetch(id: K, sink: Observer[A]): Unit diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala deleted file mode 100644 index a469830..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala +++ /dev/null @@ -1,120 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package state - -import com.raquo.airstream.core.EventStream -import cz.e_bs.cmi.mdr.pdb.{UserInfo, OsobniCislo} -import com.raquo.airstream.core.Observer -import scala.scalajs.js -import scala.scalajs.js.JSON -import zio.json._ -import com.raquo.airstream.eventbus.EventBus -import com.raquo.airstream.ownership.Owner -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.UserFunction -import cz.e_bs.cmi.mdr.pdb.UserContract -import fiftyforms.services.files.File - -trait AppState - extends connectors.DetailPageConnector.AppState - with connectors.DetailParametruPageConnector.AppState - with connectors.DetailKriteriaPageConnector.AppState - with pages.detail.UpravDukaz.State: - def users: EventStream[List[UserInfo]] - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - -class MockAppState(implicit owner: Owner, router: Router[Page]) - extends AppState: - - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonDecoder[UserFunction] = DeriveJsonDecoder.gen - given JsonDecoder[UserContract] = DeriveJsonDecoder.gen - given JsonDecoder[UserInfo] = DeriveJsonDecoder.gen - - given JsonDecoder[ParameterCriteria] = DeriveJsonDecoder.gen - given JsonDecoder[Parameter] = DeriveJsonDecoder.gen - - private val actions = EventBus[Action]() - private val (parametersStream, pushParameters) = - EventStream.withCallback[List[Parameter]] - private val (usersStream, pushUsers) = - EventStream.withCallback[List[UserInfo]] - private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] - private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] - - private val mockData: List[UserInfo] = - mockUsers - .asInstanceOf[js.Dictionary[js.Object]] - .values - // TODO: is there a more efficient way to parse from JS object directly? - .map { o => - val parsed = JSON.stringify(o).fromJson[UserInfo] - parsed.left.foreach(org.scalajs.dom.console.log(_)) - parsed - } - .collect { case Right(u) => - u - } - .toList - - private val mockParameters: List[Parameter] = - pdbParams - .asInstanceOf[js.Dictionary[js.Object]] - .values - .map(o => JSON.stringify(o).fromJson[Parameter]) - .collect { case Right(p) => p } - .toList - - // TODO: Extract to separate event handler - actions.events.foreach { - case FetchDirectory => pushUsers(mockData) - case FetchUserDetails(osc) => - mockData.find(_.personalNumber == osc).foreach { o => - pushDetails(o) - router.replaceState(Page.Detail(o)) - } - case FetchParameters(osc) => - pushParameters(mockParameters) - case FetchParameter(osc, paramId) => - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(Page.DetailParametru(o, p)) - case FetchParameterCriteria(osc, paramId, critId, page) => - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - c <- p.criteria.find(_.id == critId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(page(o, p, c)) - case NavigateTo(page) => router.pushState(page) - case FetchAvailableFiles(osc) => - pushFiles( - List( - File("https://tc163.cmi.cz/here", "Example file") - ) - ) - } - - override def users: EventStream[List[UserInfo]] = - usersStream.debugWithName("users") - - override def details: EventStream[UserInfo] = - detailsStream.debugWithName("details") - - override def parameters: EventStream[List[Parameter]] = - parametersStream.debugWithName("parameters") - - override def availableFiles: EventStream[List[File]] = - filesStream.debugWithName("available files") - - override def actionBus: Observer[Action] = - actions.writer.debugWithName("actions writer") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala deleted file mode 100644 index a3b00af..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala +++ /dev/null @@ -1,103 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import scala.scalajs.js.annotation.JSExportTopLevel -import scala.scalajs.js.annotation.JSExport -import scala.scalajs.js.annotation.JSImport -import scala.scalajs.js -import org.scalajs.dom -import com.raquo.laminar.api.L.{*, given} -import scala.scalajs.js.Date -import com.raquo.waypoint.Router -import com.raquo.waypoint.SplitRender -import cz.e_bs.cmi.mdr.pdb.app.services.DataFetcher -import scala.scalajs.js.JSON -import zio.json._ -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.state.AppState - -@js.native -@JSImport("stylesheets/main.css", JSImport.Namespace) -object Css extends js.Any - -@js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native -@JSImport("params/pdb-params.json", JSImport.Default) -object pdbParams extends js.Object - -@JSExportTopLevel("app") -object Main: - - @JSExport - def main(args: Array[String]): Unit = - import Routes.given - onLoad { - setupAirstream() - val appContainer = dom.document.querySelector("#app") - val _ = - render( - appContainer, - renderPage(state.MockAppState(using unsafeWindowOwner, router)) - ) - } - - private def onLoad(f: => Unit): Unit = - documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) - - private def setupAirstream()(using router: Router[Page]): Unit = - AirstreamError.registerUnhandledErrorCallback(err => - router.forcePage( - Page.UnhandledError( - Some(err.getClass.getName), // TODO: Fill only in dev mode - Some(err.getMessage) - ) - ) - ) - - def renderPage(state: AppState)(using - router: Router[Page] - ): HtmlElement = - val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) - .collectSignal[Page.Detail]( - connectors - .DetailPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailParametru]( - connectors - .DetailParametruPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailKriteria]( - connectors - .DetailKriteriaPageConnector(state)(_) - .apply - ) - .collectSignal[Page.UpravDukazKriteria]( - pages.detail.UpravDukaz.Connector(state)(_).apply - ) - .collectStatic(Page.Dashboard)( - connectors.DashboardPageConnector(state.actionBus).apply - ) - .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) - ) - .collect[Page.UnhandledError](pg => - pages.errors - .UnhandledErrorPage( - pages.errors.UnhandledErrorPage - .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), - state.actionBus - ) - ) - .collectStatic(Page.Directory)( - connectors - .DirectoryPageConnector(state.users, state.actionBus) - .apply - ) - div(cls := "h-full", child <-- pageSplitter.$view) - - // Pull in the stylesheet - val css: Css.type = Css diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala deleted file mode 100644 index 3bbc8c0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ /dev/null @@ -1,198 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.* -import org.scalajs.dom -import zio.json.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import scala.scalajs.js -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.Page.Titled - -// enum is not working with Waypoints' SplitRender collectStatic -sealed abstract class Page( - val id: String, - val title: String, - val parent: Option[Page] -) { - val path: Vector[Page] = - parent match - case None => Vector(this) - case Some(p) => p.path :+ this - - val isRoot: Boolean = parent.isEmpty -} - -object Page: - - case class Titled[V](value: V, title: Option[String] = None): - val show: String = title.getOrElse(value.toString) - - case object Directory extends Page("directory", "Adresář", None) - - case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) - - case class Detail(osobniCislo: Titled[OsobniCislo]) - extends Page("user", osobniCislo.show, Some(Directory)) - - object Detail { - def apply(o: UserInfo): Detail = Detail( - Titled(o.personalNumber, Some(o.name)) - ) - } - - case class DetailParametru( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String] - ) extends Page( - "parameter", - parametr.show, - Some(Detail(osobniCislo)) - ) - - object DetailParametru { - def apply(o: UserInfo, p: Parameter): DetailParametru = - DetailParametru( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)) - ) - } - - case class DetailKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "criteria", - kriterium.show, - Some(DetailParametru(osobniCislo, parametr)) - ) - - object DetailKriteria { - def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = - DetailKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class UpravDukazKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "addProof", - "Důkaz", - Some(DetailKriteria(osobniCislo, parametr, kriterium)) - ) - - object UpravDukazKriteria { - def apply( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): UpravDukazKriteria = - UpravDukazKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class NotFound(url: String) extends Page("404", "404", Some(Directory)) - - case class UnhandledError( - errorName: Option[String], - errorMessage: Option[String] - ) extends Page("500", "Unexpected error", Some(Directory)) - -object Routes: - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) - given [V: JsonEncoder]: JsonEncoder[Titled[V]] = - DeriveJsonEncoder.gen[Titled[V]] - given [V: JsonDecoder]: JsonDecoder[Titled[V]] = - DeriveJsonDecoder.gen[Titled[V]] - given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] - given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] - - val base = - js.`import`.meta.env.BASE_URL - .asInstanceOf[String] + "app" - - val homePage: Page = Page.Directory - - given router: Router[Page] = Router[Page]( - routes = List( - Route.static(homePage, root / endOfSegments, basePath = base), - Route.static( - Page.Dashboard, - root / "dashboard" / endOfSegments, - basePath = base - ), - Route[Page.Detail, String]( - encode = _.osobniCislo.value.toString, - decode = osc => Page.Detail(Titled(OsobniCislo(osc))), - root / "osoba" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.DetailParametru, (String, String)]( - encode = p => (p.osobniCislo.value.toString, p.parametr.value), - decode = - p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / endOfSegments, - basePath = base - ), - Route[Page.DetailKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.DetailKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.UpravDukazKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.UpravDukazKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / "edit" / endOfSegments, - basePath = base - ) - ), - serializePage = _.toJson, - deserializePage = _.fromJson[Page] - .fold(s => throw IllegalStateException(s), identity), - getPageTitle = _.title, - routeFallback = url => Page.NotFound(url), - deserializeFallback = _ => Page.Dashboard - )( - $popStateEvent = windowEvents.onPopState, - owner = unsafeWindowOwner - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala deleted file mode 100644 index b24b82b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -sealed trait Action - -case object FetchDirectory extends Action -case class FetchUserDetails(osc: OsobniCislo) extends Action -case class FetchParameters(osc: OsobniCislo) extends Action -case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action -case class FetchParameterCriteria( - osc: OsobniCislo, - paramId: String, - critId: String, - page: (UserInfo, Parameter, ParameterCriteria) => Page -) extends Action -case class FetchAvailableFiles(osc: OsobniCislo) extends Action -case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala deleted file mode 100644 index c637d35..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala +++ /dev/null @@ -1,82 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.{UserProfile, UserInfo, OsobniCislo} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo -import cz.e_bs.cmi.mdr.pdb.UserFunction - -object AppPage: - // TODO: pages by logged in user - val pages: List[Page] = List(Page.Directory, Page.Dashboard) - - import NavigationBar.{Logo, MenuItem} - - val logo = Logo( - "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", - "Workflow" - ) - - // TODO: menu items by user profile - val userMenu = - List( - MenuItem("Your Profile"), - MenuItem("Settings"), - MenuItem("Sign out") - ) - - // TODO: load user profile - val $userProfile = Var( - UserProfile( - "tom", - UserInfo( - OsobniCislo("1031"), - "tom", - "Tom", - "Cook", - None, - None, - Some("tom@example.com"), - Some("+420 222 866 180"), - Some( - UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") - ), - Nil, - None - ) - ) - ) - - val $userInfo = $userProfile.signal.map(_.userInfo) - - type ViewModel = Option[HtmlElement] - def apply( - actionBus: Observer[Action] - )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using - router: Router[Page] - ): HtmlElement = - PageLayout(actionBus)( - $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => - PageLayout.ViewModel( - NavigationBar.ViewModel( - u, - pages.map(p => - NavigationBar.Link( - () => - PageLink( - p, - actionBus - ), - p == cp - ) - ), - userMenu, - logo - ), - c - ) - ), - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala deleted file mode 100644 index bf473c6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ /dev/null @@ -1,143 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden -import fiftyforms.ui.components.tailwind.list.IconText.ViewModel -import fiftyforms.ui.components.tailwind.Icons -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Action - -object Breadcrumbs: - - private def slash = - import svg.{*, given} - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-300", - xmlns := "http://www.w3.org/2000/svg", - fill := "currentColor", - viewBox := "0 0 20 20", - ariaHidden := true, - path( - d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" - ) - ) - - def backIcon = - Icons.solid - .`arrow-narrow-left`() - .amend( - svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" - ) - - object Link: - case class ViewModel( - page: Page, - icon: Option[SvgElement], - extraClasses: String, - text: String, - textClasses: Option[String] = None - ) - - val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" - - def shortHome(p: Page) = ViewModel( - p, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - "Zpět na úvodní stránku", - None - ) - - def fullHome(p: Page) = ViewModel( - p, - Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), - "text-gray-400 hover:text-gray-500", - "Domů", - Some("sr-only") - ) - - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - PageLink - .container($m.map(_.page), actionBus) - .amend( - cls <-- $m.map(_.extraClasses), - child.maybe <-- $m.map(_.icon), - span( - cls <-- $m.map(_.textClasses.getOrElse("")), - child.text <-- $m.map(_.text) - ) - ) - - object FullBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - ol( - role := "list", - cls := "flex items-center space-x-4", - children <-- $m.map(_.path).split(_.id)((_, _, $p) => - li( - div( - cls := "flex items-center", - child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), - Link( - $p.map(p => - if (p.isRoot) then Link.fullHome(p) - else - Link.ViewModel( - p, - None, - s"ml-4 max-w-xs truncate ${Link.baseClasses}", - p.title - ) - ), - actionBus - ) - ) - ) - ) - ) - - object ShortBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - Link( - $m.map { p => - val target = p.parent.getOrElse(p) - if target.isRoot then Link.shortHome(target) - else - Link.ViewModel( - target, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - s"Zpět na ${target.title}" - ) - }, - actionBus - ) - - def apply(actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - nav( - cls := "flex", - aria.label := "Breadcrumb", - div( - cls := "flex sm:hidden", - ShortBreadcrumbs(router.$currentPage, actionBus) - ), - div( - cls := "hidden sm:block", - FullBreadcrumbs(router.$currentPage, actionBus) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala deleted file mode 100644 index 0db2047..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ /dev/null @@ -1,242 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent -import fiftyforms.ui.components.tailwind.Icons -import fiftyforms.ui.components.tailwind.Avatar -import cz.e_bs.cmi.mdr.pdb.UserInfo -import io.laminext.syntax.core.* - -object NavigationBar: - - case class Logo(img: String, name: String) - case class Link(a: () => Anchor, active: Boolean) - case class MenuItem(title: String) - - case class ViewModel( - userInfo: UserInfo, - pages: List[Link], - userMenu: List[MenuItem], - logo: Logo - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - val $userInfo = $m.map(_.userInfo) - val mobileMenuOpen = Var(false) - - val desktopOnly = cls("hidden md:block") - val mobileOnly = cls("md:hidden") - - inline def avatarImage(size: Int = 8) = - Avatar($userInfo.map(_.img)).avatarImage(size) - - def notificationButton = button( - tpe := "button", - cls := List( - "bg-indigo-600", - "focus:outline-none", - "focus:ring-2", - "focus:ring-offset-2", - "focus:ring-offset-indigo-600", - "focus:ring-white", - "hover:text-white", - "p-1", - "rounded-full", - "text-indigo-200" - ), - span(cls := "sr-only", "View notifications"), - Icons.outline.bell() - ) - - def userProfile: HtmlElement = - val menuOpen = Var(false) - - def menuItem(item: MenuItem, idx: Int): HtmlElement = - a( - href := "#", - cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", - role := "menuitem", - tabIndex := -1, - idAttr := s"user-menu-item-$idx", - item.title - ) - - div( - cls := "ml-3 relative", - div( - button( - tpe := "button", - cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - idAttr := "user-menu-button", - aria.expanded <-- menuOpen.signal, - aria.hasPopup := true, - span(cls := "sr-only", "Open user menu"), - child <-- avatarImage(), - onClick.preventDefault.mapTo( - !menuOpen.now() - ) --> menuOpen.writer - ) - ), - /* - * */ - div( - cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", - cls <-- menuOpen.signal.map { o => - if (o) "md:block" else "md:hidden" - }, - role := "menu", - aria.orientation := "vertical", - aria.labelledBy := "user-menu-button", - tabIndex := -1, - // : keyboard navigation - children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) - ) - ) - - def mobileProfile = - def menuItem(item: MenuItem): HtmlElement = - a( - href := "#", - cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", - item.title - ) - - div( - cls := "pt-4 pb-3 border-t border-indigo-700", - div( - cls := "flex items-center px-5", - div( - cls := "flex-shrink-0", - child <-- avatarImage(10) - ), - div( - cls := "ml-3", - div( - cls := "text-base font-medium text-white", - child.text <-- $userInfo.map(_.name) - ), - child.maybe <-- $userInfo.map( - _.email.map(e => - div( - cls := "text-sm font-medium text-indigo-300", - e - ) - ) - ) - ), - notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) - ), - div( - cls := "mt-3 px-2 space-y-1", - children <-- $m.map(_.userMenu.map(menuItem)) - ) - ) - - def pageLink(page: Link): Anchor = - page - .a() - .amend( - cls := "text-white px-3 py-2 rounded-md text-sm font-medium", - cls := Seq( - "bg-indigo-700" -> page.active, - "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active - ), - ariaCurrent := (if page.active then "page" else "false") - ) - - def logoImg: Image = - img( - cls := "h-8 w-8", - src <-- $m.map(_.logo.img), - alt <-- $m.map(_.logo.name) - ) - - def mobileMenuButton = button( - tpe := "button", - cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - aria.controls := "mobile-menu", - aria.expanded <-- mobileMenuOpen.signal, - span(cls := "sr-only", "Open main menu"), - Icons.outline - .menu() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") - ), - Icons.outline - .x() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") - ), - onClick.preventDefault.mapTo( - !mobileMenuOpen.now() - ) --> mobileMenuOpen.writer - ) - - def navBarLeft = - div( - cls := "flex items-center", - div(cls := "flex-shrink-0", logoImg), - div( - desktopOnly, - div( - cls := "ml-10 flex items-baseline space-x-4", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ) - ) - ) - - def navBarRight = - div( - desktopOnly, - div( - cls := "ml-4 flex items-center md:ml-6", - notificationButton, - userProfile - ) - ) - - def navBarMobile = - div( - cls := "-mr-2 flex", - mobileOnly, - mobileMenuButton - ) - - def navBar = - div( - cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex items-center justify-between h-16", - navBarLeft, - navBarRight, - navBarMobile - ) - ) - - def mobileMenu = - div( - mobileOnly, - cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }, - idAttr := "mobile-menu", - div( - cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ), - mobileProfile - ) - - nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala deleted file mode 100644 index 75339df..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object PageHeader: - def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = - header( - cls := "bg-white shadow-sm", - div( - cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", - h1( - cls := "text-lg leading-6 font-semibold text-gray-900", - Breadcrumbs(actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala deleted file mode 100644 index 5096463..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ /dev/null @@ -1,30 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import fiftyforms.ui.components.tailwind.Loading -import io.laminext.syntax.core.* - -object PageLayout: - case class ViewModel( - navigation: NavigationBar.ViewModel, - content: Option[HtmlElement] - ) - def apply(actionBus: Observer[Action])( - $m: Signal[ViewModel], - mods: Modifier[HtmlElement]* - )(using router: Router[Page]): HtmlElement = - val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) - div( - cls := "h-full flex flex-col", - NavigationBar($m.map(_.navigation)), - child.maybe <-- router.$currentPage.map(_.isRoot) - .switch(None, Some(PageHeader(actionBus))), - main( - cls := "flex-grow-1 overflow-y-auto", - mods, - child <-- $maybeContent.map(_.getOrElse(Loading)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala deleted file mode 100644 index 62d820f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.LinkSupport.* -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo - -object PageLink: - type ViewModel = Page - def apply($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = - a(mods($m, actions), child.text <-- $m.map(_.title)) - - def apply(m: ViewModel, actions: Observer[Action])(using - router: Router[Page] - ): Anchor = apply(Val(m), actions) - - def container($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = a(mods($m, actions)) - - def container(m: ViewModel, actions: Observer[Action])(using - Router[Page] - ): Anchor = container(Val(m), actions) - - private def mods($m: Signal[Page], actions: Observer[Action])(using - router: Router[Page] - ): Modifier[Anchor] = - Seq( - href <-- $m.map(router.absoluteUrlForPage).recover { case _ => - Some("invalid url") - }, - composeEvents(onClick.noKeyMod.preventDefault)( - _.sample($m) - .map(NavigateTo.apply) - ) --> actions - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala deleted file mode 100644 index 87f825a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.pages.dashboard.DashboardPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -class DashboardPageConnector(actionBus: Observer[Action])(using - router: Router[Page] -): - def apply: HtmlElement = - AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala deleted file mode 100644 index e9b0f71..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala +++ /dev/null @@ -1,76 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailKriteriaPage -import pages.detail.components.DukazyKriteria -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -object DetailKriteriaPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailKriteriaPageConnector( - state: DetailKriteriaPageConnector.AppState -)( - $page: Signal[Page.DetailKriteria] -)(using Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => - (p.osobniCislo.value, p.parametr.value, p.kriterium.value) - )((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map( - FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) - ) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, s, $s) => - DetailKriteriaPage($s)(state.actionBus.contramap { - case DukazyKriteria.Add => - NavigateTo( - Page.UpravDukazKriteria( - Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), - Page.Titled(s.parametr.id, Some(s.parametr.nazev)), - Page.Titled(s.kriterium.id, Some(s.kriterium.id)) - ) - ) - }) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): DetailKriteriaPage.ViewModel = - DetailKriteriaPage.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala deleted file mode 100644 index 4d8024f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ /dev/null @@ -1,57 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import pages.detail.DetailPage -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import fiftyforms.ui.components.tailwind.Color - -object DetailPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailPageConnector(state: DetailPageConnector.AppState)( - $page: Signal[Page.Detail] -)(using router: Router[Page]): - val $oscChangeSignal = $page.splitOne(_.osobniCislo.value)((osc, _, _) => osc) - val $pageChangeSignal = - $oscChangeSignal.flatMap(osc => - EventStream.fromSeq(Seq(FetchUserDetails(osc), FetchParameters(osc))) - ) - // TODO: filter the value based on the current osc - // OSC change will fetch new data, but still - // - we need to be sure that what we got is really what we ought to display - // - we want to display stale data accordingly (at least with loading indicator) - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - def apply: HtmlElement = - AppPage(state.actionBus)( - $data.combineWithFn($params)(_ zip _) - .map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: List[Parameter] - ): DetailPage.ViewModel = - DetailPage.ViewModel( - o.toDetailOsoby, - p.map( - _.toParametr(param => - PageLink.container(Page.DetailParametru(o, param), state.actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala deleted file mode 100644 index fdaa7f8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailParametruPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink - -object DetailParametruPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailParametruPageConnector( - state: DetailParametruPageConnector.AppState -)( - $page: Signal[Page.DetailParametru] -)(using router: Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => (p.osobniCislo.value, p.parametr.value))((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map(FetchParameter(_, _)) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - } yield (da, pb) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailParametruPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter - ): DetailParametruPage.ViewModel = - DetailParametruPage.ViewModel( - o.toDetailOsoby, - p.toParametr(p => - PageLink.container(Page.DetailParametru(o, p), state.actionBus) - ), - p.criteria.map( - _.toKriterium { c => - PageLink.container( - Page.DetailKriteria(o, p, c), - state.actionBus - ) - } - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala deleted file mode 100644 index 049aa22..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -case class DirectoryPageConnector( - $input: EventStream[List[UserInfo]], - actionBus: Observer[Action] -)(using router: Router[Page]): - val $data = $input.startWithNone - val $actionSignal = EventStream.fromValue(FetchDirectory) - - def apply: HtmlElement = - AppPage(actionBus)( - $data.split(_ => ())((_, _, s) => - pages.directory.DirectoryPage( - s.map( - _.map( - _.toUserRow(u => - PageLink.container( - Page.Detail(Page.Titled(u.personalNumber)), - actionBus - ) - ) - ) - ) - ) - ), - $actionSignal --> actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala deleted file mode 100644 index 88e7450..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ /dev/null @@ -1,68 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii -import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria -import fiftyforms.ui.components.tailwind.Color - -extension (o: UserInfo) - def toDetailOsoby: DetailOsoby.ViewModel = - DetailOsoby.ViewModel( - o.personalNumber, - o.name, - o.email, - o.phone, - o.img, - o.mainFunction.map(f => - DetailOsoby.Funkce.ViewModel(f.name, f.dept, f.ou) - ), - o.userContracts.headOption.map(c => - DetailOsoby.PracovniPomer.ViewModel(c.rel, c.startDate, c.endDate) - ) - ) - -extension (param: Parameter) - def toParametr(container: Parameter => Anchor): DetailParametru.ViewModel = - DetailParametru.ViewModel( - id = param.id, - nazev = param.name, - popis = param.description, - status = "Nesplněno", - statusColor = Color.red, - a = container(param) - ) - -extension (crit: ParameterCriteria) - def toKriterium( - container: ParameterCriteria => Anchor - ): DetailKriteria.ViewModel = - DetailKriteria.ViewModel( - nazev = crit.criteriumText, - kapitola = crit.chapterId, - bod = crit.itemId, - status = "Nesplněno", - statusColor = Color.red, - splneno = false, - dukazy = Nil, - container = container(crit) - ) - -extension (user: UserInfo) - def toUserRow( - container: UserInfo => HtmlElement = (_: UserInfo) => div() - ): UserRow.ViewModel = - UserRow.ViewModel( - osobniCislo = user.personalNumber.toString, - celeJmeno = user.name, - prijmeni = user.surname, - hlavniFunkce = user.mainFunction.map(_.name), - img = user.img, - container = () => container(user) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala deleted file mode 100644 index 7d5b2c1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.dashboard - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.Page - -object DashboardPage: - - def render: HtmlElement = - div("Dashboard page") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala deleted file mode 100644 index 9ef0698..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala +++ /dev/null @@ -1,31 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailKriteriaPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply($m: Signal[ViewModel])( - action: Observer[DukazyKriteria.Action] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - DukazyKriteria($m.map(_.kriterium.dukazy))(action) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala deleted file mode 100644 index 3b316e1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object DetailPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametry: SeznamParametru.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby($m.map(_.osoba)), - SeznamParametru($m.map(_.parametry)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala deleted file mode 100644 index 17224cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala +++ /dev/null @@ -1,24 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailParametruPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriteria: SeznamKriterii.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby.header($m.map(_.osoba)), - DetailParametru($m.map(_.parametr)), - SeznamKriterii($m.map(_.kriteria)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala deleted file mode 100644 index dc912fa..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ /dev/null @@ -1,107 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb -package app -package pages.detail - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.UpravDukazForm -import fiftyforms.services.files.File - -object UpravDukaz: - - type ThisPage = Page.UpravDukazKriteria - type PageKey = (OsobniCislo, String, String) - - trait State { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def availableFiles: EventStream[List[File]] - def actionBus: Observer[Action] - } - - def keyOfPage(page: ThisPage): PageKey = - (page.osobniCislo.value, page.parametr.value, page.kriterium.value) - - def onChangeAction(key: PageKey): Action = - FetchParameterCriteria( - key._1, - key._2, - key._3, - Page.UpravDukazKriteria(_, _, _) - ) - - class Connector(state: State)($page: Signal[ThisPage])(using Router[Page]): - val $paramChangeSignal = $page.splitOne(keyOfPage)((x, _, _) => x) - val $pageChangeSignal = $paramChangeSignal.map(onChangeAction) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.split(_ => ())((_, s, $s) => - PageComponent( - $s.map(buildModel), - state.availableFiles, - state.actionBus.contramap { - case UpravDukazForm.Cancelled => - NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) - case UpravDukazForm.AvailableFilesRequested => - FetchAvailableFiles(s._1.personalNumber) - } - ) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): PageComponent.ViewModel = - import connectors.* - PageComponent.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) - - object PageComponent: - import components.* - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply( - $m: Signal[ViewModel], - availableFilesStream: EventStream[List[File]], - events: Observer[UpravDukazForm.Event] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - UpravDukazForm(availableFilesStream)(events) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala deleted file mode 100644 index ca311b0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.Icons -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Color - -object DetailKriteria: - case class ViewModel( - nazev: String, - kapitola: String, - bod: String, - status: String, - statusColor: Color, - splneno: Boolean, - dukazy: List[DukazKriteria.ViewModel], - container: HtmlElement = div() - ) { - val id = s"${kapitola}${bod}" - } - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h3( - cls := "text-xl leading-6 font-bold text-gray-900", - child.text <-- $m.map(_.nazev) - ), - h3( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - "Kapitola ", - child.text <-- $m.map(_.kapitola), - " bod ", - child <-- $m.map(_.bod) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala deleted file mode 100644 index ae90290..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala +++ /dev/null @@ -1,100 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Avatar - -object DetailOsoby: - - object Funkce: - case class ViewModel( - nazev: String, - stredisko: String, - voj: String - ) - - def render($m: Signal[ViewModel]): HtmlElement = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.nazev), - span( - cls := "hidden md:inline", - " @ ", - child.text <-- $m.map(_.stredisko), - ", ", - child.text <-- $m.map(_.voj) - ) - ) - - object PracovniPomer: - case class ViewModel( - druh: String, - pocatek: LocalDate, - konec: Option[LocalDate] - ) - - def render($m: Signal[ViewModel]): HtmlElement = - import fiftyforms.ui.components.tailwind.CustomAttrs.datetime - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.druh), - " od ", - time( - datetime <-- $m.map(_.pocatek.toString), - child.text <-- $m.map(_.pocatek.toString) - ) - ) - - case class ViewModel( - osobniCislo: OsobniCislo, - jmeno: String, - email: Option[String], - telefon: Option[String], - img: Option[String], - hlavniFunkce: Option[Funkce.ViewModel], - pracovniPomer: Option[PracovniPomer.ViewModel] - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-4", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(16) - ), - div( - h1( - cls := "text-2xl font-bold text-gray-900", - child.text <-- $m.map(_.jmeno) - ), - child.maybe <-- $m.map(_.hlavniFunkce).split(_ => ())((_, _, d) => - Funkce.render(d) - ), - child.maybe <-- $m.map(_.pracovniPomer).split(_ => ())((_, _, d) => - PracovniPomer.render(d) - ) - ) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-5", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(8) - ), - div( - h1( - cls := "text-lg font-medium text-gray-800", - child.text <-- $m.map(_.jmeno) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala deleted file mode 100644 index e11cb62..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color - -object DetailParametru: - case class ViewModel( - id: String, - nazev: String, - popis: String, - status: String, - statusColor: Color, - a: Anchor - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h2( - cls := "text-2xl leading-6 font-medium text-gray-900", - child.text <-- $m.map(_.nazev) - ), - p( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - child.text <-- $m.map(_.popis) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - h2( - cls := "text-lg leading-6 font-medium text-gray-600", - child.text <-- $m.map(_.nazev) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala deleted file mode 100644 index a00ede8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala +++ /dev/null @@ -1,177 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.CustomAttrs - -object DukazKriteria: - case class Osoba(osobniCislo: String, jmeno: String) - case class Udalost(osoba: Osoba, datum: LocalDate) - case class Dokument( - url: String, - nazev: String, - pridal: Udalost, - odebral: Option[Udalost] - ) - case class ViewModel( - dokumenty: List[Dokument], - autorizoval: Option[Udalost], - platiDo: Option[LocalDate], - poznámka: Option[String] - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-lg", - div( - cls := "px-4 py-5 sm:px-6", - h3( - cls := "text-lg leading-6 font-medium text-gray-900", - """Applicant Information""" - ), - p( - cls := "mt-1 max-w-2xl text-sm text-gray-500", - """Personal details and application.""" - ) - ), - div( - cls := "border-t border-gray-200 px-4 py-5 sm:px-6", - dl( - cls := "grid grid-cols-1 gap-x-4 gap-y-8 sm:grid-cols-2", - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Full name""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Margot Foster""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Application for""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Backend Developer""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Email address""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """margotfoster@example.com""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Salary expectation""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """$120,000""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """About""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Fugiat ipsum ipsum deserunt culpa aute sint do nostrud anim incididunt cillum culpa consequat. Excepteur qui ipsum aliquip consequat sint. Sit id mollit nulla mollit nostrud in ea officia proident. Irure nostrud pariatur mollit ad adipisicing reprehenderit deserunt qui eu.""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """Attachments""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - ul( - role := "list", - cls := "border border-gray-200 rounded-md divide-y divide-gray-200", - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """resume_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ), - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """coverletter_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala deleted file mode 100644 index 22f134f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala +++ /dev/null @@ -1,35 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object DukazyKriteria: - sealed trait Action - case object Add extends Action - - type ViewModel = List[DukazKriteria.ViewModel] - - def apply($m: Signal[ViewModel])(actions: Observer[Action]): HtmlElement = - div( - child.maybe <-- $m.map(m => - if (m.isEmpty) then Some(prazdnyDukaz(actions)) - else None - ), - children <-- $m.map(_.zipWithIndex).split(_._2)((_, _, $s) => - DukazKriteria($s.map(_._1)) - ) - ) - - private def prazdnyDukaz(actions: Observer[Action]): HtmlElement = - button( - tpe := "button", - onClick.preventDefault.mapTo(Add) --> actions, - cls := "relative block w-full border-2 border-gray-300 border-dashed rounded-lg p-12 text-center hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - Icons.outline - .`document-add`(12) - .amend(svg.cls := "mx-auto text-gray-400"), - span( - cls := "mt-2 block text-sm font-medium text-gray-900", - "Přidat důkaz" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala deleted file mode 100644 index f628697..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.list.StackedList -import fiftyforms.ui.components.tailwind.list.ListRow -import fiftyforms.ui.components.tailwind.list.RowTag -import fiftyforms.ui.components.tailwind.list.RowNext -import java.time.LocalDate - -object SeznamKriterii: - type ViewModel = List[DetailKriteria.ViewModel] - - private val kritList = new StackedList[DetailKriteria.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - kritList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = - p(cls := "text-sm text-gray-500", s"${i.kapitola}${i.bod}"), - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.container - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala deleted file mode 100644 index 2bfe4bc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.list.{ - StackedList, - ListRow, - RowTag, - PropList, - IconText, - RowNext -} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.LinkSupport.* - -object SeznamParametru: - type ViewModel = List[DetailParametru.ViewModel] - - private val parametrList = new StackedList[DetailParametru.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - parametrList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = emptyNode, - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.a - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala deleted file mode 100644 index f048e9d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ /dev/null @@ -1,101 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.form.* -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement -import fiftyforms.services.files.components.tailwind.FilePicker -import cz.e_bs.cmi.mdr.pdb.frontend.AutorizujDukaz -import cz.e_bs.cmi.mdr.pdb.frontend.DocumentRef -import fiftyforms.services.files.File - -object UpravDukazForm: - sealed trait Event - case object Cancelled extends Event - case object AvailableFilesRequested extends Event - def apply(availableFilesStream: EventStream[List[File]])( - updates: Observer[Event] - ): HtmlElement = - val (filesStream, filesObserver) = - EventStream.withObserver[FilePicker.Event] - val files = Var[List[File]](Nil) - def submitButtons: HtmlElement = - div( - cls := "pt-5", - div( - cls := "flex justify-end", - button( - tpe := "button", - cls := "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zrušit", - onClick.mapTo(Cancelled) --> updates - ), - button( - tpe := "submit", - cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - disabled <-- files.signal.map(_.isEmpty), - "Autorizovat" - ) - ) - ) - - div( - filesStream.collect { case FilePicker.SelectionUpdated(files) => - files.to(List) - } --> files.writer, - filesStream.collect { case FilePicker.AvailableFilesRequested => - AvailableFilesRequested - } --> updates, - cls := "bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6", - Form( - Form.Body( - Form.Section( - FormHeader( - FormHeader.ViewModel( - "Doložení kritéria", - "Sestavte doklady poskytující důkaz kritéria a potvrďte odesláním formuláře. Případné limitace či jiné relevantní údaje vepište do pole Komentář." - ) - ), - FormFields( - FormRow( - "dokumenty", - "Dokumenty", - FilePicker(files.signal, availableFilesStream)(filesObserver) - .amend(idAttr := "dokumenty", cls("max-w-lg")) - ).toHtml, - FormRow( - "platnost", - "Platnost", - input( - idAttr := "platnost", - name := "platnost", - tpe := "date", - autoComplete := "date", - cls := "block max-w-lg w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" - ) - ).toHtml, - FormRow( - "komentar", - "Komentář", - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - textArea( - idAttr := "komentar", - name := "about", - rows := 3, - cls := "max-w-lg shadow-sm block w-full focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border border-gray-300 rounded-md" - ), - p( - cls := "mt-2 text-sm text-gray-500", - "Doplňte prosím potřebné informace související s doložením kritéria, včetně případných limitací." - ) - ) - ).toHtml - ) - ) - ), - submitButtons - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala deleted file mode 100644 index 41033f6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -import components._ - -object DirectoryPage: - - type ViewModel = List[UserRow.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - val (actionsStream, actionObserver) = - EventStream.withObserver[SearchForm.Action] - val $filter = actionsStream - .collect { - case SearchForm.ClearFilter => None - case SearchForm.SetFilter(t) => Some(t) - } - .startWith(None) - val byLetter = for { - d <- $m - f <- $filter - } yield for { - (letter, users) <- d - .filter { user => - f match - case None => true - case Some(t) => user.search.contains(t) - } - .groupBy(_.prijmeni.head) - .to(List) - .sortBy(_._1) - } yield (letter.toString, users.sortBy(_.prijmeni)) - - div( - cls := "h-full max-w-7xl mx-auto order-first flex flex-col flex-shrink-0", - SearchForm(actionObserver), - Directory(byLetter) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala deleted file mode 100644 index 73f915d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -object Directory: - - object Header: - type ViewModel = String - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500", - h3(child.text <-- $m) - ) - - object UserList: - type ViewModel = List[UserRow.ViewModel] - def apply($m: Signal[ViewModel]): HtmlElement = - ul( - role := "list", - cls := "relative z-0 divide-y divide-gray-200", - children <-- $m.split(_.osobniCislo)((_, _, s) => UserRow(s)) - ) - - type ViewModel = List[(String, List[UserRow.ViewModel])] - def apply($m: Signal[ViewModel]): HtmlElement = - nav( - cls := "flex-1 min-h-0 overflow-y-auto", - aria.label := "Directory", - children <-- $m.split(_._1)((_, _, s) => - div( - cls := "relative", - Header(s.map(_._1)), - UserList(s.map(_._2)) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala deleted file mode 100644 index e182910..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala +++ /dev/null @@ -1,54 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object SearchForm: - sealed trait Action - case object SubmitFilter extends Action - sealed trait FilterAction extends Action - case object ClearFilter extends FilterAction - case class SetFilter(value: String) extends FilterAction - - def apply(actions: Observer[Action]): HtmlElement = - div( - cls := "px-6 pt-4 pb-4", - form( - cls := "flex space-x-4", - onSubmit.mapTo(SubmitFilter) --> actions, - div( - cls := "flex-1 min-w-0", - label( - forId := "search", - cls := "sr-only", - "Hledat" - ), - div( - cls := "relative rounded-md shadow-sm", - div( - cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", - Icons.solid.search().amend(svg.cls := "text-gray-400") - ), - input( - tpe := "search", - name := "search", - idAttr := "search", - cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", - placeholder := "Hledat", - composeEvents(onInput.mapToValue.setAsValue.map(SetFilter(_)))( - _.throttle(500) - ) --> actions - ) - ) - ), - button( - tpe := "submit", - cls := "inline-flex justify-center px-3.5 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500", - Icons.solid.filter().amend(svg.cls := "text-gray-400"), - span( - cls := "sr-only", - "Hledat" - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala deleted file mode 100644 index 82f7723..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala +++ /dev/null @@ -1,52 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.airstream.core.Signal -import fiftyforms.ui.components.tailwind.Avatar - -object UserRow: - case class ViewModel( - osobniCislo: String, - celeJmeno: String, - prijmeni: String, - hlavniFunkce: Option[String], - img: Option[String], - container: () => HtmlElement = () => div() - ) { - val search = prijmeni.toLowerCase - } - - def apply($m: Signal[ViewModel]): HtmlElement = - inline def avatarImage = - Avatar($m.map(_.img)).avatarImage(10) - - li( - div( - cls := "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500", - div( - cls := "flex-shrink-0", - child <-- avatarImage - ), - div( - cls := "flex-1 min-w-0", - child <-- $m.map { o => - o.container() - .amend( - cls := "focus:outline-none", - span( - cls := "absolute inset-0", - aria.hidden := true - ), - p( - cls := "text-sm font-medium text-gray-900", - o.celeJmeno - ), - p( - cls := "text-sm text-gray-500 truncate", - o.hlavniFunkce - ) - ) - } - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala deleted file mode 100644 index 467c5f5..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala +++ /dev/null @@ -1,99 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router - -object ErrorPage: - case class ViewModel( - homePage: Page, - errorName: String, - title: String, - subTitle: String - ) - - def apply(m: ViewModel, actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - val ViewModel(homePage, errorName, title, subTitle) = m - div( - cls := "min-h-full pt-16 pb-12 flex flex-col bg-white", - main( - cls := "flex-grow flex flex-col justify-center max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex-shrink-0 flex justify-center", - a( - href := "/", - cls := "inline-flex", - span( - cls := "sr-only", - """Workflow""" - ), - img( - cls := "h-12 w-auto", - src := "https://tailwindui.com/img/logos/workflow-mark.svg?color=indigo&shade=600", - alt := "" - ) - ) - ), - div( - cls := "py-16", - div( - cls := "text-center", - p( - cls := "text-sm font-semibold text-indigo-600 uppercase tracking-wide", - errorName - ), - h1( - cls := "mt-2 text-4xl font-extrabold text-gray-900 tracking-tight sm:text-5xl", - title - ), - p( - cls := "mt-2 text-base text-gray-500", - subTitle - ), - div( - cls := "mt-6", - PageLink - .container(homePage, actionBus) - .amend( - cls := "text-base font-medium text-indigo-600 hover:text-indigo-500", - """Go back home""" - ) - ) - ) - ) - ), - footer( - cls := "flex-shrink-0 max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", - nav( - cls := "flex justify-center space-x-4", - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Contact Support""" - ), - span( - cls := "inline-block border-l border-gray-300", - aria.hidden := true - ), - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Status""" - ), - span( - cls := "inline-block border-l border-gray-300", - aria.hidden := true - ), - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Twitter""" - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala deleted file mode 100644 index bd9baec..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action - -object NotFoundPage: - def apply(homePage: Page, url: String, actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - ErrorPage( - ErrorPage.ViewModel( - homePage, - "404 error", - "Page not found.", - s"Sorry, but page $url doesn't exist." - ), - actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala deleted file mode 100644 index 476fe3c..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action - -object UnhandledErrorPage: - - case class ViewModel( - homePage: Page, - errorName: Option[String], - errorMessage: Option[String] - ) - - def apply(m: ViewModel, actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - ErrorPage( - ErrorPage.ViewModel( - m.homePage, - "Unexpected error occurred", - m.errorName.getOrElse( - "Uh oh!" - ), // TODO: translations, better text than uh oh - m.errorMessage.getOrElse( - "This wasn't supposed to happen! Please try again." - ) - ), - actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala deleted file mode 100644 index 25f059f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala +++ /dev/null @@ -1,6 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.services - -import com.raquo.airstream.core.Observer - -trait DataFetcher[K, A]: - def fetch(id: K, sink: Observer[A]): Unit diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala deleted file mode 100644 index a469830..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala +++ /dev/null @@ -1,120 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package state - -import com.raquo.airstream.core.EventStream -import cz.e_bs.cmi.mdr.pdb.{UserInfo, OsobniCislo} -import com.raquo.airstream.core.Observer -import scala.scalajs.js -import scala.scalajs.js.JSON -import zio.json._ -import com.raquo.airstream.eventbus.EventBus -import com.raquo.airstream.ownership.Owner -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.UserFunction -import cz.e_bs.cmi.mdr.pdb.UserContract -import fiftyforms.services.files.File - -trait AppState - extends connectors.DetailPageConnector.AppState - with connectors.DetailParametruPageConnector.AppState - with connectors.DetailKriteriaPageConnector.AppState - with pages.detail.UpravDukaz.State: - def users: EventStream[List[UserInfo]] - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - -class MockAppState(implicit owner: Owner, router: Router[Page]) - extends AppState: - - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonDecoder[UserFunction] = DeriveJsonDecoder.gen - given JsonDecoder[UserContract] = DeriveJsonDecoder.gen - given JsonDecoder[UserInfo] = DeriveJsonDecoder.gen - - given JsonDecoder[ParameterCriteria] = DeriveJsonDecoder.gen - given JsonDecoder[Parameter] = DeriveJsonDecoder.gen - - private val actions = EventBus[Action]() - private val (parametersStream, pushParameters) = - EventStream.withCallback[List[Parameter]] - private val (usersStream, pushUsers) = - EventStream.withCallback[List[UserInfo]] - private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] - private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] - - private val mockData: List[UserInfo] = - mockUsers - .asInstanceOf[js.Dictionary[js.Object]] - .values - // TODO: is there a more efficient way to parse from JS object directly? - .map { o => - val parsed = JSON.stringify(o).fromJson[UserInfo] - parsed.left.foreach(org.scalajs.dom.console.log(_)) - parsed - } - .collect { case Right(u) => - u - } - .toList - - private val mockParameters: List[Parameter] = - pdbParams - .asInstanceOf[js.Dictionary[js.Object]] - .values - .map(o => JSON.stringify(o).fromJson[Parameter]) - .collect { case Right(p) => p } - .toList - - // TODO: Extract to separate event handler - actions.events.foreach { - case FetchDirectory => pushUsers(mockData) - case FetchUserDetails(osc) => - mockData.find(_.personalNumber == osc).foreach { o => - pushDetails(o) - router.replaceState(Page.Detail(o)) - } - case FetchParameters(osc) => - pushParameters(mockParameters) - case FetchParameter(osc, paramId) => - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(Page.DetailParametru(o, p)) - case FetchParameterCriteria(osc, paramId, critId, page) => - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - c <- p.criteria.find(_.id == critId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(page(o, p, c)) - case NavigateTo(page) => router.pushState(page) - case FetchAvailableFiles(osc) => - pushFiles( - List( - File("https://tc163.cmi.cz/here", "Example file") - ) - ) - } - - override def users: EventStream[List[UserInfo]] = - usersStream.debugWithName("users") - - override def details: EventStream[UserInfo] = - detailsStream.debugWithName("details") - - override def parameters: EventStream[List[Parameter]] = - parametersStream.debugWithName("parameters") - - override def availableFiles: EventStream[List[File]] = - filesStream.debugWithName("available files") - - override def actionBus: Observer[Action] = - actions.writer.debugWithName("actions writer") diff --git a/app/src/main/scala/mdr/pdb/app/Main.scala b/app/src/main/scala/mdr/pdb/app/Main.scala new file mode 100644 index 0000000..081b4ee --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/Main.scala @@ -0,0 +1,103 @@ +package mdr.pdb.app + +import scala.scalajs.js.annotation.JSExportTopLevel +import scala.scalajs.js.annotation.JSExport +import scala.scalajs.js.annotation.JSImport +import scala.scalajs.js +import org.scalajs.dom +import com.raquo.laminar.api.L.{*, given} +import scala.scalajs.js.Date +import com.raquo.waypoint.Router +import com.raquo.waypoint.SplitRender +import mdr.pdb.app.services.DataFetcher +import scala.scalajs.js.JSON +import zio.json._ +import mdr.pdb.UserInfo +import mdr.pdb.app.state.AppState + +@js.native +@JSImport("stylesheets/main.css", JSImport.Namespace) +object Css extends js.Any + +@js.native +@JSImport("data/users.json", JSImport.Default) +object mockUsers extends js.Object + +@js.native +@JSImport("params/pdb-params.json", JSImport.Default) +object pdbParams extends js.Object + +@JSExportTopLevel("app") +object Main: + + @JSExport + def main(args: Array[String]): Unit = + import Routes.given + onLoad { + setupAirstream() + val appContainer = dom.document.querySelector("#app") + val _ = + render( + appContainer, + renderPage(state.MockAppState(using unsafeWindowOwner, router)) + ) + } + + private def onLoad(f: => Unit): Unit = + documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) + + private def setupAirstream()(using router: Router[Page]): Unit = + AirstreamError.registerUnhandledErrorCallback(err => + router.forcePage( + Page.UnhandledError( + Some(err.getClass.getName), // TODO: Fill only in dev mode + Some(err.getMessage) + ) + ) + ) + + def renderPage(state: AppState)(using + router: Router[Page] + ): HtmlElement = + val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) + .collectSignal[Page.Detail]( + connectors + .DetailPageConnector(state)(_) + .apply + ) + .collectSignal[Page.DetailParametru]( + connectors + .DetailParametruPageConnector(state)(_) + .apply + ) + .collectSignal[Page.DetailKriteria]( + connectors + .DetailKriteriaPageConnector(state)(_) + .apply + ) + .collectSignal[Page.UpravDukazKriteria]( + pages.detail.UpravDukaz.Connector(state)(_).apply + ) + .collectStatic(Page.Dashboard)( + connectors.DashboardPageConnector(state.actionBus).apply + ) + .collect[Page.NotFound](pg => + pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) + ) + .collect[Page.UnhandledError](pg => + pages.errors + .UnhandledErrorPage( + pages.errors.UnhandledErrorPage + .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), + state.actionBus + ) + ) + .collectStatic(Page.Directory)( + connectors + .DirectoryPageConnector(state.users, state.actionBus) + .apply + ) + div(cls := "h-full", child <-- pageSplitter.$view) + + // Pull in the stylesheet + val css: Css.type = Css diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala deleted file mode 100644 index a3b00af..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala +++ /dev/null @@ -1,103 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import scala.scalajs.js.annotation.JSExportTopLevel -import scala.scalajs.js.annotation.JSExport -import scala.scalajs.js.annotation.JSImport -import scala.scalajs.js -import org.scalajs.dom -import com.raquo.laminar.api.L.{*, given} -import scala.scalajs.js.Date -import com.raquo.waypoint.Router -import com.raquo.waypoint.SplitRender -import cz.e_bs.cmi.mdr.pdb.app.services.DataFetcher -import scala.scalajs.js.JSON -import zio.json._ -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.state.AppState - -@js.native -@JSImport("stylesheets/main.css", JSImport.Namespace) -object Css extends js.Any - -@js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native -@JSImport("params/pdb-params.json", JSImport.Default) -object pdbParams extends js.Object - -@JSExportTopLevel("app") -object Main: - - @JSExport - def main(args: Array[String]): Unit = - import Routes.given - onLoad { - setupAirstream() - val appContainer = dom.document.querySelector("#app") - val _ = - render( - appContainer, - renderPage(state.MockAppState(using unsafeWindowOwner, router)) - ) - } - - private def onLoad(f: => Unit): Unit = - documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) - - private def setupAirstream()(using router: Router[Page]): Unit = - AirstreamError.registerUnhandledErrorCallback(err => - router.forcePage( - Page.UnhandledError( - Some(err.getClass.getName), // TODO: Fill only in dev mode - Some(err.getMessage) - ) - ) - ) - - def renderPage(state: AppState)(using - router: Router[Page] - ): HtmlElement = - val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) - .collectSignal[Page.Detail]( - connectors - .DetailPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailParametru]( - connectors - .DetailParametruPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailKriteria]( - connectors - .DetailKriteriaPageConnector(state)(_) - .apply - ) - .collectSignal[Page.UpravDukazKriteria]( - pages.detail.UpravDukaz.Connector(state)(_).apply - ) - .collectStatic(Page.Dashboard)( - connectors.DashboardPageConnector(state.actionBus).apply - ) - .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) - ) - .collect[Page.UnhandledError](pg => - pages.errors - .UnhandledErrorPage( - pages.errors.UnhandledErrorPage - .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), - state.actionBus - ) - ) - .collectStatic(Page.Directory)( - connectors - .DirectoryPageConnector(state.users, state.actionBus) - .apply - ) - div(cls := "h-full", child <-- pageSplitter.$view) - - // Pull in the stylesheet - val css: Css.type = Css diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala deleted file mode 100644 index 3bbc8c0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ /dev/null @@ -1,198 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.* -import org.scalajs.dom -import zio.json.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import scala.scalajs.js -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.Page.Titled - -// enum is not working with Waypoints' SplitRender collectStatic -sealed abstract class Page( - val id: String, - val title: String, - val parent: Option[Page] -) { - val path: Vector[Page] = - parent match - case None => Vector(this) - case Some(p) => p.path :+ this - - val isRoot: Boolean = parent.isEmpty -} - -object Page: - - case class Titled[V](value: V, title: Option[String] = None): - val show: String = title.getOrElse(value.toString) - - case object Directory extends Page("directory", "Adresář", None) - - case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) - - case class Detail(osobniCislo: Titled[OsobniCislo]) - extends Page("user", osobniCislo.show, Some(Directory)) - - object Detail { - def apply(o: UserInfo): Detail = Detail( - Titled(o.personalNumber, Some(o.name)) - ) - } - - case class DetailParametru( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String] - ) extends Page( - "parameter", - parametr.show, - Some(Detail(osobniCislo)) - ) - - object DetailParametru { - def apply(o: UserInfo, p: Parameter): DetailParametru = - DetailParametru( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)) - ) - } - - case class DetailKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "criteria", - kriterium.show, - Some(DetailParametru(osobniCislo, parametr)) - ) - - object DetailKriteria { - def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = - DetailKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class UpravDukazKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "addProof", - "Důkaz", - Some(DetailKriteria(osobniCislo, parametr, kriterium)) - ) - - object UpravDukazKriteria { - def apply( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): UpravDukazKriteria = - UpravDukazKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class NotFound(url: String) extends Page("404", "404", Some(Directory)) - - case class UnhandledError( - errorName: Option[String], - errorMessage: Option[String] - ) extends Page("500", "Unexpected error", Some(Directory)) - -object Routes: - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) - given [V: JsonEncoder]: JsonEncoder[Titled[V]] = - DeriveJsonEncoder.gen[Titled[V]] - given [V: JsonDecoder]: JsonDecoder[Titled[V]] = - DeriveJsonDecoder.gen[Titled[V]] - given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] - given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] - - val base = - js.`import`.meta.env.BASE_URL - .asInstanceOf[String] + "app" - - val homePage: Page = Page.Directory - - given router: Router[Page] = Router[Page]( - routes = List( - Route.static(homePage, root / endOfSegments, basePath = base), - Route.static( - Page.Dashboard, - root / "dashboard" / endOfSegments, - basePath = base - ), - Route[Page.Detail, String]( - encode = _.osobniCislo.value.toString, - decode = osc => Page.Detail(Titled(OsobniCislo(osc))), - root / "osoba" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.DetailParametru, (String, String)]( - encode = p => (p.osobniCislo.value.toString, p.parametr.value), - decode = - p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / endOfSegments, - basePath = base - ), - Route[Page.DetailKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.DetailKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.UpravDukazKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.UpravDukazKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / "edit" / endOfSegments, - basePath = base - ) - ), - serializePage = _.toJson, - deserializePage = _.fromJson[Page] - .fold(s => throw IllegalStateException(s), identity), - getPageTitle = _.title, - routeFallback = url => Page.NotFound(url), - deserializeFallback = _ => Page.Dashboard - )( - $popStateEvent = windowEvents.onPopState, - owner = unsafeWindowOwner - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala deleted file mode 100644 index b24b82b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -sealed trait Action - -case object FetchDirectory extends Action -case class FetchUserDetails(osc: OsobniCislo) extends Action -case class FetchParameters(osc: OsobniCislo) extends Action -case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action -case class FetchParameterCriteria( - osc: OsobniCislo, - paramId: String, - critId: String, - page: (UserInfo, Parameter, ParameterCriteria) => Page -) extends Action -case class FetchAvailableFiles(osc: OsobniCislo) extends Action -case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala deleted file mode 100644 index c637d35..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala +++ /dev/null @@ -1,82 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.{UserProfile, UserInfo, OsobniCislo} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo -import cz.e_bs.cmi.mdr.pdb.UserFunction - -object AppPage: - // TODO: pages by logged in user - val pages: List[Page] = List(Page.Directory, Page.Dashboard) - - import NavigationBar.{Logo, MenuItem} - - val logo = Logo( - "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", - "Workflow" - ) - - // TODO: menu items by user profile - val userMenu = - List( - MenuItem("Your Profile"), - MenuItem("Settings"), - MenuItem("Sign out") - ) - - // TODO: load user profile - val $userProfile = Var( - UserProfile( - "tom", - UserInfo( - OsobniCislo("1031"), - "tom", - "Tom", - "Cook", - None, - None, - Some("tom@example.com"), - Some("+420 222 866 180"), - Some( - UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") - ), - Nil, - None - ) - ) - ) - - val $userInfo = $userProfile.signal.map(_.userInfo) - - type ViewModel = Option[HtmlElement] - def apply( - actionBus: Observer[Action] - )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using - router: Router[Page] - ): HtmlElement = - PageLayout(actionBus)( - $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => - PageLayout.ViewModel( - NavigationBar.ViewModel( - u, - pages.map(p => - NavigationBar.Link( - () => - PageLink( - p, - actionBus - ), - p == cp - ) - ), - userMenu, - logo - ), - c - ) - ), - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala deleted file mode 100644 index bf473c6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ /dev/null @@ -1,143 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden -import fiftyforms.ui.components.tailwind.list.IconText.ViewModel -import fiftyforms.ui.components.tailwind.Icons -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Action - -object Breadcrumbs: - - private def slash = - import svg.{*, given} - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-300", - xmlns := "http://www.w3.org/2000/svg", - fill := "currentColor", - viewBox := "0 0 20 20", - ariaHidden := true, - path( - d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" - ) - ) - - def backIcon = - Icons.solid - .`arrow-narrow-left`() - .amend( - svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" - ) - - object Link: - case class ViewModel( - page: Page, - icon: Option[SvgElement], - extraClasses: String, - text: String, - textClasses: Option[String] = None - ) - - val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" - - def shortHome(p: Page) = ViewModel( - p, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - "Zpět na úvodní stránku", - None - ) - - def fullHome(p: Page) = ViewModel( - p, - Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), - "text-gray-400 hover:text-gray-500", - "Domů", - Some("sr-only") - ) - - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - PageLink - .container($m.map(_.page), actionBus) - .amend( - cls <-- $m.map(_.extraClasses), - child.maybe <-- $m.map(_.icon), - span( - cls <-- $m.map(_.textClasses.getOrElse("")), - child.text <-- $m.map(_.text) - ) - ) - - object FullBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - ol( - role := "list", - cls := "flex items-center space-x-4", - children <-- $m.map(_.path).split(_.id)((_, _, $p) => - li( - div( - cls := "flex items-center", - child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), - Link( - $p.map(p => - if (p.isRoot) then Link.fullHome(p) - else - Link.ViewModel( - p, - None, - s"ml-4 max-w-xs truncate ${Link.baseClasses}", - p.title - ) - ), - actionBus - ) - ) - ) - ) - ) - - object ShortBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - Link( - $m.map { p => - val target = p.parent.getOrElse(p) - if target.isRoot then Link.shortHome(target) - else - Link.ViewModel( - target, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - s"Zpět na ${target.title}" - ) - }, - actionBus - ) - - def apply(actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - nav( - cls := "flex", - aria.label := "Breadcrumb", - div( - cls := "flex sm:hidden", - ShortBreadcrumbs(router.$currentPage, actionBus) - ), - div( - cls := "hidden sm:block", - FullBreadcrumbs(router.$currentPage, actionBus) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala deleted file mode 100644 index 0db2047..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ /dev/null @@ -1,242 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent -import fiftyforms.ui.components.tailwind.Icons -import fiftyforms.ui.components.tailwind.Avatar -import cz.e_bs.cmi.mdr.pdb.UserInfo -import io.laminext.syntax.core.* - -object NavigationBar: - - case class Logo(img: String, name: String) - case class Link(a: () => Anchor, active: Boolean) - case class MenuItem(title: String) - - case class ViewModel( - userInfo: UserInfo, - pages: List[Link], - userMenu: List[MenuItem], - logo: Logo - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - val $userInfo = $m.map(_.userInfo) - val mobileMenuOpen = Var(false) - - val desktopOnly = cls("hidden md:block") - val mobileOnly = cls("md:hidden") - - inline def avatarImage(size: Int = 8) = - Avatar($userInfo.map(_.img)).avatarImage(size) - - def notificationButton = button( - tpe := "button", - cls := List( - "bg-indigo-600", - "focus:outline-none", - "focus:ring-2", - "focus:ring-offset-2", - "focus:ring-offset-indigo-600", - "focus:ring-white", - "hover:text-white", - "p-1", - "rounded-full", - "text-indigo-200" - ), - span(cls := "sr-only", "View notifications"), - Icons.outline.bell() - ) - - def userProfile: HtmlElement = - val menuOpen = Var(false) - - def menuItem(item: MenuItem, idx: Int): HtmlElement = - a( - href := "#", - cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", - role := "menuitem", - tabIndex := -1, - idAttr := s"user-menu-item-$idx", - item.title - ) - - div( - cls := "ml-3 relative", - div( - button( - tpe := "button", - cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - idAttr := "user-menu-button", - aria.expanded <-- menuOpen.signal, - aria.hasPopup := true, - span(cls := "sr-only", "Open user menu"), - child <-- avatarImage(), - onClick.preventDefault.mapTo( - !menuOpen.now() - ) --> menuOpen.writer - ) - ), - /* - * */ - div( - cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", - cls <-- menuOpen.signal.map { o => - if (o) "md:block" else "md:hidden" - }, - role := "menu", - aria.orientation := "vertical", - aria.labelledBy := "user-menu-button", - tabIndex := -1, - // : keyboard navigation - children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) - ) - ) - - def mobileProfile = - def menuItem(item: MenuItem): HtmlElement = - a( - href := "#", - cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", - item.title - ) - - div( - cls := "pt-4 pb-3 border-t border-indigo-700", - div( - cls := "flex items-center px-5", - div( - cls := "flex-shrink-0", - child <-- avatarImage(10) - ), - div( - cls := "ml-3", - div( - cls := "text-base font-medium text-white", - child.text <-- $userInfo.map(_.name) - ), - child.maybe <-- $userInfo.map( - _.email.map(e => - div( - cls := "text-sm font-medium text-indigo-300", - e - ) - ) - ) - ), - notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) - ), - div( - cls := "mt-3 px-2 space-y-1", - children <-- $m.map(_.userMenu.map(menuItem)) - ) - ) - - def pageLink(page: Link): Anchor = - page - .a() - .amend( - cls := "text-white px-3 py-2 rounded-md text-sm font-medium", - cls := Seq( - "bg-indigo-700" -> page.active, - "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active - ), - ariaCurrent := (if page.active then "page" else "false") - ) - - def logoImg: Image = - img( - cls := "h-8 w-8", - src <-- $m.map(_.logo.img), - alt <-- $m.map(_.logo.name) - ) - - def mobileMenuButton = button( - tpe := "button", - cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - aria.controls := "mobile-menu", - aria.expanded <-- mobileMenuOpen.signal, - span(cls := "sr-only", "Open main menu"), - Icons.outline - .menu() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") - ), - Icons.outline - .x() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") - ), - onClick.preventDefault.mapTo( - !mobileMenuOpen.now() - ) --> mobileMenuOpen.writer - ) - - def navBarLeft = - div( - cls := "flex items-center", - div(cls := "flex-shrink-0", logoImg), - div( - desktopOnly, - div( - cls := "ml-10 flex items-baseline space-x-4", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ) - ) - ) - - def navBarRight = - div( - desktopOnly, - div( - cls := "ml-4 flex items-center md:ml-6", - notificationButton, - userProfile - ) - ) - - def navBarMobile = - div( - cls := "-mr-2 flex", - mobileOnly, - mobileMenuButton - ) - - def navBar = - div( - cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex items-center justify-between h-16", - navBarLeft, - navBarRight, - navBarMobile - ) - ) - - def mobileMenu = - div( - mobileOnly, - cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }, - idAttr := "mobile-menu", - div( - cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ), - mobileProfile - ) - - nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala deleted file mode 100644 index 75339df..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object PageHeader: - def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = - header( - cls := "bg-white shadow-sm", - div( - cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", - h1( - cls := "text-lg leading-6 font-semibold text-gray-900", - Breadcrumbs(actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala deleted file mode 100644 index 5096463..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ /dev/null @@ -1,30 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import fiftyforms.ui.components.tailwind.Loading -import io.laminext.syntax.core.* - -object PageLayout: - case class ViewModel( - navigation: NavigationBar.ViewModel, - content: Option[HtmlElement] - ) - def apply(actionBus: Observer[Action])( - $m: Signal[ViewModel], - mods: Modifier[HtmlElement]* - )(using router: Router[Page]): HtmlElement = - val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) - div( - cls := "h-full flex flex-col", - NavigationBar($m.map(_.navigation)), - child.maybe <-- router.$currentPage.map(_.isRoot) - .switch(None, Some(PageHeader(actionBus))), - main( - cls := "flex-grow-1 overflow-y-auto", - mods, - child <-- $maybeContent.map(_.getOrElse(Loading)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala deleted file mode 100644 index 62d820f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.LinkSupport.* -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo - -object PageLink: - type ViewModel = Page - def apply($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = - a(mods($m, actions), child.text <-- $m.map(_.title)) - - def apply(m: ViewModel, actions: Observer[Action])(using - router: Router[Page] - ): Anchor = apply(Val(m), actions) - - def container($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = a(mods($m, actions)) - - def container(m: ViewModel, actions: Observer[Action])(using - Router[Page] - ): Anchor = container(Val(m), actions) - - private def mods($m: Signal[Page], actions: Observer[Action])(using - router: Router[Page] - ): Modifier[Anchor] = - Seq( - href <-- $m.map(router.absoluteUrlForPage).recover { case _ => - Some("invalid url") - }, - composeEvents(onClick.noKeyMod.preventDefault)( - _.sample($m) - .map(NavigateTo.apply) - ) --> actions - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala deleted file mode 100644 index 87f825a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.pages.dashboard.DashboardPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -class DashboardPageConnector(actionBus: Observer[Action])(using - router: Router[Page] -): - def apply: HtmlElement = - AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala deleted file mode 100644 index e9b0f71..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala +++ /dev/null @@ -1,76 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailKriteriaPage -import pages.detail.components.DukazyKriteria -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -object DetailKriteriaPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailKriteriaPageConnector( - state: DetailKriteriaPageConnector.AppState -)( - $page: Signal[Page.DetailKriteria] -)(using Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => - (p.osobniCislo.value, p.parametr.value, p.kriterium.value) - )((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map( - FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) - ) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, s, $s) => - DetailKriteriaPage($s)(state.actionBus.contramap { - case DukazyKriteria.Add => - NavigateTo( - Page.UpravDukazKriteria( - Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), - Page.Titled(s.parametr.id, Some(s.parametr.nazev)), - Page.Titled(s.kriterium.id, Some(s.kriterium.id)) - ) - ) - }) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): DetailKriteriaPage.ViewModel = - DetailKriteriaPage.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala deleted file mode 100644 index 4d8024f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ /dev/null @@ -1,57 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import pages.detail.DetailPage -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import fiftyforms.ui.components.tailwind.Color - -object DetailPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailPageConnector(state: DetailPageConnector.AppState)( - $page: Signal[Page.Detail] -)(using router: Router[Page]): - val $oscChangeSignal = $page.splitOne(_.osobniCislo.value)((osc, _, _) => osc) - val $pageChangeSignal = - $oscChangeSignal.flatMap(osc => - EventStream.fromSeq(Seq(FetchUserDetails(osc), FetchParameters(osc))) - ) - // TODO: filter the value based on the current osc - // OSC change will fetch new data, but still - // - we need to be sure that what we got is really what we ought to display - // - we want to display stale data accordingly (at least with loading indicator) - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - def apply: HtmlElement = - AppPage(state.actionBus)( - $data.combineWithFn($params)(_ zip _) - .map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: List[Parameter] - ): DetailPage.ViewModel = - DetailPage.ViewModel( - o.toDetailOsoby, - p.map( - _.toParametr(param => - PageLink.container(Page.DetailParametru(o, param), state.actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala deleted file mode 100644 index fdaa7f8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailParametruPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink - -object DetailParametruPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailParametruPageConnector( - state: DetailParametruPageConnector.AppState -)( - $page: Signal[Page.DetailParametru] -)(using router: Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => (p.osobniCislo.value, p.parametr.value))((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map(FetchParameter(_, _)) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - } yield (da, pb) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailParametruPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter - ): DetailParametruPage.ViewModel = - DetailParametruPage.ViewModel( - o.toDetailOsoby, - p.toParametr(p => - PageLink.container(Page.DetailParametru(o, p), state.actionBus) - ), - p.criteria.map( - _.toKriterium { c => - PageLink.container( - Page.DetailKriteria(o, p, c), - state.actionBus - ) - } - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala deleted file mode 100644 index 049aa22..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -case class DirectoryPageConnector( - $input: EventStream[List[UserInfo]], - actionBus: Observer[Action] -)(using router: Router[Page]): - val $data = $input.startWithNone - val $actionSignal = EventStream.fromValue(FetchDirectory) - - def apply: HtmlElement = - AppPage(actionBus)( - $data.split(_ => ())((_, _, s) => - pages.directory.DirectoryPage( - s.map( - _.map( - _.toUserRow(u => - PageLink.container( - Page.Detail(Page.Titled(u.personalNumber)), - actionBus - ) - ) - ) - ) - ) - ), - $actionSignal --> actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala deleted file mode 100644 index 88e7450..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ /dev/null @@ -1,68 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii -import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria -import fiftyforms.ui.components.tailwind.Color - -extension (o: UserInfo) - def toDetailOsoby: DetailOsoby.ViewModel = - DetailOsoby.ViewModel( - o.personalNumber, - o.name, - o.email, - o.phone, - o.img, - o.mainFunction.map(f => - DetailOsoby.Funkce.ViewModel(f.name, f.dept, f.ou) - ), - o.userContracts.headOption.map(c => - DetailOsoby.PracovniPomer.ViewModel(c.rel, c.startDate, c.endDate) - ) - ) - -extension (param: Parameter) - def toParametr(container: Parameter => Anchor): DetailParametru.ViewModel = - DetailParametru.ViewModel( - id = param.id, - nazev = param.name, - popis = param.description, - status = "Nesplněno", - statusColor = Color.red, - a = container(param) - ) - -extension (crit: ParameterCriteria) - def toKriterium( - container: ParameterCriteria => Anchor - ): DetailKriteria.ViewModel = - DetailKriteria.ViewModel( - nazev = crit.criteriumText, - kapitola = crit.chapterId, - bod = crit.itemId, - status = "Nesplněno", - statusColor = Color.red, - splneno = false, - dukazy = Nil, - container = container(crit) - ) - -extension (user: UserInfo) - def toUserRow( - container: UserInfo => HtmlElement = (_: UserInfo) => div() - ): UserRow.ViewModel = - UserRow.ViewModel( - osobniCislo = user.personalNumber.toString, - celeJmeno = user.name, - prijmeni = user.surname, - hlavniFunkce = user.mainFunction.map(_.name), - img = user.img, - container = () => container(user) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala deleted file mode 100644 index 7d5b2c1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.dashboard - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.Page - -object DashboardPage: - - def render: HtmlElement = - div("Dashboard page") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala deleted file mode 100644 index 9ef0698..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala +++ /dev/null @@ -1,31 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailKriteriaPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply($m: Signal[ViewModel])( - action: Observer[DukazyKriteria.Action] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - DukazyKriteria($m.map(_.kriterium.dukazy))(action) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala deleted file mode 100644 index 3b316e1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object DetailPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametry: SeznamParametru.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby($m.map(_.osoba)), - SeznamParametru($m.map(_.parametry)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala deleted file mode 100644 index 17224cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala +++ /dev/null @@ -1,24 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailParametruPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriteria: SeznamKriterii.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby.header($m.map(_.osoba)), - DetailParametru($m.map(_.parametr)), - SeznamKriterii($m.map(_.kriteria)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala deleted file mode 100644 index dc912fa..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ /dev/null @@ -1,107 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb -package app -package pages.detail - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.UpravDukazForm -import fiftyforms.services.files.File - -object UpravDukaz: - - type ThisPage = Page.UpravDukazKriteria - type PageKey = (OsobniCislo, String, String) - - trait State { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def availableFiles: EventStream[List[File]] - def actionBus: Observer[Action] - } - - def keyOfPage(page: ThisPage): PageKey = - (page.osobniCislo.value, page.parametr.value, page.kriterium.value) - - def onChangeAction(key: PageKey): Action = - FetchParameterCriteria( - key._1, - key._2, - key._3, - Page.UpravDukazKriteria(_, _, _) - ) - - class Connector(state: State)($page: Signal[ThisPage])(using Router[Page]): - val $paramChangeSignal = $page.splitOne(keyOfPage)((x, _, _) => x) - val $pageChangeSignal = $paramChangeSignal.map(onChangeAction) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.split(_ => ())((_, s, $s) => - PageComponent( - $s.map(buildModel), - state.availableFiles, - state.actionBus.contramap { - case UpravDukazForm.Cancelled => - NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) - case UpravDukazForm.AvailableFilesRequested => - FetchAvailableFiles(s._1.personalNumber) - } - ) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): PageComponent.ViewModel = - import connectors.* - PageComponent.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) - - object PageComponent: - import components.* - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply( - $m: Signal[ViewModel], - availableFilesStream: EventStream[List[File]], - events: Observer[UpravDukazForm.Event] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - UpravDukazForm(availableFilesStream)(events) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala deleted file mode 100644 index ca311b0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.Icons -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Color - -object DetailKriteria: - case class ViewModel( - nazev: String, - kapitola: String, - bod: String, - status: String, - statusColor: Color, - splneno: Boolean, - dukazy: List[DukazKriteria.ViewModel], - container: HtmlElement = div() - ) { - val id = s"${kapitola}${bod}" - } - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h3( - cls := "text-xl leading-6 font-bold text-gray-900", - child.text <-- $m.map(_.nazev) - ), - h3( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - "Kapitola ", - child.text <-- $m.map(_.kapitola), - " bod ", - child <-- $m.map(_.bod) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala deleted file mode 100644 index ae90290..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala +++ /dev/null @@ -1,100 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Avatar - -object DetailOsoby: - - object Funkce: - case class ViewModel( - nazev: String, - stredisko: String, - voj: String - ) - - def render($m: Signal[ViewModel]): HtmlElement = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.nazev), - span( - cls := "hidden md:inline", - " @ ", - child.text <-- $m.map(_.stredisko), - ", ", - child.text <-- $m.map(_.voj) - ) - ) - - object PracovniPomer: - case class ViewModel( - druh: String, - pocatek: LocalDate, - konec: Option[LocalDate] - ) - - def render($m: Signal[ViewModel]): HtmlElement = - import fiftyforms.ui.components.tailwind.CustomAttrs.datetime - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.druh), - " od ", - time( - datetime <-- $m.map(_.pocatek.toString), - child.text <-- $m.map(_.pocatek.toString) - ) - ) - - case class ViewModel( - osobniCislo: OsobniCislo, - jmeno: String, - email: Option[String], - telefon: Option[String], - img: Option[String], - hlavniFunkce: Option[Funkce.ViewModel], - pracovniPomer: Option[PracovniPomer.ViewModel] - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-4", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(16) - ), - div( - h1( - cls := "text-2xl font-bold text-gray-900", - child.text <-- $m.map(_.jmeno) - ), - child.maybe <-- $m.map(_.hlavniFunkce).split(_ => ())((_, _, d) => - Funkce.render(d) - ), - child.maybe <-- $m.map(_.pracovniPomer).split(_ => ())((_, _, d) => - PracovniPomer.render(d) - ) - ) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-5", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(8) - ), - div( - h1( - cls := "text-lg font-medium text-gray-800", - child.text <-- $m.map(_.jmeno) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala deleted file mode 100644 index e11cb62..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color - -object DetailParametru: - case class ViewModel( - id: String, - nazev: String, - popis: String, - status: String, - statusColor: Color, - a: Anchor - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h2( - cls := "text-2xl leading-6 font-medium text-gray-900", - child.text <-- $m.map(_.nazev) - ), - p( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - child.text <-- $m.map(_.popis) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - h2( - cls := "text-lg leading-6 font-medium text-gray-600", - child.text <-- $m.map(_.nazev) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala deleted file mode 100644 index a00ede8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala +++ /dev/null @@ -1,177 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.CustomAttrs - -object DukazKriteria: - case class Osoba(osobniCislo: String, jmeno: String) - case class Udalost(osoba: Osoba, datum: LocalDate) - case class Dokument( - url: String, - nazev: String, - pridal: Udalost, - odebral: Option[Udalost] - ) - case class ViewModel( - dokumenty: List[Dokument], - autorizoval: Option[Udalost], - platiDo: Option[LocalDate], - poznámka: Option[String] - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-lg", - div( - cls := "px-4 py-5 sm:px-6", - h3( - cls := "text-lg leading-6 font-medium text-gray-900", - """Applicant Information""" - ), - p( - cls := "mt-1 max-w-2xl text-sm text-gray-500", - """Personal details and application.""" - ) - ), - div( - cls := "border-t border-gray-200 px-4 py-5 sm:px-6", - dl( - cls := "grid grid-cols-1 gap-x-4 gap-y-8 sm:grid-cols-2", - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Full name""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Margot Foster""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Application for""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Backend Developer""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Email address""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """margotfoster@example.com""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Salary expectation""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """$120,000""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """About""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Fugiat ipsum ipsum deserunt culpa aute sint do nostrud anim incididunt cillum culpa consequat. Excepteur qui ipsum aliquip consequat sint. Sit id mollit nulla mollit nostrud in ea officia proident. Irure nostrud pariatur mollit ad adipisicing reprehenderit deserunt qui eu.""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """Attachments""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - ul( - role := "list", - cls := "border border-gray-200 rounded-md divide-y divide-gray-200", - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """resume_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ), - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """coverletter_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala deleted file mode 100644 index 22f134f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala +++ /dev/null @@ -1,35 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object DukazyKriteria: - sealed trait Action - case object Add extends Action - - type ViewModel = List[DukazKriteria.ViewModel] - - def apply($m: Signal[ViewModel])(actions: Observer[Action]): HtmlElement = - div( - child.maybe <-- $m.map(m => - if (m.isEmpty) then Some(prazdnyDukaz(actions)) - else None - ), - children <-- $m.map(_.zipWithIndex).split(_._2)((_, _, $s) => - DukazKriteria($s.map(_._1)) - ) - ) - - private def prazdnyDukaz(actions: Observer[Action]): HtmlElement = - button( - tpe := "button", - onClick.preventDefault.mapTo(Add) --> actions, - cls := "relative block w-full border-2 border-gray-300 border-dashed rounded-lg p-12 text-center hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - Icons.outline - .`document-add`(12) - .amend(svg.cls := "mx-auto text-gray-400"), - span( - cls := "mt-2 block text-sm font-medium text-gray-900", - "Přidat důkaz" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala deleted file mode 100644 index f628697..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.list.StackedList -import fiftyforms.ui.components.tailwind.list.ListRow -import fiftyforms.ui.components.tailwind.list.RowTag -import fiftyforms.ui.components.tailwind.list.RowNext -import java.time.LocalDate - -object SeznamKriterii: - type ViewModel = List[DetailKriteria.ViewModel] - - private val kritList = new StackedList[DetailKriteria.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - kritList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = - p(cls := "text-sm text-gray-500", s"${i.kapitola}${i.bod}"), - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.container - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala deleted file mode 100644 index 2bfe4bc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.list.{ - StackedList, - ListRow, - RowTag, - PropList, - IconText, - RowNext -} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.LinkSupport.* - -object SeznamParametru: - type ViewModel = List[DetailParametru.ViewModel] - - private val parametrList = new StackedList[DetailParametru.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - parametrList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = emptyNode, - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.a - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala deleted file mode 100644 index f048e9d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ /dev/null @@ -1,101 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.form.* -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement -import fiftyforms.services.files.components.tailwind.FilePicker -import cz.e_bs.cmi.mdr.pdb.frontend.AutorizujDukaz -import cz.e_bs.cmi.mdr.pdb.frontend.DocumentRef -import fiftyforms.services.files.File - -object UpravDukazForm: - sealed trait Event - case object Cancelled extends Event - case object AvailableFilesRequested extends Event - def apply(availableFilesStream: EventStream[List[File]])( - updates: Observer[Event] - ): HtmlElement = - val (filesStream, filesObserver) = - EventStream.withObserver[FilePicker.Event] - val files = Var[List[File]](Nil) - def submitButtons: HtmlElement = - div( - cls := "pt-5", - div( - cls := "flex justify-end", - button( - tpe := "button", - cls := "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zrušit", - onClick.mapTo(Cancelled) --> updates - ), - button( - tpe := "submit", - cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - disabled <-- files.signal.map(_.isEmpty), - "Autorizovat" - ) - ) - ) - - div( - filesStream.collect { case FilePicker.SelectionUpdated(files) => - files.to(List) - } --> files.writer, - filesStream.collect { case FilePicker.AvailableFilesRequested => - AvailableFilesRequested - } --> updates, - cls := "bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6", - Form( - Form.Body( - Form.Section( - FormHeader( - FormHeader.ViewModel( - "Doložení kritéria", - "Sestavte doklady poskytující důkaz kritéria a potvrďte odesláním formuláře. Případné limitace či jiné relevantní údaje vepište do pole Komentář." - ) - ), - FormFields( - FormRow( - "dokumenty", - "Dokumenty", - FilePicker(files.signal, availableFilesStream)(filesObserver) - .amend(idAttr := "dokumenty", cls("max-w-lg")) - ).toHtml, - FormRow( - "platnost", - "Platnost", - input( - idAttr := "platnost", - name := "platnost", - tpe := "date", - autoComplete := "date", - cls := "block max-w-lg w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" - ) - ).toHtml, - FormRow( - "komentar", - "Komentář", - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - textArea( - idAttr := "komentar", - name := "about", - rows := 3, - cls := "max-w-lg shadow-sm block w-full focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border border-gray-300 rounded-md" - ), - p( - cls := "mt-2 text-sm text-gray-500", - "Doplňte prosím potřebné informace související s doložením kritéria, včetně případných limitací." - ) - ) - ).toHtml - ) - ) - ), - submitButtons - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala deleted file mode 100644 index 41033f6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -import components._ - -object DirectoryPage: - - type ViewModel = List[UserRow.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - val (actionsStream, actionObserver) = - EventStream.withObserver[SearchForm.Action] - val $filter = actionsStream - .collect { - case SearchForm.ClearFilter => None - case SearchForm.SetFilter(t) => Some(t) - } - .startWith(None) - val byLetter = for { - d <- $m - f <- $filter - } yield for { - (letter, users) <- d - .filter { user => - f match - case None => true - case Some(t) => user.search.contains(t) - } - .groupBy(_.prijmeni.head) - .to(List) - .sortBy(_._1) - } yield (letter.toString, users.sortBy(_.prijmeni)) - - div( - cls := "h-full max-w-7xl mx-auto order-first flex flex-col flex-shrink-0", - SearchForm(actionObserver), - Directory(byLetter) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala deleted file mode 100644 index 73f915d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -object Directory: - - object Header: - type ViewModel = String - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500", - h3(child.text <-- $m) - ) - - object UserList: - type ViewModel = List[UserRow.ViewModel] - def apply($m: Signal[ViewModel]): HtmlElement = - ul( - role := "list", - cls := "relative z-0 divide-y divide-gray-200", - children <-- $m.split(_.osobniCislo)((_, _, s) => UserRow(s)) - ) - - type ViewModel = List[(String, List[UserRow.ViewModel])] - def apply($m: Signal[ViewModel]): HtmlElement = - nav( - cls := "flex-1 min-h-0 overflow-y-auto", - aria.label := "Directory", - children <-- $m.split(_._1)((_, _, s) => - div( - cls := "relative", - Header(s.map(_._1)), - UserList(s.map(_._2)) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala deleted file mode 100644 index e182910..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala +++ /dev/null @@ -1,54 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object SearchForm: - sealed trait Action - case object SubmitFilter extends Action - sealed trait FilterAction extends Action - case object ClearFilter extends FilterAction - case class SetFilter(value: String) extends FilterAction - - def apply(actions: Observer[Action]): HtmlElement = - div( - cls := "px-6 pt-4 pb-4", - form( - cls := "flex space-x-4", - onSubmit.mapTo(SubmitFilter) --> actions, - div( - cls := "flex-1 min-w-0", - label( - forId := "search", - cls := "sr-only", - "Hledat" - ), - div( - cls := "relative rounded-md shadow-sm", - div( - cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", - Icons.solid.search().amend(svg.cls := "text-gray-400") - ), - input( - tpe := "search", - name := "search", - idAttr := "search", - cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", - placeholder := "Hledat", - composeEvents(onInput.mapToValue.setAsValue.map(SetFilter(_)))( - _.throttle(500) - ) --> actions - ) - ) - ), - button( - tpe := "submit", - cls := "inline-flex justify-center px-3.5 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500", - Icons.solid.filter().amend(svg.cls := "text-gray-400"), - span( - cls := "sr-only", - "Hledat" - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala deleted file mode 100644 index 82f7723..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala +++ /dev/null @@ -1,52 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.airstream.core.Signal -import fiftyforms.ui.components.tailwind.Avatar - -object UserRow: - case class ViewModel( - osobniCislo: String, - celeJmeno: String, - prijmeni: String, - hlavniFunkce: Option[String], - img: Option[String], - container: () => HtmlElement = () => div() - ) { - val search = prijmeni.toLowerCase - } - - def apply($m: Signal[ViewModel]): HtmlElement = - inline def avatarImage = - Avatar($m.map(_.img)).avatarImage(10) - - li( - div( - cls := "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500", - div( - cls := "flex-shrink-0", - child <-- avatarImage - ), - div( - cls := "flex-1 min-w-0", - child <-- $m.map { o => - o.container() - .amend( - cls := "focus:outline-none", - span( - cls := "absolute inset-0", - aria.hidden := true - ), - p( - cls := "text-sm font-medium text-gray-900", - o.celeJmeno - ), - p( - cls := "text-sm text-gray-500 truncate", - o.hlavniFunkce - ) - ) - } - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala deleted file mode 100644 index 467c5f5..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala +++ /dev/null @@ -1,99 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router - -object ErrorPage: - case class ViewModel( - homePage: Page, - errorName: String, - title: String, - subTitle: String - ) - - def apply(m: ViewModel, actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - val ViewModel(homePage, errorName, title, subTitle) = m - div( - cls := "min-h-full pt-16 pb-12 flex flex-col bg-white", - main( - cls := "flex-grow flex flex-col justify-center max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex-shrink-0 flex justify-center", - a( - href := "/", - cls := "inline-flex", - span( - cls := "sr-only", - """Workflow""" - ), - img( - cls := "h-12 w-auto", - src := "https://tailwindui.com/img/logos/workflow-mark.svg?color=indigo&shade=600", - alt := "" - ) - ) - ), - div( - cls := "py-16", - div( - cls := "text-center", - p( - cls := "text-sm font-semibold text-indigo-600 uppercase tracking-wide", - errorName - ), - h1( - cls := "mt-2 text-4xl font-extrabold text-gray-900 tracking-tight sm:text-5xl", - title - ), - p( - cls := "mt-2 text-base text-gray-500", - subTitle - ), - div( - cls := "mt-6", - PageLink - .container(homePage, actionBus) - .amend( - cls := "text-base font-medium text-indigo-600 hover:text-indigo-500", - """Go back home""" - ) - ) - ) - ) - ), - footer( - cls := "flex-shrink-0 max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", - nav( - cls := "flex justify-center space-x-4", - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Contact Support""" - ), - span( - cls := "inline-block border-l border-gray-300", - aria.hidden := true - ), - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Status""" - ), - span( - cls := "inline-block border-l border-gray-300", - aria.hidden := true - ), - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Twitter""" - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala deleted file mode 100644 index bd9baec..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action - -object NotFoundPage: - def apply(homePage: Page, url: String, actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - ErrorPage( - ErrorPage.ViewModel( - homePage, - "404 error", - "Page not found.", - s"Sorry, but page $url doesn't exist." - ), - actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala deleted file mode 100644 index 476fe3c..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action - -object UnhandledErrorPage: - - case class ViewModel( - homePage: Page, - errorName: Option[String], - errorMessage: Option[String] - ) - - def apply(m: ViewModel, actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - ErrorPage( - ErrorPage.ViewModel( - m.homePage, - "Unexpected error occurred", - m.errorName.getOrElse( - "Uh oh!" - ), // TODO: translations, better text than uh oh - m.errorMessage.getOrElse( - "This wasn't supposed to happen! Please try again." - ) - ), - actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala deleted file mode 100644 index 25f059f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala +++ /dev/null @@ -1,6 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.services - -import com.raquo.airstream.core.Observer - -trait DataFetcher[K, A]: - def fetch(id: K, sink: Observer[A]): Unit diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala deleted file mode 100644 index a469830..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala +++ /dev/null @@ -1,120 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package state - -import com.raquo.airstream.core.EventStream -import cz.e_bs.cmi.mdr.pdb.{UserInfo, OsobniCislo} -import com.raquo.airstream.core.Observer -import scala.scalajs.js -import scala.scalajs.js.JSON -import zio.json._ -import com.raquo.airstream.eventbus.EventBus -import com.raquo.airstream.ownership.Owner -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.UserFunction -import cz.e_bs.cmi.mdr.pdb.UserContract -import fiftyforms.services.files.File - -trait AppState - extends connectors.DetailPageConnector.AppState - with connectors.DetailParametruPageConnector.AppState - with connectors.DetailKriteriaPageConnector.AppState - with pages.detail.UpravDukaz.State: - def users: EventStream[List[UserInfo]] - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - -class MockAppState(implicit owner: Owner, router: Router[Page]) - extends AppState: - - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonDecoder[UserFunction] = DeriveJsonDecoder.gen - given JsonDecoder[UserContract] = DeriveJsonDecoder.gen - given JsonDecoder[UserInfo] = DeriveJsonDecoder.gen - - given JsonDecoder[ParameterCriteria] = DeriveJsonDecoder.gen - given JsonDecoder[Parameter] = DeriveJsonDecoder.gen - - private val actions = EventBus[Action]() - private val (parametersStream, pushParameters) = - EventStream.withCallback[List[Parameter]] - private val (usersStream, pushUsers) = - EventStream.withCallback[List[UserInfo]] - private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] - private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] - - private val mockData: List[UserInfo] = - mockUsers - .asInstanceOf[js.Dictionary[js.Object]] - .values - // TODO: is there a more efficient way to parse from JS object directly? - .map { o => - val parsed = JSON.stringify(o).fromJson[UserInfo] - parsed.left.foreach(org.scalajs.dom.console.log(_)) - parsed - } - .collect { case Right(u) => - u - } - .toList - - private val mockParameters: List[Parameter] = - pdbParams - .asInstanceOf[js.Dictionary[js.Object]] - .values - .map(o => JSON.stringify(o).fromJson[Parameter]) - .collect { case Right(p) => p } - .toList - - // TODO: Extract to separate event handler - actions.events.foreach { - case FetchDirectory => pushUsers(mockData) - case FetchUserDetails(osc) => - mockData.find(_.personalNumber == osc).foreach { o => - pushDetails(o) - router.replaceState(Page.Detail(o)) - } - case FetchParameters(osc) => - pushParameters(mockParameters) - case FetchParameter(osc, paramId) => - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(Page.DetailParametru(o, p)) - case FetchParameterCriteria(osc, paramId, critId, page) => - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - c <- p.criteria.find(_.id == critId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(page(o, p, c)) - case NavigateTo(page) => router.pushState(page) - case FetchAvailableFiles(osc) => - pushFiles( - List( - File("https://tc163.cmi.cz/here", "Example file") - ) - ) - } - - override def users: EventStream[List[UserInfo]] = - usersStream.debugWithName("users") - - override def details: EventStream[UserInfo] = - detailsStream.debugWithName("details") - - override def parameters: EventStream[List[Parameter]] = - parametersStream.debugWithName("parameters") - - override def availableFiles: EventStream[List[File]] = - filesStream.debugWithName("available files") - - override def actionBus: Observer[Action] = - actions.writer.debugWithName("actions writer") diff --git a/app/src/main/scala/mdr/pdb/app/Main.scala b/app/src/main/scala/mdr/pdb/app/Main.scala new file mode 100644 index 0000000..081b4ee --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/Main.scala @@ -0,0 +1,103 @@ +package mdr.pdb.app + +import scala.scalajs.js.annotation.JSExportTopLevel +import scala.scalajs.js.annotation.JSExport +import scala.scalajs.js.annotation.JSImport +import scala.scalajs.js +import org.scalajs.dom +import com.raquo.laminar.api.L.{*, given} +import scala.scalajs.js.Date +import com.raquo.waypoint.Router +import com.raquo.waypoint.SplitRender +import mdr.pdb.app.services.DataFetcher +import scala.scalajs.js.JSON +import zio.json._ +import mdr.pdb.UserInfo +import mdr.pdb.app.state.AppState + +@js.native +@JSImport("stylesheets/main.css", JSImport.Namespace) +object Css extends js.Any + +@js.native +@JSImport("data/users.json", JSImport.Default) +object mockUsers extends js.Object + +@js.native +@JSImport("params/pdb-params.json", JSImport.Default) +object pdbParams extends js.Object + +@JSExportTopLevel("app") +object Main: + + @JSExport + def main(args: Array[String]): Unit = + import Routes.given + onLoad { + setupAirstream() + val appContainer = dom.document.querySelector("#app") + val _ = + render( + appContainer, + renderPage(state.MockAppState(using unsafeWindowOwner, router)) + ) + } + + private def onLoad(f: => Unit): Unit = + documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) + + private def setupAirstream()(using router: Router[Page]): Unit = + AirstreamError.registerUnhandledErrorCallback(err => + router.forcePage( + Page.UnhandledError( + Some(err.getClass.getName), // TODO: Fill only in dev mode + Some(err.getMessage) + ) + ) + ) + + def renderPage(state: AppState)(using + router: Router[Page] + ): HtmlElement = + val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) + .collectSignal[Page.Detail]( + connectors + .DetailPageConnector(state)(_) + .apply + ) + .collectSignal[Page.DetailParametru]( + connectors + .DetailParametruPageConnector(state)(_) + .apply + ) + .collectSignal[Page.DetailKriteria]( + connectors + .DetailKriteriaPageConnector(state)(_) + .apply + ) + .collectSignal[Page.UpravDukazKriteria]( + pages.detail.UpravDukaz.Connector(state)(_).apply + ) + .collectStatic(Page.Dashboard)( + connectors.DashboardPageConnector(state.actionBus).apply + ) + .collect[Page.NotFound](pg => + pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) + ) + .collect[Page.UnhandledError](pg => + pages.errors + .UnhandledErrorPage( + pages.errors.UnhandledErrorPage + .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), + state.actionBus + ) + ) + .collectStatic(Page.Directory)( + connectors + .DirectoryPageConnector(state.users, state.actionBus) + .apply + ) + div(cls := "h-full", child <-- pageSplitter.$view) + + // Pull in the stylesheet + val css: Css.type = Css diff --git a/app/src/main/scala/mdr/pdb/app/Routes.scala b/app/src/main/scala/mdr/pdb/app/Routes.scala new file mode 100644 index 0000000..2cc860c --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/Routes.scala @@ -0,0 +1,198 @@ +package mdr.pdb.app + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.* +import org.scalajs.dom +import zio.json.{*, given} +import mdr.pdb.OsobniCislo + +import scala.scalajs.js +import mdr.pdb.UserInfo +import mdr.pdb.Parameter +import mdr.pdb.ParameterCriteria +import mdr.pdb.app.Page.Titled + +// enum is not working with Waypoints' SplitRender collectStatic +sealed abstract class Page( + val id: String, + val title: String, + val parent: Option[Page] +) { + val path: Vector[Page] = + parent match + case None => Vector(this) + case Some(p) => p.path :+ this + + val isRoot: Boolean = parent.isEmpty +} + +object Page: + + case class Titled[V](value: V, title: Option[String] = None): + val show: String = title.getOrElse(value.toString) + + case object Directory extends Page("directory", "Adresář", None) + + case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) + + case class Detail(osobniCislo: Titled[OsobniCislo]) + extends Page("user", osobniCislo.show, Some(Directory)) + + object Detail { + def apply(o: UserInfo): Detail = Detail( + Titled(o.personalNumber, Some(o.name)) + ) + } + + case class DetailParametru( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String] + ) extends Page( + "parameter", + parametr.show, + Some(Detail(osobniCislo)) + ) + + object DetailParametru { + def apply(o: UserInfo, p: Parameter): DetailParametru = + DetailParametru( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)) + ) + } + + case class DetailKriteria( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String], + kriterium: Titled[String] + ) extends Page( + "criteria", + kriterium.show, + Some(DetailParametru(osobniCislo, parametr)) + ) + + object DetailKriteria { + def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = + DetailKriteria( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)), + Titled(k.id, Some(k.id)) + ) + } + + case class UpravDukazKriteria( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String], + kriterium: Titled[String] + ) extends Page( + "addProof", + "Důkaz", + Some(DetailKriteria(osobniCislo, parametr, kriterium)) + ) + + object UpravDukazKriteria { + def apply( + o: UserInfo, + p: Parameter, + k: ParameterCriteria + ): UpravDukazKriteria = + UpravDukazKriteria( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)), + Titled(k.id, Some(k.id)) + ) + } + + case class NotFound(url: String) extends Page("404", "404", Some(Directory)) + + case class UnhandledError( + errorName: Option[String], + errorMessage: Option[String] + ) extends Page("500", "Unexpected error", Some(Directory)) + +object Routes: + given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) + given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) + given [V: JsonEncoder]: JsonEncoder[Titled[V]] = + DeriveJsonEncoder.gen[Titled[V]] + given [V: JsonDecoder]: JsonDecoder[Titled[V]] = + DeriveJsonDecoder.gen[Titled[V]] + given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] + given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] + + val base = + js.`import`.meta.env.BASE_URL + .asInstanceOf[String] + "app" + + val homePage: Page = Page.Directory + + given router: Router[Page] = Router[Page]( + routes = List( + Route.static(homePage, root / endOfSegments, basePath = base), + Route.static( + Page.Dashboard, + root / "dashboard" / endOfSegments, + basePath = base + ), + Route[Page.Detail, String]( + encode = _.osobniCislo.value.toString, + decode = osc => Page.Detail(Titled(OsobniCislo(osc))), + root / "osoba" / segment[String] / endOfSegments, + basePath = base + ), + Route[Page.DetailParametru, (String, String)]( + encode = p => (p.osobniCislo.value.toString, p.parametr.value), + decode = + p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / endOfSegments, + basePath = base + ), + Route[Page.DetailKriteria, (String, String, String)]( + encode = p => + ( + p.osobniCislo.value.toString, + p.parametr.value, + p.kriterium.value.replaceAll("\\.", "--") + ), + decode = p => + Page.DetailKriteria( + Titled(OsobniCislo(p._1)), + Titled(p._2), + Titled(p._3.replaceAll("--", ".")) + ), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / "kriterium" / segment[String] / endOfSegments, + basePath = base + ), + Route[Page.UpravDukazKriteria, (String, String, String)]( + encode = p => + ( + p.osobniCislo.value.toString, + p.parametr.value, + p.kriterium.value.replaceAll("\\.", "--") + ), + decode = p => + Page.UpravDukazKriteria( + Titled(OsobniCislo(p._1)), + Titled(p._2), + Titled(p._3.replaceAll("--", ".")) + ), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / "kriterium" / segment[String] / "edit" / endOfSegments, + basePath = base + ) + ), + serializePage = _.toJson, + deserializePage = _.fromJson[Page] + .fold(s => throw IllegalStateException(s), identity), + getPageTitle = _.title, + routeFallback = url => Page.NotFound(url), + deserializeFallback = _ => Page.Dashboard + )( + $popStateEvent = windowEvents.onPopState, + owner = unsafeWindowOwner + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala deleted file mode 100644 index a3b00af..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala +++ /dev/null @@ -1,103 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import scala.scalajs.js.annotation.JSExportTopLevel -import scala.scalajs.js.annotation.JSExport -import scala.scalajs.js.annotation.JSImport -import scala.scalajs.js -import org.scalajs.dom -import com.raquo.laminar.api.L.{*, given} -import scala.scalajs.js.Date -import com.raquo.waypoint.Router -import com.raquo.waypoint.SplitRender -import cz.e_bs.cmi.mdr.pdb.app.services.DataFetcher -import scala.scalajs.js.JSON -import zio.json._ -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.state.AppState - -@js.native -@JSImport("stylesheets/main.css", JSImport.Namespace) -object Css extends js.Any - -@js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native -@JSImport("params/pdb-params.json", JSImport.Default) -object pdbParams extends js.Object - -@JSExportTopLevel("app") -object Main: - - @JSExport - def main(args: Array[String]): Unit = - import Routes.given - onLoad { - setupAirstream() - val appContainer = dom.document.querySelector("#app") - val _ = - render( - appContainer, - renderPage(state.MockAppState(using unsafeWindowOwner, router)) - ) - } - - private def onLoad(f: => Unit): Unit = - documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) - - private def setupAirstream()(using router: Router[Page]): Unit = - AirstreamError.registerUnhandledErrorCallback(err => - router.forcePage( - Page.UnhandledError( - Some(err.getClass.getName), // TODO: Fill only in dev mode - Some(err.getMessage) - ) - ) - ) - - def renderPage(state: AppState)(using - router: Router[Page] - ): HtmlElement = - val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) - .collectSignal[Page.Detail]( - connectors - .DetailPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailParametru]( - connectors - .DetailParametruPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailKriteria]( - connectors - .DetailKriteriaPageConnector(state)(_) - .apply - ) - .collectSignal[Page.UpravDukazKriteria]( - pages.detail.UpravDukaz.Connector(state)(_).apply - ) - .collectStatic(Page.Dashboard)( - connectors.DashboardPageConnector(state.actionBus).apply - ) - .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) - ) - .collect[Page.UnhandledError](pg => - pages.errors - .UnhandledErrorPage( - pages.errors.UnhandledErrorPage - .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), - state.actionBus - ) - ) - .collectStatic(Page.Directory)( - connectors - .DirectoryPageConnector(state.users, state.actionBus) - .apply - ) - div(cls := "h-full", child <-- pageSplitter.$view) - - // Pull in the stylesheet - val css: Css.type = Css diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala deleted file mode 100644 index 3bbc8c0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ /dev/null @@ -1,198 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.* -import org.scalajs.dom -import zio.json.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import scala.scalajs.js -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.Page.Titled - -// enum is not working with Waypoints' SplitRender collectStatic -sealed abstract class Page( - val id: String, - val title: String, - val parent: Option[Page] -) { - val path: Vector[Page] = - parent match - case None => Vector(this) - case Some(p) => p.path :+ this - - val isRoot: Boolean = parent.isEmpty -} - -object Page: - - case class Titled[V](value: V, title: Option[String] = None): - val show: String = title.getOrElse(value.toString) - - case object Directory extends Page("directory", "Adresář", None) - - case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) - - case class Detail(osobniCislo: Titled[OsobniCislo]) - extends Page("user", osobniCislo.show, Some(Directory)) - - object Detail { - def apply(o: UserInfo): Detail = Detail( - Titled(o.personalNumber, Some(o.name)) - ) - } - - case class DetailParametru( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String] - ) extends Page( - "parameter", - parametr.show, - Some(Detail(osobniCislo)) - ) - - object DetailParametru { - def apply(o: UserInfo, p: Parameter): DetailParametru = - DetailParametru( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)) - ) - } - - case class DetailKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "criteria", - kriterium.show, - Some(DetailParametru(osobniCislo, parametr)) - ) - - object DetailKriteria { - def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = - DetailKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class UpravDukazKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "addProof", - "Důkaz", - Some(DetailKriteria(osobniCislo, parametr, kriterium)) - ) - - object UpravDukazKriteria { - def apply( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): UpravDukazKriteria = - UpravDukazKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class NotFound(url: String) extends Page("404", "404", Some(Directory)) - - case class UnhandledError( - errorName: Option[String], - errorMessage: Option[String] - ) extends Page("500", "Unexpected error", Some(Directory)) - -object Routes: - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) - given [V: JsonEncoder]: JsonEncoder[Titled[V]] = - DeriveJsonEncoder.gen[Titled[V]] - given [V: JsonDecoder]: JsonDecoder[Titled[V]] = - DeriveJsonDecoder.gen[Titled[V]] - given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] - given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] - - val base = - js.`import`.meta.env.BASE_URL - .asInstanceOf[String] + "app" - - val homePage: Page = Page.Directory - - given router: Router[Page] = Router[Page]( - routes = List( - Route.static(homePage, root / endOfSegments, basePath = base), - Route.static( - Page.Dashboard, - root / "dashboard" / endOfSegments, - basePath = base - ), - Route[Page.Detail, String]( - encode = _.osobniCislo.value.toString, - decode = osc => Page.Detail(Titled(OsobniCislo(osc))), - root / "osoba" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.DetailParametru, (String, String)]( - encode = p => (p.osobniCislo.value.toString, p.parametr.value), - decode = - p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / endOfSegments, - basePath = base - ), - Route[Page.DetailKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.DetailKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.UpravDukazKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.UpravDukazKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / "edit" / endOfSegments, - basePath = base - ) - ), - serializePage = _.toJson, - deserializePage = _.fromJson[Page] - .fold(s => throw IllegalStateException(s), identity), - getPageTitle = _.title, - routeFallback = url => Page.NotFound(url), - deserializeFallback = _ => Page.Dashboard - )( - $popStateEvent = windowEvents.onPopState, - owner = unsafeWindowOwner - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala deleted file mode 100644 index b24b82b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -sealed trait Action - -case object FetchDirectory extends Action -case class FetchUserDetails(osc: OsobniCislo) extends Action -case class FetchParameters(osc: OsobniCislo) extends Action -case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action -case class FetchParameterCriteria( - osc: OsobniCislo, - paramId: String, - critId: String, - page: (UserInfo, Parameter, ParameterCriteria) => Page -) extends Action -case class FetchAvailableFiles(osc: OsobniCislo) extends Action -case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala deleted file mode 100644 index c637d35..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala +++ /dev/null @@ -1,82 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.{UserProfile, UserInfo, OsobniCislo} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo -import cz.e_bs.cmi.mdr.pdb.UserFunction - -object AppPage: - // TODO: pages by logged in user - val pages: List[Page] = List(Page.Directory, Page.Dashboard) - - import NavigationBar.{Logo, MenuItem} - - val logo = Logo( - "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", - "Workflow" - ) - - // TODO: menu items by user profile - val userMenu = - List( - MenuItem("Your Profile"), - MenuItem("Settings"), - MenuItem("Sign out") - ) - - // TODO: load user profile - val $userProfile = Var( - UserProfile( - "tom", - UserInfo( - OsobniCislo("1031"), - "tom", - "Tom", - "Cook", - None, - None, - Some("tom@example.com"), - Some("+420 222 866 180"), - Some( - UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") - ), - Nil, - None - ) - ) - ) - - val $userInfo = $userProfile.signal.map(_.userInfo) - - type ViewModel = Option[HtmlElement] - def apply( - actionBus: Observer[Action] - )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using - router: Router[Page] - ): HtmlElement = - PageLayout(actionBus)( - $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => - PageLayout.ViewModel( - NavigationBar.ViewModel( - u, - pages.map(p => - NavigationBar.Link( - () => - PageLink( - p, - actionBus - ), - p == cp - ) - ), - userMenu, - logo - ), - c - ) - ), - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala deleted file mode 100644 index bf473c6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ /dev/null @@ -1,143 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden -import fiftyforms.ui.components.tailwind.list.IconText.ViewModel -import fiftyforms.ui.components.tailwind.Icons -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Action - -object Breadcrumbs: - - private def slash = - import svg.{*, given} - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-300", - xmlns := "http://www.w3.org/2000/svg", - fill := "currentColor", - viewBox := "0 0 20 20", - ariaHidden := true, - path( - d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" - ) - ) - - def backIcon = - Icons.solid - .`arrow-narrow-left`() - .amend( - svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" - ) - - object Link: - case class ViewModel( - page: Page, - icon: Option[SvgElement], - extraClasses: String, - text: String, - textClasses: Option[String] = None - ) - - val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" - - def shortHome(p: Page) = ViewModel( - p, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - "Zpět na úvodní stránku", - None - ) - - def fullHome(p: Page) = ViewModel( - p, - Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), - "text-gray-400 hover:text-gray-500", - "Domů", - Some("sr-only") - ) - - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - PageLink - .container($m.map(_.page), actionBus) - .amend( - cls <-- $m.map(_.extraClasses), - child.maybe <-- $m.map(_.icon), - span( - cls <-- $m.map(_.textClasses.getOrElse("")), - child.text <-- $m.map(_.text) - ) - ) - - object FullBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - ol( - role := "list", - cls := "flex items-center space-x-4", - children <-- $m.map(_.path).split(_.id)((_, _, $p) => - li( - div( - cls := "flex items-center", - child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), - Link( - $p.map(p => - if (p.isRoot) then Link.fullHome(p) - else - Link.ViewModel( - p, - None, - s"ml-4 max-w-xs truncate ${Link.baseClasses}", - p.title - ) - ), - actionBus - ) - ) - ) - ) - ) - - object ShortBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - Link( - $m.map { p => - val target = p.parent.getOrElse(p) - if target.isRoot then Link.shortHome(target) - else - Link.ViewModel( - target, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - s"Zpět na ${target.title}" - ) - }, - actionBus - ) - - def apply(actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - nav( - cls := "flex", - aria.label := "Breadcrumb", - div( - cls := "flex sm:hidden", - ShortBreadcrumbs(router.$currentPage, actionBus) - ), - div( - cls := "hidden sm:block", - FullBreadcrumbs(router.$currentPage, actionBus) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala deleted file mode 100644 index 0db2047..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ /dev/null @@ -1,242 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent -import fiftyforms.ui.components.tailwind.Icons -import fiftyforms.ui.components.tailwind.Avatar -import cz.e_bs.cmi.mdr.pdb.UserInfo -import io.laminext.syntax.core.* - -object NavigationBar: - - case class Logo(img: String, name: String) - case class Link(a: () => Anchor, active: Boolean) - case class MenuItem(title: String) - - case class ViewModel( - userInfo: UserInfo, - pages: List[Link], - userMenu: List[MenuItem], - logo: Logo - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - val $userInfo = $m.map(_.userInfo) - val mobileMenuOpen = Var(false) - - val desktopOnly = cls("hidden md:block") - val mobileOnly = cls("md:hidden") - - inline def avatarImage(size: Int = 8) = - Avatar($userInfo.map(_.img)).avatarImage(size) - - def notificationButton = button( - tpe := "button", - cls := List( - "bg-indigo-600", - "focus:outline-none", - "focus:ring-2", - "focus:ring-offset-2", - "focus:ring-offset-indigo-600", - "focus:ring-white", - "hover:text-white", - "p-1", - "rounded-full", - "text-indigo-200" - ), - span(cls := "sr-only", "View notifications"), - Icons.outline.bell() - ) - - def userProfile: HtmlElement = - val menuOpen = Var(false) - - def menuItem(item: MenuItem, idx: Int): HtmlElement = - a( - href := "#", - cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", - role := "menuitem", - tabIndex := -1, - idAttr := s"user-menu-item-$idx", - item.title - ) - - div( - cls := "ml-3 relative", - div( - button( - tpe := "button", - cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - idAttr := "user-menu-button", - aria.expanded <-- menuOpen.signal, - aria.hasPopup := true, - span(cls := "sr-only", "Open user menu"), - child <-- avatarImage(), - onClick.preventDefault.mapTo( - !menuOpen.now() - ) --> menuOpen.writer - ) - ), - /* - * */ - div( - cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", - cls <-- menuOpen.signal.map { o => - if (o) "md:block" else "md:hidden" - }, - role := "menu", - aria.orientation := "vertical", - aria.labelledBy := "user-menu-button", - tabIndex := -1, - // : keyboard navigation - children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) - ) - ) - - def mobileProfile = - def menuItem(item: MenuItem): HtmlElement = - a( - href := "#", - cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", - item.title - ) - - div( - cls := "pt-4 pb-3 border-t border-indigo-700", - div( - cls := "flex items-center px-5", - div( - cls := "flex-shrink-0", - child <-- avatarImage(10) - ), - div( - cls := "ml-3", - div( - cls := "text-base font-medium text-white", - child.text <-- $userInfo.map(_.name) - ), - child.maybe <-- $userInfo.map( - _.email.map(e => - div( - cls := "text-sm font-medium text-indigo-300", - e - ) - ) - ) - ), - notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) - ), - div( - cls := "mt-3 px-2 space-y-1", - children <-- $m.map(_.userMenu.map(menuItem)) - ) - ) - - def pageLink(page: Link): Anchor = - page - .a() - .amend( - cls := "text-white px-3 py-2 rounded-md text-sm font-medium", - cls := Seq( - "bg-indigo-700" -> page.active, - "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active - ), - ariaCurrent := (if page.active then "page" else "false") - ) - - def logoImg: Image = - img( - cls := "h-8 w-8", - src <-- $m.map(_.logo.img), - alt <-- $m.map(_.logo.name) - ) - - def mobileMenuButton = button( - tpe := "button", - cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - aria.controls := "mobile-menu", - aria.expanded <-- mobileMenuOpen.signal, - span(cls := "sr-only", "Open main menu"), - Icons.outline - .menu() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") - ), - Icons.outline - .x() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") - ), - onClick.preventDefault.mapTo( - !mobileMenuOpen.now() - ) --> mobileMenuOpen.writer - ) - - def navBarLeft = - div( - cls := "flex items-center", - div(cls := "flex-shrink-0", logoImg), - div( - desktopOnly, - div( - cls := "ml-10 flex items-baseline space-x-4", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ) - ) - ) - - def navBarRight = - div( - desktopOnly, - div( - cls := "ml-4 flex items-center md:ml-6", - notificationButton, - userProfile - ) - ) - - def navBarMobile = - div( - cls := "-mr-2 flex", - mobileOnly, - mobileMenuButton - ) - - def navBar = - div( - cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex items-center justify-between h-16", - navBarLeft, - navBarRight, - navBarMobile - ) - ) - - def mobileMenu = - div( - mobileOnly, - cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }, - idAttr := "mobile-menu", - div( - cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ), - mobileProfile - ) - - nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala deleted file mode 100644 index 75339df..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object PageHeader: - def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = - header( - cls := "bg-white shadow-sm", - div( - cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", - h1( - cls := "text-lg leading-6 font-semibold text-gray-900", - Breadcrumbs(actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala deleted file mode 100644 index 5096463..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ /dev/null @@ -1,30 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import fiftyforms.ui.components.tailwind.Loading -import io.laminext.syntax.core.* - -object PageLayout: - case class ViewModel( - navigation: NavigationBar.ViewModel, - content: Option[HtmlElement] - ) - def apply(actionBus: Observer[Action])( - $m: Signal[ViewModel], - mods: Modifier[HtmlElement]* - )(using router: Router[Page]): HtmlElement = - val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) - div( - cls := "h-full flex flex-col", - NavigationBar($m.map(_.navigation)), - child.maybe <-- router.$currentPage.map(_.isRoot) - .switch(None, Some(PageHeader(actionBus))), - main( - cls := "flex-grow-1 overflow-y-auto", - mods, - child <-- $maybeContent.map(_.getOrElse(Loading)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala deleted file mode 100644 index 62d820f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.LinkSupport.* -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo - -object PageLink: - type ViewModel = Page - def apply($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = - a(mods($m, actions), child.text <-- $m.map(_.title)) - - def apply(m: ViewModel, actions: Observer[Action])(using - router: Router[Page] - ): Anchor = apply(Val(m), actions) - - def container($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = a(mods($m, actions)) - - def container(m: ViewModel, actions: Observer[Action])(using - Router[Page] - ): Anchor = container(Val(m), actions) - - private def mods($m: Signal[Page], actions: Observer[Action])(using - router: Router[Page] - ): Modifier[Anchor] = - Seq( - href <-- $m.map(router.absoluteUrlForPage).recover { case _ => - Some("invalid url") - }, - composeEvents(onClick.noKeyMod.preventDefault)( - _.sample($m) - .map(NavigateTo.apply) - ) --> actions - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala deleted file mode 100644 index 87f825a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.pages.dashboard.DashboardPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -class DashboardPageConnector(actionBus: Observer[Action])(using - router: Router[Page] -): - def apply: HtmlElement = - AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala deleted file mode 100644 index e9b0f71..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala +++ /dev/null @@ -1,76 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailKriteriaPage -import pages.detail.components.DukazyKriteria -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -object DetailKriteriaPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailKriteriaPageConnector( - state: DetailKriteriaPageConnector.AppState -)( - $page: Signal[Page.DetailKriteria] -)(using Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => - (p.osobniCislo.value, p.parametr.value, p.kriterium.value) - )((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map( - FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) - ) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, s, $s) => - DetailKriteriaPage($s)(state.actionBus.contramap { - case DukazyKriteria.Add => - NavigateTo( - Page.UpravDukazKriteria( - Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), - Page.Titled(s.parametr.id, Some(s.parametr.nazev)), - Page.Titled(s.kriterium.id, Some(s.kriterium.id)) - ) - ) - }) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): DetailKriteriaPage.ViewModel = - DetailKriteriaPage.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala deleted file mode 100644 index 4d8024f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ /dev/null @@ -1,57 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import pages.detail.DetailPage -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import fiftyforms.ui.components.tailwind.Color - -object DetailPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailPageConnector(state: DetailPageConnector.AppState)( - $page: Signal[Page.Detail] -)(using router: Router[Page]): - val $oscChangeSignal = $page.splitOne(_.osobniCislo.value)((osc, _, _) => osc) - val $pageChangeSignal = - $oscChangeSignal.flatMap(osc => - EventStream.fromSeq(Seq(FetchUserDetails(osc), FetchParameters(osc))) - ) - // TODO: filter the value based on the current osc - // OSC change will fetch new data, but still - // - we need to be sure that what we got is really what we ought to display - // - we want to display stale data accordingly (at least with loading indicator) - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - def apply: HtmlElement = - AppPage(state.actionBus)( - $data.combineWithFn($params)(_ zip _) - .map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: List[Parameter] - ): DetailPage.ViewModel = - DetailPage.ViewModel( - o.toDetailOsoby, - p.map( - _.toParametr(param => - PageLink.container(Page.DetailParametru(o, param), state.actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala deleted file mode 100644 index fdaa7f8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailParametruPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink - -object DetailParametruPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailParametruPageConnector( - state: DetailParametruPageConnector.AppState -)( - $page: Signal[Page.DetailParametru] -)(using router: Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => (p.osobniCislo.value, p.parametr.value))((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map(FetchParameter(_, _)) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - } yield (da, pb) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailParametruPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter - ): DetailParametruPage.ViewModel = - DetailParametruPage.ViewModel( - o.toDetailOsoby, - p.toParametr(p => - PageLink.container(Page.DetailParametru(o, p), state.actionBus) - ), - p.criteria.map( - _.toKriterium { c => - PageLink.container( - Page.DetailKriteria(o, p, c), - state.actionBus - ) - } - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala deleted file mode 100644 index 049aa22..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -case class DirectoryPageConnector( - $input: EventStream[List[UserInfo]], - actionBus: Observer[Action] -)(using router: Router[Page]): - val $data = $input.startWithNone - val $actionSignal = EventStream.fromValue(FetchDirectory) - - def apply: HtmlElement = - AppPage(actionBus)( - $data.split(_ => ())((_, _, s) => - pages.directory.DirectoryPage( - s.map( - _.map( - _.toUserRow(u => - PageLink.container( - Page.Detail(Page.Titled(u.personalNumber)), - actionBus - ) - ) - ) - ) - ) - ), - $actionSignal --> actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala deleted file mode 100644 index 88e7450..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ /dev/null @@ -1,68 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii -import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria -import fiftyforms.ui.components.tailwind.Color - -extension (o: UserInfo) - def toDetailOsoby: DetailOsoby.ViewModel = - DetailOsoby.ViewModel( - o.personalNumber, - o.name, - o.email, - o.phone, - o.img, - o.mainFunction.map(f => - DetailOsoby.Funkce.ViewModel(f.name, f.dept, f.ou) - ), - o.userContracts.headOption.map(c => - DetailOsoby.PracovniPomer.ViewModel(c.rel, c.startDate, c.endDate) - ) - ) - -extension (param: Parameter) - def toParametr(container: Parameter => Anchor): DetailParametru.ViewModel = - DetailParametru.ViewModel( - id = param.id, - nazev = param.name, - popis = param.description, - status = "Nesplněno", - statusColor = Color.red, - a = container(param) - ) - -extension (crit: ParameterCriteria) - def toKriterium( - container: ParameterCriteria => Anchor - ): DetailKriteria.ViewModel = - DetailKriteria.ViewModel( - nazev = crit.criteriumText, - kapitola = crit.chapterId, - bod = crit.itemId, - status = "Nesplněno", - statusColor = Color.red, - splneno = false, - dukazy = Nil, - container = container(crit) - ) - -extension (user: UserInfo) - def toUserRow( - container: UserInfo => HtmlElement = (_: UserInfo) => div() - ): UserRow.ViewModel = - UserRow.ViewModel( - osobniCislo = user.personalNumber.toString, - celeJmeno = user.name, - prijmeni = user.surname, - hlavniFunkce = user.mainFunction.map(_.name), - img = user.img, - container = () => container(user) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala deleted file mode 100644 index 7d5b2c1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.dashboard - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.Page - -object DashboardPage: - - def render: HtmlElement = - div("Dashboard page") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala deleted file mode 100644 index 9ef0698..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala +++ /dev/null @@ -1,31 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailKriteriaPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply($m: Signal[ViewModel])( - action: Observer[DukazyKriteria.Action] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - DukazyKriteria($m.map(_.kriterium.dukazy))(action) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala deleted file mode 100644 index 3b316e1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object DetailPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametry: SeznamParametru.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby($m.map(_.osoba)), - SeznamParametru($m.map(_.parametry)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala deleted file mode 100644 index 17224cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala +++ /dev/null @@ -1,24 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailParametruPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriteria: SeznamKriterii.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby.header($m.map(_.osoba)), - DetailParametru($m.map(_.parametr)), - SeznamKriterii($m.map(_.kriteria)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala deleted file mode 100644 index dc912fa..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ /dev/null @@ -1,107 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb -package app -package pages.detail - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.UpravDukazForm -import fiftyforms.services.files.File - -object UpravDukaz: - - type ThisPage = Page.UpravDukazKriteria - type PageKey = (OsobniCislo, String, String) - - trait State { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def availableFiles: EventStream[List[File]] - def actionBus: Observer[Action] - } - - def keyOfPage(page: ThisPage): PageKey = - (page.osobniCislo.value, page.parametr.value, page.kriterium.value) - - def onChangeAction(key: PageKey): Action = - FetchParameterCriteria( - key._1, - key._2, - key._3, - Page.UpravDukazKriteria(_, _, _) - ) - - class Connector(state: State)($page: Signal[ThisPage])(using Router[Page]): - val $paramChangeSignal = $page.splitOne(keyOfPage)((x, _, _) => x) - val $pageChangeSignal = $paramChangeSignal.map(onChangeAction) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.split(_ => ())((_, s, $s) => - PageComponent( - $s.map(buildModel), - state.availableFiles, - state.actionBus.contramap { - case UpravDukazForm.Cancelled => - NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) - case UpravDukazForm.AvailableFilesRequested => - FetchAvailableFiles(s._1.personalNumber) - } - ) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): PageComponent.ViewModel = - import connectors.* - PageComponent.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) - - object PageComponent: - import components.* - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply( - $m: Signal[ViewModel], - availableFilesStream: EventStream[List[File]], - events: Observer[UpravDukazForm.Event] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - UpravDukazForm(availableFilesStream)(events) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala deleted file mode 100644 index ca311b0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.Icons -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Color - -object DetailKriteria: - case class ViewModel( - nazev: String, - kapitola: String, - bod: String, - status: String, - statusColor: Color, - splneno: Boolean, - dukazy: List[DukazKriteria.ViewModel], - container: HtmlElement = div() - ) { - val id = s"${kapitola}${bod}" - } - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h3( - cls := "text-xl leading-6 font-bold text-gray-900", - child.text <-- $m.map(_.nazev) - ), - h3( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - "Kapitola ", - child.text <-- $m.map(_.kapitola), - " bod ", - child <-- $m.map(_.bod) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala deleted file mode 100644 index ae90290..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala +++ /dev/null @@ -1,100 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Avatar - -object DetailOsoby: - - object Funkce: - case class ViewModel( - nazev: String, - stredisko: String, - voj: String - ) - - def render($m: Signal[ViewModel]): HtmlElement = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.nazev), - span( - cls := "hidden md:inline", - " @ ", - child.text <-- $m.map(_.stredisko), - ", ", - child.text <-- $m.map(_.voj) - ) - ) - - object PracovniPomer: - case class ViewModel( - druh: String, - pocatek: LocalDate, - konec: Option[LocalDate] - ) - - def render($m: Signal[ViewModel]): HtmlElement = - import fiftyforms.ui.components.tailwind.CustomAttrs.datetime - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.druh), - " od ", - time( - datetime <-- $m.map(_.pocatek.toString), - child.text <-- $m.map(_.pocatek.toString) - ) - ) - - case class ViewModel( - osobniCislo: OsobniCislo, - jmeno: String, - email: Option[String], - telefon: Option[String], - img: Option[String], - hlavniFunkce: Option[Funkce.ViewModel], - pracovniPomer: Option[PracovniPomer.ViewModel] - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-4", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(16) - ), - div( - h1( - cls := "text-2xl font-bold text-gray-900", - child.text <-- $m.map(_.jmeno) - ), - child.maybe <-- $m.map(_.hlavniFunkce).split(_ => ())((_, _, d) => - Funkce.render(d) - ), - child.maybe <-- $m.map(_.pracovniPomer).split(_ => ())((_, _, d) => - PracovniPomer.render(d) - ) - ) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-5", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(8) - ), - div( - h1( - cls := "text-lg font-medium text-gray-800", - child.text <-- $m.map(_.jmeno) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala deleted file mode 100644 index e11cb62..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color - -object DetailParametru: - case class ViewModel( - id: String, - nazev: String, - popis: String, - status: String, - statusColor: Color, - a: Anchor - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h2( - cls := "text-2xl leading-6 font-medium text-gray-900", - child.text <-- $m.map(_.nazev) - ), - p( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - child.text <-- $m.map(_.popis) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - h2( - cls := "text-lg leading-6 font-medium text-gray-600", - child.text <-- $m.map(_.nazev) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala deleted file mode 100644 index a00ede8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala +++ /dev/null @@ -1,177 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.CustomAttrs - -object DukazKriteria: - case class Osoba(osobniCislo: String, jmeno: String) - case class Udalost(osoba: Osoba, datum: LocalDate) - case class Dokument( - url: String, - nazev: String, - pridal: Udalost, - odebral: Option[Udalost] - ) - case class ViewModel( - dokumenty: List[Dokument], - autorizoval: Option[Udalost], - platiDo: Option[LocalDate], - poznámka: Option[String] - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-lg", - div( - cls := "px-4 py-5 sm:px-6", - h3( - cls := "text-lg leading-6 font-medium text-gray-900", - """Applicant Information""" - ), - p( - cls := "mt-1 max-w-2xl text-sm text-gray-500", - """Personal details and application.""" - ) - ), - div( - cls := "border-t border-gray-200 px-4 py-5 sm:px-6", - dl( - cls := "grid grid-cols-1 gap-x-4 gap-y-8 sm:grid-cols-2", - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Full name""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Margot Foster""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Application for""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Backend Developer""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Email address""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """margotfoster@example.com""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Salary expectation""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """$120,000""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """About""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Fugiat ipsum ipsum deserunt culpa aute sint do nostrud anim incididunt cillum culpa consequat. Excepteur qui ipsum aliquip consequat sint. Sit id mollit nulla mollit nostrud in ea officia proident. Irure nostrud pariatur mollit ad adipisicing reprehenderit deserunt qui eu.""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """Attachments""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - ul( - role := "list", - cls := "border border-gray-200 rounded-md divide-y divide-gray-200", - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """resume_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ), - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """coverletter_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala deleted file mode 100644 index 22f134f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala +++ /dev/null @@ -1,35 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object DukazyKriteria: - sealed trait Action - case object Add extends Action - - type ViewModel = List[DukazKriteria.ViewModel] - - def apply($m: Signal[ViewModel])(actions: Observer[Action]): HtmlElement = - div( - child.maybe <-- $m.map(m => - if (m.isEmpty) then Some(prazdnyDukaz(actions)) - else None - ), - children <-- $m.map(_.zipWithIndex).split(_._2)((_, _, $s) => - DukazKriteria($s.map(_._1)) - ) - ) - - private def prazdnyDukaz(actions: Observer[Action]): HtmlElement = - button( - tpe := "button", - onClick.preventDefault.mapTo(Add) --> actions, - cls := "relative block w-full border-2 border-gray-300 border-dashed rounded-lg p-12 text-center hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - Icons.outline - .`document-add`(12) - .amend(svg.cls := "mx-auto text-gray-400"), - span( - cls := "mt-2 block text-sm font-medium text-gray-900", - "Přidat důkaz" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala deleted file mode 100644 index f628697..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.list.StackedList -import fiftyforms.ui.components.tailwind.list.ListRow -import fiftyforms.ui.components.tailwind.list.RowTag -import fiftyforms.ui.components.tailwind.list.RowNext -import java.time.LocalDate - -object SeznamKriterii: - type ViewModel = List[DetailKriteria.ViewModel] - - private val kritList = new StackedList[DetailKriteria.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - kritList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = - p(cls := "text-sm text-gray-500", s"${i.kapitola}${i.bod}"), - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.container - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala deleted file mode 100644 index 2bfe4bc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.list.{ - StackedList, - ListRow, - RowTag, - PropList, - IconText, - RowNext -} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.LinkSupport.* - -object SeznamParametru: - type ViewModel = List[DetailParametru.ViewModel] - - private val parametrList = new StackedList[DetailParametru.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - parametrList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = emptyNode, - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.a - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala deleted file mode 100644 index f048e9d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ /dev/null @@ -1,101 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.form.* -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement -import fiftyforms.services.files.components.tailwind.FilePicker -import cz.e_bs.cmi.mdr.pdb.frontend.AutorizujDukaz -import cz.e_bs.cmi.mdr.pdb.frontend.DocumentRef -import fiftyforms.services.files.File - -object UpravDukazForm: - sealed trait Event - case object Cancelled extends Event - case object AvailableFilesRequested extends Event - def apply(availableFilesStream: EventStream[List[File]])( - updates: Observer[Event] - ): HtmlElement = - val (filesStream, filesObserver) = - EventStream.withObserver[FilePicker.Event] - val files = Var[List[File]](Nil) - def submitButtons: HtmlElement = - div( - cls := "pt-5", - div( - cls := "flex justify-end", - button( - tpe := "button", - cls := "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zrušit", - onClick.mapTo(Cancelled) --> updates - ), - button( - tpe := "submit", - cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - disabled <-- files.signal.map(_.isEmpty), - "Autorizovat" - ) - ) - ) - - div( - filesStream.collect { case FilePicker.SelectionUpdated(files) => - files.to(List) - } --> files.writer, - filesStream.collect { case FilePicker.AvailableFilesRequested => - AvailableFilesRequested - } --> updates, - cls := "bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6", - Form( - Form.Body( - Form.Section( - FormHeader( - FormHeader.ViewModel( - "Doložení kritéria", - "Sestavte doklady poskytující důkaz kritéria a potvrďte odesláním formuláře. Případné limitace či jiné relevantní údaje vepište do pole Komentář." - ) - ), - FormFields( - FormRow( - "dokumenty", - "Dokumenty", - FilePicker(files.signal, availableFilesStream)(filesObserver) - .amend(idAttr := "dokumenty", cls("max-w-lg")) - ).toHtml, - FormRow( - "platnost", - "Platnost", - input( - idAttr := "platnost", - name := "platnost", - tpe := "date", - autoComplete := "date", - cls := "block max-w-lg w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" - ) - ).toHtml, - FormRow( - "komentar", - "Komentář", - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - textArea( - idAttr := "komentar", - name := "about", - rows := 3, - cls := "max-w-lg shadow-sm block w-full focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border border-gray-300 rounded-md" - ), - p( - cls := "mt-2 text-sm text-gray-500", - "Doplňte prosím potřebné informace související s doložením kritéria, včetně případných limitací." - ) - ) - ).toHtml - ) - ) - ), - submitButtons - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala deleted file mode 100644 index 41033f6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -import components._ - -object DirectoryPage: - - type ViewModel = List[UserRow.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - val (actionsStream, actionObserver) = - EventStream.withObserver[SearchForm.Action] - val $filter = actionsStream - .collect { - case SearchForm.ClearFilter => None - case SearchForm.SetFilter(t) => Some(t) - } - .startWith(None) - val byLetter = for { - d <- $m - f <- $filter - } yield for { - (letter, users) <- d - .filter { user => - f match - case None => true - case Some(t) => user.search.contains(t) - } - .groupBy(_.prijmeni.head) - .to(List) - .sortBy(_._1) - } yield (letter.toString, users.sortBy(_.prijmeni)) - - div( - cls := "h-full max-w-7xl mx-auto order-first flex flex-col flex-shrink-0", - SearchForm(actionObserver), - Directory(byLetter) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala deleted file mode 100644 index 73f915d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -object Directory: - - object Header: - type ViewModel = String - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500", - h3(child.text <-- $m) - ) - - object UserList: - type ViewModel = List[UserRow.ViewModel] - def apply($m: Signal[ViewModel]): HtmlElement = - ul( - role := "list", - cls := "relative z-0 divide-y divide-gray-200", - children <-- $m.split(_.osobniCislo)((_, _, s) => UserRow(s)) - ) - - type ViewModel = List[(String, List[UserRow.ViewModel])] - def apply($m: Signal[ViewModel]): HtmlElement = - nav( - cls := "flex-1 min-h-0 overflow-y-auto", - aria.label := "Directory", - children <-- $m.split(_._1)((_, _, s) => - div( - cls := "relative", - Header(s.map(_._1)), - UserList(s.map(_._2)) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala deleted file mode 100644 index e182910..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala +++ /dev/null @@ -1,54 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object SearchForm: - sealed trait Action - case object SubmitFilter extends Action - sealed trait FilterAction extends Action - case object ClearFilter extends FilterAction - case class SetFilter(value: String) extends FilterAction - - def apply(actions: Observer[Action]): HtmlElement = - div( - cls := "px-6 pt-4 pb-4", - form( - cls := "flex space-x-4", - onSubmit.mapTo(SubmitFilter) --> actions, - div( - cls := "flex-1 min-w-0", - label( - forId := "search", - cls := "sr-only", - "Hledat" - ), - div( - cls := "relative rounded-md shadow-sm", - div( - cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", - Icons.solid.search().amend(svg.cls := "text-gray-400") - ), - input( - tpe := "search", - name := "search", - idAttr := "search", - cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", - placeholder := "Hledat", - composeEvents(onInput.mapToValue.setAsValue.map(SetFilter(_)))( - _.throttle(500) - ) --> actions - ) - ) - ), - button( - tpe := "submit", - cls := "inline-flex justify-center px-3.5 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500", - Icons.solid.filter().amend(svg.cls := "text-gray-400"), - span( - cls := "sr-only", - "Hledat" - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala deleted file mode 100644 index 82f7723..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala +++ /dev/null @@ -1,52 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.airstream.core.Signal -import fiftyforms.ui.components.tailwind.Avatar - -object UserRow: - case class ViewModel( - osobniCislo: String, - celeJmeno: String, - prijmeni: String, - hlavniFunkce: Option[String], - img: Option[String], - container: () => HtmlElement = () => div() - ) { - val search = prijmeni.toLowerCase - } - - def apply($m: Signal[ViewModel]): HtmlElement = - inline def avatarImage = - Avatar($m.map(_.img)).avatarImage(10) - - li( - div( - cls := "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500", - div( - cls := "flex-shrink-0", - child <-- avatarImage - ), - div( - cls := "flex-1 min-w-0", - child <-- $m.map { o => - o.container() - .amend( - cls := "focus:outline-none", - span( - cls := "absolute inset-0", - aria.hidden := true - ), - p( - cls := "text-sm font-medium text-gray-900", - o.celeJmeno - ), - p( - cls := "text-sm text-gray-500 truncate", - o.hlavniFunkce - ) - ) - } - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala deleted file mode 100644 index 467c5f5..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala +++ /dev/null @@ -1,99 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router - -object ErrorPage: - case class ViewModel( - homePage: Page, - errorName: String, - title: String, - subTitle: String - ) - - def apply(m: ViewModel, actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - val ViewModel(homePage, errorName, title, subTitle) = m - div( - cls := "min-h-full pt-16 pb-12 flex flex-col bg-white", - main( - cls := "flex-grow flex flex-col justify-center max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex-shrink-0 flex justify-center", - a( - href := "/", - cls := "inline-flex", - span( - cls := "sr-only", - """Workflow""" - ), - img( - cls := "h-12 w-auto", - src := "https://tailwindui.com/img/logos/workflow-mark.svg?color=indigo&shade=600", - alt := "" - ) - ) - ), - div( - cls := "py-16", - div( - cls := "text-center", - p( - cls := "text-sm font-semibold text-indigo-600 uppercase tracking-wide", - errorName - ), - h1( - cls := "mt-2 text-4xl font-extrabold text-gray-900 tracking-tight sm:text-5xl", - title - ), - p( - cls := "mt-2 text-base text-gray-500", - subTitle - ), - div( - cls := "mt-6", - PageLink - .container(homePage, actionBus) - .amend( - cls := "text-base font-medium text-indigo-600 hover:text-indigo-500", - """Go back home""" - ) - ) - ) - ) - ), - footer( - cls := "flex-shrink-0 max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", - nav( - cls := "flex justify-center space-x-4", - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Contact Support""" - ), - span( - cls := "inline-block border-l border-gray-300", - aria.hidden := true - ), - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Status""" - ), - span( - cls := "inline-block border-l border-gray-300", - aria.hidden := true - ), - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Twitter""" - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala deleted file mode 100644 index bd9baec..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action - -object NotFoundPage: - def apply(homePage: Page, url: String, actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - ErrorPage( - ErrorPage.ViewModel( - homePage, - "404 error", - "Page not found.", - s"Sorry, but page $url doesn't exist." - ), - actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala deleted file mode 100644 index 476fe3c..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action - -object UnhandledErrorPage: - - case class ViewModel( - homePage: Page, - errorName: Option[String], - errorMessage: Option[String] - ) - - def apply(m: ViewModel, actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - ErrorPage( - ErrorPage.ViewModel( - m.homePage, - "Unexpected error occurred", - m.errorName.getOrElse( - "Uh oh!" - ), // TODO: translations, better text than uh oh - m.errorMessage.getOrElse( - "This wasn't supposed to happen! Please try again." - ) - ), - actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala deleted file mode 100644 index 25f059f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala +++ /dev/null @@ -1,6 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.services - -import com.raquo.airstream.core.Observer - -trait DataFetcher[K, A]: - def fetch(id: K, sink: Observer[A]): Unit diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala deleted file mode 100644 index a469830..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala +++ /dev/null @@ -1,120 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package state - -import com.raquo.airstream.core.EventStream -import cz.e_bs.cmi.mdr.pdb.{UserInfo, OsobniCislo} -import com.raquo.airstream.core.Observer -import scala.scalajs.js -import scala.scalajs.js.JSON -import zio.json._ -import com.raquo.airstream.eventbus.EventBus -import com.raquo.airstream.ownership.Owner -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.UserFunction -import cz.e_bs.cmi.mdr.pdb.UserContract -import fiftyforms.services.files.File - -trait AppState - extends connectors.DetailPageConnector.AppState - with connectors.DetailParametruPageConnector.AppState - with connectors.DetailKriteriaPageConnector.AppState - with pages.detail.UpravDukaz.State: - def users: EventStream[List[UserInfo]] - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - -class MockAppState(implicit owner: Owner, router: Router[Page]) - extends AppState: - - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonDecoder[UserFunction] = DeriveJsonDecoder.gen - given JsonDecoder[UserContract] = DeriveJsonDecoder.gen - given JsonDecoder[UserInfo] = DeriveJsonDecoder.gen - - given JsonDecoder[ParameterCriteria] = DeriveJsonDecoder.gen - given JsonDecoder[Parameter] = DeriveJsonDecoder.gen - - private val actions = EventBus[Action]() - private val (parametersStream, pushParameters) = - EventStream.withCallback[List[Parameter]] - private val (usersStream, pushUsers) = - EventStream.withCallback[List[UserInfo]] - private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] - private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] - - private val mockData: List[UserInfo] = - mockUsers - .asInstanceOf[js.Dictionary[js.Object]] - .values - // TODO: is there a more efficient way to parse from JS object directly? - .map { o => - val parsed = JSON.stringify(o).fromJson[UserInfo] - parsed.left.foreach(org.scalajs.dom.console.log(_)) - parsed - } - .collect { case Right(u) => - u - } - .toList - - private val mockParameters: List[Parameter] = - pdbParams - .asInstanceOf[js.Dictionary[js.Object]] - .values - .map(o => JSON.stringify(o).fromJson[Parameter]) - .collect { case Right(p) => p } - .toList - - // TODO: Extract to separate event handler - actions.events.foreach { - case FetchDirectory => pushUsers(mockData) - case FetchUserDetails(osc) => - mockData.find(_.personalNumber == osc).foreach { o => - pushDetails(o) - router.replaceState(Page.Detail(o)) - } - case FetchParameters(osc) => - pushParameters(mockParameters) - case FetchParameter(osc, paramId) => - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(Page.DetailParametru(o, p)) - case FetchParameterCriteria(osc, paramId, critId, page) => - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - c <- p.criteria.find(_.id == critId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(page(o, p, c)) - case NavigateTo(page) => router.pushState(page) - case FetchAvailableFiles(osc) => - pushFiles( - List( - File("https://tc163.cmi.cz/here", "Example file") - ) - ) - } - - override def users: EventStream[List[UserInfo]] = - usersStream.debugWithName("users") - - override def details: EventStream[UserInfo] = - detailsStream.debugWithName("details") - - override def parameters: EventStream[List[Parameter]] = - parametersStream.debugWithName("parameters") - - override def availableFiles: EventStream[List[File]] = - filesStream.debugWithName("available files") - - override def actionBus: Observer[Action] = - actions.writer.debugWithName("actions writer") diff --git a/app/src/main/scala/mdr/pdb/app/Main.scala b/app/src/main/scala/mdr/pdb/app/Main.scala new file mode 100644 index 0000000..081b4ee --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/Main.scala @@ -0,0 +1,103 @@ +package mdr.pdb.app + +import scala.scalajs.js.annotation.JSExportTopLevel +import scala.scalajs.js.annotation.JSExport +import scala.scalajs.js.annotation.JSImport +import scala.scalajs.js +import org.scalajs.dom +import com.raquo.laminar.api.L.{*, given} +import scala.scalajs.js.Date +import com.raquo.waypoint.Router +import com.raquo.waypoint.SplitRender +import mdr.pdb.app.services.DataFetcher +import scala.scalajs.js.JSON +import zio.json._ +import mdr.pdb.UserInfo +import mdr.pdb.app.state.AppState + +@js.native +@JSImport("stylesheets/main.css", JSImport.Namespace) +object Css extends js.Any + +@js.native +@JSImport("data/users.json", JSImport.Default) +object mockUsers extends js.Object + +@js.native +@JSImport("params/pdb-params.json", JSImport.Default) +object pdbParams extends js.Object + +@JSExportTopLevel("app") +object Main: + + @JSExport + def main(args: Array[String]): Unit = + import Routes.given + onLoad { + setupAirstream() + val appContainer = dom.document.querySelector("#app") + val _ = + render( + appContainer, + renderPage(state.MockAppState(using unsafeWindowOwner, router)) + ) + } + + private def onLoad(f: => Unit): Unit = + documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) + + private def setupAirstream()(using router: Router[Page]): Unit = + AirstreamError.registerUnhandledErrorCallback(err => + router.forcePage( + Page.UnhandledError( + Some(err.getClass.getName), // TODO: Fill only in dev mode + Some(err.getMessage) + ) + ) + ) + + def renderPage(state: AppState)(using + router: Router[Page] + ): HtmlElement = + val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) + .collectSignal[Page.Detail]( + connectors + .DetailPageConnector(state)(_) + .apply + ) + .collectSignal[Page.DetailParametru]( + connectors + .DetailParametruPageConnector(state)(_) + .apply + ) + .collectSignal[Page.DetailKriteria]( + connectors + .DetailKriteriaPageConnector(state)(_) + .apply + ) + .collectSignal[Page.UpravDukazKriteria]( + pages.detail.UpravDukaz.Connector(state)(_).apply + ) + .collectStatic(Page.Dashboard)( + connectors.DashboardPageConnector(state.actionBus).apply + ) + .collect[Page.NotFound](pg => + pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) + ) + .collect[Page.UnhandledError](pg => + pages.errors + .UnhandledErrorPage( + pages.errors.UnhandledErrorPage + .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), + state.actionBus + ) + ) + .collectStatic(Page.Directory)( + connectors + .DirectoryPageConnector(state.users, state.actionBus) + .apply + ) + div(cls := "h-full", child <-- pageSplitter.$view) + + // Pull in the stylesheet + val css: Css.type = Css diff --git a/app/src/main/scala/mdr/pdb/app/Routes.scala b/app/src/main/scala/mdr/pdb/app/Routes.scala new file mode 100644 index 0000000..2cc860c --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/Routes.scala @@ -0,0 +1,198 @@ +package mdr.pdb.app + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.* +import org.scalajs.dom +import zio.json.{*, given} +import mdr.pdb.OsobniCislo + +import scala.scalajs.js +import mdr.pdb.UserInfo +import mdr.pdb.Parameter +import mdr.pdb.ParameterCriteria +import mdr.pdb.app.Page.Titled + +// enum is not working with Waypoints' SplitRender collectStatic +sealed abstract class Page( + val id: String, + val title: String, + val parent: Option[Page] +) { + val path: Vector[Page] = + parent match + case None => Vector(this) + case Some(p) => p.path :+ this + + val isRoot: Boolean = parent.isEmpty +} + +object Page: + + case class Titled[V](value: V, title: Option[String] = None): + val show: String = title.getOrElse(value.toString) + + case object Directory extends Page("directory", "Adresář", None) + + case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) + + case class Detail(osobniCislo: Titled[OsobniCislo]) + extends Page("user", osobniCislo.show, Some(Directory)) + + object Detail { + def apply(o: UserInfo): Detail = Detail( + Titled(o.personalNumber, Some(o.name)) + ) + } + + case class DetailParametru( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String] + ) extends Page( + "parameter", + parametr.show, + Some(Detail(osobniCislo)) + ) + + object DetailParametru { + def apply(o: UserInfo, p: Parameter): DetailParametru = + DetailParametru( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)) + ) + } + + case class DetailKriteria( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String], + kriterium: Titled[String] + ) extends Page( + "criteria", + kriterium.show, + Some(DetailParametru(osobniCislo, parametr)) + ) + + object DetailKriteria { + def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = + DetailKriteria( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)), + Titled(k.id, Some(k.id)) + ) + } + + case class UpravDukazKriteria( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String], + kriterium: Titled[String] + ) extends Page( + "addProof", + "Důkaz", + Some(DetailKriteria(osobniCislo, parametr, kriterium)) + ) + + object UpravDukazKriteria { + def apply( + o: UserInfo, + p: Parameter, + k: ParameterCriteria + ): UpravDukazKriteria = + UpravDukazKriteria( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)), + Titled(k.id, Some(k.id)) + ) + } + + case class NotFound(url: String) extends Page("404", "404", Some(Directory)) + + case class UnhandledError( + errorName: Option[String], + errorMessage: Option[String] + ) extends Page("500", "Unexpected error", Some(Directory)) + +object Routes: + given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) + given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) + given [V: JsonEncoder]: JsonEncoder[Titled[V]] = + DeriveJsonEncoder.gen[Titled[V]] + given [V: JsonDecoder]: JsonDecoder[Titled[V]] = + DeriveJsonDecoder.gen[Titled[V]] + given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] + given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] + + val base = + js.`import`.meta.env.BASE_URL + .asInstanceOf[String] + "app" + + val homePage: Page = Page.Directory + + given router: Router[Page] = Router[Page]( + routes = List( + Route.static(homePage, root / endOfSegments, basePath = base), + Route.static( + Page.Dashboard, + root / "dashboard" / endOfSegments, + basePath = base + ), + Route[Page.Detail, String]( + encode = _.osobniCislo.value.toString, + decode = osc => Page.Detail(Titled(OsobniCislo(osc))), + root / "osoba" / segment[String] / endOfSegments, + basePath = base + ), + Route[Page.DetailParametru, (String, String)]( + encode = p => (p.osobniCislo.value.toString, p.parametr.value), + decode = + p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / endOfSegments, + basePath = base + ), + Route[Page.DetailKriteria, (String, String, String)]( + encode = p => + ( + p.osobniCislo.value.toString, + p.parametr.value, + p.kriterium.value.replaceAll("\\.", "--") + ), + decode = p => + Page.DetailKriteria( + Titled(OsobniCislo(p._1)), + Titled(p._2), + Titled(p._3.replaceAll("--", ".")) + ), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / "kriterium" / segment[String] / endOfSegments, + basePath = base + ), + Route[Page.UpravDukazKriteria, (String, String, String)]( + encode = p => + ( + p.osobniCislo.value.toString, + p.parametr.value, + p.kriterium.value.replaceAll("\\.", "--") + ), + decode = p => + Page.UpravDukazKriteria( + Titled(OsobniCislo(p._1)), + Titled(p._2), + Titled(p._3.replaceAll("--", ".")) + ), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / "kriterium" / segment[String] / "edit" / endOfSegments, + basePath = base + ) + ), + serializePage = _.toJson, + deserializePage = _.fromJson[Page] + .fold(s => throw IllegalStateException(s), identity), + getPageTitle = _.title, + routeFallback = url => Page.NotFound(url), + deserializeFallback = _ => Page.Dashboard + )( + $popStateEvent = windowEvents.onPopState, + owner = unsafeWindowOwner + ) diff --git a/app/src/main/scala/mdr/pdb/app/actions.scala b/app/src/main/scala/mdr/pdb/app/actions.scala new file mode 100644 index 0000000..d078b3a --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/actions.scala @@ -0,0 +1,21 @@ +package mdr.pdb.app + +import mdr.pdb.OsobniCislo +import mdr.pdb.UserInfo +import mdr.pdb.Parameter +import mdr.pdb.ParameterCriteria + +sealed trait Action + +case object FetchDirectory extends Action +case class FetchUserDetails(osc: OsobniCislo) extends Action +case class FetchParameters(osc: OsobniCislo) extends Action +case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action +case class FetchParameterCriteria( + osc: OsobniCislo, + paramId: String, + critId: String, + page: (UserInfo, Parameter, ParameterCriteria) => Page +) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action +case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala deleted file mode 100644 index a3b00af..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala +++ /dev/null @@ -1,103 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import scala.scalajs.js.annotation.JSExportTopLevel -import scala.scalajs.js.annotation.JSExport -import scala.scalajs.js.annotation.JSImport -import scala.scalajs.js -import org.scalajs.dom -import com.raquo.laminar.api.L.{*, given} -import scala.scalajs.js.Date -import com.raquo.waypoint.Router -import com.raquo.waypoint.SplitRender -import cz.e_bs.cmi.mdr.pdb.app.services.DataFetcher -import scala.scalajs.js.JSON -import zio.json._ -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.state.AppState - -@js.native -@JSImport("stylesheets/main.css", JSImport.Namespace) -object Css extends js.Any - -@js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native -@JSImport("params/pdb-params.json", JSImport.Default) -object pdbParams extends js.Object - -@JSExportTopLevel("app") -object Main: - - @JSExport - def main(args: Array[String]): Unit = - import Routes.given - onLoad { - setupAirstream() - val appContainer = dom.document.querySelector("#app") - val _ = - render( - appContainer, - renderPage(state.MockAppState(using unsafeWindowOwner, router)) - ) - } - - private def onLoad(f: => Unit): Unit = - documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) - - private def setupAirstream()(using router: Router[Page]): Unit = - AirstreamError.registerUnhandledErrorCallback(err => - router.forcePage( - Page.UnhandledError( - Some(err.getClass.getName), // TODO: Fill only in dev mode - Some(err.getMessage) - ) - ) - ) - - def renderPage(state: AppState)(using - router: Router[Page] - ): HtmlElement = - val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) - .collectSignal[Page.Detail]( - connectors - .DetailPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailParametru]( - connectors - .DetailParametruPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailKriteria]( - connectors - .DetailKriteriaPageConnector(state)(_) - .apply - ) - .collectSignal[Page.UpravDukazKriteria]( - pages.detail.UpravDukaz.Connector(state)(_).apply - ) - .collectStatic(Page.Dashboard)( - connectors.DashboardPageConnector(state.actionBus).apply - ) - .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) - ) - .collect[Page.UnhandledError](pg => - pages.errors - .UnhandledErrorPage( - pages.errors.UnhandledErrorPage - .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), - state.actionBus - ) - ) - .collectStatic(Page.Directory)( - connectors - .DirectoryPageConnector(state.users, state.actionBus) - .apply - ) - div(cls := "h-full", child <-- pageSplitter.$view) - - // Pull in the stylesheet - val css: Css.type = Css diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala deleted file mode 100644 index 3bbc8c0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ /dev/null @@ -1,198 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.* -import org.scalajs.dom -import zio.json.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import scala.scalajs.js -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.Page.Titled - -// enum is not working with Waypoints' SplitRender collectStatic -sealed abstract class Page( - val id: String, - val title: String, - val parent: Option[Page] -) { - val path: Vector[Page] = - parent match - case None => Vector(this) - case Some(p) => p.path :+ this - - val isRoot: Boolean = parent.isEmpty -} - -object Page: - - case class Titled[V](value: V, title: Option[String] = None): - val show: String = title.getOrElse(value.toString) - - case object Directory extends Page("directory", "Adresář", None) - - case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) - - case class Detail(osobniCislo: Titled[OsobniCislo]) - extends Page("user", osobniCislo.show, Some(Directory)) - - object Detail { - def apply(o: UserInfo): Detail = Detail( - Titled(o.personalNumber, Some(o.name)) - ) - } - - case class DetailParametru( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String] - ) extends Page( - "parameter", - parametr.show, - Some(Detail(osobniCislo)) - ) - - object DetailParametru { - def apply(o: UserInfo, p: Parameter): DetailParametru = - DetailParametru( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)) - ) - } - - case class DetailKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "criteria", - kriterium.show, - Some(DetailParametru(osobniCislo, parametr)) - ) - - object DetailKriteria { - def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = - DetailKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class UpravDukazKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "addProof", - "Důkaz", - Some(DetailKriteria(osobniCislo, parametr, kriterium)) - ) - - object UpravDukazKriteria { - def apply( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): UpravDukazKriteria = - UpravDukazKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class NotFound(url: String) extends Page("404", "404", Some(Directory)) - - case class UnhandledError( - errorName: Option[String], - errorMessage: Option[String] - ) extends Page("500", "Unexpected error", Some(Directory)) - -object Routes: - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) - given [V: JsonEncoder]: JsonEncoder[Titled[V]] = - DeriveJsonEncoder.gen[Titled[V]] - given [V: JsonDecoder]: JsonDecoder[Titled[V]] = - DeriveJsonDecoder.gen[Titled[V]] - given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] - given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] - - val base = - js.`import`.meta.env.BASE_URL - .asInstanceOf[String] + "app" - - val homePage: Page = Page.Directory - - given router: Router[Page] = Router[Page]( - routes = List( - Route.static(homePage, root / endOfSegments, basePath = base), - Route.static( - Page.Dashboard, - root / "dashboard" / endOfSegments, - basePath = base - ), - Route[Page.Detail, String]( - encode = _.osobniCislo.value.toString, - decode = osc => Page.Detail(Titled(OsobniCislo(osc))), - root / "osoba" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.DetailParametru, (String, String)]( - encode = p => (p.osobniCislo.value.toString, p.parametr.value), - decode = - p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / endOfSegments, - basePath = base - ), - Route[Page.DetailKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.DetailKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.UpravDukazKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.UpravDukazKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / "edit" / endOfSegments, - basePath = base - ) - ), - serializePage = _.toJson, - deserializePage = _.fromJson[Page] - .fold(s => throw IllegalStateException(s), identity), - getPageTitle = _.title, - routeFallback = url => Page.NotFound(url), - deserializeFallback = _ => Page.Dashboard - )( - $popStateEvent = windowEvents.onPopState, - owner = unsafeWindowOwner - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala deleted file mode 100644 index b24b82b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -sealed trait Action - -case object FetchDirectory extends Action -case class FetchUserDetails(osc: OsobniCislo) extends Action -case class FetchParameters(osc: OsobniCislo) extends Action -case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action -case class FetchParameterCriteria( - osc: OsobniCislo, - paramId: String, - critId: String, - page: (UserInfo, Parameter, ParameterCriteria) => Page -) extends Action -case class FetchAvailableFiles(osc: OsobniCislo) extends Action -case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala deleted file mode 100644 index c637d35..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala +++ /dev/null @@ -1,82 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.{UserProfile, UserInfo, OsobniCislo} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo -import cz.e_bs.cmi.mdr.pdb.UserFunction - -object AppPage: - // TODO: pages by logged in user - val pages: List[Page] = List(Page.Directory, Page.Dashboard) - - import NavigationBar.{Logo, MenuItem} - - val logo = Logo( - "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", - "Workflow" - ) - - // TODO: menu items by user profile - val userMenu = - List( - MenuItem("Your Profile"), - MenuItem("Settings"), - MenuItem("Sign out") - ) - - // TODO: load user profile - val $userProfile = Var( - UserProfile( - "tom", - UserInfo( - OsobniCislo("1031"), - "tom", - "Tom", - "Cook", - None, - None, - Some("tom@example.com"), - Some("+420 222 866 180"), - Some( - UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") - ), - Nil, - None - ) - ) - ) - - val $userInfo = $userProfile.signal.map(_.userInfo) - - type ViewModel = Option[HtmlElement] - def apply( - actionBus: Observer[Action] - )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using - router: Router[Page] - ): HtmlElement = - PageLayout(actionBus)( - $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => - PageLayout.ViewModel( - NavigationBar.ViewModel( - u, - pages.map(p => - NavigationBar.Link( - () => - PageLink( - p, - actionBus - ), - p == cp - ) - ), - userMenu, - logo - ), - c - ) - ), - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala deleted file mode 100644 index bf473c6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ /dev/null @@ -1,143 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden -import fiftyforms.ui.components.tailwind.list.IconText.ViewModel -import fiftyforms.ui.components.tailwind.Icons -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Action - -object Breadcrumbs: - - private def slash = - import svg.{*, given} - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-300", - xmlns := "http://www.w3.org/2000/svg", - fill := "currentColor", - viewBox := "0 0 20 20", - ariaHidden := true, - path( - d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" - ) - ) - - def backIcon = - Icons.solid - .`arrow-narrow-left`() - .amend( - svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" - ) - - object Link: - case class ViewModel( - page: Page, - icon: Option[SvgElement], - extraClasses: String, - text: String, - textClasses: Option[String] = None - ) - - val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" - - def shortHome(p: Page) = ViewModel( - p, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - "Zpět na úvodní stránku", - None - ) - - def fullHome(p: Page) = ViewModel( - p, - Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), - "text-gray-400 hover:text-gray-500", - "Domů", - Some("sr-only") - ) - - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - PageLink - .container($m.map(_.page), actionBus) - .amend( - cls <-- $m.map(_.extraClasses), - child.maybe <-- $m.map(_.icon), - span( - cls <-- $m.map(_.textClasses.getOrElse("")), - child.text <-- $m.map(_.text) - ) - ) - - object FullBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - ol( - role := "list", - cls := "flex items-center space-x-4", - children <-- $m.map(_.path).split(_.id)((_, _, $p) => - li( - div( - cls := "flex items-center", - child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), - Link( - $p.map(p => - if (p.isRoot) then Link.fullHome(p) - else - Link.ViewModel( - p, - None, - s"ml-4 max-w-xs truncate ${Link.baseClasses}", - p.title - ) - ), - actionBus - ) - ) - ) - ) - ) - - object ShortBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - Link( - $m.map { p => - val target = p.parent.getOrElse(p) - if target.isRoot then Link.shortHome(target) - else - Link.ViewModel( - target, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - s"Zpět na ${target.title}" - ) - }, - actionBus - ) - - def apply(actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - nav( - cls := "flex", - aria.label := "Breadcrumb", - div( - cls := "flex sm:hidden", - ShortBreadcrumbs(router.$currentPage, actionBus) - ), - div( - cls := "hidden sm:block", - FullBreadcrumbs(router.$currentPage, actionBus) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala deleted file mode 100644 index 0db2047..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ /dev/null @@ -1,242 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent -import fiftyforms.ui.components.tailwind.Icons -import fiftyforms.ui.components.tailwind.Avatar -import cz.e_bs.cmi.mdr.pdb.UserInfo -import io.laminext.syntax.core.* - -object NavigationBar: - - case class Logo(img: String, name: String) - case class Link(a: () => Anchor, active: Boolean) - case class MenuItem(title: String) - - case class ViewModel( - userInfo: UserInfo, - pages: List[Link], - userMenu: List[MenuItem], - logo: Logo - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - val $userInfo = $m.map(_.userInfo) - val mobileMenuOpen = Var(false) - - val desktopOnly = cls("hidden md:block") - val mobileOnly = cls("md:hidden") - - inline def avatarImage(size: Int = 8) = - Avatar($userInfo.map(_.img)).avatarImage(size) - - def notificationButton = button( - tpe := "button", - cls := List( - "bg-indigo-600", - "focus:outline-none", - "focus:ring-2", - "focus:ring-offset-2", - "focus:ring-offset-indigo-600", - "focus:ring-white", - "hover:text-white", - "p-1", - "rounded-full", - "text-indigo-200" - ), - span(cls := "sr-only", "View notifications"), - Icons.outline.bell() - ) - - def userProfile: HtmlElement = - val menuOpen = Var(false) - - def menuItem(item: MenuItem, idx: Int): HtmlElement = - a( - href := "#", - cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", - role := "menuitem", - tabIndex := -1, - idAttr := s"user-menu-item-$idx", - item.title - ) - - div( - cls := "ml-3 relative", - div( - button( - tpe := "button", - cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - idAttr := "user-menu-button", - aria.expanded <-- menuOpen.signal, - aria.hasPopup := true, - span(cls := "sr-only", "Open user menu"), - child <-- avatarImage(), - onClick.preventDefault.mapTo( - !menuOpen.now() - ) --> menuOpen.writer - ) - ), - /* - * */ - div( - cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", - cls <-- menuOpen.signal.map { o => - if (o) "md:block" else "md:hidden" - }, - role := "menu", - aria.orientation := "vertical", - aria.labelledBy := "user-menu-button", - tabIndex := -1, - // : keyboard navigation - children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) - ) - ) - - def mobileProfile = - def menuItem(item: MenuItem): HtmlElement = - a( - href := "#", - cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", - item.title - ) - - div( - cls := "pt-4 pb-3 border-t border-indigo-700", - div( - cls := "flex items-center px-5", - div( - cls := "flex-shrink-0", - child <-- avatarImage(10) - ), - div( - cls := "ml-3", - div( - cls := "text-base font-medium text-white", - child.text <-- $userInfo.map(_.name) - ), - child.maybe <-- $userInfo.map( - _.email.map(e => - div( - cls := "text-sm font-medium text-indigo-300", - e - ) - ) - ) - ), - notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) - ), - div( - cls := "mt-3 px-2 space-y-1", - children <-- $m.map(_.userMenu.map(menuItem)) - ) - ) - - def pageLink(page: Link): Anchor = - page - .a() - .amend( - cls := "text-white px-3 py-2 rounded-md text-sm font-medium", - cls := Seq( - "bg-indigo-700" -> page.active, - "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active - ), - ariaCurrent := (if page.active then "page" else "false") - ) - - def logoImg: Image = - img( - cls := "h-8 w-8", - src <-- $m.map(_.logo.img), - alt <-- $m.map(_.logo.name) - ) - - def mobileMenuButton = button( - tpe := "button", - cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - aria.controls := "mobile-menu", - aria.expanded <-- mobileMenuOpen.signal, - span(cls := "sr-only", "Open main menu"), - Icons.outline - .menu() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") - ), - Icons.outline - .x() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") - ), - onClick.preventDefault.mapTo( - !mobileMenuOpen.now() - ) --> mobileMenuOpen.writer - ) - - def navBarLeft = - div( - cls := "flex items-center", - div(cls := "flex-shrink-0", logoImg), - div( - desktopOnly, - div( - cls := "ml-10 flex items-baseline space-x-4", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ) - ) - ) - - def navBarRight = - div( - desktopOnly, - div( - cls := "ml-4 flex items-center md:ml-6", - notificationButton, - userProfile - ) - ) - - def navBarMobile = - div( - cls := "-mr-2 flex", - mobileOnly, - mobileMenuButton - ) - - def navBar = - div( - cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex items-center justify-between h-16", - navBarLeft, - navBarRight, - navBarMobile - ) - ) - - def mobileMenu = - div( - mobileOnly, - cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }, - idAttr := "mobile-menu", - div( - cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ), - mobileProfile - ) - - nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala deleted file mode 100644 index 75339df..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object PageHeader: - def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = - header( - cls := "bg-white shadow-sm", - div( - cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", - h1( - cls := "text-lg leading-6 font-semibold text-gray-900", - Breadcrumbs(actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala deleted file mode 100644 index 5096463..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ /dev/null @@ -1,30 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import fiftyforms.ui.components.tailwind.Loading -import io.laminext.syntax.core.* - -object PageLayout: - case class ViewModel( - navigation: NavigationBar.ViewModel, - content: Option[HtmlElement] - ) - def apply(actionBus: Observer[Action])( - $m: Signal[ViewModel], - mods: Modifier[HtmlElement]* - )(using router: Router[Page]): HtmlElement = - val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) - div( - cls := "h-full flex flex-col", - NavigationBar($m.map(_.navigation)), - child.maybe <-- router.$currentPage.map(_.isRoot) - .switch(None, Some(PageHeader(actionBus))), - main( - cls := "flex-grow-1 overflow-y-auto", - mods, - child <-- $maybeContent.map(_.getOrElse(Loading)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala deleted file mode 100644 index 62d820f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.LinkSupport.* -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo - -object PageLink: - type ViewModel = Page - def apply($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = - a(mods($m, actions), child.text <-- $m.map(_.title)) - - def apply(m: ViewModel, actions: Observer[Action])(using - router: Router[Page] - ): Anchor = apply(Val(m), actions) - - def container($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = a(mods($m, actions)) - - def container(m: ViewModel, actions: Observer[Action])(using - Router[Page] - ): Anchor = container(Val(m), actions) - - private def mods($m: Signal[Page], actions: Observer[Action])(using - router: Router[Page] - ): Modifier[Anchor] = - Seq( - href <-- $m.map(router.absoluteUrlForPage).recover { case _ => - Some("invalid url") - }, - composeEvents(onClick.noKeyMod.preventDefault)( - _.sample($m) - .map(NavigateTo.apply) - ) --> actions - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala deleted file mode 100644 index 87f825a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.pages.dashboard.DashboardPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -class DashboardPageConnector(actionBus: Observer[Action])(using - router: Router[Page] -): - def apply: HtmlElement = - AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala deleted file mode 100644 index e9b0f71..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala +++ /dev/null @@ -1,76 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailKriteriaPage -import pages.detail.components.DukazyKriteria -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -object DetailKriteriaPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailKriteriaPageConnector( - state: DetailKriteriaPageConnector.AppState -)( - $page: Signal[Page.DetailKriteria] -)(using Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => - (p.osobniCislo.value, p.parametr.value, p.kriterium.value) - )((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map( - FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) - ) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, s, $s) => - DetailKriteriaPage($s)(state.actionBus.contramap { - case DukazyKriteria.Add => - NavigateTo( - Page.UpravDukazKriteria( - Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), - Page.Titled(s.parametr.id, Some(s.parametr.nazev)), - Page.Titled(s.kriterium.id, Some(s.kriterium.id)) - ) - ) - }) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): DetailKriteriaPage.ViewModel = - DetailKriteriaPage.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala deleted file mode 100644 index 4d8024f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ /dev/null @@ -1,57 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import pages.detail.DetailPage -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import fiftyforms.ui.components.tailwind.Color - -object DetailPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailPageConnector(state: DetailPageConnector.AppState)( - $page: Signal[Page.Detail] -)(using router: Router[Page]): - val $oscChangeSignal = $page.splitOne(_.osobniCislo.value)((osc, _, _) => osc) - val $pageChangeSignal = - $oscChangeSignal.flatMap(osc => - EventStream.fromSeq(Seq(FetchUserDetails(osc), FetchParameters(osc))) - ) - // TODO: filter the value based on the current osc - // OSC change will fetch new data, but still - // - we need to be sure that what we got is really what we ought to display - // - we want to display stale data accordingly (at least with loading indicator) - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - def apply: HtmlElement = - AppPage(state.actionBus)( - $data.combineWithFn($params)(_ zip _) - .map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: List[Parameter] - ): DetailPage.ViewModel = - DetailPage.ViewModel( - o.toDetailOsoby, - p.map( - _.toParametr(param => - PageLink.container(Page.DetailParametru(o, param), state.actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala deleted file mode 100644 index fdaa7f8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailParametruPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink - -object DetailParametruPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailParametruPageConnector( - state: DetailParametruPageConnector.AppState -)( - $page: Signal[Page.DetailParametru] -)(using router: Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => (p.osobniCislo.value, p.parametr.value))((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map(FetchParameter(_, _)) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - } yield (da, pb) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailParametruPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter - ): DetailParametruPage.ViewModel = - DetailParametruPage.ViewModel( - o.toDetailOsoby, - p.toParametr(p => - PageLink.container(Page.DetailParametru(o, p), state.actionBus) - ), - p.criteria.map( - _.toKriterium { c => - PageLink.container( - Page.DetailKriteria(o, p, c), - state.actionBus - ) - } - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala deleted file mode 100644 index 049aa22..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -case class DirectoryPageConnector( - $input: EventStream[List[UserInfo]], - actionBus: Observer[Action] -)(using router: Router[Page]): - val $data = $input.startWithNone - val $actionSignal = EventStream.fromValue(FetchDirectory) - - def apply: HtmlElement = - AppPage(actionBus)( - $data.split(_ => ())((_, _, s) => - pages.directory.DirectoryPage( - s.map( - _.map( - _.toUserRow(u => - PageLink.container( - Page.Detail(Page.Titled(u.personalNumber)), - actionBus - ) - ) - ) - ) - ) - ), - $actionSignal --> actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala deleted file mode 100644 index 88e7450..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ /dev/null @@ -1,68 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii -import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria -import fiftyforms.ui.components.tailwind.Color - -extension (o: UserInfo) - def toDetailOsoby: DetailOsoby.ViewModel = - DetailOsoby.ViewModel( - o.personalNumber, - o.name, - o.email, - o.phone, - o.img, - o.mainFunction.map(f => - DetailOsoby.Funkce.ViewModel(f.name, f.dept, f.ou) - ), - o.userContracts.headOption.map(c => - DetailOsoby.PracovniPomer.ViewModel(c.rel, c.startDate, c.endDate) - ) - ) - -extension (param: Parameter) - def toParametr(container: Parameter => Anchor): DetailParametru.ViewModel = - DetailParametru.ViewModel( - id = param.id, - nazev = param.name, - popis = param.description, - status = "Nesplněno", - statusColor = Color.red, - a = container(param) - ) - -extension (crit: ParameterCriteria) - def toKriterium( - container: ParameterCriteria => Anchor - ): DetailKriteria.ViewModel = - DetailKriteria.ViewModel( - nazev = crit.criteriumText, - kapitola = crit.chapterId, - bod = crit.itemId, - status = "Nesplněno", - statusColor = Color.red, - splneno = false, - dukazy = Nil, - container = container(crit) - ) - -extension (user: UserInfo) - def toUserRow( - container: UserInfo => HtmlElement = (_: UserInfo) => div() - ): UserRow.ViewModel = - UserRow.ViewModel( - osobniCislo = user.personalNumber.toString, - celeJmeno = user.name, - prijmeni = user.surname, - hlavniFunkce = user.mainFunction.map(_.name), - img = user.img, - container = () => container(user) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala deleted file mode 100644 index 7d5b2c1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.dashboard - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.Page - -object DashboardPage: - - def render: HtmlElement = - div("Dashboard page") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala deleted file mode 100644 index 9ef0698..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala +++ /dev/null @@ -1,31 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailKriteriaPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply($m: Signal[ViewModel])( - action: Observer[DukazyKriteria.Action] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - DukazyKriteria($m.map(_.kriterium.dukazy))(action) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala deleted file mode 100644 index 3b316e1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object DetailPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametry: SeznamParametru.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby($m.map(_.osoba)), - SeznamParametru($m.map(_.parametry)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala deleted file mode 100644 index 17224cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala +++ /dev/null @@ -1,24 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailParametruPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriteria: SeznamKriterii.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby.header($m.map(_.osoba)), - DetailParametru($m.map(_.parametr)), - SeznamKriterii($m.map(_.kriteria)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala deleted file mode 100644 index dc912fa..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ /dev/null @@ -1,107 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb -package app -package pages.detail - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.UpravDukazForm -import fiftyforms.services.files.File - -object UpravDukaz: - - type ThisPage = Page.UpravDukazKriteria - type PageKey = (OsobniCislo, String, String) - - trait State { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def availableFiles: EventStream[List[File]] - def actionBus: Observer[Action] - } - - def keyOfPage(page: ThisPage): PageKey = - (page.osobniCislo.value, page.parametr.value, page.kriterium.value) - - def onChangeAction(key: PageKey): Action = - FetchParameterCriteria( - key._1, - key._2, - key._3, - Page.UpravDukazKriteria(_, _, _) - ) - - class Connector(state: State)($page: Signal[ThisPage])(using Router[Page]): - val $paramChangeSignal = $page.splitOne(keyOfPage)((x, _, _) => x) - val $pageChangeSignal = $paramChangeSignal.map(onChangeAction) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.split(_ => ())((_, s, $s) => - PageComponent( - $s.map(buildModel), - state.availableFiles, - state.actionBus.contramap { - case UpravDukazForm.Cancelled => - NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) - case UpravDukazForm.AvailableFilesRequested => - FetchAvailableFiles(s._1.personalNumber) - } - ) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): PageComponent.ViewModel = - import connectors.* - PageComponent.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) - - object PageComponent: - import components.* - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply( - $m: Signal[ViewModel], - availableFilesStream: EventStream[List[File]], - events: Observer[UpravDukazForm.Event] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - UpravDukazForm(availableFilesStream)(events) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala deleted file mode 100644 index ca311b0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.Icons -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Color - -object DetailKriteria: - case class ViewModel( - nazev: String, - kapitola: String, - bod: String, - status: String, - statusColor: Color, - splneno: Boolean, - dukazy: List[DukazKriteria.ViewModel], - container: HtmlElement = div() - ) { - val id = s"${kapitola}${bod}" - } - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h3( - cls := "text-xl leading-6 font-bold text-gray-900", - child.text <-- $m.map(_.nazev) - ), - h3( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - "Kapitola ", - child.text <-- $m.map(_.kapitola), - " bod ", - child <-- $m.map(_.bod) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala deleted file mode 100644 index ae90290..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala +++ /dev/null @@ -1,100 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Avatar - -object DetailOsoby: - - object Funkce: - case class ViewModel( - nazev: String, - stredisko: String, - voj: String - ) - - def render($m: Signal[ViewModel]): HtmlElement = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.nazev), - span( - cls := "hidden md:inline", - " @ ", - child.text <-- $m.map(_.stredisko), - ", ", - child.text <-- $m.map(_.voj) - ) - ) - - object PracovniPomer: - case class ViewModel( - druh: String, - pocatek: LocalDate, - konec: Option[LocalDate] - ) - - def render($m: Signal[ViewModel]): HtmlElement = - import fiftyforms.ui.components.tailwind.CustomAttrs.datetime - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.druh), - " od ", - time( - datetime <-- $m.map(_.pocatek.toString), - child.text <-- $m.map(_.pocatek.toString) - ) - ) - - case class ViewModel( - osobniCislo: OsobniCislo, - jmeno: String, - email: Option[String], - telefon: Option[String], - img: Option[String], - hlavniFunkce: Option[Funkce.ViewModel], - pracovniPomer: Option[PracovniPomer.ViewModel] - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-4", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(16) - ), - div( - h1( - cls := "text-2xl font-bold text-gray-900", - child.text <-- $m.map(_.jmeno) - ), - child.maybe <-- $m.map(_.hlavniFunkce).split(_ => ())((_, _, d) => - Funkce.render(d) - ), - child.maybe <-- $m.map(_.pracovniPomer).split(_ => ())((_, _, d) => - PracovniPomer.render(d) - ) - ) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-5", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(8) - ), - div( - h1( - cls := "text-lg font-medium text-gray-800", - child.text <-- $m.map(_.jmeno) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala deleted file mode 100644 index e11cb62..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color - -object DetailParametru: - case class ViewModel( - id: String, - nazev: String, - popis: String, - status: String, - statusColor: Color, - a: Anchor - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h2( - cls := "text-2xl leading-6 font-medium text-gray-900", - child.text <-- $m.map(_.nazev) - ), - p( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - child.text <-- $m.map(_.popis) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - h2( - cls := "text-lg leading-6 font-medium text-gray-600", - child.text <-- $m.map(_.nazev) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala deleted file mode 100644 index a00ede8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala +++ /dev/null @@ -1,177 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.CustomAttrs - -object DukazKriteria: - case class Osoba(osobniCislo: String, jmeno: String) - case class Udalost(osoba: Osoba, datum: LocalDate) - case class Dokument( - url: String, - nazev: String, - pridal: Udalost, - odebral: Option[Udalost] - ) - case class ViewModel( - dokumenty: List[Dokument], - autorizoval: Option[Udalost], - platiDo: Option[LocalDate], - poznámka: Option[String] - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-lg", - div( - cls := "px-4 py-5 sm:px-6", - h3( - cls := "text-lg leading-6 font-medium text-gray-900", - """Applicant Information""" - ), - p( - cls := "mt-1 max-w-2xl text-sm text-gray-500", - """Personal details and application.""" - ) - ), - div( - cls := "border-t border-gray-200 px-4 py-5 sm:px-6", - dl( - cls := "grid grid-cols-1 gap-x-4 gap-y-8 sm:grid-cols-2", - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Full name""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Margot Foster""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Application for""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Backend Developer""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Email address""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """margotfoster@example.com""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Salary expectation""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """$120,000""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """About""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Fugiat ipsum ipsum deserunt culpa aute sint do nostrud anim incididunt cillum culpa consequat. Excepteur qui ipsum aliquip consequat sint. Sit id mollit nulla mollit nostrud in ea officia proident. Irure nostrud pariatur mollit ad adipisicing reprehenderit deserunt qui eu.""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """Attachments""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - ul( - role := "list", - cls := "border border-gray-200 rounded-md divide-y divide-gray-200", - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """resume_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ), - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """coverletter_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala deleted file mode 100644 index 22f134f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala +++ /dev/null @@ -1,35 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object DukazyKriteria: - sealed trait Action - case object Add extends Action - - type ViewModel = List[DukazKriteria.ViewModel] - - def apply($m: Signal[ViewModel])(actions: Observer[Action]): HtmlElement = - div( - child.maybe <-- $m.map(m => - if (m.isEmpty) then Some(prazdnyDukaz(actions)) - else None - ), - children <-- $m.map(_.zipWithIndex).split(_._2)((_, _, $s) => - DukazKriteria($s.map(_._1)) - ) - ) - - private def prazdnyDukaz(actions: Observer[Action]): HtmlElement = - button( - tpe := "button", - onClick.preventDefault.mapTo(Add) --> actions, - cls := "relative block w-full border-2 border-gray-300 border-dashed rounded-lg p-12 text-center hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - Icons.outline - .`document-add`(12) - .amend(svg.cls := "mx-auto text-gray-400"), - span( - cls := "mt-2 block text-sm font-medium text-gray-900", - "Přidat důkaz" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala deleted file mode 100644 index f628697..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.list.StackedList -import fiftyforms.ui.components.tailwind.list.ListRow -import fiftyforms.ui.components.tailwind.list.RowTag -import fiftyforms.ui.components.tailwind.list.RowNext -import java.time.LocalDate - -object SeznamKriterii: - type ViewModel = List[DetailKriteria.ViewModel] - - private val kritList = new StackedList[DetailKriteria.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - kritList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = - p(cls := "text-sm text-gray-500", s"${i.kapitola}${i.bod}"), - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.container - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala deleted file mode 100644 index 2bfe4bc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.list.{ - StackedList, - ListRow, - RowTag, - PropList, - IconText, - RowNext -} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.LinkSupport.* - -object SeznamParametru: - type ViewModel = List[DetailParametru.ViewModel] - - private val parametrList = new StackedList[DetailParametru.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - parametrList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = emptyNode, - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.a - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala deleted file mode 100644 index f048e9d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ /dev/null @@ -1,101 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.form.* -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement -import fiftyforms.services.files.components.tailwind.FilePicker -import cz.e_bs.cmi.mdr.pdb.frontend.AutorizujDukaz -import cz.e_bs.cmi.mdr.pdb.frontend.DocumentRef -import fiftyforms.services.files.File - -object UpravDukazForm: - sealed trait Event - case object Cancelled extends Event - case object AvailableFilesRequested extends Event - def apply(availableFilesStream: EventStream[List[File]])( - updates: Observer[Event] - ): HtmlElement = - val (filesStream, filesObserver) = - EventStream.withObserver[FilePicker.Event] - val files = Var[List[File]](Nil) - def submitButtons: HtmlElement = - div( - cls := "pt-5", - div( - cls := "flex justify-end", - button( - tpe := "button", - cls := "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zrušit", - onClick.mapTo(Cancelled) --> updates - ), - button( - tpe := "submit", - cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - disabled <-- files.signal.map(_.isEmpty), - "Autorizovat" - ) - ) - ) - - div( - filesStream.collect { case FilePicker.SelectionUpdated(files) => - files.to(List) - } --> files.writer, - filesStream.collect { case FilePicker.AvailableFilesRequested => - AvailableFilesRequested - } --> updates, - cls := "bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6", - Form( - Form.Body( - Form.Section( - FormHeader( - FormHeader.ViewModel( - "Doložení kritéria", - "Sestavte doklady poskytující důkaz kritéria a potvrďte odesláním formuláře. Případné limitace či jiné relevantní údaje vepište do pole Komentář." - ) - ), - FormFields( - FormRow( - "dokumenty", - "Dokumenty", - FilePicker(files.signal, availableFilesStream)(filesObserver) - .amend(idAttr := "dokumenty", cls("max-w-lg")) - ).toHtml, - FormRow( - "platnost", - "Platnost", - input( - idAttr := "platnost", - name := "platnost", - tpe := "date", - autoComplete := "date", - cls := "block max-w-lg w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" - ) - ).toHtml, - FormRow( - "komentar", - "Komentář", - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - textArea( - idAttr := "komentar", - name := "about", - rows := 3, - cls := "max-w-lg shadow-sm block w-full focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border border-gray-300 rounded-md" - ), - p( - cls := "mt-2 text-sm text-gray-500", - "Doplňte prosím potřebné informace související s doložením kritéria, včetně případných limitací." - ) - ) - ).toHtml - ) - ) - ), - submitButtons - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala deleted file mode 100644 index 41033f6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -import components._ - -object DirectoryPage: - - type ViewModel = List[UserRow.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - val (actionsStream, actionObserver) = - EventStream.withObserver[SearchForm.Action] - val $filter = actionsStream - .collect { - case SearchForm.ClearFilter => None - case SearchForm.SetFilter(t) => Some(t) - } - .startWith(None) - val byLetter = for { - d <- $m - f <- $filter - } yield for { - (letter, users) <- d - .filter { user => - f match - case None => true - case Some(t) => user.search.contains(t) - } - .groupBy(_.prijmeni.head) - .to(List) - .sortBy(_._1) - } yield (letter.toString, users.sortBy(_.prijmeni)) - - div( - cls := "h-full max-w-7xl mx-auto order-first flex flex-col flex-shrink-0", - SearchForm(actionObserver), - Directory(byLetter) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala deleted file mode 100644 index 73f915d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -object Directory: - - object Header: - type ViewModel = String - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500", - h3(child.text <-- $m) - ) - - object UserList: - type ViewModel = List[UserRow.ViewModel] - def apply($m: Signal[ViewModel]): HtmlElement = - ul( - role := "list", - cls := "relative z-0 divide-y divide-gray-200", - children <-- $m.split(_.osobniCislo)((_, _, s) => UserRow(s)) - ) - - type ViewModel = List[(String, List[UserRow.ViewModel])] - def apply($m: Signal[ViewModel]): HtmlElement = - nav( - cls := "flex-1 min-h-0 overflow-y-auto", - aria.label := "Directory", - children <-- $m.split(_._1)((_, _, s) => - div( - cls := "relative", - Header(s.map(_._1)), - UserList(s.map(_._2)) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala deleted file mode 100644 index e182910..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala +++ /dev/null @@ -1,54 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object SearchForm: - sealed trait Action - case object SubmitFilter extends Action - sealed trait FilterAction extends Action - case object ClearFilter extends FilterAction - case class SetFilter(value: String) extends FilterAction - - def apply(actions: Observer[Action]): HtmlElement = - div( - cls := "px-6 pt-4 pb-4", - form( - cls := "flex space-x-4", - onSubmit.mapTo(SubmitFilter) --> actions, - div( - cls := "flex-1 min-w-0", - label( - forId := "search", - cls := "sr-only", - "Hledat" - ), - div( - cls := "relative rounded-md shadow-sm", - div( - cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", - Icons.solid.search().amend(svg.cls := "text-gray-400") - ), - input( - tpe := "search", - name := "search", - idAttr := "search", - cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", - placeholder := "Hledat", - composeEvents(onInput.mapToValue.setAsValue.map(SetFilter(_)))( - _.throttle(500) - ) --> actions - ) - ) - ), - button( - tpe := "submit", - cls := "inline-flex justify-center px-3.5 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500", - Icons.solid.filter().amend(svg.cls := "text-gray-400"), - span( - cls := "sr-only", - "Hledat" - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala deleted file mode 100644 index 82f7723..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala +++ /dev/null @@ -1,52 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.airstream.core.Signal -import fiftyforms.ui.components.tailwind.Avatar - -object UserRow: - case class ViewModel( - osobniCislo: String, - celeJmeno: String, - prijmeni: String, - hlavniFunkce: Option[String], - img: Option[String], - container: () => HtmlElement = () => div() - ) { - val search = prijmeni.toLowerCase - } - - def apply($m: Signal[ViewModel]): HtmlElement = - inline def avatarImage = - Avatar($m.map(_.img)).avatarImage(10) - - li( - div( - cls := "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500", - div( - cls := "flex-shrink-0", - child <-- avatarImage - ), - div( - cls := "flex-1 min-w-0", - child <-- $m.map { o => - o.container() - .amend( - cls := "focus:outline-none", - span( - cls := "absolute inset-0", - aria.hidden := true - ), - p( - cls := "text-sm font-medium text-gray-900", - o.celeJmeno - ), - p( - cls := "text-sm text-gray-500 truncate", - o.hlavniFunkce - ) - ) - } - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala deleted file mode 100644 index 467c5f5..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala +++ /dev/null @@ -1,99 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router - -object ErrorPage: - case class ViewModel( - homePage: Page, - errorName: String, - title: String, - subTitle: String - ) - - def apply(m: ViewModel, actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - val ViewModel(homePage, errorName, title, subTitle) = m - div( - cls := "min-h-full pt-16 pb-12 flex flex-col bg-white", - main( - cls := "flex-grow flex flex-col justify-center max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex-shrink-0 flex justify-center", - a( - href := "/", - cls := "inline-flex", - span( - cls := "sr-only", - """Workflow""" - ), - img( - cls := "h-12 w-auto", - src := "https://tailwindui.com/img/logos/workflow-mark.svg?color=indigo&shade=600", - alt := "" - ) - ) - ), - div( - cls := "py-16", - div( - cls := "text-center", - p( - cls := "text-sm font-semibold text-indigo-600 uppercase tracking-wide", - errorName - ), - h1( - cls := "mt-2 text-4xl font-extrabold text-gray-900 tracking-tight sm:text-5xl", - title - ), - p( - cls := "mt-2 text-base text-gray-500", - subTitle - ), - div( - cls := "mt-6", - PageLink - .container(homePage, actionBus) - .amend( - cls := "text-base font-medium text-indigo-600 hover:text-indigo-500", - """Go back home""" - ) - ) - ) - ) - ), - footer( - cls := "flex-shrink-0 max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", - nav( - cls := "flex justify-center space-x-4", - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Contact Support""" - ), - span( - cls := "inline-block border-l border-gray-300", - aria.hidden := true - ), - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Status""" - ), - span( - cls := "inline-block border-l border-gray-300", - aria.hidden := true - ), - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Twitter""" - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala deleted file mode 100644 index bd9baec..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action - -object NotFoundPage: - def apply(homePage: Page, url: String, actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - ErrorPage( - ErrorPage.ViewModel( - homePage, - "404 error", - "Page not found.", - s"Sorry, but page $url doesn't exist." - ), - actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala deleted file mode 100644 index 476fe3c..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action - -object UnhandledErrorPage: - - case class ViewModel( - homePage: Page, - errorName: Option[String], - errorMessage: Option[String] - ) - - def apply(m: ViewModel, actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - ErrorPage( - ErrorPage.ViewModel( - m.homePage, - "Unexpected error occurred", - m.errorName.getOrElse( - "Uh oh!" - ), // TODO: translations, better text than uh oh - m.errorMessage.getOrElse( - "This wasn't supposed to happen! Please try again." - ) - ), - actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala deleted file mode 100644 index 25f059f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala +++ /dev/null @@ -1,6 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.services - -import com.raquo.airstream.core.Observer - -trait DataFetcher[K, A]: - def fetch(id: K, sink: Observer[A]): Unit diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala deleted file mode 100644 index a469830..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala +++ /dev/null @@ -1,120 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package state - -import com.raquo.airstream.core.EventStream -import cz.e_bs.cmi.mdr.pdb.{UserInfo, OsobniCislo} -import com.raquo.airstream.core.Observer -import scala.scalajs.js -import scala.scalajs.js.JSON -import zio.json._ -import com.raquo.airstream.eventbus.EventBus -import com.raquo.airstream.ownership.Owner -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.UserFunction -import cz.e_bs.cmi.mdr.pdb.UserContract -import fiftyforms.services.files.File - -trait AppState - extends connectors.DetailPageConnector.AppState - with connectors.DetailParametruPageConnector.AppState - with connectors.DetailKriteriaPageConnector.AppState - with pages.detail.UpravDukaz.State: - def users: EventStream[List[UserInfo]] - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - -class MockAppState(implicit owner: Owner, router: Router[Page]) - extends AppState: - - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonDecoder[UserFunction] = DeriveJsonDecoder.gen - given JsonDecoder[UserContract] = DeriveJsonDecoder.gen - given JsonDecoder[UserInfo] = DeriveJsonDecoder.gen - - given JsonDecoder[ParameterCriteria] = DeriveJsonDecoder.gen - given JsonDecoder[Parameter] = DeriveJsonDecoder.gen - - private val actions = EventBus[Action]() - private val (parametersStream, pushParameters) = - EventStream.withCallback[List[Parameter]] - private val (usersStream, pushUsers) = - EventStream.withCallback[List[UserInfo]] - private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] - private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] - - private val mockData: List[UserInfo] = - mockUsers - .asInstanceOf[js.Dictionary[js.Object]] - .values - // TODO: is there a more efficient way to parse from JS object directly? - .map { o => - val parsed = JSON.stringify(o).fromJson[UserInfo] - parsed.left.foreach(org.scalajs.dom.console.log(_)) - parsed - } - .collect { case Right(u) => - u - } - .toList - - private val mockParameters: List[Parameter] = - pdbParams - .asInstanceOf[js.Dictionary[js.Object]] - .values - .map(o => JSON.stringify(o).fromJson[Parameter]) - .collect { case Right(p) => p } - .toList - - // TODO: Extract to separate event handler - actions.events.foreach { - case FetchDirectory => pushUsers(mockData) - case FetchUserDetails(osc) => - mockData.find(_.personalNumber == osc).foreach { o => - pushDetails(o) - router.replaceState(Page.Detail(o)) - } - case FetchParameters(osc) => - pushParameters(mockParameters) - case FetchParameter(osc, paramId) => - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(Page.DetailParametru(o, p)) - case FetchParameterCriteria(osc, paramId, critId, page) => - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - c <- p.criteria.find(_.id == critId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(page(o, p, c)) - case NavigateTo(page) => router.pushState(page) - case FetchAvailableFiles(osc) => - pushFiles( - List( - File("https://tc163.cmi.cz/here", "Example file") - ) - ) - } - - override def users: EventStream[List[UserInfo]] = - usersStream.debugWithName("users") - - override def details: EventStream[UserInfo] = - detailsStream.debugWithName("details") - - override def parameters: EventStream[List[Parameter]] = - parametersStream.debugWithName("parameters") - - override def availableFiles: EventStream[List[File]] = - filesStream.debugWithName("available files") - - override def actionBus: Observer[Action] = - actions.writer.debugWithName("actions writer") diff --git a/app/src/main/scala/mdr/pdb/app/Main.scala b/app/src/main/scala/mdr/pdb/app/Main.scala new file mode 100644 index 0000000..081b4ee --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/Main.scala @@ -0,0 +1,103 @@ +package mdr.pdb.app + +import scala.scalajs.js.annotation.JSExportTopLevel +import scala.scalajs.js.annotation.JSExport +import scala.scalajs.js.annotation.JSImport +import scala.scalajs.js +import org.scalajs.dom +import com.raquo.laminar.api.L.{*, given} +import scala.scalajs.js.Date +import com.raquo.waypoint.Router +import com.raquo.waypoint.SplitRender +import mdr.pdb.app.services.DataFetcher +import scala.scalajs.js.JSON +import zio.json._ +import mdr.pdb.UserInfo +import mdr.pdb.app.state.AppState + +@js.native +@JSImport("stylesheets/main.css", JSImport.Namespace) +object Css extends js.Any + +@js.native +@JSImport("data/users.json", JSImport.Default) +object mockUsers extends js.Object + +@js.native +@JSImport("params/pdb-params.json", JSImport.Default) +object pdbParams extends js.Object + +@JSExportTopLevel("app") +object Main: + + @JSExport + def main(args: Array[String]): Unit = + import Routes.given + onLoad { + setupAirstream() + val appContainer = dom.document.querySelector("#app") + val _ = + render( + appContainer, + renderPage(state.MockAppState(using unsafeWindowOwner, router)) + ) + } + + private def onLoad(f: => Unit): Unit = + documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) + + private def setupAirstream()(using router: Router[Page]): Unit = + AirstreamError.registerUnhandledErrorCallback(err => + router.forcePage( + Page.UnhandledError( + Some(err.getClass.getName), // TODO: Fill only in dev mode + Some(err.getMessage) + ) + ) + ) + + def renderPage(state: AppState)(using + router: Router[Page] + ): HtmlElement = + val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) + .collectSignal[Page.Detail]( + connectors + .DetailPageConnector(state)(_) + .apply + ) + .collectSignal[Page.DetailParametru]( + connectors + .DetailParametruPageConnector(state)(_) + .apply + ) + .collectSignal[Page.DetailKriteria]( + connectors + .DetailKriteriaPageConnector(state)(_) + .apply + ) + .collectSignal[Page.UpravDukazKriteria]( + pages.detail.UpravDukaz.Connector(state)(_).apply + ) + .collectStatic(Page.Dashboard)( + connectors.DashboardPageConnector(state.actionBus).apply + ) + .collect[Page.NotFound](pg => + pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) + ) + .collect[Page.UnhandledError](pg => + pages.errors + .UnhandledErrorPage( + pages.errors.UnhandledErrorPage + .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), + state.actionBus + ) + ) + .collectStatic(Page.Directory)( + connectors + .DirectoryPageConnector(state.users, state.actionBus) + .apply + ) + div(cls := "h-full", child <-- pageSplitter.$view) + + // Pull in the stylesheet + val css: Css.type = Css diff --git a/app/src/main/scala/mdr/pdb/app/Routes.scala b/app/src/main/scala/mdr/pdb/app/Routes.scala new file mode 100644 index 0000000..2cc860c --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/Routes.scala @@ -0,0 +1,198 @@ +package mdr.pdb.app + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.* +import org.scalajs.dom +import zio.json.{*, given} +import mdr.pdb.OsobniCislo + +import scala.scalajs.js +import mdr.pdb.UserInfo +import mdr.pdb.Parameter +import mdr.pdb.ParameterCriteria +import mdr.pdb.app.Page.Titled + +// enum is not working with Waypoints' SplitRender collectStatic +sealed abstract class Page( + val id: String, + val title: String, + val parent: Option[Page] +) { + val path: Vector[Page] = + parent match + case None => Vector(this) + case Some(p) => p.path :+ this + + val isRoot: Boolean = parent.isEmpty +} + +object Page: + + case class Titled[V](value: V, title: Option[String] = None): + val show: String = title.getOrElse(value.toString) + + case object Directory extends Page("directory", "Adresář", None) + + case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) + + case class Detail(osobniCislo: Titled[OsobniCislo]) + extends Page("user", osobniCislo.show, Some(Directory)) + + object Detail { + def apply(o: UserInfo): Detail = Detail( + Titled(o.personalNumber, Some(o.name)) + ) + } + + case class DetailParametru( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String] + ) extends Page( + "parameter", + parametr.show, + Some(Detail(osobniCislo)) + ) + + object DetailParametru { + def apply(o: UserInfo, p: Parameter): DetailParametru = + DetailParametru( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)) + ) + } + + case class DetailKriteria( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String], + kriterium: Titled[String] + ) extends Page( + "criteria", + kriterium.show, + Some(DetailParametru(osobniCislo, parametr)) + ) + + object DetailKriteria { + def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = + DetailKriteria( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)), + Titled(k.id, Some(k.id)) + ) + } + + case class UpravDukazKriteria( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String], + kriterium: Titled[String] + ) extends Page( + "addProof", + "Důkaz", + Some(DetailKriteria(osobniCislo, parametr, kriterium)) + ) + + object UpravDukazKriteria { + def apply( + o: UserInfo, + p: Parameter, + k: ParameterCriteria + ): UpravDukazKriteria = + UpravDukazKriteria( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)), + Titled(k.id, Some(k.id)) + ) + } + + case class NotFound(url: String) extends Page("404", "404", Some(Directory)) + + case class UnhandledError( + errorName: Option[String], + errorMessage: Option[String] + ) extends Page("500", "Unexpected error", Some(Directory)) + +object Routes: + given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) + given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) + given [V: JsonEncoder]: JsonEncoder[Titled[V]] = + DeriveJsonEncoder.gen[Titled[V]] + given [V: JsonDecoder]: JsonDecoder[Titled[V]] = + DeriveJsonDecoder.gen[Titled[V]] + given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] + given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] + + val base = + js.`import`.meta.env.BASE_URL + .asInstanceOf[String] + "app" + + val homePage: Page = Page.Directory + + given router: Router[Page] = Router[Page]( + routes = List( + Route.static(homePage, root / endOfSegments, basePath = base), + Route.static( + Page.Dashboard, + root / "dashboard" / endOfSegments, + basePath = base + ), + Route[Page.Detail, String]( + encode = _.osobniCislo.value.toString, + decode = osc => Page.Detail(Titled(OsobniCislo(osc))), + root / "osoba" / segment[String] / endOfSegments, + basePath = base + ), + Route[Page.DetailParametru, (String, String)]( + encode = p => (p.osobniCislo.value.toString, p.parametr.value), + decode = + p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / endOfSegments, + basePath = base + ), + Route[Page.DetailKriteria, (String, String, String)]( + encode = p => + ( + p.osobniCislo.value.toString, + p.parametr.value, + p.kriterium.value.replaceAll("\\.", "--") + ), + decode = p => + Page.DetailKriteria( + Titled(OsobniCislo(p._1)), + Titled(p._2), + Titled(p._3.replaceAll("--", ".")) + ), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / "kriterium" / segment[String] / endOfSegments, + basePath = base + ), + Route[Page.UpravDukazKriteria, (String, String, String)]( + encode = p => + ( + p.osobniCislo.value.toString, + p.parametr.value, + p.kriterium.value.replaceAll("\\.", "--") + ), + decode = p => + Page.UpravDukazKriteria( + Titled(OsobniCislo(p._1)), + Titled(p._2), + Titled(p._3.replaceAll("--", ".")) + ), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / "kriterium" / segment[String] / "edit" / endOfSegments, + basePath = base + ) + ), + serializePage = _.toJson, + deserializePage = _.fromJson[Page] + .fold(s => throw IllegalStateException(s), identity), + getPageTitle = _.title, + routeFallback = url => Page.NotFound(url), + deserializeFallback = _ => Page.Dashboard + )( + $popStateEvent = windowEvents.onPopState, + owner = unsafeWindowOwner + ) diff --git a/app/src/main/scala/mdr/pdb/app/actions.scala b/app/src/main/scala/mdr/pdb/app/actions.scala new file mode 100644 index 0000000..d078b3a --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/actions.scala @@ -0,0 +1,21 @@ +package mdr.pdb.app + +import mdr.pdb.OsobniCislo +import mdr.pdb.UserInfo +import mdr.pdb.Parameter +import mdr.pdb.ParameterCriteria + +sealed trait Action + +case object FetchDirectory extends Action +case class FetchUserDetails(osc: OsobniCislo) extends Action +case class FetchParameters(osc: OsobniCislo) extends Action +case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action +case class FetchParameterCriteria( + osc: OsobniCislo, + paramId: String, + critId: String, + page: (UserInfo, Parameter, ParameterCriteria) => Page +) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action +case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/mdr/pdb/app/components/AppPage.scala new file mode 100644 index 0000000..130c817 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/AppPage.scala @@ -0,0 +1,82 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.Page +import mdr.pdb.{UserProfile, UserInfo, OsobniCislo} +import com.raquo.waypoint.Router +import mdr.pdb.app.Action +import mdr.pdb.app.NavigateTo +import mdr.pdb.UserFunction + +object AppPage: + // TODO: pages by logged in user + val pages: List[Page] = List(Page.Directory, Page.Dashboard) + + import NavigationBar.{Logo, MenuItem} + + val logo = Logo( + "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", + "Workflow" + ) + + // TODO: menu items by user profile + val userMenu = + List( + MenuItem("Your Profile"), + MenuItem("Settings"), + MenuItem("Sign out") + ) + + // TODO: load user profile + val $userProfile = Var( + UserProfile( + "tom", + UserInfo( + OsobniCislo("1031"), + "tom", + "Tom", + "Cook", + None, + None, + Some("tom@example.com"), + Some("+420 222 866 180"), + Some( + UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") + ), + Nil, + None + ) + ) + ) + + val $userInfo = $userProfile.signal.map(_.userInfo) + + type ViewModel = Option[HtmlElement] + def apply( + actionBus: Observer[Action] + )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using + router: Router[Page] + ): HtmlElement = + PageLayout(actionBus)( + $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => + PageLayout.ViewModel( + NavigationBar.ViewModel( + u, + pages.map(p => + NavigationBar.Link( + () => + PageLink( + p, + actionBus + ), + p == cp + ) + ), + userMenu, + logo + ), + c + ) + ), + mods + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala deleted file mode 100644 index a3b00af..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala +++ /dev/null @@ -1,103 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import scala.scalajs.js.annotation.JSExportTopLevel -import scala.scalajs.js.annotation.JSExport -import scala.scalajs.js.annotation.JSImport -import scala.scalajs.js -import org.scalajs.dom -import com.raquo.laminar.api.L.{*, given} -import scala.scalajs.js.Date -import com.raquo.waypoint.Router -import com.raquo.waypoint.SplitRender -import cz.e_bs.cmi.mdr.pdb.app.services.DataFetcher -import scala.scalajs.js.JSON -import zio.json._ -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.state.AppState - -@js.native -@JSImport("stylesheets/main.css", JSImport.Namespace) -object Css extends js.Any - -@js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native -@JSImport("params/pdb-params.json", JSImport.Default) -object pdbParams extends js.Object - -@JSExportTopLevel("app") -object Main: - - @JSExport - def main(args: Array[String]): Unit = - import Routes.given - onLoad { - setupAirstream() - val appContainer = dom.document.querySelector("#app") - val _ = - render( - appContainer, - renderPage(state.MockAppState(using unsafeWindowOwner, router)) - ) - } - - private def onLoad(f: => Unit): Unit = - documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) - - private def setupAirstream()(using router: Router[Page]): Unit = - AirstreamError.registerUnhandledErrorCallback(err => - router.forcePage( - Page.UnhandledError( - Some(err.getClass.getName), // TODO: Fill only in dev mode - Some(err.getMessage) - ) - ) - ) - - def renderPage(state: AppState)(using - router: Router[Page] - ): HtmlElement = - val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) - .collectSignal[Page.Detail]( - connectors - .DetailPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailParametru]( - connectors - .DetailParametruPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailKriteria]( - connectors - .DetailKriteriaPageConnector(state)(_) - .apply - ) - .collectSignal[Page.UpravDukazKriteria]( - pages.detail.UpravDukaz.Connector(state)(_).apply - ) - .collectStatic(Page.Dashboard)( - connectors.DashboardPageConnector(state.actionBus).apply - ) - .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) - ) - .collect[Page.UnhandledError](pg => - pages.errors - .UnhandledErrorPage( - pages.errors.UnhandledErrorPage - .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), - state.actionBus - ) - ) - .collectStatic(Page.Directory)( - connectors - .DirectoryPageConnector(state.users, state.actionBus) - .apply - ) - div(cls := "h-full", child <-- pageSplitter.$view) - - // Pull in the stylesheet - val css: Css.type = Css diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala deleted file mode 100644 index 3bbc8c0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ /dev/null @@ -1,198 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.* -import org.scalajs.dom -import zio.json.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import scala.scalajs.js -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.Page.Titled - -// enum is not working with Waypoints' SplitRender collectStatic -sealed abstract class Page( - val id: String, - val title: String, - val parent: Option[Page] -) { - val path: Vector[Page] = - parent match - case None => Vector(this) - case Some(p) => p.path :+ this - - val isRoot: Boolean = parent.isEmpty -} - -object Page: - - case class Titled[V](value: V, title: Option[String] = None): - val show: String = title.getOrElse(value.toString) - - case object Directory extends Page("directory", "Adresář", None) - - case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) - - case class Detail(osobniCislo: Titled[OsobniCislo]) - extends Page("user", osobniCislo.show, Some(Directory)) - - object Detail { - def apply(o: UserInfo): Detail = Detail( - Titled(o.personalNumber, Some(o.name)) - ) - } - - case class DetailParametru( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String] - ) extends Page( - "parameter", - parametr.show, - Some(Detail(osobniCislo)) - ) - - object DetailParametru { - def apply(o: UserInfo, p: Parameter): DetailParametru = - DetailParametru( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)) - ) - } - - case class DetailKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "criteria", - kriterium.show, - Some(DetailParametru(osobniCislo, parametr)) - ) - - object DetailKriteria { - def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = - DetailKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class UpravDukazKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "addProof", - "Důkaz", - Some(DetailKriteria(osobniCislo, parametr, kriterium)) - ) - - object UpravDukazKriteria { - def apply( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): UpravDukazKriteria = - UpravDukazKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class NotFound(url: String) extends Page("404", "404", Some(Directory)) - - case class UnhandledError( - errorName: Option[String], - errorMessage: Option[String] - ) extends Page("500", "Unexpected error", Some(Directory)) - -object Routes: - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) - given [V: JsonEncoder]: JsonEncoder[Titled[V]] = - DeriveJsonEncoder.gen[Titled[V]] - given [V: JsonDecoder]: JsonDecoder[Titled[V]] = - DeriveJsonDecoder.gen[Titled[V]] - given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] - given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] - - val base = - js.`import`.meta.env.BASE_URL - .asInstanceOf[String] + "app" - - val homePage: Page = Page.Directory - - given router: Router[Page] = Router[Page]( - routes = List( - Route.static(homePage, root / endOfSegments, basePath = base), - Route.static( - Page.Dashboard, - root / "dashboard" / endOfSegments, - basePath = base - ), - Route[Page.Detail, String]( - encode = _.osobniCislo.value.toString, - decode = osc => Page.Detail(Titled(OsobniCislo(osc))), - root / "osoba" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.DetailParametru, (String, String)]( - encode = p => (p.osobniCislo.value.toString, p.parametr.value), - decode = - p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / endOfSegments, - basePath = base - ), - Route[Page.DetailKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.DetailKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.UpravDukazKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.UpravDukazKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / "edit" / endOfSegments, - basePath = base - ) - ), - serializePage = _.toJson, - deserializePage = _.fromJson[Page] - .fold(s => throw IllegalStateException(s), identity), - getPageTitle = _.title, - routeFallback = url => Page.NotFound(url), - deserializeFallback = _ => Page.Dashboard - )( - $popStateEvent = windowEvents.onPopState, - owner = unsafeWindowOwner - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala deleted file mode 100644 index b24b82b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -sealed trait Action - -case object FetchDirectory extends Action -case class FetchUserDetails(osc: OsobniCislo) extends Action -case class FetchParameters(osc: OsobniCislo) extends Action -case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action -case class FetchParameterCriteria( - osc: OsobniCislo, - paramId: String, - critId: String, - page: (UserInfo, Parameter, ParameterCriteria) => Page -) extends Action -case class FetchAvailableFiles(osc: OsobniCislo) extends Action -case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala deleted file mode 100644 index c637d35..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala +++ /dev/null @@ -1,82 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.{UserProfile, UserInfo, OsobniCislo} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo -import cz.e_bs.cmi.mdr.pdb.UserFunction - -object AppPage: - // TODO: pages by logged in user - val pages: List[Page] = List(Page.Directory, Page.Dashboard) - - import NavigationBar.{Logo, MenuItem} - - val logo = Logo( - "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", - "Workflow" - ) - - // TODO: menu items by user profile - val userMenu = - List( - MenuItem("Your Profile"), - MenuItem("Settings"), - MenuItem("Sign out") - ) - - // TODO: load user profile - val $userProfile = Var( - UserProfile( - "tom", - UserInfo( - OsobniCislo("1031"), - "tom", - "Tom", - "Cook", - None, - None, - Some("tom@example.com"), - Some("+420 222 866 180"), - Some( - UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") - ), - Nil, - None - ) - ) - ) - - val $userInfo = $userProfile.signal.map(_.userInfo) - - type ViewModel = Option[HtmlElement] - def apply( - actionBus: Observer[Action] - )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using - router: Router[Page] - ): HtmlElement = - PageLayout(actionBus)( - $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => - PageLayout.ViewModel( - NavigationBar.ViewModel( - u, - pages.map(p => - NavigationBar.Link( - () => - PageLink( - p, - actionBus - ), - p == cp - ) - ), - userMenu, - logo - ), - c - ) - ), - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala deleted file mode 100644 index bf473c6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ /dev/null @@ -1,143 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden -import fiftyforms.ui.components.tailwind.list.IconText.ViewModel -import fiftyforms.ui.components.tailwind.Icons -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Action - -object Breadcrumbs: - - private def slash = - import svg.{*, given} - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-300", - xmlns := "http://www.w3.org/2000/svg", - fill := "currentColor", - viewBox := "0 0 20 20", - ariaHidden := true, - path( - d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" - ) - ) - - def backIcon = - Icons.solid - .`arrow-narrow-left`() - .amend( - svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" - ) - - object Link: - case class ViewModel( - page: Page, - icon: Option[SvgElement], - extraClasses: String, - text: String, - textClasses: Option[String] = None - ) - - val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" - - def shortHome(p: Page) = ViewModel( - p, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - "Zpět na úvodní stránku", - None - ) - - def fullHome(p: Page) = ViewModel( - p, - Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), - "text-gray-400 hover:text-gray-500", - "Domů", - Some("sr-only") - ) - - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - PageLink - .container($m.map(_.page), actionBus) - .amend( - cls <-- $m.map(_.extraClasses), - child.maybe <-- $m.map(_.icon), - span( - cls <-- $m.map(_.textClasses.getOrElse("")), - child.text <-- $m.map(_.text) - ) - ) - - object FullBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - ol( - role := "list", - cls := "flex items-center space-x-4", - children <-- $m.map(_.path).split(_.id)((_, _, $p) => - li( - div( - cls := "flex items-center", - child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), - Link( - $p.map(p => - if (p.isRoot) then Link.fullHome(p) - else - Link.ViewModel( - p, - None, - s"ml-4 max-w-xs truncate ${Link.baseClasses}", - p.title - ) - ), - actionBus - ) - ) - ) - ) - ) - - object ShortBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - Link( - $m.map { p => - val target = p.parent.getOrElse(p) - if target.isRoot then Link.shortHome(target) - else - Link.ViewModel( - target, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - s"Zpět na ${target.title}" - ) - }, - actionBus - ) - - def apply(actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - nav( - cls := "flex", - aria.label := "Breadcrumb", - div( - cls := "flex sm:hidden", - ShortBreadcrumbs(router.$currentPage, actionBus) - ), - div( - cls := "hidden sm:block", - FullBreadcrumbs(router.$currentPage, actionBus) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala deleted file mode 100644 index 0db2047..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ /dev/null @@ -1,242 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent -import fiftyforms.ui.components.tailwind.Icons -import fiftyforms.ui.components.tailwind.Avatar -import cz.e_bs.cmi.mdr.pdb.UserInfo -import io.laminext.syntax.core.* - -object NavigationBar: - - case class Logo(img: String, name: String) - case class Link(a: () => Anchor, active: Boolean) - case class MenuItem(title: String) - - case class ViewModel( - userInfo: UserInfo, - pages: List[Link], - userMenu: List[MenuItem], - logo: Logo - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - val $userInfo = $m.map(_.userInfo) - val mobileMenuOpen = Var(false) - - val desktopOnly = cls("hidden md:block") - val mobileOnly = cls("md:hidden") - - inline def avatarImage(size: Int = 8) = - Avatar($userInfo.map(_.img)).avatarImage(size) - - def notificationButton = button( - tpe := "button", - cls := List( - "bg-indigo-600", - "focus:outline-none", - "focus:ring-2", - "focus:ring-offset-2", - "focus:ring-offset-indigo-600", - "focus:ring-white", - "hover:text-white", - "p-1", - "rounded-full", - "text-indigo-200" - ), - span(cls := "sr-only", "View notifications"), - Icons.outline.bell() - ) - - def userProfile: HtmlElement = - val menuOpen = Var(false) - - def menuItem(item: MenuItem, idx: Int): HtmlElement = - a( - href := "#", - cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", - role := "menuitem", - tabIndex := -1, - idAttr := s"user-menu-item-$idx", - item.title - ) - - div( - cls := "ml-3 relative", - div( - button( - tpe := "button", - cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - idAttr := "user-menu-button", - aria.expanded <-- menuOpen.signal, - aria.hasPopup := true, - span(cls := "sr-only", "Open user menu"), - child <-- avatarImage(), - onClick.preventDefault.mapTo( - !menuOpen.now() - ) --> menuOpen.writer - ) - ), - /* - * */ - div( - cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", - cls <-- menuOpen.signal.map { o => - if (o) "md:block" else "md:hidden" - }, - role := "menu", - aria.orientation := "vertical", - aria.labelledBy := "user-menu-button", - tabIndex := -1, - // : keyboard navigation - children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) - ) - ) - - def mobileProfile = - def menuItem(item: MenuItem): HtmlElement = - a( - href := "#", - cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", - item.title - ) - - div( - cls := "pt-4 pb-3 border-t border-indigo-700", - div( - cls := "flex items-center px-5", - div( - cls := "flex-shrink-0", - child <-- avatarImage(10) - ), - div( - cls := "ml-3", - div( - cls := "text-base font-medium text-white", - child.text <-- $userInfo.map(_.name) - ), - child.maybe <-- $userInfo.map( - _.email.map(e => - div( - cls := "text-sm font-medium text-indigo-300", - e - ) - ) - ) - ), - notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) - ), - div( - cls := "mt-3 px-2 space-y-1", - children <-- $m.map(_.userMenu.map(menuItem)) - ) - ) - - def pageLink(page: Link): Anchor = - page - .a() - .amend( - cls := "text-white px-3 py-2 rounded-md text-sm font-medium", - cls := Seq( - "bg-indigo-700" -> page.active, - "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active - ), - ariaCurrent := (if page.active then "page" else "false") - ) - - def logoImg: Image = - img( - cls := "h-8 w-8", - src <-- $m.map(_.logo.img), - alt <-- $m.map(_.logo.name) - ) - - def mobileMenuButton = button( - tpe := "button", - cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - aria.controls := "mobile-menu", - aria.expanded <-- mobileMenuOpen.signal, - span(cls := "sr-only", "Open main menu"), - Icons.outline - .menu() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") - ), - Icons.outline - .x() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") - ), - onClick.preventDefault.mapTo( - !mobileMenuOpen.now() - ) --> mobileMenuOpen.writer - ) - - def navBarLeft = - div( - cls := "flex items-center", - div(cls := "flex-shrink-0", logoImg), - div( - desktopOnly, - div( - cls := "ml-10 flex items-baseline space-x-4", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ) - ) - ) - - def navBarRight = - div( - desktopOnly, - div( - cls := "ml-4 flex items-center md:ml-6", - notificationButton, - userProfile - ) - ) - - def navBarMobile = - div( - cls := "-mr-2 flex", - mobileOnly, - mobileMenuButton - ) - - def navBar = - div( - cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex items-center justify-between h-16", - navBarLeft, - navBarRight, - navBarMobile - ) - ) - - def mobileMenu = - div( - mobileOnly, - cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }, - idAttr := "mobile-menu", - div( - cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ), - mobileProfile - ) - - nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala deleted file mode 100644 index 75339df..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object PageHeader: - def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = - header( - cls := "bg-white shadow-sm", - div( - cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", - h1( - cls := "text-lg leading-6 font-semibold text-gray-900", - Breadcrumbs(actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala deleted file mode 100644 index 5096463..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ /dev/null @@ -1,30 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import fiftyforms.ui.components.tailwind.Loading -import io.laminext.syntax.core.* - -object PageLayout: - case class ViewModel( - navigation: NavigationBar.ViewModel, - content: Option[HtmlElement] - ) - def apply(actionBus: Observer[Action])( - $m: Signal[ViewModel], - mods: Modifier[HtmlElement]* - )(using router: Router[Page]): HtmlElement = - val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) - div( - cls := "h-full flex flex-col", - NavigationBar($m.map(_.navigation)), - child.maybe <-- router.$currentPage.map(_.isRoot) - .switch(None, Some(PageHeader(actionBus))), - main( - cls := "flex-grow-1 overflow-y-auto", - mods, - child <-- $maybeContent.map(_.getOrElse(Loading)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala deleted file mode 100644 index 62d820f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.LinkSupport.* -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo - -object PageLink: - type ViewModel = Page - def apply($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = - a(mods($m, actions), child.text <-- $m.map(_.title)) - - def apply(m: ViewModel, actions: Observer[Action])(using - router: Router[Page] - ): Anchor = apply(Val(m), actions) - - def container($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = a(mods($m, actions)) - - def container(m: ViewModel, actions: Observer[Action])(using - Router[Page] - ): Anchor = container(Val(m), actions) - - private def mods($m: Signal[Page], actions: Observer[Action])(using - router: Router[Page] - ): Modifier[Anchor] = - Seq( - href <-- $m.map(router.absoluteUrlForPage).recover { case _ => - Some("invalid url") - }, - composeEvents(onClick.noKeyMod.preventDefault)( - _.sample($m) - .map(NavigateTo.apply) - ) --> actions - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala deleted file mode 100644 index 87f825a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.pages.dashboard.DashboardPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -class DashboardPageConnector(actionBus: Observer[Action])(using - router: Router[Page] -): - def apply: HtmlElement = - AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala deleted file mode 100644 index e9b0f71..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala +++ /dev/null @@ -1,76 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailKriteriaPage -import pages.detail.components.DukazyKriteria -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -object DetailKriteriaPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailKriteriaPageConnector( - state: DetailKriteriaPageConnector.AppState -)( - $page: Signal[Page.DetailKriteria] -)(using Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => - (p.osobniCislo.value, p.parametr.value, p.kriterium.value) - )((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map( - FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) - ) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, s, $s) => - DetailKriteriaPage($s)(state.actionBus.contramap { - case DukazyKriteria.Add => - NavigateTo( - Page.UpravDukazKriteria( - Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), - Page.Titled(s.parametr.id, Some(s.parametr.nazev)), - Page.Titled(s.kriterium.id, Some(s.kriterium.id)) - ) - ) - }) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): DetailKriteriaPage.ViewModel = - DetailKriteriaPage.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala deleted file mode 100644 index 4d8024f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ /dev/null @@ -1,57 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import pages.detail.DetailPage -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import fiftyforms.ui.components.tailwind.Color - -object DetailPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailPageConnector(state: DetailPageConnector.AppState)( - $page: Signal[Page.Detail] -)(using router: Router[Page]): - val $oscChangeSignal = $page.splitOne(_.osobniCislo.value)((osc, _, _) => osc) - val $pageChangeSignal = - $oscChangeSignal.flatMap(osc => - EventStream.fromSeq(Seq(FetchUserDetails(osc), FetchParameters(osc))) - ) - // TODO: filter the value based on the current osc - // OSC change will fetch new data, but still - // - we need to be sure that what we got is really what we ought to display - // - we want to display stale data accordingly (at least with loading indicator) - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - def apply: HtmlElement = - AppPage(state.actionBus)( - $data.combineWithFn($params)(_ zip _) - .map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: List[Parameter] - ): DetailPage.ViewModel = - DetailPage.ViewModel( - o.toDetailOsoby, - p.map( - _.toParametr(param => - PageLink.container(Page.DetailParametru(o, param), state.actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala deleted file mode 100644 index fdaa7f8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailParametruPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink - -object DetailParametruPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailParametruPageConnector( - state: DetailParametruPageConnector.AppState -)( - $page: Signal[Page.DetailParametru] -)(using router: Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => (p.osobniCislo.value, p.parametr.value))((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map(FetchParameter(_, _)) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - } yield (da, pb) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailParametruPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter - ): DetailParametruPage.ViewModel = - DetailParametruPage.ViewModel( - o.toDetailOsoby, - p.toParametr(p => - PageLink.container(Page.DetailParametru(o, p), state.actionBus) - ), - p.criteria.map( - _.toKriterium { c => - PageLink.container( - Page.DetailKriteria(o, p, c), - state.actionBus - ) - } - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala deleted file mode 100644 index 049aa22..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -case class DirectoryPageConnector( - $input: EventStream[List[UserInfo]], - actionBus: Observer[Action] -)(using router: Router[Page]): - val $data = $input.startWithNone - val $actionSignal = EventStream.fromValue(FetchDirectory) - - def apply: HtmlElement = - AppPage(actionBus)( - $data.split(_ => ())((_, _, s) => - pages.directory.DirectoryPage( - s.map( - _.map( - _.toUserRow(u => - PageLink.container( - Page.Detail(Page.Titled(u.personalNumber)), - actionBus - ) - ) - ) - ) - ) - ), - $actionSignal --> actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala deleted file mode 100644 index 88e7450..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ /dev/null @@ -1,68 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii -import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria -import fiftyforms.ui.components.tailwind.Color - -extension (o: UserInfo) - def toDetailOsoby: DetailOsoby.ViewModel = - DetailOsoby.ViewModel( - o.personalNumber, - o.name, - o.email, - o.phone, - o.img, - o.mainFunction.map(f => - DetailOsoby.Funkce.ViewModel(f.name, f.dept, f.ou) - ), - o.userContracts.headOption.map(c => - DetailOsoby.PracovniPomer.ViewModel(c.rel, c.startDate, c.endDate) - ) - ) - -extension (param: Parameter) - def toParametr(container: Parameter => Anchor): DetailParametru.ViewModel = - DetailParametru.ViewModel( - id = param.id, - nazev = param.name, - popis = param.description, - status = "Nesplněno", - statusColor = Color.red, - a = container(param) - ) - -extension (crit: ParameterCriteria) - def toKriterium( - container: ParameterCriteria => Anchor - ): DetailKriteria.ViewModel = - DetailKriteria.ViewModel( - nazev = crit.criteriumText, - kapitola = crit.chapterId, - bod = crit.itemId, - status = "Nesplněno", - statusColor = Color.red, - splneno = false, - dukazy = Nil, - container = container(crit) - ) - -extension (user: UserInfo) - def toUserRow( - container: UserInfo => HtmlElement = (_: UserInfo) => div() - ): UserRow.ViewModel = - UserRow.ViewModel( - osobniCislo = user.personalNumber.toString, - celeJmeno = user.name, - prijmeni = user.surname, - hlavniFunkce = user.mainFunction.map(_.name), - img = user.img, - container = () => container(user) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala deleted file mode 100644 index 7d5b2c1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.dashboard - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.Page - -object DashboardPage: - - def render: HtmlElement = - div("Dashboard page") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala deleted file mode 100644 index 9ef0698..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala +++ /dev/null @@ -1,31 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailKriteriaPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply($m: Signal[ViewModel])( - action: Observer[DukazyKriteria.Action] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - DukazyKriteria($m.map(_.kriterium.dukazy))(action) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala deleted file mode 100644 index 3b316e1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object DetailPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametry: SeznamParametru.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby($m.map(_.osoba)), - SeznamParametru($m.map(_.parametry)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala deleted file mode 100644 index 17224cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala +++ /dev/null @@ -1,24 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailParametruPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriteria: SeznamKriterii.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby.header($m.map(_.osoba)), - DetailParametru($m.map(_.parametr)), - SeznamKriterii($m.map(_.kriteria)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala deleted file mode 100644 index dc912fa..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ /dev/null @@ -1,107 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb -package app -package pages.detail - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.UpravDukazForm -import fiftyforms.services.files.File - -object UpravDukaz: - - type ThisPage = Page.UpravDukazKriteria - type PageKey = (OsobniCislo, String, String) - - trait State { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def availableFiles: EventStream[List[File]] - def actionBus: Observer[Action] - } - - def keyOfPage(page: ThisPage): PageKey = - (page.osobniCislo.value, page.parametr.value, page.kriterium.value) - - def onChangeAction(key: PageKey): Action = - FetchParameterCriteria( - key._1, - key._2, - key._3, - Page.UpravDukazKriteria(_, _, _) - ) - - class Connector(state: State)($page: Signal[ThisPage])(using Router[Page]): - val $paramChangeSignal = $page.splitOne(keyOfPage)((x, _, _) => x) - val $pageChangeSignal = $paramChangeSignal.map(onChangeAction) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.split(_ => ())((_, s, $s) => - PageComponent( - $s.map(buildModel), - state.availableFiles, - state.actionBus.contramap { - case UpravDukazForm.Cancelled => - NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) - case UpravDukazForm.AvailableFilesRequested => - FetchAvailableFiles(s._1.personalNumber) - } - ) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): PageComponent.ViewModel = - import connectors.* - PageComponent.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) - - object PageComponent: - import components.* - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply( - $m: Signal[ViewModel], - availableFilesStream: EventStream[List[File]], - events: Observer[UpravDukazForm.Event] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - UpravDukazForm(availableFilesStream)(events) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala deleted file mode 100644 index ca311b0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.Icons -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Color - -object DetailKriteria: - case class ViewModel( - nazev: String, - kapitola: String, - bod: String, - status: String, - statusColor: Color, - splneno: Boolean, - dukazy: List[DukazKriteria.ViewModel], - container: HtmlElement = div() - ) { - val id = s"${kapitola}${bod}" - } - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h3( - cls := "text-xl leading-6 font-bold text-gray-900", - child.text <-- $m.map(_.nazev) - ), - h3( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - "Kapitola ", - child.text <-- $m.map(_.kapitola), - " bod ", - child <-- $m.map(_.bod) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala deleted file mode 100644 index ae90290..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala +++ /dev/null @@ -1,100 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Avatar - -object DetailOsoby: - - object Funkce: - case class ViewModel( - nazev: String, - stredisko: String, - voj: String - ) - - def render($m: Signal[ViewModel]): HtmlElement = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.nazev), - span( - cls := "hidden md:inline", - " @ ", - child.text <-- $m.map(_.stredisko), - ", ", - child.text <-- $m.map(_.voj) - ) - ) - - object PracovniPomer: - case class ViewModel( - druh: String, - pocatek: LocalDate, - konec: Option[LocalDate] - ) - - def render($m: Signal[ViewModel]): HtmlElement = - import fiftyforms.ui.components.tailwind.CustomAttrs.datetime - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.druh), - " od ", - time( - datetime <-- $m.map(_.pocatek.toString), - child.text <-- $m.map(_.pocatek.toString) - ) - ) - - case class ViewModel( - osobniCislo: OsobniCislo, - jmeno: String, - email: Option[String], - telefon: Option[String], - img: Option[String], - hlavniFunkce: Option[Funkce.ViewModel], - pracovniPomer: Option[PracovniPomer.ViewModel] - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-4", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(16) - ), - div( - h1( - cls := "text-2xl font-bold text-gray-900", - child.text <-- $m.map(_.jmeno) - ), - child.maybe <-- $m.map(_.hlavniFunkce).split(_ => ())((_, _, d) => - Funkce.render(d) - ), - child.maybe <-- $m.map(_.pracovniPomer).split(_ => ())((_, _, d) => - PracovniPomer.render(d) - ) - ) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-5", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(8) - ), - div( - h1( - cls := "text-lg font-medium text-gray-800", - child.text <-- $m.map(_.jmeno) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala deleted file mode 100644 index e11cb62..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color - -object DetailParametru: - case class ViewModel( - id: String, - nazev: String, - popis: String, - status: String, - statusColor: Color, - a: Anchor - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h2( - cls := "text-2xl leading-6 font-medium text-gray-900", - child.text <-- $m.map(_.nazev) - ), - p( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - child.text <-- $m.map(_.popis) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - h2( - cls := "text-lg leading-6 font-medium text-gray-600", - child.text <-- $m.map(_.nazev) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala deleted file mode 100644 index a00ede8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala +++ /dev/null @@ -1,177 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.CustomAttrs - -object DukazKriteria: - case class Osoba(osobniCislo: String, jmeno: String) - case class Udalost(osoba: Osoba, datum: LocalDate) - case class Dokument( - url: String, - nazev: String, - pridal: Udalost, - odebral: Option[Udalost] - ) - case class ViewModel( - dokumenty: List[Dokument], - autorizoval: Option[Udalost], - platiDo: Option[LocalDate], - poznámka: Option[String] - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-lg", - div( - cls := "px-4 py-5 sm:px-6", - h3( - cls := "text-lg leading-6 font-medium text-gray-900", - """Applicant Information""" - ), - p( - cls := "mt-1 max-w-2xl text-sm text-gray-500", - """Personal details and application.""" - ) - ), - div( - cls := "border-t border-gray-200 px-4 py-5 sm:px-6", - dl( - cls := "grid grid-cols-1 gap-x-4 gap-y-8 sm:grid-cols-2", - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Full name""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Margot Foster""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Application for""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Backend Developer""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Email address""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """margotfoster@example.com""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Salary expectation""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """$120,000""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """About""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Fugiat ipsum ipsum deserunt culpa aute sint do nostrud anim incididunt cillum culpa consequat. Excepteur qui ipsum aliquip consequat sint. Sit id mollit nulla mollit nostrud in ea officia proident. Irure nostrud pariatur mollit ad adipisicing reprehenderit deserunt qui eu.""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """Attachments""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - ul( - role := "list", - cls := "border border-gray-200 rounded-md divide-y divide-gray-200", - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """resume_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ), - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """coverletter_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala deleted file mode 100644 index 22f134f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala +++ /dev/null @@ -1,35 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object DukazyKriteria: - sealed trait Action - case object Add extends Action - - type ViewModel = List[DukazKriteria.ViewModel] - - def apply($m: Signal[ViewModel])(actions: Observer[Action]): HtmlElement = - div( - child.maybe <-- $m.map(m => - if (m.isEmpty) then Some(prazdnyDukaz(actions)) - else None - ), - children <-- $m.map(_.zipWithIndex).split(_._2)((_, _, $s) => - DukazKriteria($s.map(_._1)) - ) - ) - - private def prazdnyDukaz(actions: Observer[Action]): HtmlElement = - button( - tpe := "button", - onClick.preventDefault.mapTo(Add) --> actions, - cls := "relative block w-full border-2 border-gray-300 border-dashed rounded-lg p-12 text-center hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - Icons.outline - .`document-add`(12) - .amend(svg.cls := "mx-auto text-gray-400"), - span( - cls := "mt-2 block text-sm font-medium text-gray-900", - "Přidat důkaz" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala deleted file mode 100644 index f628697..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.list.StackedList -import fiftyforms.ui.components.tailwind.list.ListRow -import fiftyforms.ui.components.tailwind.list.RowTag -import fiftyforms.ui.components.tailwind.list.RowNext -import java.time.LocalDate - -object SeznamKriterii: - type ViewModel = List[DetailKriteria.ViewModel] - - private val kritList = new StackedList[DetailKriteria.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - kritList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = - p(cls := "text-sm text-gray-500", s"${i.kapitola}${i.bod}"), - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.container - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala deleted file mode 100644 index 2bfe4bc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.list.{ - StackedList, - ListRow, - RowTag, - PropList, - IconText, - RowNext -} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.LinkSupport.* - -object SeznamParametru: - type ViewModel = List[DetailParametru.ViewModel] - - private val parametrList = new StackedList[DetailParametru.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - parametrList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = emptyNode, - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.a - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala deleted file mode 100644 index f048e9d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ /dev/null @@ -1,101 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.form.* -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement -import fiftyforms.services.files.components.tailwind.FilePicker -import cz.e_bs.cmi.mdr.pdb.frontend.AutorizujDukaz -import cz.e_bs.cmi.mdr.pdb.frontend.DocumentRef -import fiftyforms.services.files.File - -object UpravDukazForm: - sealed trait Event - case object Cancelled extends Event - case object AvailableFilesRequested extends Event - def apply(availableFilesStream: EventStream[List[File]])( - updates: Observer[Event] - ): HtmlElement = - val (filesStream, filesObserver) = - EventStream.withObserver[FilePicker.Event] - val files = Var[List[File]](Nil) - def submitButtons: HtmlElement = - div( - cls := "pt-5", - div( - cls := "flex justify-end", - button( - tpe := "button", - cls := "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zrušit", - onClick.mapTo(Cancelled) --> updates - ), - button( - tpe := "submit", - cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - disabled <-- files.signal.map(_.isEmpty), - "Autorizovat" - ) - ) - ) - - div( - filesStream.collect { case FilePicker.SelectionUpdated(files) => - files.to(List) - } --> files.writer, - filesStream.collect { case FilePicker.AvailableFilesRequested => - AvailableFilesRequested - } --> updates, - cls := "bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6", - Form( - Form.Body( - Form.Section( - FormHeader( - FormHeader.ViewModel( - "Doložení kritéria", - "Sestavte doklady poskytující důkaz kritéria a potvrďte odesláním formuláře. Případné limitace či jiné relevantní údaje vepište do pole Komentář." - ) - ), - FormFields( - FormRow( - "dokumenty", - "Dokumenty", - FilePicker(files.signal, availableFilesStream)(filesObserver) - .amend(idAttr := "dokumenty", cls("max-w-lg")) - ).toHtml, - FormRow( - "platnost", - "Platnost", - input( - idAttr := "platnost", - name := "platnost", - tpe := "date", - autoComplete := "date", - cls := "block max-w-lg w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" - ) - ).toHtml, - FormRow( - "komentar", - "Komentář", - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - textArea( - idAttr := "komentar", - name := "about", - rows := 3, - cls := "max-w-lg shadow-sm block w-full focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border border-gray-300 rounded-md" - ), - p( - cls := "mt-2 text-sm text-gray-500", - "Doplňte prosím potřebné informace související s doložením kritéria, včetně případných limitací." - ) - ) - ).toHtml - ) - ) - ), - submitButtons - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala deleted file mode 100644 index 41033f6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -import components._ - -object DirectoryPage: - - type ViewModel = List[UserRow.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - val (actionsStream, actionObserver) = - EventStream.withObserver[SearchForm.Action] - val $filter = actionsStream - .collect { - case SearchForm.ClearFilter => None - case SearchForm.SetFilter(t) => Some(t) - } - .startWith(None) - val byLetter = for { - d <- $m - f <- $filter - } yield for { - (letter, users) <- d - .filter { user => - f match - case None => true - case Some(t) => user.search.contains(t) - } - .groupBy(_.prijmeni.head) - .to(List) - .sortBy(_._1) - } yield (letter.toString, users.sortBy(_.prijmeni)) - - div( - cls := "h-full max-w-7xl mx-auto order-first flex flex-col flex-shrink-0", - SearchForm(actionObserver), - Directory(byLetter) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala deleted file mode 100644 index 73f915d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -object Directory: - - object Header: - type ViewModel = String - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500", - h3(child.text <-- $m) - ) - - object UserList: - type ViewModel = List[UserRow.ViewModel] - def apply($m: Signal[ViewModel]): HtmlElement = - ul( - role := "list", - cls := "relative z-0 divide-y divide-gray-200", - children <-- $m.split(_.osobniCislo)((_, _, s) => UserRow(s)) - ) - - type ViewModel = List[(String, List[UserRow.ViewModel])] - def apply($m: Signal[ViewModel]): HtmlElement = - nav( - cls := "flex-1 min-h-0 overflow-y-auto", - aria.label := "Directory", - children <-- $m.split(_._1)((_, _, s) => - div( - cls := "relative", - Header(s.map(_._1)), - UserList(s.map(_._2)) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala deleted file mode 100644 index e182910..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala +++ /dev/null @@ -1,54 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object SearchForm: - sealed trait Action - case object SubmitFilter extends Action - sealed trait FilterAction extends Action - case object ClearFilter extends FilterAction - case class SetFilter(value: String) extends FilterAction - - def apply(actions: Observer[Action]): HtmlElement = - div( - cls := "px-6 pt-4 pb-4", - form( - cls := "flex space-x-4", - onSubmit.mapTo(SubmitFilter) --> actions, - div( - cls := "flex-1 min-w-0", - label( - forId := "search", - cls := "sr-only", - "Hledat" - ), - div( - cls := "relative rounded-md shadow-sm", - div( - cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", - Icons.solid.search().amend(svg.cls := "text-gray-400") - ), - input( - tpe := "search", - name := "search", - idAttr := "search", - cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", - placeholder := "Hledat", - composeEvents(onInput.mapToValue.setAsValue.map(SetFilter(_)))( - _.throttle(500) - ) --> actions - ) - ) - ), - button( - tpe := "submit", - cls := "inline-flex justify-center px-3.5 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500", - Icons.solid.filter().amend(svg.cls := "text-gray-400"), - span( - cls := "sr-only", - "Hledat" - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala deleted file mode 100644 index 82f7723..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala +++ /dev/null @@ -1,52 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.airstream.core.Signal -import fiftyforms.ui.components.tailwind.Avatar - -object UserRow: - case class ViewModel( - osobniCislo: String, - celeJmeno: String, - prijmeni: String, - hlavniFunkce: Option[String], - img: Option[String], - container: () => HtmlElement = () => div() - ) { - val search = prijmeni.toLowerCase - } - - def apply($m: Signal[ViewModel]): HtmlElement = - inline def avatarImage = - Avatar($m.map(_.img)).avatarImage(10) - - li( - div( - cls := "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500", - div( - cls := "flex-shrink-0", - child <-- avatarImage - ), - div( - cls := "flex-1 min-w-0", - child <-- $m.map { o => - o.container() - .amend( - cls := "focus:outline-none", - span( - cls := "absolute inset-0", - aria.hidden := true - ), - p( - cls := "text-sm font-medium text-gray-900", - o.celeJmeno - ), - p( - cls := "text-sm text-gray-500 truncate", - o.hlavniFunkce - ) - ) - } - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala deleted file mode 100644 index 467c5f5..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala +++ /dev/null @@ -1,99 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router - -object ErrorPage: - case class ViewModel( - homePage: Page, - errorName: String, - title: String, - subTitle: String - ) - - def apply(m: ViewModel, actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - val ViewModel(homePage, errorName, title, subTitle) = m - div( - cls := "min-h-full pt-16 pb-12 flex flex-col bg-white", - main( - cls := "flex-grow flex flex-col justify-center max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex-shrink-0 flex justify-center", - a( - href := "/", - cls := "inline-flex", - span( - cls := "sr-only", - """Workflow""" - ), - img( - cls := "h-12 w-auto", - src := "https://tailwindui.com/img/logos/workflow-mark.svg?color=indigo&shade=600", - alt := "" - ) - ) - ), - div( - cls := "py-16", - div( - cls := "text-center", - p( - cls := "text-sm font-semibold text-indigo-600 uppercase tracking-wide", - errorName - ), - h1( - cls := "mt-2 text-4xl font-extrabold text-gray-900 tracking-tight sm:text-5xl", - title - ), - p( - cls := "mt-2 text-base text-gray-500", - subTitle - ), - div( - cls := "mt-6", - PageLink - .container(homePage, actionBus) - .amend( - cls := "text-base font-medium text-indigo-600 hover:text-indigo-500", - """Go back home""" - ) - ) - ) - ) - ), - footer( - cls := "flex-shrink-0 max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", - nav( - cls := "flex justify-center space-x-4", - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Contact Support""" - ), - span( - cls := "inline-block border-l border-gray-300", - aria.hidden := true - ), - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Status""" - ), - span( - cls := "inline-block border-l border-gray-300", - aria.hidden := true - ), - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Twitter""" - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala deleted file mode 100644 index bd9baec..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action - -object NotFoundPage: - def apply(homePage: Page, url: String, actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - ErrorPage( - ErrorPage.ViewModel( - homePage, - "404 error", - "Page not found.", - s"Sorry, but page $url doesn't exist." - ), - actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala deleted file mode 100644 index 476fe3c..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action - -object UnhandledErrorPage: - - case class ViewModel( - homePage: Page, - errorName: Option[String], - errorMessage: Option[String] - ) - - def apply(m: ViewModel, actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - ErrorPage( - ErrorPage.ViewModel( - m.homePage, - "Unexpected error occurred", - m.errorName.getOrElse( - "Uh oh!" - ), // TODO: translations, better text than uh oh - m.errorMessage.getOrElse( - "This wasn't supposed to happen! Please try again." - ) - ), - actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala deleted file mode 100644 index 25f059f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala +++ /dev/null @@ -1,6 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.services - -import com.raquo.airstream.core.Observer - -trait DataFetcher[K, A]: - def fetch(id: K, sink: Observer[A]): Unit diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala deleted file mode 100644 index a469830..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala +++ /dev/null @@ -1,120 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package state - -import com.raquo.airstream.core.EventStream -import cz.e_bs.cmi.mdr.pdb.{UserInfo, OsobniCislo} -import com.raquo.airstream.core.Observer -import scala.scalajs.js -import scala.scalajs.js.JSON -import zio.json._ -import com.raquo.airstream.eventbus.EventBus -import com.raquo.airstream.ownership.Owner -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.UserFunction -import cz.e_bs.cmi.mdr.pdb.UserContract -import fiftyforms.services.files.File - -trait AppState - extends connectors.DetailPageConnector.AppState - with connectors.DetailParametruPageConnector.AppState - with connectors.DetailKriteriaPageConnector.AppState - with pages.detail.UpravDukaz.State: - def users: EventStream[List[UserInfo]] - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - -class MockAppState(implicit owner: Owner, router: Router[Page]) - extends AppState: - - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonDecoder[UserFunction] = DeriveJsonDecoder.gen - given JsonDecoder[UserContract] = DeriveJsonDecoder.gen - given JsonDecoder[UserInfo] = DeriveJsonDecoder.gen - - given JsonDecoder[ParameterCriteria] = DeriveJsonDecoder.gen - given JsonDecoder[Parameter] = DeriveJsonDecoder.gen - - private val actions = EventBus[Action]() - private val (parametersStream, pushParameters) = - EventStream.withCallback[List[Parameter]] - private val (usersStream, pushUsers) = - EventStream.withCallback[List[UserInfo]] - private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] - private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] - - private val mockData: List[UserInfo] = - mockUsers - .asInstanceOf[js.Dictionary[js.Object]] - .values - // TODO: is there a more efficient way to parse from JS object directly? - .map { o => - val parsed = JSON.stringify(o).fromJson[UserInfo] - parsed.left.foreach(org.scalajs.dom.console.log(_)) - parsed - } - .collect { case Right(u) => - u - } - .toList - - private val mockParameters: List[Parameter] = - pdbParams - .asInstanceOf[js.Dictionary[js.Object]] - .values - .map(o => JSON.stringify(o).fromJson[Parameter]) - .collect { case Right(p) => p } - .toList - - // TODO: Extract to separate event handler - actions.events.foreach { - case FetchDirectory => pushUsers(mockData) - case FetchUserDetails(osc) => - mockData.find(_.personalNumber == osc).foreach { o => - pushDetails(o) - router.replaceState(Page.Detail(o)) - } - case FetchParameters(osc) => - pushParameters(mockParameters) - case FetchParameter(osc, paramId) => - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(Page.DetailParametru(o, p)) - case FetchParameterCriteria(osc, paramId, critId, page) => - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - c <- p.criteria.find(_.id == critId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(page(o, p, c)) - case NavigateTo(page) => router.pushState(page) - case FetchAvailableFiles(osc) => - pushFiles( - List( - File("https://tc163.cmi.cz/here", "Example file") - ) - ) - } - - override def users: EventStream[List[UserInfo]] = - usersStream.debugWithName("users") - - override def details: EventStream[UserInfo] = - detailsStream.debugWithName("details") - - override def parameters: EventStream[List[Parameter]] = - parametersStream.debugWithName("parameters") - - override def availableFiles: EventStream[List[File]] = - filesStream.debugWithName("available files") - - override def actionBus: Observer[Action] = - actions.writer.debugWithName("actions writer") diff --git a/app/src/main/scala/mdr/pdb/app/Main.scala b/app/src/main/scala/mdr/pdb/app/Main.scala new file mode 100644 index 0000000..081b4ee --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/Main.scala @@ -0,0 +1,103 @@ +package mdr.pdb.app + +import scala.scalajs.js.annotation.JSExportTopLevel +import scala.scalajs.js.annotation.JSExport +import scala.scalajs.js.annotation.JSImport +import scala.scalajs.js +import org.scalajs.dom +import com.raquo.laminar.api.L.{*, given} +import scala.scalajs.js.Date +import com.raquo.waypoint.Router +import com.raquo.waypoint.SplitRender +import mdr.pdb.app.services.DataFetcher +import scala.scalajs.js.JSON +import zio.json._ +import mdr.pdb.UserInfo +import mdr.pdb.app.state.AppState + +@js.native +@JSImport("stylesheets/main.css", JSImport.Namespace) +object Css extends js.Any + +@js.native +@JSImport("data/users.json", JSImport.Default) +object mockUsers extends js.Object + +@js.native +@JSImport("params/pdb-params.json", JSImport.Default) +object pdbParams extends js.Object + +@JSExportTopLevel("app") +object Main: + + @JSExport + def main(args: Array[String]): Unit = + import Routes.given + onLoad { + setupAirstream() + val appContainer = dom.document.querySelector("#app") + val _ = + render( + appContainer, + renderPage(state.MockAppState(using unsafeWindowOwner, router)) + ) + } + + private def onLoad(f: => Unit): Unit = + documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) + + private def setupAirstream()(using router: Router[Page]): Unit = + AirstreamError.registerUnhandledErrorCallback(err => + router.forcePage( + Page.UnhandledError( + Some(err.getClass.getName), // TODO: Fill only in dev mode + Some(err.getMessage) + ) + ) + ) + + def renderPage(state: AppState)(using + router: Router[Page] + ): HtmlElement = + val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) + .collectSignal[Page.Detail]( + connectors + .DetailPageConnector(state)(_) + .apply + ) + .collectSignal[Page.DetailParametru]( + connectors + .DetailParametruPageConnector(state)(_) + .apply + ) + .collectSignal[Page.DetailKriteria]( + connectors + .DetailKriteriaPageConnector(state)(_) + .apply + ) + .collectSignal[Page.UpravDukazKriteria]( + pages.detail.UpravDukaz.Connector(state)(_).apply + ) + .collectStatic(Page.Dashboard)( + connectors.DashboardPageConnector(state.actionBus).apply + ) + .collect[Page.NotFound](pg => + pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) + ) + .collect[Page.UnhandledError](pg => + pages.errors + .UnhandledErrorPage( + pages.errors.UnhandledErrorPage + .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), + state.actionBus + ) + ) + .collectStatic(Page.Directory)( + connectors + .DirectoryPageConnector(state.users, state.actionBus) + .apply + ) + div(cls := "h-full", child <-- pageSplitter.$view) + + // Pull in the stylesheet + val css: Css.type = Css diff --git a/app/src/main/scala/mdr/pdb/app/Routes.scala b/app/src/main/scala/mdr/pdb/app/Routes.scala new file mode 100644 index 0000000..2cc860c --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/Routes.scala @@ -0,0 +1,198 @@ +package mdr.pdb.app + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.* +import org.scalajs.dom +import zio.json.{*, given} +import mdr.pdb.OsobniCislo + +import scala.scalajs.js +import mdr.pdb.UserInfo +import mdr.pdb.Parameter +import mdr.pdb.ParameterCriteria +import mdr.pdb.app.Page.Titled + +// enum is not working with Waypoints' SplitRender collectStatic +sealed abstract class Page( + val id: String, + val title: String, + val parent: Option[Page] +) { + val path: Vector[Page] = + parent match + case None => Vector(this) + case Some(p) => p.path :+ this + + val isRoot: Boolean = parent.isEmpty +} + +object Page: + + case class Titled[V](value: V, title: Option[String] = None): + val show: String = title.getOrElse(value.toString) + + case object Directory extends Page("directory", "Adresář", None) + + case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) + + case class Detail(osobniCislo: Titled[OsobniCislo]) + extends Page("user", osobniCislo.show, Some(Directory)) + + object Detail { + def apply(o: UserInfo): Detail = Detail( + Titled(o.personalNumber, Some(o.name)) + ) + } + + case class DetailParametru( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String] + ) extends Page( + "parameter", + parametr.show, + Some(Detail(osobniCislo)) + ) + + object DetailParametru { + def apply(o: UserInfo, p: Parameter): DetailParametru = + DetailParametru( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)) + ) + } + + case class DetailKriteria( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String], + kriterium: Titled[String] + ) extends Page( + "criteria", + kriterium.show, + Some(DetailParametru(osobniCislo, parametr)) + ) + + object DetailKriteria { + def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = + DetailKriteria( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)), + Titled(k.id, Some(k.id)) + ) + } + + case class UpravDukazKriteria( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String], + kriterium: Titled[String] + ) extends Page( + "addProof", + "Důkaz", + Some(DetailKriteria(osobniCislo, parametr, kriterium)) + ) + + object UpravDukazKriteria { + def apply( + o: UserInfo, + p: Parameter, + k: ParameterCriteria + ): UpravDukazKriteria = + UpravDukazKriteria( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)), + Titled(k.id, Some(k.id)) + ) + } + + case class NotFound(url: String) extends Page("404", "404", Some(Directory)) + + case class UnhandledError( + errorName: Option[String], + errorMessage: Option[String] + ) extends Page("500", "Unexpected error", Some(Directory)) + +object Routes: + given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) + given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) + given [V: JsonEncoder]: JsonEncoder[Titled[V]] = + DeriveJsonEncoder.gen[Titled[V]] + given [V: JsonDecoder]: JsonDecoder[Titled[V]] = + DeriveJsonDecoder.gen[Titled[V]] + given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] + given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] + + val base = + js.`import`.meta.env.BASE_URL + .asInstanceOf[String] + "app" + + val homePage: Page = Page.Directory + + given router: Router[Page] = Router[Page]( + routes = List( + Route.static(homePage, root / endOfSegments, basePath = base), + Route.static( + Page.Dashboard, + root / "dashboard" / endOfSegments, + basePath = base + ), + Route[Page.Detail, String]( + encode = _.osobniCislo.value.toString, + decode = osc => Page.Detail(Titled(OsobniCislo(osc))), + root / "osoba" / segment[String] / endOfSegments, + basePath = base + ), + Route[Page.DetailParametru, (String, String)]( + encode = p => (p.osobniCislo.value.toString, p.parametr.value), + decode = + p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / endOfSegments, + basePath = base + ), + Route[Page.DetailKriteria, (String, String, String)]( + encode = p => + ( + p.osobniCislo.value.toString, + p.parametr.value, + p.kriterium.value.replaceAll("\\.", "--") + ), + decode = p => + Page.DetailKriteria( + Titled(OsobniCislo(p._1)), + Titled(p._2), + Titled(p._3.replaceAll("--", ".")) + ), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / "kriterium" / segment[String] / endOfSegments, + basePath = base + ), + Route[Page.UpravDukazKriteria, (String, String, String)]( + encode = p => + ( + p.osobniCislo.value.toString, + p.parametr.value, + p.kriterium.value.replaceAll("\\.", "--") + ), + decode = p => + Page.UpravDukazKriteria( + Titled(OsobniCislo(p._1)), + Titled(p._2), + Titled(p._3.replaceAll("--", ".")) + ), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / "kriterium" / segment[String] / "edit" / endOfSegments, + basePath = base + ) + ), + serializePage = _.toJson, + deserializePage = _.fromJson[Page] + .fold(s => throw IllegalStateException(s), identity), + getPageTitle = _.title, + routeFallback = url => Page.NotFound(url), + deserializeFallback = _ => Page.Dashboard + )( + $popStateEvent = windowEvents.onPopState, + owner = unsafeWindowOwner + ) diff --git a/app/src/main/scala/mdr/pdb/app/actions.scala b/app/src/main/scala/mdr/pdb/app/actions.scala new file mode 100644 index 0000000..d078b3a --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/actions.scala @@ -0,0 +1,21 @@ +package mdr.pdb.app + +import mdr.pdb.OsobniCislo +import mdr.pdb.UserInfo +import mdr.pdb.Parameter +import mdr.pdb.ParameterCriteria + +sealed trait Action + +case object FetchDirectory extends Action +case class FetchUserDetails(osc: OsobniCislo) extends Action +case class FetchParameters(osc: OsobniCislo) extends Action +case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action +case class FetchParameterCriteria( + osc: OsobniCislo, + paramId: String, + critId: String, + page: (UserInfo, Parameter, ParameterCriteria) => Page +) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action +case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/mdr/pdb/app/components/AppPage.scala new file mode 100644 index 0000000..130c817 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/AppPage.scala @@ -0,0 +1,82 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.Page +import mdr.pdb.{UserProfile, UserInfo, OsobniCislo} +import com.raquo.waypoint.Router +import mdr.pdb.app.Action +import mdr.pdb.app.NavigateTo +import mdr.pdb.UserFunction + +object AppPage: + // TODO: pages by logged in user + val pages: List[Page] = List(Page.Directory, Page.Dashboard) + + import NavigationBar.{Logo, MenuItem} + + val logo = Logo( + "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", + "Workflow" + ) + + // TODO: menu items by user profile + val userMenu = + List( + MenuItem("Your Profile"), + MenuItem("Settings"), + MenuItem("Sign out") + ) + + // TODO: load user profile + val $userProfile = Var( + UserProfile( + "tom", + UserInfo( + OsobniCislo("1031"), + "tom", + "Tom", + "Cook", + None, + None, + Some("tom@example.com"), + Some("+420 222 866 180"), + Some( + UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") + ), + Nil, + None + ) + ) + ) + + val $userInfo = $userProfile.signal.map(_.userInfo) + + type ViewModel = Option[HtmlElement] + def apply( + actionBus: Observer[Action] + )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using + router: Router[Page] + ): HtmlElement = + PageLayout(actionBus)( + $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => + PageLayout.ViewModel( + NavigationBar.ViewModel( + u, + pages.map(p => + NavigationBar.Link( + () => + PageLink( + p, + actionBus + ), + p == cp + ) + ), + userMenu, + logo + ), + c + ) + ), + mods + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala new file mode 100644 index 0000000..9375398 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala @@ -0,0 +1,143 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.tailwind.list.IconText.ViewModel +import fiftyforms.ui.components.tailwind.Icons +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.Page + +import com.raquo.laminar.api.L.{*, given} +import io.laminext.syntax.core.{*, given} +import mdr.pdb.app.Action + +object Breadcrumbs: + + private def slash = + import svg.{*, given} + svg( + cls := "flex-shrink-0 h-5 w-5 text-gray-300", + xmlns := "http://www.w3.org/2000/svg", + fill := "currentColor", + viewBox := "0 0 20 20", + ariaHidden := true, + path( + d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" + ) + ) + + def backIcon = + Icons.solid + .`arrow-narrow-left`() + .amend( + svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" + ) + + object Link: + case class ViewModel( + page: Page, + icon: Option[SvgElement], + extraClasses: String, + text: String, + textClasses: Option[String] = None + ) + + val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" + + def shortHome(p: Page) = ViewModel( + p, + Some(backIcon), + "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", + "Zpět na úvodní stránku", + None + ) + + def fullHome(p: Page) = ViewModel( + p, + Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), + "text-gray-400 hover:text-gray-500", + "Domů", + Some("sr-only") + ) + + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + PageLink + .container($m.map(_.page), actionBus) + .amend( + cls <-- $m.map(_.extraClasses), + child.maybe <-- $m.map(_.icon), + span( + cls <-- $m.map(_.textClasses.getOrElse("")), + child.text <-- $m.map(_.text) + ) + ) + + object FullBreadcrumbs: + type ViewModel = Page + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + ol( + role := "list", + cls := "flex items-center space-x-4", + children <-- $m.map(_.path).split(_.id)((_, _, $p) => + li( + div( + cls := "flex items-center", + child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), + Link( + $p.map(p => + if (p.isRoot) then Link.fullHome(p) + else + Link.ViewModel( + p, + None, + s"ml-4 max-w-xs truncate ${Link.baseClasses}", + p.title + ) + ), + actionBus + ) + ) + ) + ) + ) + + object ShortBreadcrumbs: + type ViewModel = Page + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + Link( + $m.map { p => + val target = p.parent.getOrElse(p) + if target.isRoot then Link.shortHome(target) + else + Link.ViewModel( + target, + Some(backIcon), + "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", + s"Zpět na ${target.title}" + ) + }, + actionBus + ) + + def apply(actionBus: Observer[Action])(using + router: Router[Page] + ): HtmlElement = + nav( + cls := "flex", + aria.label := "Breadcrumb", + div( + cls := "flex sm:hidden", + ShortBreadcrumbs(router.$currentPage, actionBus) + ), + div( + cls := "hidden sm:block", + FullBreadcrumbs(router.$currentPage, actionBus) + ) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala deleted file mode 100644 index a3b00af..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala +++ /dev/null @@ -1,103 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import scala.scalajs.js.annotation.JSExportTopLevel -import scala.scalajs.js.annotation.JSExport -import scala.scalajs.js.annotation.JSImport -import scala.scalajs.js -import org.scalajs.dom -import com.raquo.laminar.api.L.{*, given} -import scala.scalajs.js.Date -import com.raquo.waypoint.Router -import com.raquo.waypoint.SplitRender -import cz.e_bs.cmi.mdr.pdb.app.services.DataFetcher -import scala.scalajs.js.JSON -import zio.json._ -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.state.AppState - -@js.native -@JSImport("stylesheets/main.css", JSImport.Namespace) -object Css extends js.Any - -@js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native -@JSImport("params/pdb-params.json", JSImport.Default) -object pdbParams extends js.Object - -@JSExportTopLevel("app") -object Main: - - @JSExport - def main(args: Array[String]): Unit = - import Routes.given - onLoad { - setupAirstream() - val appContainer = dom.document.querySelector("#app") - val _ = - render( - appContainer, - renderPage(state.MockAppState(using unsafeWindowOwner, router)) - ) - } - - private def onLoad(f: => Unit): Unit = - documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) - - private def setupAirstream()(using router: Router[Page]): Unit = - AirstreamError.registerUnhandledErrorCallback(err => - router.forcePage( - Page.UnhandledError( - Some(err.getClass.getName), // TODO: Fill only in dev mode - Some(err.getMessage) - ) - ) - ) - - def renderPage(state: AppState)(using - router: Router[Page] - ): HtmlElement = - val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) - .collectSignal[Page.Detail]( - connectors - .DetailPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailParametru]( - connectors - .DetailParametruPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailKriteria]( - connectors - .DetailKriteriaPageConnector(state)(_) - .apply - ) - .collectSignal[Page.UpravDukazKriteria]( - pages.detail.UpravDukaz.Connector(state)(_).apply - ) - .collectStatic(Page.Dashboard)( - connectors.DashboardPageConnector(state.actionBus).apply - ) - .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) - ) - .collect[Page.UnhandledError](pg => - pages.errors - .UnhandledErrorPage( - pages.errors.UnhandledErrorPage - .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), - state.actionBus - ) - ) - .collectStatic(Page.Directory)( - connectors - .DirectoryPageConnector(state.users, state.actionBus) - .apply - ) - div(cls := "h-full", child <-- pageSplitter.$view) - - // Pull in the stylesheet - val css: Css.type = Css diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala deleted file mode 100644 index 3bbc8c0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ /dev/null @@ -1,198 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.* -import org.scalajs.dom -import zio.json.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import scala.scalajs.js -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.Page.Titled - -// enum is not working with Waypoints' SplitRender collectStatic -sealed abstract class Page( - val id: String, - val title: String, - val parent: Option[Page] -) { - val path: Vector[Page] = - parent match - case None => Vector(this) - case Some(p) => p.path :+ this - - val isRoot: Boolean = parent.isEmpty -} - -object Page: - - case class Titled[V](value: V, title: Option[String] = None): - val show: String = title.getOrElse(value.toString) - - case object Directory extends Page("directory", "Adresář", None) - - case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) - - case class Detail(osobniCislo: Titled[OsobniCislo]) - extends Page("user", osobniCislo.show, Some(Directory)) - - object Detail { - def apply(o: UserInfo): Detail = Detail( - Titled(o.personalNumber, Some(o.name)) - ) - } - - case class DetailParametru( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String] - ) extends Page( - "parameter", - parametr.show, - Some(Detail(osobniCislo)) - ) - - object DetailParametru { - def apply(o: UserInfo, p: Parameter): DetailParametru = - DetailParametru( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)) - ) - } - - case class DetailKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "criteria", - kriterium.show, - Some(DetailParametru(osobniCislo, parametr)) - ) - - object DetailKriteria { - def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = - DetailKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class UpravDukazKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "addProof", - "Důkaz", - Some(DetailKriteria(osobniCislo, parametr, kriterium)) - ) - - object UpravDukazKriteria { - def apply( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): UpravDukazKriteria = - UpravDukazKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class NotFound(url: String) extends Page("404", "404", Some(Directory)) - - case class UnhandledError( - errorName: Option[String], - errorMessage: Option[String] - ) extends Page("500", "Unexpected error", Some(Directory)) - -object Routes: - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) - given [V: JsonEncoder]: JsonEncoder[Titled[V]] = - DeriveJsonEncoder.gen[Titled[V]] - given [V: JsonDecoder]: JsonDecoder[Titled[V]] = - DeriveJsonDecoder.gen[Titled[V]] - given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] - given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] - - val base = - js.`import`.meta.env.BASE_URL - .asInstanceOf[String] + "app" - - val homePage: Page = Page.Directory - - given router: Router[Page] = Router[Page]( - routes = List( - Route.static(homePage, root / endOfSegments, basePath = base), - Route.static( - Page.Dashboard, - root / "dashboard" / endOfSegments, - basePath = base - ), - Route[Page.Detail, String]( - encode = _.osobniCislo.value.toString, - decode = osc => Page.Detail(Titled(OsobniCislo(osc))), - root / "osoba" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.DetailParametru, (String, String)]( - encode = p => (p.osobniCislo.value.toString, p.parametr.value), - decode = - p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / endOfSegments, - basePath = base - ), - Route[Page.DetailKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.DetailKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.UpravDukazKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.UpravDukazKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / "edit" / endOfSegments, - basePath = base - ) - ), - serializePage = _.toJson, - deserializePage = _.fromJson[Page] - .fold(s => throw IllegalStateException(s), identity), - getPageTitle = _.title, - routeFallback = url => Page.NotFound(url), - deserializeFallback = _ => Page.Dashboard - )( - $popStateEvent = windowEvents.onPopState, - owner = unsafeWindowOwner - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala deleted file mode 100644 index b24b82b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -sealed trait Action - -case object FetchDirectory extends Action -case class FetchUserDetails(osc: OsobniCislo) extends Action -case class FetchParameters(osc: OsobniCislo) extends Action -case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action -case class FetchParameterCriteria( - osc: OsobniCislo, - paramId: String, - critId: String, - page: (UserInfo, Parameter, ParameterCriteria) => Page -) extends Action -case class FetchAvailableFiles(osc: OsobniCislo) extends Action -case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala deleted file mode 100644 index c637d35..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala +++ /dev/null @@ -1,82 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.{UserProfile, UserInfo, OsobniCislo} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo -import cz.e_bs.cmi.mdr.pdb.UserFunction - -object AppPage: - // TODO: pages by logged in user - val pages: List[Page] = List(Page.Directory, Page.Dashboard) - - import NavigationBar.{Logo, MenuItem} - - val logo = Logo( - "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", - "Workflow" - ) - - // TODO: menu items by user profile - val userMenu = - List( - MenuItem("Your Profile"), - MenuItem("Settings"), - MenuItem("Sign out") - ) - - // TODO: load user profile - val $userProfile = Var( - UserProfile( - "tom", - UserInfo( - OsobniCislo("1031"), - "tom", - "Tom", - "Cook", - None, - None, - Some("tom@example.com"), - Some("+420 222 866 180"), - Some( - UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") - ), - Nil, - None - ) - ) - ) - - val $userInfo = $userProfile.signal.map(_.userInfo) - - type ViewModel = Option[HtmlElement] - def apply( - actionBus: Observer[Action] - )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using - router: Router[Page] - ): HtmlElement = - PageLayout(actionBus)( - $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => - PageLayout.ViewModel( - NavigationBar.ViewModel( - u, - pages.map(p => - NavigationBar.Link( - () => - PageLink( - p, - actionBus - ), - p == cp - ) - ), - userMenu, - logo - ), - c - ) - ), - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala deleted file mode 100644 index bf473c6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ /dev/null @@ -1,143 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden -import fiftyforms.ui.components.tailwind.list.IconText.ViewModel -import fiftyforms.ui.components.tailwind.Icons -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Action - -object Breadcrumbs: - - private def slash = - import svg.{*, given} - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-300", - xmlns := "http://www.w3.org/2000/svg", - fill := "currentColor", - viewBox := "0 0 20 20", - ariaHidden := true, - path( - d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" - ) - ) - - def backIcon = - Icons.solid - .`arrow-narrow-left`() - .amend( - svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" - ) - - object Link: - case class ViewModel( - page: Page, - icon: Option[SvgElement], - extraClasses: String, - text: String, - textClasses: Option[String] = None - ) - - val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" - - def shortHome(p: Page) = ViewModel( - p, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - "Zpět na úvodní stránku", - None - ) - - def fullHome(p: Page) = ViewModel( - p, - Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), - "text-gray-400 hover:text-gray-500", - "Domů", - Some("sr-only") - ) - - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - PageLink - .container($m.map(_.page), actionBus) - .amend( - cls <-- $m.map(_.extraClasses), - child.maybe <-- $m.map(_.icon), - span( - cls <-- $m.map(_.textClasses.getOrElse("")), - child.text <-- $m.map(_.text) - ) - ) - - object FullBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - ol( - role := "list", - cls := "flex items-center space-x-4", - children <-- $m.map(_.path).split(_.id)((_, _, $p) => - li( - div( - cls := "flex items-center", - child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), - Link( - $p.map(p => - if (p.isRoot) then Link.fullHome(p) - else - Link.ViewModel( - p, - None, - s"ml-4 max-w-xs truncate ${Link.baseClasses}", - p.title - ) - ), - actionBus - ) - ) - ) - ) - ) - - object ShortBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - Link( - $m.map { p => - val target = p.parent.getOrElse(p) - if target.isRoot then Link.shortHome(target) - else - Link.ViewModel( - target, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - s"Zpět na ${target.title}" - ) - }, - actionBus - ) - - def apply(actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - nav( - cls := "flex", - aria.label := "Breadcrumb", - div( - cls := "flex sm:hidden", - ShortBreadcrumbs(router.$currentPage, actionBus) - ), - div( - cls := "hidden sm:block", - FullBreadcrumbs(router.$currentPage, actionBus) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala deleted file mode 100644 index 0db2047..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ /dev/null @@ -1,242 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent -import fiftyforms.ui.components.tailwind.Icons -import fiftyforms.ui.components.tailwind.Avatar -import cz.e_bs.cmi.mdr.pdb.UserInfo -import io.laminext.syntax.core.* - -object NavigationBar: - - case class Logo(img: String, name: String) - case class Link(a: () => Anchor, active: Boolean) - case class MenuItem(title: String) - - case class ViewModel( - userInfo: UserInfo, - pages: List[Link], - userMenu: List[MenuItem], - logo: Logo - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - val $userInfo = $m.map(_.userInfo) - val mobileMenuOpen = Var(false) - - val desktopOnly = cls("hidden md:block") - val mobileOnly = cls("md:hidden") - - inline def avatarImage(size: Int = 8) = - Avatar($userInfo.map(_.img)).avatarImage(size) - - def notificationButton = button( - tpe := "button", - cls := List( - "bg-indigo-600", - "focus:outline-none", - "focus:ring-2", - "focus:ring-offset-2", - "focus:ring-offset-indigo-600", - "focus:ring-white", - "hover:text-white", - "p-1", - "rounded-full", - "text-indigo-200" - ), - span(cls := "sr-only", "View notifications"), - Icons.outline.bell() - ) - - def userProfile: HtmlElement = - val menuOpen = Var(false) - - def menuItem(item: MenuItem, idx: Int): HtmlElement = - a( - href := "#", - cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", - role := "menuitem", - tabIndex := -1, - idAttr := s"user-menu-item-$idx", - item.title - ) - - div( - cls := "ml-3 relative", - div( - button( - tpe := "button", - cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - idAttr := "user-menu-button", - aria.expanded <-- menuOpen.signal, - aria.hasPopup := true, - span(cls := "sr-only", "Open user menu"), - child <-- avatarImage(), - onClick.preventDefault.mapTo( - !menuOpen.now() - ) --> menuOpen.writer - ) - ), - /* - * */ - div( - cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", - cls <-- menuOpen.signal.map { o => - if (o) "md:block" else "md:hidden" - }, - role := "menu", - aria.orientation := "vertical", - aria.labelledBy := "user-menu-button", - tabIndex := -1, - // : keyboard navigation - children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) - ) - ) - - def mobileProfile = - def menuItem(item: MenuItem): HtmlElement = - a( - href := "#", - cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", - item.title - ) - - div( - cls := "pt-4 pb-3 border-t border-indigo-700", - div( - cls := "flex items-center px-5", - div( - cls := "flex-shrink-0", - child <-- avatarImage(10) - ), - div( - cls := "ml-3", - div( - cls := "text-base font-medium text-white", - child.text <-- $userInfo.map(_.name) - ), - child.maybe <-- $userInfo.map( - _.email.map(e => - div( - cls := "text-sm font-medium text-indigo-300", - e - ) - ) - ) - ), - notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) - ), - div( - cls := "mt-3 px-2 space-y-1", - children <-- $m.map(_.userMenu.map(menuItem)) - ) - ) - - def pageLink(page: Link): Anchor = - page - .a() - .amend( - cls := "text-white px-3 py-2 rounded-md text-sm font-medium", - cls := Seq( - "bg-indigo-700" -> page.active, - "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active - ), - ariaCurrent := (if page.active then "page" else "false") - ) - - def logoImg: Image = - img( - cls := "h-8 w-8", - src <-- $m.map(_.logo.img), - alt <-- $m.map(_.logo.name) - ) - - def mobileMenuButton = button( - tpe := "button", - cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - aria.controls := "mobile-menu", - aria.expanded <-- mobileMenuOpen.signal, - span(cls := "sr-only", "Open main menu"), - Icons.outline - .menu() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") - ), - Icons.outline - .x() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") - ), - onClick.preventDefault.mapTo( - !mobileMenuOpen.now() - ) --> mobileMenuOpen.writer - ) - - def navBarLeft = - div( - cls := "flex items-center", - div(cls := "flex-shrink-0", logoImg), - div( - desktopOnly, - div( - cls := "ml-10 flex items-baseline space-x-4", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ) - ) - ) - - def navBarRight = - div( - desktopOnly, - div( - cls := "ml-4 flex items-center md:ml-6", - notificationButton, - userProfile - ) - ) - - def navBarMobile = - div( - cls := "-mr-2 flex", - mobileOnly, - mobileMenuButton - ) - - def navBar = - div( - cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex items-center justify-between h-16", - navBarLeft, - navBarRight, - navBarMobile - ) - ) - - def mobileMenu = - div( - mobileOnly, - cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }, - idAttr := "mobile-menu", - div( - cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ), - mobileProfile - ) - - nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala deleted file mode 100644 index 75339df..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object PageHeader: - def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = - header( - cls := "bg-white shadow-sm", - div( - cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", - h1( - cls := "text-lg leading-6 font-semibold text-gray-900", - Breadcrumbs(actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala deleted file mode 100644 index 5096463..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ /dev/null @@ -1,30 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import fiftyforms.ui.components.tailwind.Loading -import io.laminext.syntax.core.* - -object PageLayout: - case class ViewModel( - navigation: NavigationBar.ViewModel, - content: Option[HtmlElement] - ) - def apply(actionBus: Observer[Action])( - $m: Signal[ViewModel], - mods: Modifier[HtmlElement]* - )(using router: Router[Page]): HtmlElement = - val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) - div( - cls := "h-full flex flex-col", - NavigationBar($m.map(_.navigation)), - child.maybe <-- router.$currentPage.map(_.isRoot) - .switch(None, Some(PageHeader(actionBus))), - main( - cls := "flex-grow-1 overflow-y-auto", - mods, - child <-- $maybeContent.map(_.getOrElse(Loading)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala deleted file mode 100644 index 62d820f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.LinkSupport.* -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo - -object PageLink: - type ViewModel = Page - def apply($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = - a(mods($m, actions), child.text <-- $m.map(_.title)) - - def apply(m: ViewModel, actions: Observer[Action])(using - router: Router[Page] - ): Anchor = apply(Val(m), actions) - - def container($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = a(mods($m, actions)) - - def container(m: ViewModel, actions: Observer[Action])(using - Router[Page] - ): Anchor = container(Val(m), actions) - - private def mods($m: Signal[Page], actions: Observer[Action])(using - router: Router[Page] - ): Modifier[Anchor] = - Seq( - href <-- $m.map(router.absoluteUrlForPage).recover { case _ => - Some("invalid url") - }, - composeEvents(onClick.noKeyMod.preventDefault)( - _.sample($m) - .map(NavigateTo.apply) - ) --> actions - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala deleted file mode 100644 index 87f825a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.pages.dashboard.DashboardPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -class DashboardPageConnector(actionBus: Observer[Action])(using - router: Router[Page] -): - def apply: HtmlElement = - AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala deleted file mode 100644 index e9b0f71..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala +++ /dev/null @@ -1,76 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailKriteriaPage -import pages.detail.components.DukazyKriteria -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -object DetailKriteriaPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailKriteriaPageConnector( - state: DetailKriteriaPageConnector.AppState -)( - $page: Signal[Page.DetailKriteria] -)(using Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => - (p.osobniCislo.value, p.parametr.value, p.kriterium.value) - )((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map( - FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) - ) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, s, $s) => - DetailKriteriaPage($s)(state.actionBus.contramap { - case DukazyKriteria.Add => - NavigateTo( - Page.UpravDukazKriteria( - Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), - Page.Titled(s.parametr.id, Some(s.parametr.nazev)), - Page.Titled(s.kriterium.id, Some(s.kriterium.id)) - ) - ) - }) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): DetailKriteriaPage.ViewModel = - DetailKriteriaPage.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala deleted file mode 100644 index 4d8024f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ /dev/null @@ -1,57 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import pages.detail.DetailPage -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import fiftyforms.ui.components.tailwind.Color - -object DetailPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailPageConnector(state: DetailPageConnector.AppState)( - $page: Signal[Page.Detail] -)(using router: Router[Page]): - val $oscChangeSignal = $page.splitOne(_.osobniCislo.value)((osc, _, _) => osc) - val $pageChangeSignal = - $oscChangeSignal.flatMap(osc => - EventStream.fromSeq(Seq(FetchUserDetails(osc), FetchParameters(osc))) - ) - // TODO: filter the value based on the current osc - // OSC change will fetch new data, but still - // - we need to be sure that what we got is really what we ought to display - // - we want to display stale data accordingly (at least with loading indicator) - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - def apply: HtmlElement = - AppPage(state.actionBus)( - $data.combineWithFn($params)(_ zip _) - .map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: List[Parameter] - ): DetailPage.ViewModel = - DetailPage.ViewModel( - o.toDetailOsoby, - p.map( - _.toParametr(param => - PageLink.container(Page.DetailParametru(o, param), state.actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala deleted file mode 100644 index fdaa7f8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailParametruPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink - -object DetailParametruPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailParametruPageConnector( - state: DetailParametruPageConnector.AppState -)( - $page: Signal[Page.DetailParametru] -)(using router: Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => (p.osobniCislo.value, p.parametr.value))((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map(FetchParameter(_, _)) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - } yield (da, pb) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailParametruPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter - ): DetailParametruPage.ViewModel = - DetailParametruPage.ViewModel( - o.toDetailOsoby, - p.toParametr(p => - PageLink.container(Page.DetailParametru(o, p), state.actionBus) - ), - p.criteria.map( - _.toKriterium { c => - PageLink.container( - Page.DetailKriteria(o, p, c), - state.actionBus - ) - } - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala deleted file mode 100644 index 049aa22..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -case class DirectoryPageConnector( - $input: EventStream[List[UserInfo]], - actionBus: Observer[Action] -)(using router: Router[Page]): - val $data = $input.startWithNone - val $actionSignal = EventStream.fromValue(FetchDirectory) - - def apply: HtmlElement = - AppPage(actionBus)( - $data.split(_ => ())((_, _, s) => - pages.directory.DirectoryPage( - s.map( - _.map( - _.toUserRow(u => - PageLink.container( - Page.Detail(Page.Titled(u.personalNumber)), - actionBus - ) - ) - ) - ) - ) - ), - $actionSignal --> actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala deleted file mode 100644 index 88e7450..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ /dev/null @@ -1,68 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii -import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria -import fiftyforms.ui.components.tailwind.Color - -extension (o: UserInfo) - def toDetailOsoby: DetailOsoby.ViewModel = - DetailOsoby.ViewModel( - o.personalNumber, - o.name, - o.email, - o.phone, - o.img, - o.mainFunction.map(f => - DetailOsoby.Funkce.ViewModel(f.name, f.dept, f.ou) - ), - o.userContracts.headOption.map(c => - DetailOsoby.PracovniPomer.ViewModel(c.rel, c.startDate, c.endDate) - ) - ) - -extension (param: Parameter) - def toParametr(container: Parameter => Anchor): DetailParametru.ViewModel = - DetailParametru.ViewModel( - id = param.id, - nazev = param.name, - popis = param.description, - status = "Nesplněno", - statusColor = Color.red, - a = container(param) - ) - -extension (crit: ParameterCriteria) - def toKriterium( - container: ParameterCriteria => Anchor - ): DetailKriteria.ViewModel = - DetailKriteria.ViewModel( - nazev = crit.criteriumText, - kapitola = crit.chapterId, - bod = crit.itemId, - status = "Nesplněno", - statusColor = Color.red, - splneno = false, - dukazy = Nil, - container = container(crit) - ) - -extension (user: UserInfo) - def toUserRow( - container: UserInfo => HtmlElement = (_: UserInfo) => div() - ): UserRow.ViewModel = - UserRow.ViewModel( - osobniCislo = user.personalNumber.toString, - celeJmeno = user.name, - prijmeni = user.surname, - hlavniFunkce = user.mainFunction.map(_.name), - img = user.img, - container = () => container(user) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala deleted file mode 100644 index 7d5b2c1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.dashboard - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.Page - -object DashboardPage: - - def render: HtmlElement = - div("Dashboard page") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala deleted file mode 100644 index 9ef0698..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala +++ /dev/null @@ -1,31 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailKriteriaPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply($m: Signal[ViewModel])( - action: Observer[DukazyKriteria.Action] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - DukazyKriteria($m.map(_.kriterium.dukazy))(action) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala deleted file mode 100644 index 3b316e1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object DetailPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametry: SeznamParametru.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby($m.map(_.osoba)), - SeznamParametru($m.map(_.parametry)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala deleted file mode 100644 index 17224cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala +++ /dev/null @@ -1,24 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailParametruPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriteria: SeznamKriterii.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby.header($m.map(_.osoba)), - DetailParametru($m.map(_.parametr)), - SeznamKriterii($m.map(_.kriteria)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala deleted file mode 100644 index dc912fa..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ /dev/null @@ -1,107 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb -package app -package pages.detail - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.UpravDukazForm -import fiftyforms.services.files.File - -object UpravDukaz: - - type ThisPage = Page.UpravDukazKriteria - type PageKey = (OsobniCislo, String, String) - - trait State { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def availableFiles: EventStream[List[File]] - def actionBus: Observer[Action] - } - - def keyOfPage(page: ThisPage): PageKey = - (page.osobniCislo.value, page.parametr.value, page.kriterium.value) - - def onChangeAction(key: PageKey): Action = - FetchParameterCriteria( - key._1, - key._2, - key._3, - Page.UpravDukazKriteria(_, _, _) - ) - - class Connector(state: State)($page: Signal[ThisPage])(using Router[Page]): - val $paramChangeSignal = $page.splitOne(keyOfPage)((x, _, _) => x) - val $pageChangeSignal = $paramChangeSignal.map(onChangeAction) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.split(_ => ())((_, s, $s) => - PageComponent( - $s.map(buildModel), - state.availableFiles, - state.actionBus.contramap { - case UpravDukazForm.Cancelled => - NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) - case UpravDukazForm.AvailableFilesRequested => - FetchAvailableFiles(s._1.personalNumber) - } - ) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): PageComponent.ViewModel = - import connectors.* - PageComponent.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) - - object PageComponent: - import components.* - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply( - $m: Signal[ViewModel], - availableFilesStream: EventStream[List[File]], - events: Observer[UpravDukazForm.Event] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - UpravDukazForm(availableFilesStream)(events) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala deleted file mode 100644 index ca311b0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.Icons -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Color - -object DetailKriteria: - case class ViewModel( - nazev: String, - kapitola: String, - bod: String, - status: String, - statusColor: Color, - splneno: Boolean, - dukazy: List[DukazKriteria.ViewModel], - container: HtmlElement = div() - ) { - val id = s"${kapitola}${bod}" - } - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h3( - cls := "text-xl leading-6 font-bold text-gray-900", - child.text <-- $m.map(_.nazev) - ), - h3( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - "Kapitola ", - child.text <-- $m.map(_.kapitola), - " bod ", - child <-- $m.map(_.bod) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala deleted file mode 100644 index ae90290..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala +++ /dev/null @@ -1,100 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Avatar - -object DetailOsoby: - - object Funkce: - case class ViewModel( - nazev: String, - stredisko: String, - voj: String - ) - - def render($m: Signal[ViewModel]): HtmlElement = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.nazev), - span( - cls := "hidden md:inline", - " @ ", - child.text <-- $m.map(_.stredisko), - ", ", - child.text <-- $m.map(_.voj) - ) - ) - - object PracovniPomer: - case class ViewModel( - druh: String, - pocatek: LocalDate, - konec: Option[LocalDate] - ) - - def render($m: Signal[ViewModel]): HtmlElement = - import fiftyforms.ui.components.tailwind.CustomAttrs.datetime - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.druh), - " od ", - time( - datetime <-- $m.map(_.pocatek.toString), - child.text <-- $m.map(_.pocatek.toString) - ) - ) - - case class ViewModel( - osobniCislo: OsobniCislo, - jmeno: String, - email: Option[String], - telefon: Option[String], - img: Option[String], - hlavniFunkce: Option[Funkce.ViewModel], - pracovniPomer: Option[PracovniPomer.ViewModel] - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-4", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(16) - ), - div( - h1( - cls := "text-2xl font-bold text-gray-900", - child.text <-- $m.map(_.jmeno) - ), - child.maybe <-- $m.map(_.hlavniFunkce).split(_ => ())((_, _, d) => - Funkce.render(d) - ), - child.maybe <-- $m.map(_.pracovniPomer).split(_ => ())((_, _, d) => - PracovniPomer.render(d) - ) - ) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-5", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(8) - ), - div( - h1( - cls := "text-lg font-medium text-gray-800", - child.text <-- $m.map(_.jmeno) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala deleted file mode 100644 index e11cb62..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color - -object DetailParametru: - case class ViewModel( - id: String, - nazev: String, - popis: String, - status: String, - statusColor: Color, - a: Anchor - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h2( - cls := "text-2xl leading-6 font-medium text-gray-900", - child.text <-- $m.map(_.nazev) - ), - p( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - child.text <-- $m.map(_.popis) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - h2( - cls := "text-lg leading-6 font-medium text-gray-600", - child.text <-- $m.map(_.nazev) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala deleted file mode 100644 index a00ede8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala +++ /dev/null @@ -1,177 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.CustomAttrs - -object DukazKriteria: - case class Osoba(osobniCislo: String, jmeno: String) - case class Udalost(osoba: Osoba, datum: LocalDate) - case class Dokument( - url: String, - nazev: String, - pridal: Udalost, - odebral: Option[Udalost] - ) - case class ViewModel( - dokumenty: List[Dokument], - autorizoval: Option[Udalost], - platiDo: Option[LocalDate], - poznámka: Option[String] - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-lg", - div( - cls := "px-4 py-5 sm:px-6", - h3( - cls := "text-lg leading-6 font-medium text-gray-900", - """Applicant Information""" - ), - p( - cls := "mt-1 max-w-2xl text-sm text-gray-500", - """Personal details and application.""" - ) - ), - div( - cls := "border-t border-gray-200 px-4 py-5 sm:px-6", - dl( - cls := "grid grid-cols-1 gap-x-4 gap-y-8 sm:grid-cols-2", - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Full name""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Margot Foster""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Application for""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Backend Developer""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Email address""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """margotfoster@example.com""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Salary expectation""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """$120,000""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """About""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Fugiat ipsum ipsum deserunt culpa aute sint do nostrud anim incididunt cillum culpa consequat. Excepteur qui ipsum aliquip consequat sint. Sit id mollit nulla mollit nostrud in ea officia proident. Irure nostrud pariatur mollit ad adipisicing reprehenderit deserunt qui eu.""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """Attachments""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - ul( - role := "list", - cls := "border border-gray-200 rounded-md divide-y divide-gray-200", - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """resume_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ), - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """coverletter_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala deleted file mode 100644 index 22f134f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala +++ /dev/null @@ -1,35 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object DukazyKriteria: - sealed trait Action - case object Add extends Action - - type ViewModel = List[DukazKriteria.ViewModel] - - def apply($m: Signal[ViewModel])(actions: Observer[Action]): HtmlElement = - div( - child.maybe <-- $m.map(m => - if (m.isEmpty) then Some(prazdnyDukaz(actions)) - else None - ), - children <-- $m.map(_.zipWithIndex).split(_._2)((_, _, $s) => - DukazKriteria($s.map(_._1)) - ) - ) - - private def prazdnyDukaz(actions: Observer[Action]): HtmlElement = - button( - tpe := "button", - onClick.preventDefault.mapTo(Add) --> actions, - cls := "relative block w-full border-2 border-gray-300 border-dashed rounded-lg p-12 text-center hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - Icons.outline - .`document-add`(12) - .amend(svg.cls := "mx-auto text-gray-400"), - span( - cls := "mt-2 block text-sm font-medium text-gray-900", - "Přidat důkaz" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala deleted file mode 100644 index f628697..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.list.StackedList -import fiftyforms.ui.components.tailwind.list.ListRow -import fiftyforms.ui.components.tailwind.list.RowTag -import fiftyforms.ui.components.tailwind.list.RowNext -import java.time.LocalDate - -object SeznamKriterii: - type ViewModel = List[DetailKriteria.ViewModel] - - private val kritList = new StackedList[DetailKriteria.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - kritList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = - p(cls := "text-sm text-gray-500", s"${i.kapitola}${i.bod}"), - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.container - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala deleted file mode 100644 index 2bfe4bc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.list.{ - StackedList, - ListRow, - RowTag, - PropList, - IconText, - RowNext -} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.LinkSupport.* - -object SeznamParametru: - type ViewModel = List[DetailParametru.ViewModel] - - private val parametrList = new StackedList[DetailParametru.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - parametrList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = emptyNode, - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.a - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala deleted file mode 100644 index f048e9d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ /dev/null @@ -1,101 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.form.* -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement -import fiftyforms.services.files.components.tailwind.FilePicker -import cz.e_bs.cmi.mdr.pdb.frontend.AutorizujDukaz -import cz.e_bs.cmi.mdr.pdb.frontend.DocumentRef -import fiftyforms.services.files.File - -object UpravDukazForm: - sealed trait Event - case object Cancelled extends Event - case object AvailableFilesRequested extends Event - def apply(availableFilesStream: EventStream[List[File]])( - updates: Observer[Event] - ): HtmlElement = - val (filesStream, filesObserver) = - EventStream.withObserver[FilePicker.Event] - val files = Var[List[File]](Nil) - def submitButtons: HtmlElement = - div( - cls := "pt-5", - div( - cls := "flex justify-end", - button( - tpe := "button", - cls := "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zrušit", - onClick.mapTo(Cancelled) --> updates - ), - button( - tpe := "submit", - cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - disabled <-- files.signal.map(_.isEmpty), - "Autorizovat" - ) - ) - ) - - div( - filesStream.collect { case FilePicker.SelectionUpdated(files) => - files.to(List) - } --> files.writer, - filesStream.collect { case FilePicker.AvailableFilesRequested => - AvailableFilesRequested - } --> updates, - cls := "bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6", - Form( - Form.Body( - Form.Section( - FormHeader( - FormHeader.ViewModel( - "Doložení kritéria", - "Sestavte doklady poskytující důkaz kritéria a potvrďte odesláním formuláře. Případné limitace či jiné relevantní údaje vepište do pole Komentář." - ) - ), - FormFields( - FormRow( - "dokumenty", - "Dokumenty", - FilePicker(files.signal, availableFilesStream)(filesObserver) - .amend(idAttr := "dokumenty", cls("max-w-lg")) - ).toHtml, - FormRow( - "platnost", - "Platnost", - input( - idAttr := "platnost", - name := "platnost", - tpe := "date", - autoComplete := "date", - cls := "block max-w-lg w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" - ) - ).toHtml, - FormRow( - "komentar", - "Komentář", - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - textArea( - idAttr := "komentar", - name := "about", - rows := 3, - cls := "max-w-lg shadow-sm block w-full focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border border-gray-300 rounded-md" - ), - p( - cls := "mt-2 text-sm text-gray-500", - "Doplňte prosím potřebné informace související s doložením kritéria, včetně případných limitací." - ) - ) - ).toHtml - ) - ) - ), - submitButtons - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala deleted file mode 100644 index 41033f6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -import components._ - -object DirectoryPage: - - type ViewModel = List[UserRow.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - val (actionsStream, actionObserver) = - EventStream.withObserver[SearchForm.Action] - val $filter = actionsStream - .collect { - case SearchForm.ClearFilter => None - case SearchForm.SetFilter(t) => Some(t) - } - .startWith(None) - val byLetter = for { - d <- $m - f <- $filter - } yield for { - (letter, users) <- d - .filter { user => - f match - case None => true - case Some(t) => user.search.contains(t) - } - .groupBy(_.prijmeni.head) - .to(List) - .sortBy(_._1) - } yield (letter.toString, users.sortBy(_.prijmeni)) - - div( - cls := "h-full max-w-7xl mx-auto order-first flex flex-col flex-shrink-0", - SearchForm(actionObserver), - Directory(byLetter) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala deleted file mode 100644 index 73f915d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -object Directory: - - object Header: - type ViewModel = String - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500", - h3(child.text <-- $m) - ) - - object UserList: - type ViewModel = List[UserRow.ViewModel] - def apply($m: Signal[ViewModel]): HtmlElement = - ul( - role := "list", - cls := "relative z-0 divide-y divide-gray-200", - children <-- $m.split(_.osobniCislo)((_, _, s) => UserRow(s)) - ) - - type ViewModel = List[(String, List[UserRow.ViewModel])] - def apply($m: Signal[ViewModel]): HtmlElement = - nav( - cls := "flex-1 min-h-0 overflow-y-auto", - aria.label := "Directory", - children <-- $m.split(_._1)((_, _, s) => - div( - cls := "relative", - Header(s.map(_._1)), - UserList(s.map(_._2)) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala deleted file mode 100644 index e182910..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala +++ /dev/null @@ -1,54 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object SearchForm: - sealed trait Action - case object SubmitFilter extends Action - sealed trait FilterAction extends Action - case object ClearFilter extends FilterAction - case class SetFilter(value: String) extends FilterAction - - def apply(actions: Observer[Action]): HtmlElement = - div( - cls := "px-6 pt-4 pb-4", - form( - cls := "flex space-x-4", - onSubmit.mapTo(SubmitFilter) --> actions, - div( - cls := "flex-1 min-w-0", - label( - forId := "search", - cls := "sr-only", - "Hledat" - ), - div( - cls := "relative rounded-md shadow-sm", - div( - cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", - Icons.solid.search().amend(svg.cls := "text-gray-400") - ), - input( - tpe := "search", - name := "search", - idAttr := "search", - cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", - placeholder := "Hledat", - composeEvents(onInput.mapToValue.setAsValue.map(SetFilter(_)))( - _.throttle(500) - ) --> actions - ) - ) - ), - button( - tpe := "submit", - cls := "inline-flex justify-center px-3.5 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500", - Icons.solid.filter().amend(svg.cls := "text-gray-400"), - span( - cls := "sr-only", - "Hledat" - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala deleted file mode 100644 index 82f7723..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala +++ /dev/null @@ -1,52 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.airstream.core.Signal -import fiftyforms.ui.components.tailwind.Avatar - -object UserRow: - case class ViewModel( - osobniCislo: String, - celeJmeno: String, - prijmeni: String, - hlavniFunkce: Option[String], - img: Option[String], - container: () => HtmlElement = () => div() - ) { - val search = prijmeni.toLowerCase - } - - def apply($m: Signal[ViewModel]): HtmlElement = - inline def avatarImage = - Avatar($m.map(_.img)).avatarImage(10) - - li( - div( - cls := "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500", - div( - cls := "flex-shrink-0", - child <-- avatarImage - ), - div( - cls := "flex-1 min-w-0", - child <-- $m.map { o => - o.container() - .amend( - cls := "focus:outline-none", - span( - cls := "absolute inset-0", - aria.hidden := true - ), - p( - cls := "text-sm font-medium text-gray-900", - o.celeJmeno - ), - p( - cls := "text-sm text-gray-500 truncate", - o.hlavniFunkce - ) - ) - } - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala deleted file mode 100644 index 467c5f5..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala +++ /dev/null @@ -1,99 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router - -object ErrorPage: - case class ViewModel( - homePage: Page, - errorName: String, - title: String, - subTitle: String - ) - - def apply(m: ViewModel, actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - val ViewModel(homePage, errorName, title, subTitle) = m - div( - cls := "min-h-full pt-16 pb-12 flex flex-col bg-white", - main( - cls := "flex-grow flex flex-col justify-center max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex-shrink-0 flex justify-center", - a( - href := "/", - cls := "inline-flex", - span( - cls := "sr-only", - """Workflow""" - ), - img( - cls := "h-12 w-auto", - src := "https://tailwindui.com/img/logos/workflow-mark.svg?color=indigo&shade=600", - alt := "" - ) - ) - ), - div( - cls := "py-16", - div( - cls := "text-center", - p( - cls := "text-sm font-semibold text-indigo-600 uppercase tracking-wide", - errorName - ), - h1( - cls := "mt-2 text-4xl font-extrabold text-gray-900 tracking-tight sm:text-5xl", - title - ), - p( - cls := "mt-2 text-base text-gray-500", - subTitle - ), - div( - cls := "mt-6", - PageLink - .container(homePage, actionBus) - .amend( - cls := "text-base font-medium text-indigo-600 hover:text-indigo-500", - """Go back home""" - ) - ) - ) - ) - ), - footer( - cls := "flex-shrink-0 max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", - nav( - cls := "flex justify-center space-x-4", - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Contact Support""" - ), - span( - cls := "inline-block border-l border-gray-300", - aria.hidden := true - ), - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Status""" - ), - span( - cls := "inline-block border-l border-gray-300", - aria.hidden := true - ), - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Twitter""" - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala deleted file mode 100644 index bd9baec..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action - -object NotFoundPage: - def apply(homePage: Page, url: String, actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - ErrorPage( - ErrorPage.ViewModel( - homePage, - "404 error", - "Page not found.", - s"Sorry, but page $url doesn't exist." - ), - actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala deleted file mode 100644 index 476fe3c..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action - -object UnhandledErrorPage: - - case class ViewModel( - homePage: Page, - errorName: Option[String], - errorMessage: Option[String] - ) - - def apply(m: ViewModel, actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - ErrorPage( - ErrorPage.ViewModel( - m.homePage, - "Unexpected error occurred", - m.errorName.getOrElse( - "Uh oh!" - ), // TODO: translations, better text than uh oh - m.errorMessage.getOrElse( - "This wasn't supposed to happen! Please try again." - ) - ), - actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala deleted file mode 100644 index 25f059f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala +++ /dev/null @@ -1,6 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.services - -import com.raquo.airstream.core.Observer - -trait DataFetcher[K, A]: - def fetch(id: K, sink: Observer[A]): Unit diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala deleted file mode 100644 index a469830..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala +++ /dev/null @@ -1,120 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package state - -import com.raquo.airstream.core.EventStream -import cz.e_bs.cmi.mdr.pdb.{UserInfo, OsobniCislo} -import com.raquo.airstream.core.Observer -import scala.scalajs.js -import scala.scalajs.js.JSON -import zio.json._ -import com.raquo.airstream.eventbus.EventBus -import com.raquo.airstream.ownership.Owner -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.UserFunction -import cz.e_bs.cmi.mdr.pdb.UserContract -import fiftyforms.services.files.File - -trait AppState - extends connectors.DetailPageConnector.AppState - with connectors.DetailParametruPageConnector.AppState - with connectors.DetailKriteriaPageConnector.AppState - with pages.detail.UpravDukaz.State: - def users: EventStream[List[UserInfo]] - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - -class MockAppState(implicit owner: Owner, router: Router[Page]) - extends AppState: - - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonDecoder[UserFunction] = DeriveJsonDecoder.gen - given JsonDecoder[UserContract] = DeriveJsonDecoder.gen - given JsonDecoder[UserInfo] = DeriveJsonDecoder.gen - - given JsonDecoder[ParameterCriteria] = DeriveJsonDecoder.gen - given JsonDecoder[Parameter] = DeriveJsonDecoder.gen - - private val actions = EventBus[Action]() - private val (parametersStream, pushParameters) = - EventStream.withCallback[List[Parameter]] - private val (usersStream, pushUsers) = - EventStream.withCallback[List[UserInfo]] - private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] - private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] - - private val mockData: List[UserInfo] = - mockUsers - .asInstanceOf[js.Dictionary[js.Object]] - .values - // TODO: is there a more efficient way to parse from JS object directly? - .map { o => - val parsed = JSON.stringify(o).fromJson[UserInfo] - parsed.left.foreach(org.scalajs.dom.console.log(_)) - parsed - } - .collect { case Right(u) => - u - } - .toList - - private val mockParameters: List[Parameter] = - pdbParams - .asInstanceOf[js.Dictionary[js.Object]] - .values - .map(o => JSON.stringify(o).fromJson[Parameter]) - .collect { case Right(p) => p } - .toList - - // TODO: Extract to separate event handler - actions.events.foreach { - case FetchDirectory => pushUsers(mockData) - case FetchUserDetails(osc) => - mockData.find(_.personalNumber == osc).foreach { o => - pushDetails(o) - router.replaceState(Page.Detail(o)) - } - case FetchParameters(osc) => - pushParameters(mockParameters) - case FetchParameter(osc, paramId) => - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(Page.DetailParametru(o, p)) - case FetchParameterCriteria(osc, paramId, critId, page) => - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - c <- p.criteria.find(_.id == critId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(page(o, p, c)) - case NavigateTo(page) => router.pushState(page) - case FetchAvailableFiles(osc) => - pushFiles( - List( - File("https://tc163.cmi.cz/here", "Example file") - ) - ) - } - - override def users: EventStream[List[UserInfo]] = - usersStream.debugWithName("users") - - override def details: EventStream[UserInfo] = - detailsStream.debugWithName("details") - - override def parameters: EventStream[List[Parameter]] = - parametersStream.debugWithName("parameters") - - override def availableFiles: EventStream[List[File]] = - filesStream.debugWithName("available files") - - override def actionBus: Observer[Action] = - actions.writer.debugWithName("actions writer") diff --git a/app/src/main/scala/mdr/pdb/app/Main.scala b/app/src/main/scala/mdr/pdb/app/Main.scala new file mode 100644 index 0000000..081b4ee --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/Main.scala @@ -0,0 +1,103 @@ +package mdr.pdb.app + +import scala.scalajs.js.annotation.JSExportTopLevel +import scala.scalajs.js.annotation.JSExport +import scala.scalajs.js.annotation.JSImport +import scala.scalajs.js +import org.scalajs.dom +import com.raquo.laminar.api.L.{*, given} +import scala.scalajs.js.Date +import com.raquo.waypoint.Router +import com.raquo.waypoint.SplitRender +import mdr.pdb.app.services.DataFetcher +import scala.scalajs.js.JSON +import zio.json._ +import mdr.pdb.UserInfo +import mdr.pdb.app.state.AppState + +@js.native +@JSImport("stylesheets/main.css", JSImport.Namespace) +object Css extends js.Any + +@js.native +@JSImport("data/users.json", JSImport.Default) +object mockUsers extends js.Object + +@js.native +@JSImport("params/pdb-params.json", JSImport.Default) +object pdbParams extends js.Object + +@JSExportTopLevel("app") +object Main: + + @JSExport + def main(args: Array[String]): Unit = + import Routes.given + onLoad { + setupAirstream() + val appContainer = dom.document.querySelector("#app") + val _ = + render( + appContainer, + renderPage(state.MockAppState(using unsafeWindowOwner, router)) + ) + } + + private def onLoad(f: => Unit): Unit = + documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) + + private def setupAirstream()(using router: Router[Page]): Unit = + AirstreamError.registerUnhandledErrorCallback(err => + router.forcePage( + Page.UnhandledError( + Some(err.getClass.getName), // TODO: Fill only in dev mode + Some(err.getMessage) + ) + ) + ) + + def renderPage(state: AppState)(using + router: Router[Page] + ): HtmlElement = + val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) + .collectSignal[Page.Detail]( + connectors + .DetailPageConnector(state)(_) + .apply + ) + .collectSignal[Page.DetailParametru]( + connectors + .DetailParametruPageConnector(state)(_) + .apply + ) + .collectSignal[Page.DetailKriteria]( + connectors + .DetailKriteriaPageConnector(state)(_) + .apply + ) + .collectSignal[Page.UpravDukazKriteria]( + pages.detail.UpravDukaz.Connector(state)(_).apply + ) + .collectStatic(Page.Dashboard)( + connectors.DashboardPageConnector(state.actionBus).apply + ) + .collect[Page.NotFound](pg => + pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) + ) + .collect[Page.UnhandledError](pg => + pages.errors + .UnhandledErrorPage( + pages.errors.UnhandledErrorPage + .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), + state.actionBus + ) + ) + .collectStatic(Page.Directory)( + connectors + .DirectoryPageConnector(state.users, state.actionBus) + .apply + ) + div(cls := "h-full", child <-- pageSplitter.$view) + + // Pull in the stylesheet + val css: Css.type = Css diff --git a/app/src/main/scala/mdr/pdb/app/Routes.scala b/app/src/main/scala/mdr/pdb/app/Routes.scala new file mode 100644 index 0000000..2cc860c --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/Routes.scala @@ -0,0 +1,198 @@ +package mdr.pdb.app + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.* +import org.scalajs.dom +import zio.json.{*, given} +import mdr.pdb.OsobniCislo + +import scala.scalajs.js +import mdr.pdb.UserInfo +import mdr.pdb.Parameter +import mdr.pdb.ParameterCriteria +import mdr.pdb.app.Page.Titled + +// enum is not working with Waypoints' SplitRender collectStatic +sealed abstract class Page( + val id: String, + val title: String, + val parent: Option[Page] +) { + val path: Vector[Page] = + parent match + case None => Vector(this) + case Some(p) => p.path :+ this + + val isRoot: Boolean = parent.isEmpty +} + +object Page: + + case class Titled[V](value: V, title: Option[String] = None): + val show: String = title.getOrElse(value.toString) + + case object Directory extends Page("directory", "Adresář", None) + + case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) + + case class Detail(osobniCislo: Titled[OsobniCislo]) + extends Page("user", osobniCislo.show, Some(Directory)) + + object Detail { + def apply(o: UserInfo): Detail = Detail( + Titled(o.personalNumber, Some(o.name)) + ) + } + + case class DetailParametru( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String] + ) extends Page( + "parameter", + parametr.show, + Some(Detail(osobniCislo)) + ) + + object DetailParametru { + def apply(o: UserInfo, p: Parameter): DetailParametru = + DetailParametru( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)) + ) + } + + case class DetailKriteria( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String], + kriterium: Titled[String] + ) extends Page( + "criteria", + kriterium.show, + Some(DetailParametru(osobniCislo, parametr)) + ) + + object DetailKriteria { + def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = + DetailKriteria( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)), + Titled(k.id, Some(k.id)) + ) + } + + case class UpravDukazKriteria( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String], + kriterium: Titled[String] + ) extends Page( + "addProof", + "Důkaz", + Some(DetailKriteria(osobniCislo, parametr, kriterium)) + ) + + object UpravDukazKriteria { + def apply( + o: UserInfo, + p: Parameter, + k: ParameterCriteria + ): UpravDukazKriteria = + UpravDukazKriteria( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)), + Titled(k.id, Some(k.id)) + ) + } + + case class NotFound(url: String) extends Page("404", "404", Some(Directory)) + + case class UnhandledError( + errorName: Option[String], + errorMessage: Option[String] + ) extends Page("500", "Unexpected error", Some(Directory)) + +object Routes: + given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) + given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) + given [V: JsonEncoder]: JsonEncoder[Titled[V]] = + DeriveJsonEncoder.gen[Titled[V]] + given [V: JsonDecoder]: JsonDecoder[Titled[V]] = + DeriveJsonDecoder.gen[Titled[V]] + given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] + given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] + + val base = + js.`import`.meta.env.BASE_URL + .asInstanceOf[String] + "app" + + val homePage: Page = Page.Directory + + given router: Router[Page] = Router[Page]( + routes = List( + Route.static(homePage, root / endOfSegments, basePath = base), + Route.static( + Page.Dashboard, + root / "dashboard" / endOfSegments, + basePath = base + ), + Route[Page.Detail, String]( + encode = _.osobniCislo.value.toString, + decode = osc => Page.Detail(Titled(OsobniCislo(osc))), + root / "osoba" / segment[String] / endOfSegments, + basePath = base + ), + Route[Page.DetailParametru, (String, String)]( + encode = p => (p.osobniCislo.value.toString, p.parametr.value), + decode = + p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / endOfSegments, + basePath = base + ), + Route[Page.DetailKriteria, (String, String, String)]( + encode = p => + ( + p.osobniCislo.value.toString, + p.parametr.value, + p.kriterium.value.replaceAll("\\.", "--") + ), + decode = p => + Page.DetailKriteria( + Titled(OsobniCislo(p._1)), + Titled(p._2), + Titled(p._3.replaceAll("--", ".")) + ), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / "kriterium" / segment[String] / endOfSegments, + basePath = base + ), + Route[Page.UpravDukazKriteria, (String, String, String)]( + encode = p => + ( + p.osobniCislo.value.toString, + p.parametr.value, + p.kriterium.value.replaceAll("\\.", "--") + ), + decode = p => + Page.UpravDukazKriteria( + Titled(OsobniCislo(p._1)), + Titled(p._2), + Titled(p._3.replaceAll("--", ".")) + ), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / "kriterium" / segment[String] / "edit" / endOfSegments, + basePath = base + ) + ), + serializePage = _.toJson, + deserializePage = _.fromJson[Page] + .fold(s => throw IllegalStateException(s), identity), + getPageTitle = _.title, + routeFallback = url => Page.NotFound(url), + deserializeFallback = _ => Page.Dashboard + )( + $popStateEvent = windowEvents.onPopState, + owner = unsafeWindowOwner + ) diff --git a/app/src/main/scala/mdr/pdb/app/actions.scala b/app/src/main/scala/mdr/pdb/app/actions.scala new file mode 100644 index 0000000..d078b3a --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/actions.scala @@ -0,0 +1,21 @@ +package mdr.pdb.app + +import mdr.pdb.OsobniCislo +import mdr.pdb.UserInfo +import mdr.pdb.Parameter +import mdr.pdb.ParameterCriteria + +sealed trait Action + +case object FetchDirectory extends Action +case class FetchUserDetails(osc: OsobniCislo) extends Action +case class FetchParameters(osc: OsobniCislo) extends Action +case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action +case class FetchParameterCriteria( + osc: OsobniCislo, + paramId: String, + critId: String, + page: (UserInfo, Parameter, ParameterCriteria) => Page +) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action +case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/mdr/pdb/app/components/AppPage.scala new file mode 100644 index 0000000..130c817 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/AppPage.scala @@ -0,0 +1,82 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.Page +import mdr.pdb.{UserProfile, UserInfo, OsobniCislo} +import com.raquo.waypoint.Router +import mdr.pdb.app.Action +import mdr.pdb.app.NavigateTo +import mdr.pdb.UserFunction + +object AppPage: + // TODO: pages by logged in user + val pages: List[Page] = List(Page.Directory, Page.Dashboard) + + import NavigationBar.{Logo, MenuItem} + + val logo = Logo( + "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", + "Workflow" + ) + + // TODO: menu items by user profile + val userMenu = + List( + MenuItem("Your Profile"), + MenuItem("Settings"), + MenuItem("Sign out") + ) + + // TODO: load user profile + val $userProfile = Var( + UserProfile( + "tom", + UserInfo( + OsobniCislo("1031"), + "tom", + "Tom", + "Cook", + None, + None, + Some("tom@example.com"), + Some("+420 222 866 180"), + Some( + UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") + ), + Nil, + None + ) + ) + ) + + val $userInfo = $userProfile.signal.map(_.userInfo) + + type ViewModel = Option[HtmlElement] + def apply( + actionBus: Observer[Action] + )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using + router: Router[Page] + ): HtmlElement = + PageLayout(actionBus)( + $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => + PageLayout.ViewModel( + NavigationBar.ViewModel( + u, + pages.map(p => + NavigationBar.Link( + () => + PageLink( + p, + actionBus + ), + p == cp + ) + ), + userMenu, + logo + ), + c + ) + ), + mods + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala new file mode 100644 index 0000000..9375398 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala @@ -0,0 +1,143 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.tailwind.list.IconText.ViewModel +import fiftyforms.ui.components.tailwind.Icons +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.Page + +import com.raquo.laminar.api.L.{*, given} +import io.laminext.syntax.core.{*, given} +import mdr.pdb.app.Action + +object Breadcrumbs: + + private def slash = + import svg.{*, given} + svg( + cls := "flex-shrink-0 h-5 w-5 text-gray-300", + xmlns := "http://www.w3.org/2000/svg", + fill := "currentColor", + viewBox := "0 0 20 20", + ariaHidden := true, + path( + d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" + ) + ) + + def backIcon = + Icons.solid + .`arrow-narrow-left`() + .amend( + svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" + ) + + object Link: + case class ViewModel( + page: Page, + icon: Option[SvgElement], + extraClasses: String, + text: String, + textClasses: Option[String] = None + ) + + val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" + + def shortHome(p: Page) = ViewModel( + p, + Some(backIcon), + "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", + "Zpět na úvodní stránku", + None + ) + + def fullHome(p: Page) = ViewModel( + p, + Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), + "text-gray-400 hover:text-gray-500", + "Domů", + Some("sr-only") + ) + + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + PageLink + .container($m.map(_.page), actionBus) + .amend( + cls <-- $m.map(_.extraClasses), + child.maybe <-- $m.map(_.icon), + span( + cls <-- $m.map(_.textClasses.getOrElse("")), + child.text <-- $m.map(_.text) + ) + ) + + object FullBreadcrumbs: + type ViewModel = Page + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + ol( + role := "list", + cls := "flex items-center space-x-4", + children <-- $m.map(_.path).split(_.id)((_, _, $p) => + li( + div( + cls := "flex items-center", + child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), + Link( + $p.map(p => + if (p.isRoot) then Link.fullHome(p) + else + Link.ViewModel( + p, + None, + s"ml-4 max-w-xs truncate ${Link.baseClasses}", + p.title + ) + ), + actionBus + ) + ) + ) + ) + ) + + object ShortBreadcrumbs: + type ViewModel = Page + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + Link( + $m.map { p => + val target = p.parent.getOrElse(p) + if target.isRoot then Link.shortHome(target) + else + Link.ViewModel( + target, + Some(backIcon), + "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", + s"Zpět na ${target.title}" + ) + }, + actionBus + ) + + def apply(actionBus: Observer[Action])(using + router: Router[Page] + ): HtmlElement = + nav( + cls := "flex", + aria.label := "Breadcrumb", + div( + cls := "flex sm:hidden", + ShortBreadcrumbs(router.$currentPage, actionBus) + ), + div( + cls := "hidden sm:block", + FullBreadcrumbs(router.$currentPage, actionBus) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala new file mode 100644 index 0000000..4bb8b8d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala @@ -0,0 +1,242 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent +import fiftyforms.ui.components.tailwind.Icons +import fiftyforms.ui.components.tailwind.Avatar +import mdr.pdb.UserInfo +import io.laminext.syntax.core.* + +object NavigationBar: + + case class Logo(img: String, name: String) + case class Link(a: () => Anchor, active: Boolean) + case class MenuItem(title: String) + + case class ViewModel( + userInfo: UserInfo, + pages: List[Link], + userMenu: List[MenuItem], + logo: Logo + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + val $userInfo = $m.map(_.userInfo) + val mobileMenuOpen = Var(false) + + val desktopOnly = cls("hidden md:block") + val mobileOnly = cls("md:hidden") + + inline def avatarImage(size: Int = 8) = + Avatar($userInfo.map(_.img)).avatarImage(size) + + def notificationButton = button( + tpe := "button", + cls := List( + "bg-indigo-600", + "focus:outline-none", + "focus:ring-2", + "focus:ring-offset-2", + "focus:ring-offset-indigo-600", + "focus:ring-white", + "hover:text-white", + "p-1", + "rounded-full", + "text-indigo-200" + ), + span(cls := "sr-only", "View notifications"), + Icons.outline.bell() + ) + + def userProfile: HtmlElement = + val menuOpen = Var(false) + + def menuItem(item: MenuItem, idx: Int): HtmlElement = + a( + href := "#", + cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", + role := "menuitem", + tabIndex := -1, + idAttr := s"user-menu-item-$idx", + item.title + ) + + div( + cls := "ml-3 relative", + div( + button( + tpe := "button", + cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", + idAttr := "user-menu-button", + aria.expanded <-- menuOpen.signal, + aria.hasPopup := true, + span(cls := "sr-only", "Open user menu"), + child <-- avatarImage(), + onClick.preventDefault.mapTo( + !menuOpen.now() + ) --> menuOpen.writer + ) + ), + /* + * */ + div( + cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", + cls <-- menuOpen.signal.map { o => + if (o) "md:block" else "md:hidden" + }, + role := "menu", + aria.orientation := "vertical", + aria.labelledBy := "user-menu-button", + tabIndex := -1, + // : keyboard navigation + children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) + ) + ) + + def mobileProfile = + def menuItem(item: MenuItem): HtmlElement = + a( + href := "#", + cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", + item.title + ) + + div( + cls := "pt-4 pb-3 border-t border-indigo-700", + div( + cls := "flex items-center px-5", + div( + cls := "flex-shrink-0", + child <-- avatarImage(10) + ), + div( + cls := "ml-3", + div( + cls := "text-base font-medium text-white", + child.text <-- $userInfo.map(_.name) + ), + child.maybe <-- $userInfo.map( + _.email.map(e => + div( + cls := "text-sm font-medium text-indigo-300", + e + ) + ) + ) + ), + notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) + ), + div( + cls := "mt-3 px-2 space-y-1", + children <-- $m.map(_.userMenu.map(menuItem)) + ) + ) + + def pageLink(page: Link): Anchor = + page + .a() + .amend( + cls := "text-white px-3 py-2 rounded-md text-sm font-medium", + cls := Seq( + "bg-indigo-700" -> page.active, + "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active + ), + ariaCurrent := (if page.active then "page" else "false") + ) + + def logoImg: Image = + img( + cls := "h-8 w-8", + src <-- $m.map(_.logo.img), + alt <-- $m.map(_.logo.name) + ) + + def mobileMenuButton = button( + tpe := "button", + cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", + aria.controls := "mobile-menu", + aria.expanded <-- mobileMenuOpen.signal, + span(cls := "sr-only", "Open main menu"), + Icons.outline + .menu() + .amend( + svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") + ), + Icons.outline + .x() + .amend( + svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") + ), + onClick.preventDefault.mapTo( + !mobileMenuOpen.now() + ) --> mobileMenuOpen.writer + ) + + def navBarLeft = + div( + cls := "flex items-center", + div(cls := "flex-shrink-0", logoImg), + div( + desktopOnly, + div( + cls := "ml-10 flex items-baseline space-x-4", + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) + ) + ) + ) + + def navBarRight = + div( + desktopOnly, + div( + cls := "ml-4 flex items-center md:ml-6", + notificationButton, + userProfile + ) + ) + + def navBarMobile = + div( + cls := "-mr-2 flex", + mobileOnly, + mobileMenuButton + ) + + def navBar = + div( + cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", + div( + cls := "flex items-center justify-between h-16", + navBarLeft, + navBarRight, + navBarMobile + ) + ) + + def mobileMenu = + div( + mobileOnly, + cls <-- mobileMenuOpen.signal.map { o => + if (o) "block" else "hidden" + }, + idAttr := "mobile-menu", + div( + cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) + ), + mobileProfile + ) + + nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala deleted file mode 100644 index a3b00af..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala +++ /dev/null @@ -1,103 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import scala.scalajs.js.annotation.JSExportTopLevel -import scala.scalajs.js.annotation.JSExport -import scala.scalajs.js.annotation.JSImport -import scala.scalajs.js -import org.scalajs.dom -import com.raquo.laminar.api.L.{*, given} -import scala.scalajs.js.Date -import com.raquo.waypoint.Router -import com.raquo.waypoint.SplitRender -import cz.e_bs.cmi.mdr.pdb.app.services.DataFetcher -import scala.scalajs.js.JSON -import zio.json._ -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.state.AppState - -@js.native -@JSImport("stylesheets/main.css", JSImport.Namespace) -object Css extends js.Any - -@js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native -@JSImport("params/pdb-params.json", JSImport.Default) -object pdbParams extends js.Object - -@JSExportTopLevel("app") -object Main: - - @JSExport - def main(args: Array[String]): Unit = - import Routes.given - onLoad { - setupAirstream() - val appContainer = dom.document.querySelector("#app") - val _ = - render( - appContainer, - renderPage(state.MockAppState(using unsafeWindowOwner, router)) - ) - } - - private def onLoad(f: => Unit): Unit = - documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) - - private def setupAirstream()(using router: Router[Page]): Unit = - AirstreamError.registerUnhandledErrorCallback(err => - router.forcePage( - Page.UnhandledError( - Some(err.getClass.getName), // TODO: Fill only in dev mode - Some(err.getMessage) - ) - ) - ) - - def renderPage(state: AppState)(using - router: Router[Page] - ): HtmlElement = - val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) - .collectSignal[Page.Detail]( - connectors - .DetailPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailParametru]( - connectors - .DetailParametruPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailKriteria]( - connectors - .DetailKriteriaPageConnector(state)(_) - .apply - ) - .collectSignal[Page.UpravDukazKriteria]( - pages.detail.UpravDukaz.Connector(state)(_).apply - ) - .collectStatic(Page.Dashboard)( - connectors.DashboardPageConnector(state.actionBus).apply - ) - .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) - ) - .collect[Page.UnhandledError](pg => - pages.errors - .UnhandledErrorPage( - pages.errors.UnhandledErrorPage - .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), - state.actionBus - ) - ) - .collectStatic(Page.Directory)( - connectors - .DirectoryPageConnector(state.users, state.actionBus) - .apply - ) - div(cls := "h-full", child <-- pageSplitter.$view) - - // Pull in the stylesheet - val css: Css.type = Css diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala deleted file mode 100644 index 3bbc8c0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ /dev/null @@ -1,198 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.* -import org.scalajs.dom -import zio.json.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import scala.scalajs.js -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.Page.Titled - -// enum is not working with Waypoints' SplitRender collectStatic -sealed abstract class Page( - val id: String, - val title: String, - val parent: Option[Page] -) { - val path: Vector[Page] = - parent match - case None => Vector(this) - case Some(p) => p.path :+ this - - val isRoot: Boolean = parent.isEmpty -} - -object Page: - - case class Titled[V](value: V, title: Option[String] = None): - val show: String = title.getOrElse(value.toString) - - case object Directory extends Page("directory", "Adresář", None) - - case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) - - case class Detail(osobniCislo: Titled[OsobniCislo]) - extends Page("user", osobniCislo.show, Some(Directory)) - - object Detail { - def apply(o: UserInfo): Detail = Detail( - Titled(o.personalNumber, Some(o.name)) - ) - } - - case class DetailParametru( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String] - ) extends Page( - "parameter", - parametr.show, - Some(Detail(osobniCislo)) - ) - - object DetailParametru { - def apply(o: UserInfo, p: Parameter): DetailParametru = - DetailParametru( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)) - ) - } - - case class DetailKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "criteria", - kriterium.show, - Some(DetailParametru(osobniCislo, parametr)) - ) - - object DetailKriteria { - def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = - DetailKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class UpravDukazKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "addProof", - "Důkaz", - Some(DetailKriteria(osobniCislo, parametr, kriterium)) - ) - - object UpravDukazKriteria { - def apply( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): UpravDukazKriteria = - UpravDukazKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class NotFound(url: String) extends Page("404", "404", Some(Directory)) - - case class UnhandledError( - errorName: Option[String], - errorMessage: Option[String] - ) extends Page("500", "Unexpected error", Some(Directory)) - -object Routes: - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) - given [V: JsonEncoder]: JsonEncoder[Titled[V]] = - DeriveJsonEncoder.gen[Titled[V]] - given [V: JsonDecoder]: JsonDecoder[Titled[V]] = - DeriveJsonDecoder.gen[Titled[V]] - given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] - given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] - - val base = - js.`import`.meta.env.BASE_URL - .asInstanceOf[String] + "app" - - val homePage: Page = Page.Directory - - given router: Router[Page] = Router[Page]( - routes = List( - Route.static(homePage, root / endOfSegments, basePath = base), - Route.static( - Page.Dashboard, - root / "dashboard" / endOfSegments, - basePath = base - ), - Route[Page.Detail, String]( - encode = _.osobniCislo.value.toString, - decode = osc => Page.Detail(Titled(OsobniCislo(osc))), - root / "osoba" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.DetailParametru, (String, String)]( - encode = p => (p.osobniCislo.value.toString, p.parametr.value), - decode = - p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / endOfSegments, - basePath = base - ), - Route[Page.DetailKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.DetailKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.UpravDukazKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.UpravDukazKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / "edit" / endOfSegments, - basePath = base - ) - ), - serializePage = _.toJson, - deserializePage = _.fromJson[Page] - .fold(s => throw IllegalStateException(s), identity), - getPageTitle = _.title, - routeFallback = url => Page.NotFound(url), - deserializeFallback = _ => Page.Dashboard - )( - $popStateEvent = windowEvents.onPopState, - owner = unsafeWindowOwner - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala deleted file mode 100644 index b24b82b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -sealed trait Action - -case object FetchDirectory extends Action -case class FetchUserDetails(osc: OsobniCislo) extends Action -case class FetchParameters(osc: OsobniCislo) extends Action -case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action -case class FetchParameterCriteria( - osc: OsobniCislo, - paramId: String, - critId: String, - page: (UserInfo, Parameter, ParameterCriteria) => Page -) extends Action -case class FetchAvailableFiles(osc: OsobniCislo) extends Action -case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala deleted file mode 100644 index c637d35..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala +++ /dev/null @@ -1,82 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.{UserProfile, UserInfo, OsobniCislo} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo -import cz.e_bs.cmi.mdr.pdb.UserFunction - -object AppPage: - // TODO: pages by logged in user - val pages: List[Page] = List(Page.Directory, Page.Dashboard) - - import NavigationBar.{Logo, MenuItem} - - val logo = Logo( - "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", - "Workflow" - ) - - // TODO: menu items by user profile - val userMenu = - List( - MenuItem("Your Profile"), - MenuItem("Settings"), - MenuItem("Sign out") - ) - - // TODO: load user profile - val $userProfile = Var( - UserProfile( - "tom", - UserInfo( - OsobniCislo("1031"), - "tom", - "Tom", - "Cook", - None, - None, - Some("tom@example.com"), - Some("+420 222 866 180"), - Some( - UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") - ), - Nil, - None - ) - ) - ) - - val $userInfo = $userProfile.signal.map(_.userInfo) - - type ViewModel = Option[HtmlElement] - def apply( - actionBus: Observer[Action] - )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using - router: Router[Page] - ): HtmlElement = - PageLayout(actionBus)( - $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => - PageLayout.ViewModel( - NavigationBar.ViewModel( - u, - pages.map(p => - NavigationBar.Link( - () => - PageLink( - p, - actionBus - ), - p == cp - ) - ), - userMenu, - logo - ), - c - ) - ), - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala deleted file mode 100644 index bf473c6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ /dev/null @@ -1,143 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden -import fiftyforms.ui.components.tailwind.list.IconText.ViewModel -import fiftyforms.ui.components.tailwind.Icons -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Action - -object Breadcrumbs: - - private def slash = - import svg.{*, given} - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-300", - xmlns := "http://www.w3.org/2000/svg", - fill := "currentColor", - viewBox := "0 0 20 20", - ariaHidden := true, - path( - d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" - ) - ) - - def backIcon = - Icons.solid - .`arrow-narrow-left`() - .amend( - svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" - ) - - object Link: - case class ViewModel( - page: Page, - icon: Option[SvgElement], - extraClasses: String, - text: String, - textClasses: Option[String] = None - ) - - val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" - - def shortHome(p: Page) = ViewModel( - p, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - "Zpět na úvodní stránku", - None - ) - - def fullHome(p: Page) = ViewModel( - p, - Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), - "text-gray-400 hover:text-gray-500", - "Domů", - Some("sr-only") - ) - - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - PageLink - .container($m.map(_.page), actionBus) - .amend( - cls <-- $m.map(_.extraClasses), - child.maybe <-- $m.map(_.icon), - span( - cls <-- $m.map(_.textClasses.getOrElse("")), - child.text <-- $m.map(_.text) - ) - ) - - object FullBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - ol( - role := "list", - cls := "flex items-center space-x-4", - children <-- $m.map(_.path).split(_.id)((_, _, $p) => - li( - div( - cls := "flex items-center", - child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), - Link( - $p.map(p => - if (p.isRoot) then Link.fullHome(p) - else - Link.ViewModel( - p, - None, - s"ml-4 max-w-xs truncate ${Link.baseClasses}", - p.title - ) - ), - actionBus - ) - ) - ) - ) - ) - - object ShortBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - Link( - $m.map { p => - val target = p.parent.getOrElse(p) - if target.isRoot then Link.shortHome(target) - else - Link.ViewModel( - target, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - s"Zpět na ${target.title}" - ) - }, - actionBus - ) - - def apply(actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - nav( - cls := "flex", - aria.label := "Breadcrumb", - div( - cls := "flex sm:hidden", - ShortBreadcrumbs(router.$currentPage, actionBus) - ), - div( - cls := "hidden sm:block", - FullBreadcrumbs(router.$currentPage, actionBus) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala deleted file mode 100644 index 0db2047..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ /dev/null @@ -1,242 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent -import fiftyforms.ui.components.tailwind.Icons -import fiftyforms.ui.components.tailwind.Avatar -import cz.e_bs.cmi.mdr.pdb.UserInfo -import io.laminext.syntax.core.* - -object NavigationBar: - - case class Logo(img: String, name: String) - case class Link(a: () => Anchor, active: Boolean) - case class MenuItem(title: String) - - case class ViewModel( - userInfo: UserInfo, - pages: List[Link], - userMenu: List[MenuItem], - logo: Logo - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - val $userInfo = $m.map(_.userInfo) - val mobileMenuOpen = Var(false) - - val desktopOnly = cls("hidden md:block") - val mobileOnly = cls("md:hidden") - - inline def avatarImage(size: Int = 8) = - Avatar($userInfo.map(_.img)).avatarImage(size) - - def notificationButton = button( - tpe := "button", - cls := List( - "bg-indigo-600", - "focus:outline-none", - "focus:ring-2", - "focus:ring-offset-2", - "focus:ring-offset-indigo-600", - "focus:ring-white", - "hover:text-white", - "p-1", - "rounded-full", - "text-indigo-200" - ), - span(cls := "sr-only", "View notifications"), - Icons.outline.bell() - ) - - def userProfile: HtmlElement = - val menuOpen = Var(false) - - def menuItem(item: MenuItem, idx: Int): HtmlElement = - a( - href := "#", - cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", - role := "menuitem", - tabIndex := -1, - idAttr := s"user-menu-item-$idx", - item.title - ) - - div( - cls := "ml-3 relative", - div( - button( - tpe := "button", - cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - idAttr := "user-menu-button", - aria.expanded <-- menuOpen.signal, - aria.hasPopup := true, - span(cls := "sr-only", "Open user menu"), - child <-- avatarImage(), - onClick.preventDefault.mapTo( - !menuOpen.now() - ) --> menuOpen.writer - ) - ), - /* - * */ - div( - cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", - cls <-- menuOpen.signal.map { o => - if (o) "md:block" else "md:hidden" - }, - role := "menu", - aria.orientation := "vertical", - aria.labelledBy := "user-menu-button", - tabIndex := -1, - // : keyboard navigation - children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) - ) - ) - - def mobileProfile = - def menuItem(item: MenuItem): HtmlElement = - a( - href := "#", - cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", - item.title - ) - - div( - cls := "pt-4 pb-3 border-t border-indigo-700", - div( - cls := "flex items-center px-5", - div( - cls := "flex-shrink-0", - child <-- avatarImage(10) - ), - div( - cls := "ml-3", - div( - cls := "text-base font-medium text-white", - child.text <-- $userInfo.map(_.name) - ), - child.maybe <-- $userInfo.map( - _.email.map(e => - div( - cls := "text-sm font-medium text-indigo-300", - e - ) - ) - ) - ), - notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) - ), - div( - cls := "mt-3 px-2 space-y-1", - children <-- $m.map(_.userMenu.map(menuItem)) - ) - ) - - def pageLink(page: Link): Anchor = - page - .a() - .amend( - cls := "text-white px-3 py-2 rounded-md text-sm font-medium", - cls := Seq( - "bg-indigo-700" -> page.active, - "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active - ), - ariaCurrent := (if page.active then "page" else "false") - ) - - def logoImg: Image = - img( - cls := "h-8 w-8", - src <-- $m.map(_.logo.img), - alt <-- $m.map(_.logo.name) - ) - - def mobileMenuButton = button( - tpe := "button", - cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - aria.controls := "mobile-menu", - aria.expanded <-- mobileMenuOpen.signal, - span(cls := "sr-only", "Open main menu"), - Icons.outline - .menu() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") - ), - Icons.outline - .x() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") - ), - onClick.preventDefault.mapTo( - !mobileMenuOpen.now() - ) --> mobileMenuOpen.writer - ) - - def navBarLeft = - div( - cls := "flex items-center", - div(cls := "flex-shrink-0", logoImg), - div( - desktopOnly, - div( - cls := "ml-10 flex items-baseline space-x-4", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ) - ) - ) - - def navBarRight = - div( - desktopOnly, - div( - cls := "ml-4 flex items-center md:ml-6", - notificationButton, - userProfile - ) - ) - - def navBarMobile = - div( - cls := "-mr-2 flex", - mobileOnly, - mobileMenuButton - ) - - def navBar = - div( - cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex items-center justify-between h-16", - navBarLeft, - navBarRight, - navBarMobile - ) - ) - - def mobileMenu = - div( - mobileOnly, - cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }, - idAttr := "mobile-menu", - div( - cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ), - mobileProfile - ) - - nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala deleted file mode 100644 index 75339df..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object PageHeader: - def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = - header( - cls := "bg-white shadow-sm", - div( - cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", - h1( - cls := "text-lg leading-6 font-semibold text-gray-900", - Breadcrumbs(actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala deleted file mode 100644 index 5096463..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ /dev/null @@ -1,30 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import fiftyforms.ui.components.tailwind.Loading -import io.laminext.syntax.core.* - -object PageLayout: - case class ViewModel( - navigation: NavigationBar.ViewModel, - content: Option[HtmlElement] - ) - def apply(actionBus: Observer[Action])( - $m: Signal[ViewModel], - mods: Modifier[HtmlElement]* - )(using router: Router[Page]): HtmlElement = - val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) - div( - cls := "h-full flex flex-col", - NavigationBar($m.map(_.navigation)), - child.maybe <-- router.$currentPage.map(_.isRoot) - .switch(None, Some(PageHeader(actionBus))), - main( - cls := "flex-grow-1 overflow-y-auto", - mods, - child <-- $maybeContent.map(_.getOrElse(Loading)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala deleted file mode 100644 index 62d820f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.LinkSupport.* -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo - -object PageLink: - type ViewModel = Page - def apply($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = - a(mods($m, actions), child.text <-- $m.map(_.title)) - - def apply(m: ViewModel, actions: Observer[Action])(using - router: Router[Page] - ): Anchor = apply(Val(m), actions) - - def container($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = a(mods($m, actions)) - - def container(m: ViewModel, actions: Observer[Action])(using - Router[Page] - ): Anchor = container(Val(m), actions) - - private def mods($m: Signal[Page], actions: Observer[Action])(using - router: Router[Page] - ): Modifier[Anchor] = - Seq( - href <-- $m.map(router.absoluteUrlForPage).recover { case _ => - Some("invalid url") - }, - composeEvents(onClick.noKeyMod.preventDefault)( - _.sample($m) - .map(NavigateTo.apply) - ) --> actions - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala deleted file mode 100644 index 87f825a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.pages.dashboard.DashboardPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -class DashboardPageConnector(actionBus: Observer[Action])(using - router: Router[Page] -): - def apply: HtmlElement = - AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala deleted file mode 100644 index e9b0f71..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala +++ /dev/null @@ -1,76 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailKriteriaPage -import pages.detail.components.DukazyKriteria -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -object DetailKriteriaPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailKriteriaPageConnector( - state: DetailKriteriaPageConnector.AppState -)( - $page: Signal[Page.DetailKriteria] -)(using Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => - (p.osobniCislo.value, p.parametr.value, p.kriterium.value) - )((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map( - FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) - ) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, s, $s) => - DetailKriteriaPage($s)(state.actionBus.contramap { - case DukazyKriteria.Add => - NavigateTo( - Page.UpravDukazKriteria( - Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), - Page.Titled(s.parametr.id, Some(s.parametr.nazev)), - Page.Titled(s.kriterium.id, Some(s.kriterium.id)) - ) - ) - }) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): DetailKriteriaPage.ViewModel = - DetailKriteriaPage.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala deleted file mode 100644 index 4d8024f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ /dev/null @@ -1,57 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import pages.detail.DetailPage -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import fiftyforms.ui.components.tailwind.Color - -object DetailPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailPageConnector(state: DetailPageConnector.AppState)( - $page: Signal[Page.Detail] -)(using router: Router[Page]): - val $oscChangeSignal = $page.splitOne(_.osobniCislo.value)((osc, _, _) => osc) - val $pageChangeSignal = - $oscChangeSignal.flatMap(osc => - EventStream.fromSeq(Seq(FetchUserDetails(osc), FetchParameters(osc))) - ) - // TODO: filter the value based on the current osc - // OSC change will fetch new data, but still - // - we need to be sure that what we got is really what we ought to display - // - we want to display stale data accordingly (at least with loading indicator) - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - def apply: HtmlElement = - AppPage(state.actionBus)( - $data.combineWithFn($params)(_ zip _) - .map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: List[Parameter] - ): DetailPage.ViewModel = - DetailPage.ViewModel( - o.toDetailOsoby, - p.map( - _.toParametr(param => - PageLink.container(Page.DetailParametru(o, param), state.actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala deleted file mode 100644 index fdaa7f8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailParametruPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink - -object DetailParametruPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailParametruPageConnector( - state: DetailParametruPageConnector.AppState -)( - $page: Signal[Page.DetailParametru] -)(using router: Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => (p.osobniCislo.value, p.parametr.value))((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map(FetchParameter(_, _)) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - } yield (da, pb) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailParametruPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter - ): DetailParametruPage.ViewModel = - DetailParametruPage.ViewModel( - o.toDetailOsoby, - p.toParametr(p => - PageLink.container(Page.DetailParametru(o, p), state.actionBus) - ), - p.criteria.map( - _.toKriterium { c => - PageLink.container( - Page.DetailKriteria(o, p, c), - state.actionBus - ) - } - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala deleted file mode 100644 index 049aa22..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -case class DirectoryPageConnector( - $input: EventStream[List[UserInfo]], - actionBus: Observer[Action] -)(using router: Router[Page]): - val $data = $input.startWithNone - val $actionSignal = EventStream.fromValue(FetchDirectory) - - def apply: HtmlElement = - AppPage(actionBus)( - $data.split(_ => ())((_, _, s) => - pages.directory.DirectoryPage( - s.map( - _.map( - _.toUserRow(u => - PageLink.container( - Page.Detail(Page.Titled(u.personalNumber)), - actionBus - ) - ) - ) - ) - ) - ), - $actionSignal --> actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala deleted file mode 100644 index 88e7450..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ /dev/null @@ -1,68 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii -import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria -import fiftyforms.ui.components.tailwind.Color - -extension (o: UserInfo) - def toDetailOsoby: DetailOsoby.ViewModel = - DetailOsoby.ViewModel( - o.personalNumber, - o.name, - o.email, - o.phone, - o.img, - o.mainFunction.map(f => - DetailOsoby.Funkce.ViewModel(f.name, f.dept, f.ou) - ), - o.userContracts.headOption.map(c => - DetailOsoby.PracovniPomer.ViewModel(c.rel, c.startDate, c.endDate) - ) - ) - -extension (param: Parameter) - def toParametr(container: Parameter => Anchor): DetailParametru.ViewModel = - DetailParametru.ViewModel( - id = param.id, - nazev = param.name, - popis = param.description, - status = "Nesplněno", - statusColor = Color.red, - a = container(param) - ) - -extension (crit: ParameterCriteria) - def toKriterium( - container: ParameterCriteria => Anchor - ): DetailKriteria.ViewModel = - DetailKriteria.ViewModel( - nazev = crit.criteriumText, - kapitola = crit.chapterId, - bod = crit.itemId, - status = "Nesplněno", - statusColor = Color.red, - splneno = false, - dukazy = Nil, - container = container(crit) - ) - -extension (user: UserInfo) - def toUserRow( - container: UserInfo => HtmlElement = (_: UserInfo) => div() - ): UserRow.ViewModel = - UserRow.ViewModel( - osobniCislo = user.personalNumber.toString, - celeJmeno = user.name, - prijmeni = user.surname, - hlavniFunkce = user.mainFunction.map(_.name), - img = user.img, - container = () => container(user) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala deleted file mode 100644 index 7d5b2c1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.dashboard - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.Page - -object DashboardPage: - - def render: HtmlElement = - div("Dashboard page") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala deleted file mode 100644 index 9ef0698..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala +++ /dev/null @@ -1,31 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailKriteriaPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply($m: Signal[ViewModel])( - action: Observer[DukazyKriteria.Action] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - DukazyKriteria($m.map(_.kriterium.dukazy))(action) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala deleted file mode 100644 index 3b316e1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object DetailPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametry: SeznamParametru.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby($m.map(_.osoba)), - SeznamParametru($m.map(_.parametry)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala deleted file mode 100644 index 17224cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala +++ /dev/null @@ -1,24 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailParametruPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriteria: SeznamKriterii.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby.header($m.map(_.osoba)), - DetailParametru($m.map(_.parametr)), - SeznamKriterii($m.map(_.kriteria)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala deleted file mode 100644 index dc912fa..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ /dev/null @@ -1,107 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb -package app -package pages.detail - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.UpravDukazForm -import fiftyforms.services.files.File - -object UpravDukaz: - - type ThisPage = Page.UpravDukazKriteria - type PageKey = (OsobniCislo, String, String) - - trait State { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def availableFiles: EventStream[List[File]] - def actionBus: Observer[Action] - } - - def keyOfPage(page: ThisPage): PageKey = - (page.osobniCislo.value, page.parametr.value, page.kriterium.value) - - def onChangeAction(key: PageKey): Action = - FetchParameterCriteria( - key._1, - key._2, - key._3, - Page.UpravDukazKriteria(_, _, _) - ) - - class Connector(state: State)($page: Signal[ThisPage])(using Router[Page]): - val $paramChangeSignal = $page.splitOne(keyOfPage)((x, _, _) => x) - val $pageChangeSignal = $paramChangeSignal.map(onChangeAction) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.split(_ => ())((_, s, $s) => - PageComponent( - $s.map(buildModel), - state.availableFiles, - state.actionBus.contramap { - case UpravDukazForm.Cancelled => - NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) - case UpravDukazForm.AvailableFilesRequested => - FetchAvailableFiles(s._1.personalNumber) - } - ) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): PageComponent.ViewModel = - import connectors.* - PageComponent.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) - - object PageComponent: - import components.* - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply( - $m: Signal[ViewModel], - availableFilesStream: EventStream[List[File]], - events: Observer[UpravDukazForm.Event] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - UpravDukazForm(availableFilesStream)(events) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala deleted file mode 100644 index ca311b0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.Icons -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Color - -object DetailKriteria: - case class ViewModel( - nazev: String, - kapitola: String, - bod: String, - status: String, - statusColor: Color, - splneno: Boolean, - dukazy: List[DukazKriteria.ViewModel], - container: HtmlElement = div() - ) { - val id = s"${kapitola}${bod}" - } - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h3( - cls := "text-xl leading-6 font-bold text-gray-900", - child.text <-- $m.map(_.nazev) - ), - h3( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - "Kapitola ", - child.text <-- $m.map(_.kapitola), - " bod ", - child <-- $m.map(_.bod) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala deleted file mode 100644 index ae90290..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala +++ /dev/null @@ -1,100 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Avatar - -object DetailOsoby: - - object Funkce: - case class ViewModel( - nazev: String, - stredisko: String, - voj: String - ) - - def render($m: Signal[ViewModel]): HtmlElement = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.nazev), - span( - cls := "hidden md:inline", - " @ ", - child.text <-- $m.map(_.stredisko), - ", ", - child.text <-- $m.map(_.voj) - ) - ) - - object PracovniPomer: - case class ViewModel( - druh: String, - pocatek: LocalDate, - konec: Option[LocalDate] - ) - - def render($m: Signal[ViewModel]): HtmlElement = - import fiftyforms.ui.components.tailwind.CustomAttrs.datetime - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.druh), - " od ", - time( - datetime <-- $m.map(_.pocatek.toString), - child.text <-- $m.map(_.pocatek.toString) - ) - ) - - case class ViewModel( - osobniCislo: OsobniCislo, - jmeno: String, - email: Option[String], - telefon: Option[String], - img: Option[String], - hlavniFunkce: Option[Funkce.ViewModel], - pracovniPomer: Option[PracovniPomer.ViewModel] - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-4", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(16) - ), - div( - h1( - cls := "text-2xl font-bold text-gray-900", - child.text <-- $m.map(_.jmeno) - ), - child.maybe <-- $m.map(_.hlavniFunkce).split(_ => ())((_, _, d) => - Funkce.render(d) - ), - child.maybe <-- $m.map(_.pracovniPomer).split(_ => ())((_, _, d) => - PracovniPomer.render(d) - ) - ) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-5", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(8) - ), - div( - h1( - cls := "text-lg font-medium text-gray-800", - child.text <-- $m.map(_.jmeno) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala deleted file mode 100644 index e11cb62..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color - -object DetailParametru: - case class ViewModel( - id: String, - nazev: String, - popis: String, - status: String, - statusColor: Color, - a: Anchor - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h2( - cls := "text-2xl leading-6 font-medium text-gray-900", - child.text <-- $m.map(_.nazev) - ), - p( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - child.text <-- $m.map(_.popis) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - h2( - cls := "text-lg leading-6 font-medium text-gray-600", - child.text <-- $m.map(_.nazev) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala deleted file mode 100644 index a00ede8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala +++ /dev/null @@ -1,177 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.CustomAttrs - -object DukazKriteria: - case class Osoba(osobniCislo: String, jmeno: String) - case class Udalost(osoba: Osoba, datum: LocalDate) - case class Dokument( - url: String, - nazev: String, - pridal: Udalost, - odebral: Option[Udalost] - ) - case class ViewModel( - dokumenty: List[Dokument], - autorizoval: Option[Udalost], - platiDo: Option[LocalDate], - poznámka: Option[String] - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-lg", - div( - cls := "px-4 py-5 sm:px-6", - h3( - cls := "text-lg leading-6 font-medium text-gray-900", - """Applicant Information""" - ), - p( - cls := "mt-1 max-w-2xl text-sm text-gray-500", - """Personal details and application.""" - ) - ), - div( - cls := "border-t border-gray-200 px-4 py-5 sm:px-6", - dl( - cls := "grid grid-cols-1 gap-x-4 gap-y-8 sm:grid-cols-2", - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Full name""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Margot Foster""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Application for""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Backend Developer""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Email address""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """margotfoster@example.com""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Salary expectation""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """$120,000""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """About""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Fugiat ipsum ipsum deserunt culpa aute sint do nostrud anim incididunt cillum culpa consequat. Excepteur qui ipsum aliquip consequat sint. Sit id mollit nulla mollit nostrud in ea officia proident. Irure nostrud pariatur mollit ad adipisicing reprehenderit deserunt qui eu.""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """Attachments""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - ul( - role := "list", - cls := "border border-gray-200 rounded-md divide-y divide-gray-200", - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """resume_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ), - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """coverletter_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala deleted file mode 100644 index 22f134f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala +++ /dev/null @@ -1,35 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object DukazyKriteria: - sealed trait Action - case object Add extends Action - - type ViewModel = List[DukazKriteria.ViewModel] - - def apply($m: Signal[ViewModel])(actions: Observer[Action]): HtmlElement = - div( - child.maybe <-- $m.map(m => - if (m.isEmpty) then Some(prazdnyDukaz(actions)) - else None - ), - children <-- $m.map(_.zipWithIndex).split(_._2)((_, _, $s) => - DukazKriteria($s.map(_._1)) - ) - ) - - private def prazdnyDukaz(actions: Observer[Action]): HtmlElement = - button( - tpe := "button", - onClick.preventDefault.mapTo(Add) --> actions, - cls := "relative block w-full border-2 border-gray-300 border-dashed rounded-lg p-12 text-center hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - Icons.outline - .`document-add`(12) - .amend(svg.cls := "mx-auto text-gray-400"), - span( - cls := "mt-2 block text-sm font-medium text-gray-900", - "Přidat důkaz" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala deleted file mode 100644 index f628697..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.list.StackedList -import fiftyforms.ui.components.tailwind.list.ListRow -import fiftyforms.ui.components.tailwind.list.RowTag -import fiftyforms.ui.components.tailwind.list.RowNext -import java.time.LocalDate - -object SeznamKriterii: - type ViewModel = List[DetailKriteria.ViewModel] - - private val kritList = new StackedList[DetailKriteria.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - kritList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = - p(cls := "text-sm text-gray-500", s"${i.kapitola}${i.bod}"), - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.container - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala deleted file mode 100644 index 2bfe4bc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.list.{ - StackedList, - ListRow, - RowTag, - PropList, - IconText, - RowNext -} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.LinkSupport.* - -object SeznamParametru: - type ViewModel = List[DetailParametru.ViewModel] - - private val parametrList = new StackedList[DetailParametru.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - parametrList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = emptyNode, - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.a - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala deleted file mode 100644 index f048e9d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ /dev/null @@ -1,101 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.form.* -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement -import fiftyforms.services.files.components.tailwind.FilePicker -import cz.e_bs.cmi.mdr.pdb.frontend.AutorizujDukaz -import cz.e_bs.cmi.mdr.pdb.frontend.DocumentRef -import fiftyforms.services.files.File - -object UpravDukazForm: - sealed trait Event - case object Cancelled extends Event - case object AvailableFilesRequested extends Event - def apply(availableFilesStream: EventStream[List[File]])( - updates: Observer[Event] - ): HtmlElement = - val (filesStream, filesObserver) = - EventStream.withObserver[FilePicker.Event] - val files = Var[List[File]](Nil) - def submitButtons: HtmlElement = - div( - cls := "pt-5", - div( - cls := "flex justify-end", - button( - tpe := "button", - cls := "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zrušit", - onClick.mapTo(Cancelled) --> updates - ), - button( - tpe := "submit", - cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - disabled <-- files.signal.map(_.isEmpty), - "Autorizovat" - ) - ) - ) - - div( - filesStream.collect { case FilePicker.SelectionUpdated(files) => - files.to(List) - } --> files.writer, - filesStream.collect { case FilePicker.AvailableFilesRequested => - AvailableFilesRequested - } --> updates, - cls := "bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6", - Form( - Form.Body( - Form.Section( - FormHeader( - FormHeader.ViewModel( - "Doložení kritéria", - "Sestavte doklady poskytující důkaz kritéria a potvrďte odesláním formuláře. Případné limitace či jiné relevantní údaje vepište do pole Komentář." - ) - ), - FormFields( - FormRow( - "dokumenty", - "Dokumenty", - FilePicker(files.signal, availableFilesStream)(filesObserver) - .amend(idAttr := "dokumenty", cls("max-w-lg")) - ).toHtml, - FormRow( - "platnost", - "Platnost", - input( - idAttr := "platnost", - name := "platnost", - tpe := "date", - autoComplete := "date", - cls := "block max-w-lg w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" - ) - ).toHtml, - FormRow( - "komentar", - "Komentář", - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - textArea( - idAttr := "komentar", - name := "about", - rows := 3, - cls := "max-w-lg shadow-sm block w-full focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border border-gray-300 rounded-md" - ), - p( - cls := "mt-2 text-sm text-gray-500", - "Doplňte prosím potřebné informace související s doložením kritéria, včetně případných limitací." - ) - ) - ).toHtml - ) - ) - ), - submitButtons - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala deleted file mode 100644 index 41033f6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -import components._ - -object DirectoryPage: - - type ViewModel = List[UserRow.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - val (actionsStream, actionObserver) = - EventStream.withObserver[SearchForm.Action] - val $filter = actionsStream - .collect { - case SearchForm.ClearFilter => None - case SearchForm.SetFilter(t) => Some(t) - } - .startWith(None) - val byLetter = for { - d <- $m - f <- $filter - } yield for { - (letter, users) <- d - .filter { user => - f match - case None => true - case Some(t) => user.search.contains(t) - } - .groupBy(_.prijmeni.head) - .to(List) - .sortBy(_._1) - } yield (letter.toString, users.sortBy(_.prijmeni)) - - div( - cls := "h-full max-w-7xl mx-auto order-first flex flex-col flex-shrink-0", - SearchForm(actionObserver), - Directory(byLetter) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala deleted file mode 100644 index 73f915d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -object Directory: - - object Header: - type ViewModel = String - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500", - h3(child.text <-- $m) - ) - - object UserList: - type ViewModel = List[UserRow.ViewModel] - def apply($m: Signal[ViewModel]): HtmlElement = - ul( - role := "list", - cls := "relative z-0 divide-y divide-gray-200", - children <-- $m.split(_.osobniCislo)((_, _, s) => UserRow(s)) - ) - - type ViewModel = List[(String, List[UserRow.ViewModel])] - def apply($m: Signal[ViewModel]): HtmlElement = - nav( - cls := "flex-1 min-h-0 overflow-y-auto", - aria.label := "Directory", - children <-- $m.split(_._1)((_, _, s) => - div( - cls := "relative", - Header(s.map(_._1)), - UserList(s.map(_._2)) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala deleted file mode 100644 index e182910..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala +++ /dev/null @@ -1,54 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object SearchForm: - sealed trait Action - case object SubmitFilter extends Action - sealed trait FilterAction extends Action - case object ClearFilter extends FilterAction - case class SetFilter(value: String) extends FilterAction - - def apply(actions: Observer[Action]): HtmlElement = - div( - cls := "px-6 pt-4 pb-4", - form( - cls := "flex space-x-4", - onSubmit.mapTo(SubmitFilter) --> actions, - div( - cls := "flex-1 min-w-0", - label( - forId := "search", - cls := "sr-only", - "Hledat" - ), - div( - cls := "relative rounded-md shadow-sm", - div( - cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", - Icons.solid.search().amend(svg.cls := "text-gray-400") - ), - input( - tpe := "search", - name := "search", - idAttr := "search", - cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", - placeholder := "Hledat", - composeEvents(onInput.mapToValue.setAsValue.map(SetFilter(_)))( - _.throttle(500) - ) --> actions - ) - ) - ), - button( - tpe := "submit", - cls := "inline-flex justify-center px-3.5 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500", - Icons.solid.filter().amend(svg.cls := "text-gray-400"), - span( - cls := "sr-only", - "Hledat" - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala deleted file mode 100644 index 82f7723..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala +++ /dev/null @@ -1,52 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.airstream.core.Signal -import fiftyforms.ui.components.tailwind.Avatar - -object UserRow: - case class ViewModel( - osobniCislo: String, - celeJmeno: String, - prijmeni: String, - hlavniFunkce: Option[String], - img: Option[String], - container: () => HtmlElement = () => div() - ) { - val search = prijmeni.toLowerCase - } - - def apply($m: Signal[ViewModel]): HtmlElement = - inline def avatarImage = - Avatar($m.map(_.img)).avatarImage(10) - - li( - div( - cls := "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500", - div( - cls := "flex-shrink-0", - child <-- avatarImage - ), - div( - cls := "flex-1 min-w-0", - child <-- $m.map { o => - o.container() - .amend( - cls := "focus:outline-none", - span( - cls := "absolute inset-0", - aria.hidden := true - ), - p( - cls := "text-sm font-medium text-gray-900", - o.celeJmeno - ), - p( - cls := "text-sm text-gray-500 truncate", - o.hlavniFunkce - ) - ) - } - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala deleted file mode 100644 index 467c5f5..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala +++ /dev/null @@ -1,99 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router - -object ErrorPage: - case class ViewModel( - homePage: Page, - errorName: String, - title: String, - subTitle: String - ) - - def apply(m: ViewModel, actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - val ViewModel(homePage, errorName, title, subTitle) = m - div( - cls := "min-h-full pt-16 pb-12 flex flex-col bg-white", - main( - cls := "flex-grow flex flex-col justify-center max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex-shrink-0 flex justify-center", - a( - href := "/", - cls := "inline-flex", - span( - cls := "sr-only", - """Workflow""" - ), - img( - cls := "h-12 w-auto", - src := "https://tailwindui.com/img/logos/workflow-mark.svg?color=indigo&shade=600", - alt := "" - ) - ) - ), - div( - cls := "py-16", - div( - cls := "text-center", - p( - cls := "text-sm font-semibold text-indigo-600 uppercase tracking-wide", - errorName - ), - h1( - cls := "mt-2 text-4xl font-extrabold text-gray-900 tracking-tight sm:text-5xl", - title - ), - p( - cls := "mt-2 text-base text-gray-500", - subTitle - ), - div( - cls := "mt-6", - PageLink - .container(homePage, actionBus) - .amend( - cls := "text-base font-medium text-indigo-600 hover:text-indigo-500", - """Go back home""" - ) - ) - ) - ) - ), - footer( - cls := "flex-shrink-0 max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", - nav( - cls := "flex justify-center space-x-4", - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Contact Support""" - ), - span( - cls := "inline-block border-l border-gray-300", - aria.hidden := true - ), - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Status""" - ), - span( - cls := "inline-block border-l border-gray-300", - aria.hidden := true - ), - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Twitter""" - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala deleted file mode 100644 index bd9baec..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action - -object NotFoundPage: - def apply(homePage: Page, url: String, actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - ErrorPage( - ErrorPage.ViewModel( - homePage, - "404 error", - "Page not found.", - s"Sorry, but page $url doesn't exist." - ), - actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala deleted file mode 100644 index 476fe3c..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action - -object UnhandledErrorPage: - - case class ViewModel( - homePage: Page, - errorName: Option[String], - errorMessage: Option[String] - ) - - def apply(m: ViewModel, actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - ErrorPage( - ErrorPage.ViewModel( - m.homePage, - "Unexpected error occurred", - m.errorName.getOrElse( - "Uh oh!" - ), // TODO: translations, better text than uh oh - m.errorMessage.getOrElse( - "This wasn't supposed to happen! Please try again." - ) - ), - actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala deleted file mode 100644 index 25f059f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala +++ /dev/null @@ -1,6 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.services - -import com.raquo.airstream.core.Observer - -trait DataFetcher[K, A]: - def fetch(id: K, sink: Observer[A]): Unit diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala deleted file mode 100644 index a469830..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala +++ /dev/null @@ -1,120 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package state - -import com.raquo.airstream.core.EventStream -import cz.e_bs.cmi.mdr.pdb.{UserInfo, OsobniCislo} -import com.raquo.airstream.core.Observer -import scala.scalajs.js -import scala.scalajs.js.JSON -import zio.json._ -import com.raquo.airstream.eventbus.EventBus -import com.raquo.airstream.ownership.Owner -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.UserFunction -import cz.e_bs.cmi.mdr.pdb.UserContract -import fiftyforms.services.files.File - -trait AppState - extends connectors.DetailPageConnector.AppState - with connectors.DetailParametruPageConnector.AppState - with connectors.DetailKriteriaPageConnector.AppState - with pages.detail.UpravDukaz.State: - def users: EventStream[List[UserInfo]] - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - -class MockAppState(implicit owner: Owner, router: Router[Page]) - extends AppState: - - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonDecoder[UserFunction] = DeriveJsonDecoder.gen - given JsonDecoder[UserContract] = DeriveJsonDecoder.gen - given JsonDecoder[UserInfo] = DeriveJsonDecoder.gen - - given JsonDecoder[ParameterCriteria] = DeriveJsonDecoder.gen - given JsonDecoder[Parameter] = DeriveJsonDecoder.gen - - private val actions = EventBus[Action]() - private val (parametersStream, pushParameters) = - EventStream.withCallback[List[Parameter]] - private val (usersStream, pushUsers) = - EventStream.withCallback[List[UserInfo]] - private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] - private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] - - private val mockData: List[UserInfo] = - mockUsers - .asInstanceOf[js.Dictionary[js.Object]] - .values - // TODO: is there a more efficient way to parse from JS object directly? - .map { o => - val parsed = JSON.stringify(o).fromJson[UserInfo] - parsed.left.foreach(org.scalajs.dom.console.log(_)) - parsed - } - .collect { case Right(u) => - u - } - .toList - - private val mockParameters: List[Parameter] = - pdbParams - .asInstanceOf[js.Dictionary[js.Object]] - .values - .map(o => JSON.stringify(o).fromJson[Parameter]) - .collect { case Right(p) => p } - .toList - - // TODO: Extract to separate event handler - actions.events.foreach { - case FetchDirectory => pushUsers(mockData) - case FetchUserDetails(osc) => - mockData.find(_.personalNumber == osc).foreach { o => - pushDetails(o) - router.replaceState(Page.Detail(o)) - } - case FetchParameters(osc) => - pushParameters(mockParameters) - case FetchParameter(osc, paramId) => - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(Page.DetailParametru(o, p)) - case FetchParameterCriteria(osc, paramId, critId, page) => - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - c <- p.criteria.find(_.id == critId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(page(o, p, c)) - case NavigateTo(page) => router.pushState(page) - case FetchAvailableFiles(osc) => - pushFiles( - List( - File("https://tc163.cmi.cz/here", "Example file") - ) - ) - } - - override def users: EventStream[List[UserInfo]] = - usersStream.debugWithName("users") - - override def details: EventStream[UserInfo] = - detailsStream.debugWithName("details") - - override def parameters: EventStream[List[Parameter]] = - parametersStream.debugWithName("parameters") - - override def availableFiles: EventStream[List[File]] = - filesStream.debugWithName("available files") - - override def actionBus: Observer[Action] = - actions.writer.debugWithName("actions writer") diff --git a/app/src/main/scala/mdr/pdb/app/Main.scala b/app/src/main/scala/mdr/pdb/app/Main.scala new file mode 100644 index 0000000..081b4ee --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/Main.scala @@ -0,0 +1,103 @@ +package mdr.pdb.app + +import scala.scalajs.js.annotation.JSExportTopLevel +import scala.scalajs.js.annotation.JSExport +import scala.scalajs.js.annotation.JSImport +import scala.scalajs.js +import org.scalajs.dom +import com.raquo.laminar.api.L.{*, given} +import scala.scalajs.js.Date +import com.raquo.waypoint.Router +import com.raquo.waypoint.SplitRender +import mdr.pdb.app.services.DataFetcher +import scala.scalajs.js.JSON +import zio.json._ +import mdr.pdb.UserInfo +import mdr.pdb.app.state.AppState + +@js.native +@JSImport("stylesheets/main.css", JSImport.Namespace) +object Css extends js.Any + +@js.native +@JSImport("data/users.json", JSImport.Default) +object mockUsers extends js.Object + +@js.native +@JSImport("params/pdb-params.json", JSImport.Default) +object pdbParams extends js.Object + +@JSExportTopLevel("app") +object Main: + + @JSExport + def main(args: Array[String]): Unit = + import Routes.given + onLoad { + setupAirstream() + val appContainer = dom.document.querySelector("#app") + val _ = + render( + appContainer, + renderPage(state.MockAppState(using unsafeWindowOwner, router)) + ) + } + + private def onLoad(f: => Unit): Unit = + documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) + + private def setupAirstream()(using router: Router[Page]): Unit = + AirstreamError.registerUnhandledErrorCallback(err => + router.forcePage( + Page.UnhandledError( + Some(err.getClass.getName), // TODO: Fill only in dev mode + Some(err.getMessage) + ) + ) + ) + + def renderPage(state: AppState)(using + router: Router[Page] + ): HtmlElement = + val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) + .collectSignal[Page.Detail]( + connectors + .DetailPageConnector(state)(_) + .apply + ) + .collectSignal[Page.DetailParametru]( + connectors + .DetailParametruPageConnector(state)(_) + .apply + ) + .collectSignal[Page.DetailKriteria]( + connectors + .DetailKriteriaPageConnector(state)(_) + .apply + ) + .collectSignal[Page.UpravDukazKriteria]( + pages.detail.UpravDukaz.Connector(state)(_).apply + ) + .collectStatic(Page.Dashboard)( + connectors.DashboardPageConnector(state.actionBus).apply + ) + .collect[Page.NotFound](pg => + pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) + ) + .collect[Page.UnhandledError](pg => + pages.errors + .UnhandledErrorPage( + pages.errors.UnhandledErrorPage + .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), + state.actionBus + ) + ) + .collectStatic(Page.Directory)( + connectors + .DirectoryPageConnector(state.users, state.actionBus) + .apply + ) + div(cls := "h-full", child <-- pageSplitter.$view) + + // Pull in the stylesheet + val css: Css.type = Css diff --git a/app/src/main/scala/mdr/pdb/app/Routes.scala b/app/src/main/scala/mdr/pdb/app/Routes.scala new file mode 100644 index 0000000..2cc860c --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/Routes.scala @@ -0,0 +1,198 @@ +package mdr.pdb.app + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.* +import org.scalajs.dom +import zio.json.{*, given} +import mdr.pdb.OsobniCislo + +import scala.scalajs.js +import mdr.pdb.UserInfo +import mdr.pdb.Parameter +import mdr.pdb.ParameterCriteria +import mdr.pdb.app.Page.Titled + +// enum is not working with Waypoints' SplitRender collectStatic +sealed abstract class Page( + val id: String, + val title: String, + val parent: Option[Page] +) { + val path: Vector[Page] = + parent match + case None => Vector(this) + case Some(p) => p.path :+ this + + val isRoot: Boolean = parent.isEmpty +} + +object Page: + + case class Titled[V](value: V, title: Option[String] = None): + val show: String = title.getOrElse(value.toString) + + case object Directory extends Page("directory", "Adresář", None) + + case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) + + case class Detail(osobniCislo: Titled[OsobniCislo]) + extends Page("user", osobniCislo.show, Some(Directory)) + + object Detail { + def apply(o: UserInfo): Detail = Detail( + Titled(o.personalNumber, Some(o.name)) + ) + } + + case class DetailParametru( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String] + ) extends Page( + "parameter", + parametr.show, + Some(Detail(osobniCislo)) + ) + + object DetailParametru { + def apply(o: UserInfo, p: Parameter): DetailParametru = + DetailParametru( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)) + ) + } + + case class DetailKriteria( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String], + kriterium: Titled[String] + ) extends Page( + "criteria", + kriterium.show, + Some(DetailParametru(osobniCislo, parametr)) + ) + + object DetailKriteria { + def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = + DetailKriteria( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)), + Titled(k.id, Some(k.id)) + ) + } + + case class UpravDukazKriteria( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String], + kriterium: Titled[String] + ) extends Page( + "addProof", + "Důkaz", + Some(DetailKriteria(osobniCislo, parametr, kriterium)) + ) + + object UpravDukazKriteria { + def apply( + o: UserInfo, + p: Parameter, + k: ParameterCriteria + ): UpravDukazKriteria = + UpravDukazKriteria( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)), + Titled(k.id, Some(k.id)) + ) + } + + case class NotFound(url: String) extends Page("404", "404", Some(Directory)) + + case class UnhandledError( + errorName: Option[String], + errorMessage: Option[String] + ) extends Page("500", "Unexpected error", Some(Directory)) + +object Routes: + given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) + given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) + given [V: JsonEncoder]: JsonEncoder[Titled[V]] = + DeriveJsonEncoder.gen[Titled[V]] + given [V: JsonDecoder]: JsonDecoder[Titled[V]] = + DeriveJsonDecoder.gen[Titled[V]] + given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] + given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] + + val base = + js.`import`.meta.env.BASE_URL + .asInstanceOf[String] + "app" + + val homePage: Page = Page.Directory + + given router: Router[Page] = Router[Page]( + routes = List( + Route.static(homePage, root / endOfSegments, basePath = base), + Route.static( + Page.Dashboard, + root / "dashboard" / endOfSegments, + basePath = base + ), + Route[Page.Detail, String]( + encode = _.osobniCislo.value.toString, + decode = osc => Page.Detail(Titled(OsobniCislo(osc))), + root / "osoba" / segment[String] / endOfSegments, + basePath = base + ), + Route[Page.DetailParametru, (String, String)]( + encode = p => (p.osobniCislo.value.toString, p.parametr.value), + decode = + p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / endOfSegments, + basePath = base + ), + Route[Page.DetailKriteria, (String, String, String)]( + encode = p => + ( + p.osobniCislo.value.toString, + p.parametr.value, + p.kriterium.value.replaceAll("\\.", "--") + ), + decode = p => + Page.DetailKriteria( + Titled(OsobniCislo(p._1)), + Titled(p._2), + Titled(p._3.replaceAll("--", ".")) + ), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / "kriterium" / segment[String] / endOfSegments, + basePath = base + ), + Route[Page.UpravDukazKriteria, (String, String, String)]( + encode = p => + ( + p.osobniCislo.value.toString, + p.parametr.value, + p.kriterium.value.replaceAll("\\.", "--") + ), + decode = p => + Page.UpravDukazKriteria( + Titled(OsobniCislo(p._1)), + Titled(p._2), + Titled(p._3.replaceAll("--", ".")) + ), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / "kriterium" / segment[String] / "edit" / endOfSegments, + basePath = base + ) + ), + serializePage = _.toJson, + deserializePage = _.fromJson[Page] + .fold(s => throw IllegalStateException(s), identity), + getPageTitle = _.title, + routeFallback = url => Page.NotFound(url), + deserializeFallback = _ => Page.Dashboard + )( + $popStateEvent = windowEvents.onPopState, + owner = unsafeWindowOwner + ) diff --git a/app/src/main/scala/mdr/pdb/app/actions.scala b/app/src/main/scala/mdr/pdb/app/actions.scala new file mode 100644 index 0000000..d078b3a --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/actions.scala @@ -0,0 +1,21 @@ +package mdr.pdb.app + +import mdr.pdb.OsobniCislo +import mdr.pdb.UserInfo +import mdr.pdb.Parameter +import mdr.pdb.ParameterCriteria + +sealed trait Action + +case object FetchDirectory extends Action +case class FetchUserDetails(osc: OsobniCislo) extends Action +case class FetchParameters(osc: OsobniCislo) extends Action +case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action +case class FetchParameterCriteria( + osc: OsobniCislo, + paramId: String, + critId: String, + page: (UserInfo, Parameter, ParameterCriteria) => Page +) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action +case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/mdr/pdb/app/components/AppPage.scala new file mode 100644 index 0000000..130c817 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/AppPage.scala @@ -0,0 +1,82 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.Page +import mdr.pdb.{UserProfile, UserInfo, OsobniCislo} +import com.raquo.waypoint.Router +import mdr.pdb.app.Action +import mdr.pdb.app.NavigateTo +import mdr.pdb.UserFunction + +object AppPage: + // TODO: pages by logged in user + val pages: List[Page] = List(Page.Directory, Page.Dashboard) + + import NavigationBar.{Logo, MenuItem} + + val logo = Logo( + "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", + "Workflow" + ) + + // TODO: menu items by user profile + val userMenu = + List( + MenuItem("Your Profile"), + MenuItem("Settings"), + MenuItem("Sign out") + ) + + // TODO: load user profile + val $userProfile = Var( + UserProfile( + "tom", + UserInfo( + OsobniCislo("1031"), + "tom", + "Tom", + "Cook", + None, + None, + Some("tom@example.com"), + Some("+420 222 866 180"), + Some( + UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") + ), + Nil, + None + ) + ) + ) + + val $userInfo = $userProfile.signal.map(_.userInfo) + + type ViewModel = Option[HtmlElement] + def apply( + actionBus: Observer[Action] + )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using + router: Router[Page] + ): HtmlElement = + PageLayout(actionBus)( + $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => + PageLayout.ViewModel( + NavigationBar.ViewModel( + u, + pages.map(p => + NavigationBar.Link( + () => + PageLink( + p, + actionBus + ), + p == cp + ) + ), + userMenu, + logo + ), + c + ) + ), + mods + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala new file mode 100644 index 0000000..9375398 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala @@ -0,0 +1,143 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.tailwind.list.IconText.ViewModel +import fiftyforms.ui.components.tailwind.Icons +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.Page + +import com.raquo.laminar.api.L.{*, given} +import io.laminext.syntax.core.{*, given} +import mdr.pdb.app.Action + +object Breadcrumbs: + + private def slash = + import svg.{*, given} + svg( + cls := "flex-shrink-0 h-5 w-5 text-gray-300", + xmlns := "http://www.w3.org/2000/svg", + fill := "currentColor", + viewBox := "0 0 20 20", + ariaHidden := true, + path( + d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" + ) + ) + + def backIcon = + Icons.solid + .`arrow-narrow-left`() + .amend( + svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" + ) + + object Link: + case class ViewModel( + page: Page, + icon: Option[SvgElement], + extraClasses: String, + text: String, + textClasses: Option[String] = None + ) + + val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" + + def shortHome(p: Page) = ViewModel( + p, + Some(backIcon), + "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", + "Zpět na úvodní stránku", + None + ) + + def fullHome(p: Page) = ViewModel( + p, + Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), + "text-gray-400 hover:text-gray-500", + "Domů", + Some("sr-only") + ) + + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + PageLink + .container($m.map(_.page), actionBus) + .amend( + cls <-- $m.map(_.extraClasses), + child.maybe <-- $m.map(_.icon), + span( + cls <-- $m.map(_.textClasses.getOrElse("")), + child.text <-- $m.map(_.text) + ) + ) + + object FullBreadcrumbs: + type ViewModel = Page + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + ol( + role := "list", + cls := "flex items-center space-x-4", + children <-- $m.map(_.path).split(_.id)((_, _, $p) => + li( + div( + cls := "flex items-center", + child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), + Link( + $p.map(p => + if (p.isRoot) then Link.fullHome(p) + else + Link.ViewModel( + p, + None, + s"ml-4 max-w-xs truncate ${Link.baseClasses}", + p.title + ) + ), + actionBus + ) + ) + ) + ) + ) + + object ShortBreadcrumbs: + type ViewModel = Page + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + Link( + $m.map { p => + val target = p.parent.getOrElse(p) + if target.isRoot then Link.shortHome(target) + else + Link.ViewModel( + target, + Some(backIcon), + "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", + s"Zpět na ${target.title}" + ) + }, + actionBus + ) + + def apply(actionBus: Observer[Action])(using + router: Router[Page] + ): HtmlElement = + nav( + cls := "flex", + aria.label := "Breadcrumb", + div( + cls := "flex sm:hidden", + ShortBreadcrumbs(router.$currentPage, actionBus) + ), + div( + cls := "hidden sm:block", + FullBreadcrumbs(router.$currentPage, actionBus) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala new file mode 100644 index 0000000..4bb8b8d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala @@ -0,0 +1,242 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent +import fiftyforms.ui.components.tailwind.Icons +import fiftyforms.ui.components.tailwind.Avatar +import mdr.pdb.UserInfo +import io.laminext.syntax.core.* + +object NavigationBar: + + case class Logo(img: String, name: String) + case class Link(a: () => Anchor, active: Boolean) + case class MenuItem(title: String) + + case class ViewModel( + userInfo: UserInfo, + pages: List[Link], + userMenu: List[MenuItem], + logo: Logo + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + val $userInfo = $m.map(_.userInfo) + val mobileMenuOpen = Var(false) + + val desktopOnly = cls("hidden md:block") + val mobileOnly = cls("md:hidden") + + inline def avatarImage(size: Int = 8) = + Avatar($userInfo.map(_.img)).avatarImage(size) + + def notificationButton = button( + tpe := "button", + cls := List( + "bg-indigo-600", + "focus:outline-none", + "focus:ring-2", + "focus:ring-offset-2", + "focus:ring-offset-indigo-600", + "focus:ring-white", + "hover:text-white", + "p-1", + "rounded-full", + "text-indigo-200" + ), + span(cls := "sr-only", "View notifications"), + Icons.outline.bell() + ) + + def userProfile: HtmlElement = + val menuOpen = Var(false) + + def menuItem(item: MenuItem, idx: Int): HtmlElement = + a( + href := "#", + cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", + role := "menuitem", + tabIndex := -1, + idAttr := s"user-menu-item-$idx", + item.title + ) + + div( + cls := "ml-3 relative", + div( + button( + tpe := "button", + cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", + idAttr := "user-menu-button", + aria.expanded <-- menuOpen.signal, + aria.hasPopup := true, + span(cls := "sr-only", "Open user menu"), + child <-- avatarImage(), + onClick.preventDefault.mapTo( + !menuOpen.now() + ) --> menuOpen.writer + ) + ), + /* + * */ + div( + cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", + cls <-- menuOpen.signal.map { o => + if (o) "md:block" else "md:hidden" + }, + role := "menu", + aria.orientation := "vertical", + aria.labelledBy := "user-menu-button", + tabIndex := -1, + // : keyboard navigation + children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) + ) + ) + + def mobileProfile = + def menuItem(item: MenuItem): HtmlElement = + a( + href := "#", + cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", + item.title + ) + + div( + cls := "pt-4 pb-3 border-t border-indigo-700", + div( + cls := "flex items-center px-5", + div( + cls := "flex-shrink-0", + child <-- avatarImage(10) + ), + div( + cls := "ml-3", + div( + cls := "text-base font-medium text-white", + child.text <-- $userInfo.map(_.name) + ), + child.maybe <-- $userInfo.map( + _.email.map(e => + div( + cls := "text-sm font-medium text-indigo-300", + e + ) + ) + ) + ), + notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) + ), + div( + cls := "mt-3 px-2 space-y-1", + children <-- $m.map(_.userMenu.map(menuItem)) + ) + ) + + def pageLink(page: Link): Anchor = + page + .a() + .amend( + cls := "text-white px-3 py-2 rounded-md text-sm font-medium", + cls := Seq( + "bg-indigo-700" -> page.active, + "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active + ), + ariaCurrent := (if page.active then "page" else "false") + ) + + def logoImg: Image = + img( + cls := "h-8 w-8", + src <-- $m.map(_.logo.img), + alt <-- $m.map(_.logo.name) + ) + + def mobileMenuButton = button( + tpe := "button", + cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", + aria.controls := "mobile-menu", + aria.expanded <-- mobileMenuOpen.signal, + span(cls := "sr-only", "Open main menu"), + Icons.outline + .menu() + .amend( + svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") + ), + Icons.outline + .x() + .amend( + svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") + ), + onClick.preventDefault.mapTo( + !mobileMenuOpen.now() + ) --> mobileMenuOpen.writer + ) + + def navBarLeft = + div( + cls := "flex items-center", + div(cls := "flex-shrink-0", logoImg), + div( + desktopOnly, + div( + cls := "ml-10 flex items-baseline space-x-4", + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) + ) + ) + ) + + def navBarRight = + div( + desktopOnly, + div( + cls := "ml-4 flex items-center md:ml-6", + notificationButton, + userProfile + ) + ) + + def navBarMobile = + div( + cls := "-mr-2 flex", + mobileOnly, + mobileMenuButton + ) + + def navBar = + div( + cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", + div( + cls := "flex items-center justify-between h-16", + navBarLeft, + navBarRight, + navBarMobile + ) + ) + + def mobileMenu = + div( + mobileOnly, + cls <-- mobileMenuOpen.signal.map { o => + if (o) "block" else "hidden" + }, + idAttr := "mobile-menu", + div( + cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) + ), + mobileProfile + ) + + nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala new file mode 100644 index 0000000..c4cb10d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala @@ -0,0 +1,19 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.Page +import com.raquo.waypoint.Router +import mdr.pdb.app.Action + +object PageHeader: + def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = + header( + cls := "bg-white shadow-sm", + div( + cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", + h1( + cls := "text-lg leading-6 font-semibold text-gray-900", + Breadcrumbs(actionBus) + ) + ) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala deleted file mode 100644 index a3b00af..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala +++ /dev/null @@ -1,103 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import scala.scalajs.js.annotation.JSExportTopLevel -import scala.scalajs.js.annotation.JSExport -import scala.scalajs.js.annotation.JSImport -import scala.scalajs.js -import org.scalajs.dom -import com.raquo.laminar.api.L.{*, given} -import scala.scalajs.js.Date -import com.raquo.waypoint.Router -import com.raquo.waypoint.SplitRender -import cz.e_bs.cmi.mdr.pdb.app.services.DataFetcher -import scala.scalajs.js.JSON -import zio.json._ -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.state.AppState - -@js.native -@JSImport("stylesheets/main.css", JSImport.Namespace) -object Css extends js.Any - -@js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native -@JSImport("params/pdb-params.json", JSImport.Default) -object pdbParams extends js.Object - -@JSExportTopLevel("app") -object Main: - - @JSExport - def main(args: Array[String]): Unit = - import Routes.given - onLoad { - setupAirstream() - val appContainer = dom.document.querySelector("#app") - val _ = - render( - appContainer, - renderPage(state.MockAppState(using unsafeWindowOwner, router)) - ) - } - - private def onLoad(f: => Unit): Unit = - documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) - - private def setupAirstream()(using router: Router[Page]): Unit = - AirstreamError.registerUnhandledErrorCallback(err => - router.forcePage( - Page.UnhandledError( - Some(err.getClass.getName), // TODO: Fill only in dev mode - Some(err.getMessage) - ) - ) - ) - - def renderPage(state: AppState)(using - router: Router[Page] - ): HtmlElement = - val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) - .collectSignal[Page.Detail]( - connectors - .DetailPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailParametru]( - connectors - .DetailParametruPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailKriteria]( - connectors - .DetailKriteriaPageConnector(state)(_) - .apply - ) - .collectSignal[Page.UpravDukazKriteria]( - pages.detail.UpravDukaz.Connector(state)(_).apply - ) - .collectStatic(Page.Dashboard)( - connectors.DashboardPageConnector(state.actionBus).apply - ) - .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) - ) - .collect[Page.UnhandledError](pg => - pages.errors - .UnhandledErrorPage( - pages.errors.UnhandledErrorPage - .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), - state.actionBus - ) - ) - .collectStatic(Page.Directory)( - connectors - .DirectoryPageConnector(state.users, state.actionBus) - .apply - ) - div(cls := "h-full", child <-- pageSplitter.$view) - - // Pull in the stylesheet - val css: Css.type = Css diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala deleted file mode 100644 index 3bbc8c0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ /dev/null @@ -1,198 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.* -import org.scalajs.dom -import zio.json.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import scala.scalajs.js -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.Page.Titled - -// enum is not working with Waypoints' SplitRender collectStatic -sealed abstract class Page( - val id: String, - val title: String, - val parent: Option[Page] -) { - val path: Vector[Page] = - parent match - case None => Vector(this) - case Some(p) => p.path :+ this - - val isRoot: Boolean = parent.isEmpty -} - -object Page: - - case class Titled[V](value: V, title: Option[String] = None): - val show: String = title.getOrElse(value.toString) - - case object Directory extends Page("directory", "Adresář", None) - - case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) - - case class Detail(osobniCislo: Titled[OsobniCislo]) - extends Page("user", osobniCislo.show, Some(Directory)) - - object Detail { - def apply(o: UserInfo): Detail = Detail( - Titled(o.personalNumber, Some(o.name)) - ) - } - - case class DetailParametru( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String] - ) extends Page( - "parameter", - parametr.show, - Some(Detail(osobniCislo)) - ) - - object DetailParametru { - def apply(o: UserInfo, p: Parameter): DetailParametru = - DetailParametru( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)) - ) - } - - case class DetailKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "criteria", - kriterium.show, - Some(DetailParametru(osobniCislo, parametr)) - ) - - object DetailKriteria { - def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = - DetailKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class UpravDukazKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "addProof", - "Důkaz", - Some(DetailKriteria(osobniCislo, parametr, kriterium)) - ) - - object UpravDukazKriteria { - def apply( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): UpravDukazKriteria = - UpravDukazKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class NotFound(url: String) extends Page("404", "404", Some(Directory)) - - case class UnhandledError( - errorName: Option[String], - errorMessage: Option[String] - ) extends Page("500", "Unexpected error", Some(Directory)) - -object Routes: - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) - given [V: JsonEncoder]: JsonEncoder[Titled[V]] = - DeriveJsonEncoder.gen[Titled[V]] - given [V: JsonDecoder]: JsonDecoder[Titled[V]] = - DeriveJsonDecoder.gen[Titled[V]] - given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] - given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] - - val base = - js.`import`.meta.env.BASE_URL - .asInstanceOf[String] + "app" - - val homePage: Page = Page.Directory - - given router: Router[Page] = Router[Page]( - routes = List( - Route.static(homePage, root / endOfSegments, basePath = base), - Route.static( - Page.Dashboard, - root / "dashboard" / endOfSegments, - basePath = base - ), - Route[Page.Detail, String]( - encode = _.osobniCislo.value.toString, - decode = osc => Page.Detail(Titled(OsobniCislo(osc))), - root / "osoba" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.DetailParametru, (String, String)]( - encode = p => (p.osobniCislo.value.toString, p.parametr.value), - decode = - p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / endOfSegments, - basePath = base - ), - Route[Page.DetailKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.DetailKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.UpravDukazKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.UpravDukazKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / "edit" / endOfSegments, - basePath = base - ) - ), - serializePage = _.toJson, - deserializePage = _.fromJson[Page] - .fold(s => throw IllegalStateException(s), identity), - getPageTitle = _.title, - routeFallback = url => Page.NotFound(url), - deserializeFallback = _ => Page.Dashboard - )( - $popStateEvent = windowEvents.onPopState, - owner = unsafeWindowOwner - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala deleted file mode 100644 index b24b82b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -sealed trait Action - -case object FetchDirectory extends Action -case class FetchUserDetails(osc: OsobniCislo) extends Action -case class FetchParameters(osc: OsobniCislo) extends Action -case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action -case class FetchParameterCriteria( - osc: OsobniCislo, - paramId: String, - critId: String, - page: (UserInfo, Parameter, ParameterCriteria) => Page -) extends Action -case class FetchAvailableFiles(osc: OsobniCislo) extends Action -case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala deleted file mode 100644 index c637d35..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala +++ /dev/null @@ -1,82 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.{UserProfile, UserInfo, OsobniCislo} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo -import cz.e_bs.cmi.mdr.pdb.UserFunction - -object AppPage: - // TODO: pages by logged in user - val pages: List[Page] = List(Page.Directory, Page.Dashboard) - - import NavigationBar.{Logo, MenuItem} - - val logo = Logo( - "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", - "Workflow" - ) - - // TODO: menu items by user profile - val userMenu = - List( - MenuItem("Your Profile"), - MenuItem("Settings"), - MenuItem("Sign out") - ) - - // TODO: load user profile - val $userProfile = Var( - UserProfile( - "tom", - UserInfo( - OsobniCislo("1031"), - "tom", - "Tom", - "Cook", - None, - None, - Some("tom@example.com"), - Some("+420 222 866 180"), - Some( - UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") - ), - Nil, - None - ) - ) - ) - - val $userInfo = $userProfile.signal.map(_.userInfo) - - type ViewModel = Option[HtmlElement] - def apply( - actionBus: Observer[Action] - )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using - router: Router[Page] - ): HtmlElement = - PageLayout(actionBus)( - $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => - PageLayout.ViewModel( - NavigationBar.ViewModel( - u, - pages.map(p => - NavigationBar.Link( - () => - PageLink( - p, - actionBus - ), - p == cp - ) - ), - userMenu, - logo - ), - c - ) - ), - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala deleted file mode 100644 index bf473c6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ /dev/null @@ -1,143 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden -import fiftyforms.ui.components.tailwind.list.IconText.ViewModel -import fiftyforms.ui.components.tailwind.Icons -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Action - -object Breadcrumbs: - - private def slash = - import svg.{*, given} - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-300", - xmlns := "http://www.w3.org/2000/svg", - fill := "currentColor", - viewBox := "0 0 20 20", - ariaHidden := true, - path( - d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" - ) - ) - - def backIcon = - Icons.solid - .`arrow-narrow-left`() - .amend( - svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" - ) - - object Link: - case class ViewModel( - page: Page, - icon: Option[SvgElement], - extraClasses: String, - text: String, - textClasses: Option[String] = None - ) - - val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" - - def shortHome(p: Page) = ViewModel( - p, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - "Zpět na úvodní stránku", - None - ) - - def fullHome(p: Page) = ViewModel( - p, - Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), - "text-gray-400 hover:text-gray-500", - "Domů", - Some("sr-only") - ) - - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - PageLink - .container($m.map(_.page), actionBus) - .amend( - cls <-- $m.map(_.extraClasses), - child.maybe <-- $m.map(_.icon), - span( - cls <-- $m.map(_.textClasses.getOrElse("")), - child.text <-- $m.map(_.text) - ) - ) - - object FullBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - ol( - role := "list", - cls := "flex items-center space-x-4", - children <-- $m.map(_.path).split(_.id)((_, _, $p) => - li( - div( - cls := "flex items-center", - child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), - Link( - $p.map(p => - if (p.isRoot) then Link.fullHome(p) - else - Link.ViewModel( - p, - None, - s"ml-4 max-w-xs truncate ${Link.baseClasses}", - p.title - ) - ), - actionBus - ) - ) - ) - ) - ) - - object ShortBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - Link( - $m.map { p => - val target = p.parent.getOrElse(p) - if target.isRoot then Link.shortHome(target) - else - Link.ViewModel( - target, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - s"Zpět na ${target.title}" - ) - }, - actionBus - ) - - def apply(actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - nav( - cls := "flex", - aria.label := "Breadcrumb", - div( - cls := "flex sm:hidden", - ShortBreadcrumbs(router.$currentPage, actionBus) - ), - div( - cls := "hidden sm:block", - FullBreadcrumbs(router.$currentPage, actionBus) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala deleted file mode 100644 index 0db2047..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ /dev/null @@ -1,242 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent -import fiftyforms.ui.components.tailwind.Icons -import fiftyforms.ui.components.tailwind.Avatar -import cz.e_bs.cmi.mdr.pdb.UserInfo -import io.laminext.syntax.core.* - -object NavigationBar: - - case class Logo(img: String, name: String) - case class Link(a: () => Anchor, active: Boolean) - case class MenuItem(title: String) - - case class ViewModel( - userInfo: UserInfo, - pages: List[Link], - userMenu: List[MenuItem], - logo: Logo - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - val $userInfo = $m.map(_.userInfo) - val mobileMenuOpen = Var(false) - - val desktopOnly = cls("hidden md:block") - val mobileOnly = cls("md:hidden") - - inline def avatarImage(size: Int = 8) = - Avatar($userInfo.map(_.img)).avatarImage(size) - - def notificationButton = button( - tpe := "button", - cls := List( - "bg-indigo-600", - "focus:outline-none", - "focus:ring-2", - "focus:ring-offset-2", - "focus:ring-offset-indigo-600", - "focus:ring-white", - "hover:text-white", - "p-1", - "rounded-full", - "text-indigo-200" - ), - span(cls := "sr-only", "View notifications"), - Icons.outline.bell() - ) - - def userProfile: HtmlElement = - val menuOpen = Var(false) - - def menuItem(item: MenuItem, idx: Int): HtmlElement = - a( - href := "#", - cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", - role := "menuitem", - tabIndex := -1, - idAttr := s"user-menu-item-$idx", - item.title - ) - - div( - cls := "ml-3 relative", - div( - button( - tpe := "button", - cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - idAttr := "user-menu-button", - aria.expanded <-- menuOpen.signal, - aria.hasPopup := true, - span(cls := "sr-only", "Open user menu"), - child <-- avatarImage(), - onClick.preventDefault.mapTo( - !menuOpen.now() - ) --> menuOpen.writer - ) - ), - /* - * */ - div( - cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", - cls <-- menuOpen.signal.map { o => - if (o) "md:block" else "md:hidden" - }, - role := "menu", - aria.orientation := "vertical", - aria.labelledBy := "user-menu-button", - tabIndex := -1, - // : keyboard navigation - children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) - ) - ) - - def mobileProfile = - def menuItem(item: MenuItem): HtmlElement = - a( - href := "#", - cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", - item.title - ) - - div( - cls := "pt-4 pb-3 border-t border-indigo-700", - div( - cls := "flex items-center px-5", - div( - cls := "flex-shrink-0", - child <-- avatarImage(10) - ), - div( - cls := "ml-3", - div( - cls := "text-base font-medium text-white", - child.text <-- $userInfo.map(_.name) - ), - child.maybe <-- $userInfo.map( - _.email.map(e => - div( - cls := "text-sm font-medium text-indigo-300", - e - ) - ) - ) - ), - notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) - ), - div( - cls := "mt-3 px-2 space-y-1", - children <-- $m.map(_.userMenu.map(menuItem)) - ) - ) - - def pageLink(page: Link): Anchor = - page - .a() - .amend( - cls := "text-white px-3 py-2 rounded-md text-sm font-medium", - cls := Seq( - "bg-indigo-700" -> page.active, - "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active - ), - ariaCurrent := (if page.active then "page" else "false") - ) - - def logoImg: Image = - img( - cls := "h-8 w-8", - src <-- $m.map(_.logo.img), - alt <-- $m.map(_.logo.name) - ) - - def mobileMenuButton = button( - tpe := "button", - cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - aria.controls := "mobile-menu", - aria.expanded <-- mobileMenuOpen.signal, - span(cls := "sr-only", "Open main menu"), - Icons.outline - .menu() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") - ), - Icons.outline - .x() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") - ), - onClick.preventDefault.mapTo( - !mobileMenuOpen.now() - ) --> mobileMenuOpen.writer - ) - - def navBarLeft = - div( - cls := "flex items-center", - div(cls := "flex-shrink-0", logoImg), - div( - desktopOnly, - div( - cls := "ml-10 flex items-baseline space-x-4", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ) - ) - ) - - def navBarRight = - div( - desktopOnly, - div( - cls := "ml-4 flex items-center md:ml-6", - notificationButton, - userProfile - ) - ) - - def navBarMobile = - div( - cls := "-mr-2 flex", - mobileOnly, - mobileMenuButton - ) - - def navBar = - div( - cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex items-center justify-between h-16", - navBarLeft, - navBarRight, - navBarMobile - ) - ) - - def mobileMenu = - div( - mobileOnly, - cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }, - idAttr := "mobile-menu", - div( - cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ), - mobileProfile - ) - - nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala deleted file mode 100644 index 75339df..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object PageHeader: - def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = - header( - cls := "bg-white shadow-sm", - div( - cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", - h1( - cls := "text-lg leading-6 font-semibold text-gray-900", - Breadcrumbs(actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala deleted file mode 100644 index 5096463..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ /dev/null @@ -1,30 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import fiftyforms.ui.components.tailwind.Loading -import io.laminext.syntax.core.* - -object PageLayout: - case class ViewModel( - navigation: NavigationBar.ViewModel, - content: Option[HtmlElement] - ) - def apply(actionBus: Observer[Action])( - $m: Signal[ViewModel], - mods: Modifier[HtmlElement]* - )(using router: Router[Page]): HtmlElement = - val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) - div( - cls := "h-full flex flex-col", - NavigationBar($m.map(_.navigation)), - child.maybe <-- router.$currentPage.map(_.isRoot) - .switch(None, Some(PageHeader(actionBus))), - main( - cls := "flex-grow-1 overflow-y-auto", - mods, - child <-- $maybeContent.map(_.getOrElse(Loading)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala deleted file mode 100644 index 62d820f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.LinkSupport.* -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo - -object PageLink: - type ViewModel = Page - def apply($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = - a(mods($m, actions), child.text <-- $m.map(_.title)) - - def apply(m: ViewModel, actions: Observer[Action])(using - router: Router[Page] - ): Anchor = apply(Val(m), actions) - - def container($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = a(mods($m, actions)) - - def container(m: ViewModel, actions: Observer[Action])(using - Router[Page] - ): Anchor = container(Val(m), actions) - - private def mods($m: Signal[Page], actions: Observer[Action])(using - router: Router[Page] - ): Modifier[Anchor] = - Seq( - href <-- $m.map(router.absoluteUrlForPage).recover { case _ => - Some("invalid url") - }, - composeEvents(onClick.noKeyMod.preventDefault)( - _.sample($m) - .map(NavigateTo.apply) - ) --> actions - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala deleted file mode 100644 index 87f825a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.pages.dashboard.DashboardPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -class DashboardPageConnector(actionBus: Observer[Action])(using - router: Router[Page] -): - def apply: HtmlElement = - AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala deleted file mode 100644 index e9b0f71..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala +++ /dev/null @@ -1,76 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailKriteriaPage -import pages.detail.components.DukazyKriteria -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -object DetailKriteriaPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailKriteriaPageConnector( - state: DetailKriteriaPageConnector.AppState -)( - $page: Signal[Page.DetailKriteria] -)(using Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => - (p.osobniCislo.value, p.parametr.value, p.kriterium.value) - )((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map( - FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) - ) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, s, $s) => - DetailKriteriaPage($s)(state.actionBus.contramap { - case DukazyKriteria.Add => - NavigateTo( - Page.UpravDukazKriteria( - Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), - Page.Titled(s.parametr.id, Some(s.parametr.nazev)), - Page.Titled(s.kriterium.id, Some(s.kriterium.id)) - ) - ) - }) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): DetailKriteriaPage.ViewModel = - DetailKriteriaPage.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala deleted file mode 100644 index 4d8024f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ /dev/null @@ -1,57 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import pages.detail.DetailPage -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import fiftyforms.ui.components.tailwind.Color - -object DetailPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailPageConnector(state: DetailPageConnector.AppState)( - $page: Signal[Page.Detail] -)(using router: Router[Page]): - val $oscChangeSignal = $page.splitOne(_.osobniCislo.value)((osc, _, _) => osc) - val $pageChangeSignal = - $oscChangeSignal.flatMap(osc => - EventStream.fromSeq(Seq(FetchUserDetails(osc), FetchParameters(osc))) - ) - // TODO: filter the value based on the current osc - // OSC change will fetch new data, but still - // - we need to be sure that what we got is really what we ought to display - // - we want to display stale data accordingly (at least with loading indicator) - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - def apply: HtmlElement = - AppPage(state.actionBus)( - $data.combineWithFn($params)(_ zip _) - .map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: List[Parameter] - ): DetailPage.ViewModel = - DetailPage.ViewModel( - o.toDetailOsoby, - p.map( - _.toParametr(param => - PageLink.container(Page.DetailParametru(o, param), state.actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala deleted file mode 100644 index fdaa7f8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailParametruPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink - -object DetailParametruPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailParametruPageConnector( - state: DetailParametruPageConnector.AppState -)( - $page: Signal[Page.DetailParametru] -)(using router: Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => (p.osobniCislo.value, p.parametr.value))((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map(FetchParameter(_, _)) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - } yield (da, pb) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailParametruPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter - ): DetailParametruPage.ViewModel = - DetailParametruPage.ViewModel( - o.toDetailOsoby, - p.toParametr(p => - PageLink.container(Page.DetailParametru(o, p), state.actionBus) - ), - p.criteria.map( - _.toKriterium { c => - PageLink.container( - Page.DetailKriteria(o, p, c), - state.actionBus - ) - } - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala deleted file mode 100644 index 049aa22..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -case class DirectoryPageConnector( - $input: EventStream[List[UserInfo]], - actionBus: Observer[Action] -)(using router: Router[Page]): - val $data = $input.startWithNone - val $actionSignal = EventStream.fromValue(FetchDirectory) - - def apply: HtmlElement = - AppPage(actionBus)( - $data.split(_ => ())((_, _, s) => - pages.directory.DirectoryPage( - s.map( - _.map( - _.toUserRow(u => - PageLink.container( - Page.Detail(Page.Titled(u.personalNumber)), - actionBus - ) - ) - ) - ) - ) - ), - $actionSignal --> actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala deleted file mode 100644 index 88e7450..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ /dev/null @@ -1,68 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii -import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria -import fiftyforms.ui.components.tailwind.Color - -extension (o: UserInfo) - def toDetailOsoby: DetailOsoby.ViewModel = - DetailOsoby.ViewModel( - o.personalNumber, - o.name, - o.email, - o.phone, - o.img, - o.mainFunction.map(f => - DetailOsoby.Funkce.ViewModel(f.name, f.dept, f.ou) - ), - o.userContracts.headOption.map(c => - DetailOsoby.PracovniPomer.ViewModel(c.rel, c.startDate, c.endDate) - ) - ) - -extension (param: Parameter) - def toParametr(container: Parameter => Anchor): DetailParametru.ViewModel = - DetailParametru.ViewModel( - id = param.id, - nazev = param.name, - popis = param.description, - status = "Nesplněno", - statusColor = Color.red, - a = container(param) - ) - -extension (crit: ParameterCriteria) - def toKriterium( - container: ParameterCriteria => Anchor - ): DetailKriteria.ViewModel = - DetailKriteria.ViewModel( - nazev = crit.criteriumText, - kapitola = crit.chapterId, - bod = crit.itemId, - status = "Nesplněno", - statusColor = Color.red, - splneno = false, - dukazy = Nil, - container = container(crit) - ) - -extension (user: UserInfo) - def toUserRow( - container: UserInfo => HtmlElement = (_: UserInfo) => div() - ): UserRow.ViewModel = - UserRow.ViewModel( - osobniCislo = user.personalNumber.toString, - celeJmeno = user.name, - prijmeni = user.surname, - hlavniFunkce = user.mainFunction.map(_.name), - img = user.img, - container = () => container(user) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala deleted file mode 100644 index 7d5b2c1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.dashboard - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.Page - -object DashboardPage: - - def render: HtmlElement = - div("Dashboard page") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala deleted file mode 100644 index 9ef0698..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala +++ /dev/null @@ -1,31 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailKriteriaPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply($m: Signal[ViewModel])( - action: Observer[DukazyKriteria.Action] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - DukazyKriteria($m.map(_.kriterium.dukazy))(action) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala deleted file mode 100644 index 3b316e1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object DetailPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametry: SeznamParametru.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby($m.map(_.osoba)), - SeznamParametru($m.map(_.parametry)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala deleted file mode 100644 index 17224cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala +++ /dev/null @@ -1,24 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailParametruPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriteria: SeznamKriterii.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby.header($m.map(_.osoba)), - DetailParametru($m.map(_.parametr)), - SeznamKriterii($m.map(_.kriteria)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala deleted file mode 100644 index dc912fa..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ /dev/null @@ -1,107 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb -package app -package pages.detail - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.UpravDukazForm -import fiftyforms.services.files.File - -object UpravDukaz: - - type ThisPage = Page.UpravDukazKriteria - type PageKey = (OsobniCislo, String, String) - - trait State { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def availableFiles: EventStream[List[File]] - def actionBus: Observer[Action] - } - - def keyOfPage(page: ThisPage): PageKey = - (page.osobniCislo.value, page.parametr.value, page.kriterium.value) - - def onChangeAction(key: PageKey): Action = - FetchParameterCriteria( - key._1, - key._2, - key._3, - Page.UpravDukazKriteria(_, _, _) - ) - - class Connector(state: State)($page: Signal[ThisPage])(using Router[Page]): - val $paramChangeSignal = $page.splitOne(keyOfPage)((x, _, _) => x) - val $pageChangeSignal = $paramChangeSignal.map(onChangeAction) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.split(_ => ())((_, s, $s) => - PageComponent( - $s.map(buildModel), - state.availableFiles, - state.actionBus.contramap { - case UpravDukazForm.Cancelled => - NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) - case UpravDukazForm.AvailableFilesRequested => - FetchAvailableFiles(s._1.personalNumber) - } - ) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): PageComponent.ViewModel = - import connectors.* - PageComponent.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) - - object PageComponent: - import components.* - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply( - $m: Signal[ViewModel], - availableFilesStream: EventStream[List[File]], - events: Observer[UpravDukazForm.Event] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - UpravDukazForm(availableFilesStream)(events) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala deleted file mode 100644 index ca311b0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.Icons -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Color - -object DetailKriteria: - case class ViewModel( - nazev: String, - kapitola: String, - bod: String, - status: String, - statusColor: Color, - splneno: Boolean, - dukazy: List[DukazKriteria.ViewModel], - container: HtmlElement = div() - ) { - val id = s"${kapitola}${bod}" - } - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h3( - cls := "text-xl leading-6 font-bold text-gray-900", - child.text <-- $m.map(_.nazev) - ), - h3( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - "Kapitola ", - child.text <-- $m.map(_.kapitola), - " bod ", - child <-- $m.map(_.bod) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala deleted file mode 100644 index ae90290..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala +++ /dev/null @@ -1,100 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Avatar - -object DetailOsoby: - - object Funkce: - case class ViewModel( - nazev: String, - stredisko: String, - voj: String - ) - - def render($m: Signal[ViewModel]): HtmlElement = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.nazev), - span( - cls := "hidden md:inline", - " @ ", - child.text <-- $m.map(_.stredisko), - ", ", - child.text <-- $m.map(_.voj) - ) - ) - - object PracovniPomer: - case class ViewModel( - druh: String, - pocatek: LocalDate, - konec: Option[LocalDate] - ) - - def render($m: Signal[ViewModel]): HtmlElement = - import fiftyforms.ui.components.tailwind.CustomAttrs.datetime - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.druh), - " od ", - time( - datetime <-- $m.map(_.pocatek.toString), - child.text <-- $m.map(_.pocatek.toString) - ) - ) - - case class ViewModel( - osobniCislo: OsobniCislo, - jmeno: String, - email: Option[String], - telefon: Option[String], - img: Option[String], - hlavniFunkce: Option[Funkce.ViewModel], - pracovniPomer: Option[PracovniPomer.ViewModel] - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-4", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(16) - ), - div( - h1( - cls := "text-2xl font-bold text-gray-900", - child.text <-- $m.map(_.jmeno) - ), - child.maybe <-- $m.map(_.hlavniFunkce).split(_ => ())((_, _, d) => - Funkce.render(d) - ), - child.maybe <-- $m.map(_.pracovniPomer).split(_ => ())((_, _, d) => - PracovniPomer.render(d) - ) - ) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-5", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(8) - ), - div( - h1( - cls := "text-lg font-medium text-gray-800", - child.text <-- $m.map(_.jmeno) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala deleted file mode 100644 index e11cb62..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color - -object DetailParametru: - case class ViewModel( - id: String, - nazev: String, - popis: String, - status: String, - statusColor: Color, - a: Anchor - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h2( - cls := "text-2xl leading-6 font-medium text-gray-900", - child.text <-- $m.map(_.nazev) - ), - p( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - child.text <-- $m.map(_.popis) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - h2( - cls := "text-lg leading-6 font-medium text-gray-600", - child.text <-- $m.map(_.nazev) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala deleted file mode 100644 index a00ede8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala +++ /dev/null @@ -1,177 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.CustomAttrs - -object DukazKriteria: - case class Osoba(osobniCislo: String, jmeno: String) - case class Udalost(osoba: Osoba, datum: LocalDate) - case class Dokument( - url: String, - nazev: String, - pridal: Udalost, - odebral: Option[Udalost] - ) - case class ViewModel( - dokumenty: List[Dokument], - autorizoval: Option[Udalost], - platiDo: Option[LocalDate], - poznámka: Option[String] - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-lg", - div( - cls := "px-4 py-5 sm:px-6", - h3( - cls := "text-lg leading-6 font-medium text-gray-900", - """Applicant Information""" - ), - p( - cls := "mt-1 max-w-2xl text-sm text-gray-500", - """Personal details and application.""" - ) - ), - div( - cls := "border-t border-gray-200 px-4 py-5 sm:px-6", - dl( - cls := "grid grid-cols-1 gap-x-4 gap-y-8 sm:grid-cols-2", - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Full name""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Margot Foster""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Application for""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Backend Developer""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Email address""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """margotfoster@example.com""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Salary expectation""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """$120,000""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """About""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Fugiat ipsum ipsum deserunt culpa aute sint do nostrud anim incididunt cillum culpa consequat. Excepteur qui ipsum aliquip consequat sint. Sit id mollit nulla mollit nostrud in ea officia proident. Irure nostrud pariatur mollit ad adipisicing reprehenderit deserunt qui eu.""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """Attachments""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - ul( - role := "list", - cls := "border border-gray-200 rounded-md divide-y divide-gray-200", - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """resume_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ), - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """coverletter_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala deleted file mode 100644 index 22f134f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala +++ /dev/null @@ -1,35 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object DukazyKriteria: - sealed trait Action - case object Add extends Action - - type ViewModel = List[DukazKriteria.ViewModel] - - def apply($m: Signal[ViewModel])(actions: Observer[Action]): HtmlElement = - div( - child.maybe <-- $m.map(m => - if (m.isEmpty) then Some(prazdnyDukaz(actions)) - else None - ), - children <-- $m.map(_.zipWithIndex).split(_._2)((_, _, $s) => - DukazKriteria($s.map(_._1)) - ) - ) - - private def prazdnyDukaz(actions: Observer[Action]): HtmlElement = - button( - tpe := "button", - onClick.preventDefault.mapTo(Add) --> actions, - cls := "relative block w-full border-2 border-gray-300 border-dashed rounded-lg p-12 text-center hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - Icons.outline - .`document-add`(12) - .amend(svg.cls := "mx-auto text-gray-400"), - span( - cls := "mt-2 block text-sm font-medium text-gray-900", - "Přidat důkaz" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala deleted file mode 100644 index f628697..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.list.StackedList -import fiftyforms.ui.components.tailwind.list.ListRow -import fiftyforms.ui.components.tailwind.list.RowTag -import fiftyforms.ui.components.tailwind.list.RowNext -import java.time.LocalDate - -object SeznamKriterii: - type ViewModel = List[DetailKriteria.ViewModel] - - private val kritList = new StackedList[DetailKriteria.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - kritList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = - p(cls := "text-sm text-gray-500", s"${i.kapitola}${i.bod}"), - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.container - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala deleted file mode 100644 index 2bfe4bc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.list.{ - StackedList, - ListRow, - RowTag, - PropList, - IconText, - RowNext -} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.LinkSupport.* - -object SeznamParametru: - type ViewModel = List[DetailParametru.ViewModel] - - private val parametrList = new StackedList[DetailParametru.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - parametrList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = emptyNode, - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.a - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala deleted file mode 100644 index f048e9d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ /dev/null @@ -1,101 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.form.* -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement -import fiftyforms.services.files.components.tailwind.FilePicker -import cz.e_bs.cmi.mdr.pdb.frontend.AutorizujDukaz -import cz.e_bs.cmi.mdr.pdb.frontend.DocumentRef -import fiftyforms.services.files.File - -object UpravDukazForm: - sealed trait Event - case object Cancelled extends Event - case object AvailableFilesRequested extends Event - def apply(availableFilesStream: EventStream[List[File]])( - updates: Observer[Event] - ): HtmlElement = - val (filesStream, filesObserver) = - EventStream.withObserver[FilePicker.Event] - val files = Var[List[File]](Nil) - def submitButtons: HtmlElement = - div( - cls := "pt-5", - div( - cls := "flex justify-end", - button( - tpe := "button", - cls := "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zrušit", - onClick.mapTo(Cancelled) --> updates - ), - button( - tpe := "submit", - cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - disabled <-- files.signal.map(_.isEmpty), - "Autorizovat" - ) - ) - ) - - div( - filesStream.collect { case FilePicker.SelectionUpdated(files) => - files.to(List) - } --> files.writer, - filesStream.collect { case FilePicker.AvailableFilesRequested => - AvailableFilesRequested - } --> updates, - cls := "bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6", - Form( - Form.Body( - Form.Section( - FormHeader( - FormHeader.ViewModel( - "Doložení kritéria", - "Sestavte doklady poskytující důkaz kritéria a potvrďte odesláním formuláře. Případné limitace či jiné relevantní údaje vepište do pole Komentář." - ) - ), - FormFields( - FormRow( - "dokumenty", - "Dokumenty", - FilePicker(files.signal, availableFilesStream)(filesObserver) - .amend(idAttr := "dokumenty", cls("max-w-lg")) - ).toHtml, - FormRow( - "platnost", - "Platnost", - input( - idAttr := "platnost", - name := "platnost", - tpe := "date", - autoComplete := "date", - cls := "block max-w-lg w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" - ) - ).toHtml, - FormRow( - "komentar", - "Komentář", - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - textArea( - idAttr := "komentar", - name := "about", - rows := 3, - cls := "max-w-lg shadow-sm block w-full focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border border-gray-300 rounded-md" - ), - p( - cls := "mt-2 text-sm text-gray-500", - "Doplňte prosím potřebné informace související s doložením kritéria, včetně případných limitací." - ) - ) - ).toHtml - ) - ) - ), - submitButtons - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala deleted file mode 100644 index 41033f6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -import components._ - -object DirectoryPage: - - type ViewModel = List[UserRow.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - val (actionsStream, actionObserver) = - EventStream.withObserver[SearchForm.Action] - val $filter = actionsStream - .collect { - case SearchForm.ClearFilter => None - case SearchForm.SetFilter(t) => Some(t) - } - .startWith(None) - val byLetter = for { - d <- $m - f <- $filter - } yield for { - (letter, users) <- d - .filter { user => - f match - case None => true - case Some(t) => user.search.contains(t) - } - .groupBy(_.prijmeni.head) - .to(List) - .sortBy(_._1) - } yield (letter.toString, users.sortBy(_.prijmeni)) - - div( - cls := "h-full max-w-7xl mx-auto order-first flex flex-col flex-shrink-0", - SearchForm(actionObserver), - Directory(byLetter) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala deleted file mode 100644 index 73f915d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -object Directory: - - object Header: - type ViewModel = String - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500", - h3(child.text <-- $m) - ) - - object UserList: - type ViewModel = List[UserRow.ViewModel] - def apply($m: Signal[ViewModel]): HtmlElement = - ul( - role := "list", - cls := "relative z-0 divide-y divide-gray-200", - children <-- $m.split(_.osobniCislo)((_, _, s) => UserRow(s)) - ) - - type ViewModel = List[(String, List[UserRow.ViewModel])] - def apply($m: Signal[ViewModel]): HtmlElement = - nav( - cls := "flex-1 min-h-0 overflow-y-auto", - aria.label := "Directory", - children <-- $m.split(_._1)((_, _, s) => - div( - cls := "relative", - Header(s.map(_._1)), - UserList(s.map(_._2)) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala deleted file mode 100644 index e182910..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala +++ /dev/null @@ -1,54 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object SearchForm: - sealed trait Action - case object SubmitFilter extends Action - sealed trait FilterAction extends Action - case object ClearFilter extends FilterAction - case class SetFilter(value: String) extends FilterAction - - def apply(actions: Observer[Action]): HtmlElement = - div( - cls := "px-6 pt-4 pb-4", - form( - cls := "flex space-x-4", - onSubmit.mapTo(SubmitFilter) --> actions, - div( - cls := "flex-1 min-w-0", - label( - forId := "search", - cls := "sr-only", - "Hledat" - ), - div( - cls := "relative rounded-md shadow-sm", - div( - cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", - Icons.solid.search().amend(svg.cls := "text-gray-400") - ), - input( - tpe := "search", - name := "search", - idAttr := "search", - cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", - placeholder := "Hledat", - composeEvents(onInput.mapToValue.setAsValue.map(SetFilter(_)))( - _.throttle(500) - ) --> actions - ) - ) - ), - button( - tpe := "submit", - cls := "inline-flex justify-center px-3.5 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500", - Icons.solid.filter().amend(svg.cls := "text-gray-400"), - span( - cls := "sr-only", - "Hledat" - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala deleted file mode 100644 index 82f7723..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala +++ /dev/null @@ -1,52 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.airstream.core.Signal -import fiftyforms.ui.components.tailwind.Avatar - -object UserRow: - case class ViewModel( - osobniCislo: String, - celeJmeno: String, - prijmeni: String, - hlavniFunkce: Option[String], - img: Option[String], - container: () => HtmlElement = () => div() - ) { - val search = prijmeni.toLowerCase - } - - def apply($m: Signal[ViewModel]): HtmlElement = - inline def avatarImage = - Avatar($m.map(_.img)).avatarImage(10) - - li( - div( - cls := "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500", - div( - cls := "flex-shrink-0", - child <-- avatarImage - ), - div( - cls := "flex-1 min-w-0", - child <-- $m.map { o => - o.container() - .amend( - cls := "focus:outline-none", - span( - cls := "absolute inset-0", - aria.hidden := true - ), - p( - cls := "text-sm font-medium text-gray-900", - o.celeJmeno - ), - p( - cls := "text-sm text-gray-500 truncate", - o.hlavniFunkce - ) - ) - } - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala deleted file mode 100644 index 467c5f5..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala +++ /dev/null @@ -1,99 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router - -object ErrorPage: - case class ViewModel( - homePage: Page, - errorName: String, - title: String, - subTitle: String - ) - - def apply(m: ViewModel, actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - val ViewModel(homePage, errorName, title, subTitle) = m - div( - cls := "min-h-full pt-16 pb-12 flex flex-col bg-white", - main( - cls := "flex-grow flex flex-col justify-center max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex-shrink-0 flex justify-center", - a( - href := "/", - cls := "inline-flex", - span( - cls := "sr-only", - """Workflow""" - ), - img( - cls := "h-12 w-auto", - src := "https://tailwindui.com/img/logos/workflow-mark.svg?color=indigo&shade=600", - alt := "" - ) - ) - ), - div( - cls := "py-16", - div( - cls := "text-center", - p( - cls := "text-sm font-semibold text-indigo-600 uppercase tracking-wide", - errorName - ), - h1( - cls := "mt-2 text-4xl font-extrabold text-gray-900 tracking-tight sm:text-5xl", - title - ), - p( - cls := "mt-2 text-base text-gray-500", - subTitle - ), - div( - cls := "mt-6", - PageLink - .container(homePage, actionBus) - .amend( - cls := "text-base font-medium text-indigo-600 hover:text-indigo-500", - """Go back home""" - ) - ) - ) - ) - ), - footer( - cls := "flex-shrink-0 max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", - nav( - cls := "flex justify-center space-x-4", - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Contact Support""" - ), - span( - cls := "inline-block border-l border-gray-300", - aria.hidden := true - ), - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Status""" - ), - span( - cls := "inline-block border-l border-gray-300", - aria.hidden := true - ), - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Twitter""" - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala deleted file mode 100644 index bd9baec..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action - -object NotFoundPage: - def apply(homePage: Page, url: String, actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - ErrorPage( - ErrorPage.ViewModel( - homePage, - "404 error", - "Page not found.", - s"Sorry, but page $url doesn't exist." - ), - actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala deleted file mode 100644 index 476fe3c..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action - -object UnhandledErrorPage: - - case class ViewModel( - homePage: Page, - errorName: Option[String], - errorMessage: Option[String] - ) - - def apply(m: ViewModel, actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - ErrorPage( - ErrorPage.ViewModel( - m.homePage, - "Unexpected error occurred", - m.errorName.getOrElse( - "Uh oh!" - ), // TODO: translations, better text than uh oh - m.errorMessage.getOrElse( - "This wasn't supposed to happen! Please try again." - ) - ), - actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala deleted file mode 100644 index 25f059f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala +++ /dev/null @@ -1,6 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.services - -import com.raquo.airstream.core.Observer - -trait DataFetcher[K, A]: - def fetch(id: K, sink: Observer[A]): Unit diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala deleted file mode 100644 index a469830..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala +++ /dev/null @@ -1,120 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package state - -import com.raquo.airstream.core.EventStream -import cz.e_bs.cmi.mdr.pdb.{UserInfo, OsobniCislo} -import com.raquo.airstream.core.Observer -import scala.scalajs.js -import scala.scalajs.js.JSON -import zio.json._ -import com.raquo.airstream.eventbus.EventBus -import com.raquo.airstream.ownership.Owner -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.UserFunction -import cz.e_bs.cmi.mdr.pdb.UserContract -import fiftyforms.services.files.File - -trait AppState - extends connectors.DetailPageConnector.AppState - with connectors.DetailParametruPageConnector.AppState - with connectors.DetailKriteriaPageConnector.AppState - with pages.detail.UpravDukaz.State: - def users: EventStream[List[UserInfo]] - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - -class MockAppState(implicit owner: Owner, router: Router[Page]) - extends AppState: - - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonDecoder[UserFunction] = DeriveJsonDecoder.gen - given JsonDecoder[UserContract] = DeriveJsonDecoder.gen - given JsonDecoder[UserInfo] = DeriveJsonDecoder.gen - - given JsonDecoder[ParameterCriteria] = DeriveJsonDecoder.gen - given JsonDecoder[Parameter] = DeriveJsonDecoder.gen - - private val actions = EventBus[Action]() - private val (parametersStream, pushParameters) = - EventStream.withCallback[List[Parameter]] - private val (usersStream, pushUsers) = - EventStream.withCallback[List[UserInfo]] - private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] - private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] - - private val mockData: List[UserInfo] = - mockUsers - .asInstanceOf[js.Dictionary[js.Object]] - .values - // TODO: is there a more efficient way to parse from JS object directly? - .map { o => - val parsed = JSON.stringify(o).fromJson[UserInfo] - parsed.left.foreach(org.scalajs.dom.console.log(_)) - parsed - } - .collect { case Right(u) => - u - } - .toList - - private val mockParameters: List[Parameter] = - pdbParams - .asInstanceOf[js.Dictionary[js.Object]] - .values - .map(o => JSON.stringify(o).fromJson[Parameter]) - .collect { case Right(p) => p } - .toList - - // TODO: Extract to separate event handler - actions.events.foreach { - case FetchDirectory => pushUsers(mockData) - case FetchUserDetails(osc) => - mockData.find(_.personalNumber == osc).foreach { o => - pushDetails(o) - router.replaceState(Page.Detail(o)) - } - case FetchParameters(osc) => - pushParameters(mockParameters) - case FetchParameter(osc, paramId) => - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(Page.DetailParametru(o, p)) - case FetchParameterCriteria(osc, paramId, critId, page) => - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - c <- p.criteria.find(_.id == critId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(page(o, p, c)) - case NavigateTo(page) => router.pushState(page) - case FetchAvailableFiles(osc) => - pushFiles( - List( - File("https://tc163.cmi.cz/here", "Example file") - ) - ) - } - - override def users: EventStream[List[UserInfo]] = - usersStream.debugWithName("users") - - override def details: EventStream[UserInfo] = - detailsStream.debugWithName("details") - - override def parameters: EventStream[List[Parameter]] = - parametersStream.debugWithName("parameters") - - override def availableFiles: EventStream[List[File]] = - filesStream.debugWithName("available files") - - override def actionBus: Observer[Action] = - actions.writer.debugWithName("actions writer") diff --git a/app/src/main/scala/mdr/pdb/app/Main.scala b/app/src/main/scala/mdr/pdb/app/Main.scala new file mode 100644 index 0000000..081b4ee --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/Main.scala @@ -0,0 +1,103 @@ +package mdr.pdb.app + +import scala.scalajs.js.annotation.JSExportTopLevel +import scala.scalajs.js.annotation.JSExport +import scala.scalajs.js.annotation.JSImport +import scala.scalajs.js +import org.scalajs.dom +import com.raquo.laminar.api.L.{*, given} +import scala.scalajs.js.Date +import com.raquo.waypoint.Router +import com.raquo.waypoint.SplitRender +import mdr.pdb.app.services.DataFetcher +import scala.scalajs.js.JSON +import zio.json._ +import mdr.pdb.UserInfo +import mdr.pdb.app.state.AppState + +@js.native +@JSImport("stylesheets/main.css", JSImport.Namespace) +object Css extends js.Any + +@js.native +@JSImport("data/users.json", JSImport.Default) +object mockUsers extends js.Object + +@js.native +@JSImport("params/pdb-params.json", JSImport.Default) +object pdbParams extends js.Object + +@JSExportTopLevel("app") +object Main: + + @JSExport + def main(args: Array[String]): Unit = + import Routes.given + onLoad { + setupAirstream() + val appContainer = dom.document.querySelector("#app") + val _ = + render( + appContainer, + renderPage(state.MockAppState(using unsafeWindowOwner, router)) + ) + } + + private def onLoad(f: => Unit): Unit = + documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) + + private def setupAirstream()(using router: Router[Page]): Unit = + AirstreamError.registerUnhandledErrorCallback(err => + router.forcePage( + Page.UnhandledError( + Some(err.getClass.getName), // TODO: Fill only in dev mode + Some(err.getMessage) + ) + ) + ) + + def renderPage(state: AppState)(using + router: Router[Page] + ): HtmlElement = + val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) + .collectSignal[Page.Detail]( + connectors + .DetailPageConnector(state)(_) + .apply + ) + .collectSignal[Page.DetailParametru]( + connectors + .DetailParametruPageConnector(state)(_) + .apply + ) + .collectSignal[Page.DetailKriteria]( + connectors + .DetailKriteriaPageConnector(state)(_) + .apply + ) + .collectSignal[Page.UpravDukazKriteria]( + pages.detail.UpravDukaz.Connector(state)(_).apply + ) + .collectStatic(Page.Dashboard)( + connectors.DashboardPageConnector(state.actionBus).apply + ) + .collect[Page.NotFound](pg => + pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) + ) + .collect[Page.UnhandledError](pg => + pages.errors + .UnhandledErrorPage( + pages.errors.UnhandledErrorPage + .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), + state.actionBus + ) + ) + .collectStatic(Page.Directory)( + connectors + .DirectoryPageConnector(state.users, state.actionBus) + .apply + ) + div(cls := "h-full", child <-- pageSplitter.$view) + + // Pull in the stylesheet + val css: Css.type = Css diff --git a/app/src/main/scala/mdr/pdb/app/Routes.scala b/app/src/main/scala/mdr/pdb/app/Routes.scala new file mode 100644 index 0000000..2cc860c --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/Routes.scala @@ -0,0 +1,198 @@ +package mdr.pdb.app + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.* +import org.scalajs.dom +import zio.json.{*, given} +import mdr.pdb.OsobniCislo + +import scala.scalajs.js +import mdr.pdb.UserInfo +import mdr.pdb.Parameter +import mdr.pdb.ParameterCriteria +import mdr.pdb.app.Page.Titled + +// enum is not working with Waypoints' SplitRender collectStatic +sealed abstract class Page( + val id: String, + val title: String, + val parent: Option[Page] +) { + val path: Vector[Page] = + parent match + case None => Vector(this) + case Some(p) => p.path :+ this + + val isRoot: Boolean = parent.isEmpty +} + +object Page: + + case class Titled[V](value: V, title: Option[String] = None): + val show: String = title.getOrElse(value.toString) + + case object Directory extends Page("directory", "Adresář", None) + + case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) + + case class Detail(osobniCislo: Titled[OsobniCislo]) + extends Page("user", osobniCislo.show, Some(Directory)) + + object Detail { + def apply(o: UserInfo): Detail = Detail( + Titled(o.personalNumber, Some(o.name)) + ) + } + + case class DetailParametru( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String] + ) extends Page( + "parameter", + parametr.show, + Some(Detail(osobniCislo)) + ) + + object DetailParametru { + def apply(o: UserInfo, p: Parameter): DetailParametru = + DetailParametru( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)) + ) + } + + case class DetailKriteria( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String], + kriterium: Titled[String] + ) extends Page( + "criteria", + kriterium.show, + Some(DetailParametru(osobniCislo, parametr)) + ) + + object DetailKriteria { + def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = + DetailKriteria( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)), + Titled(k.id, Some(k.id)) + ) + } + + case class UpravDukazKriteria( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String], + kriterium: Titled[String] + ) extends Page( + "addProof", + "Důkaz", + Some(DetailKriteria(osobniCislo, parametr, kriterium)) + ) + + object UpravDukazKriteria { + def apply( + o: UserInfo, + p: Parameter, + k: ParameterCriteria + ): UpravDukazKriteria = + UpravDukazKriteria( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)), + Titled(k.id, Some(k.id)) + ) + } + + case class NotFound(url: String) extends Page("404", "404", Some(Directory)) + + case class UnhandledError( + errorName: Option[String], + errorMessage: Option[String] + ) extends Page("500", "Unexpected error", Some(Directory)) + +object Routes: + given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) + given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) + given [V: JsonEncoder]: JsonEncoder[Titled[V]] = + DeriveJsonEncoder.gen[Titled[V]] + given [V: JsonDecoder]: JsonDecoder[Titled[V]] = + DeriveJsonDecoder.gen[Titled[V]] + given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] + given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] + + val base = + js.`import`.meta.env.BASE_URL + .asInstanceOf[String] + "app" + + val homePage: Page = Page.Directory + + given router: Router[Page] = Router[Page]( + routes = List( + Route.static(homePage, root / endOfSegments, basePath = base), + Route.static( + Page.Dashboard, + root / "dashboard" / endOfSegments, + basePath = base + ), + Route[Page.Detail, String]( + encode = _.osobniCislo.value.toString, + decode = osc => Page.Detail(Titled(OsobniCislo(osc))), + root / "osoba" / segment[String] / endOfSegments, + basePath = base + ), + Route[Page.DetailParametru, (String, String)]( + encode = p => (p.osobniCislo.value.toString, p.parametr.value), + decode = + p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / endOfSegments, + basePath = base + ), + Route[Page.DetailKriteria, (String, String, String)]( + encode = p => + ( + p.osobniCislo.value.toString, + p.parametr.value, + p.kriterium.value.replaceAll("\\.", "--") + ), + decode = p => + Page.DetailKriteria( + Titled(OsobniCislo(p._1)), + Titled(p._2), + Titled(p._3.replaceAll("--", ".")) + ), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / "kriterium" / segment[String] / endOfSegments, + basePath = base + ), + Route[Page.UpravDukazKriteria, (String, String, String)]( + encode = p => + ( + p.osobniCislo.value.toString, + p.parametr.value, + p.kriterium.value.replaceAll("\\.", "--") + ), + decode = p => + Page.UpravDukazKriteria( + Titled(OsobniCislo(p._1)), + Titled(p._2), + Titled(p._3.replaceAll("--", ".")) + ), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / "kriterium" / segment[String] / "edit" / endOfSegments, + basePath = base + ) + ), + serializePage = _.toJson, + deserializePage = _.fromJson[Page] + .fold(s => throw IllegalStateException(s), identity), + getPageTitle = _.title, + routeFallback = url => Page.NotFound(url), + deserializeFallback = _ => Page.Dashboard + )( + $popStateEvent = windowEvents.onPopState, + owner = unsafeWindowOwner + ) diff --git a/app/src/main/scala/mdr/pdb/app/actions.scala b/app/src/main/scala/mdr/pdb/app/actions.scala new file mode 100644 index 0000000..d078b3a --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/actions.scala @@ -0,0 +1,21 @@ +package mdr.pdb.app + +import mdr.pdb.OsobniCislo +import mdr.pdb.UserInfo +import mdr.pdb.Parameter +import mdr.pdb.ParameterCriteria + +sealed trait Action + +case object FetchDirectory extends Action +case class FetchUserDetails(osc: OsobniCislo) extends Action +case class FetchParameters(osc: OsobniCislo) extends Action +case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action +case class FetchParameterCriteria( + osc: OsobniCislo, + paramId: String, + critId: String, + page: (UserInfo, Parameter, ParameterCriteria) => Page +) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action +case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/mdr/pdb/app/components/AppPage.scala new file mode 100644 index 0000000..130c817 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/AppPage.scala @@ -0,0 +1,82 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.Page +import mdr.pdb.{UserProfile, UserInfo, OsobniCislo} +import com.raquo.waypoint.Router +import mdr.pdb.app.Action +import mdr.pdb.app.NavigateTo +import mdr.pdb.UserFunction + +object AppPage: + // TODO: pages by logged in user + val pages: List[Page] = List(Page.Directory, Page.Dashboard) + + import NavigationBar.{Logo, MenuItem} + + val logo = Logo( + "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", + "Workflow" + ) + + // TODO: menu items by user profile + val userMenu = + List( + MenuItem("Your Profile"), + MenuItem("Settings"), + MenuItem("Sign out") + ) + + // TODO: load user profile + val $userProfile = Var( + UserProfile( + "tom", + UserInfo( + OsobniCislo("1031"), + "tom", + "Tom", + "Cook", + None, + None, + Some("tom@example.com"), + Some("+420 222 866 180"), + Some( + UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") + ), + Nil, + None + ) + ) + ) + + val $userInfo = $userProfile.signal.map(_.userInfo) + + type ViewModel = Option[HtmlElement] + def apply( + actionBus: Observer[Action] + )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using + router: Router[Page] + ): HtmlElement = + PageLayout(actionBus)( + $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => + PageLayout.ViewModel( + NavigationBar.ViewModel( + u, + pages.map(p => + NavigationBar.Link( + () => + PageLink( + p, + actionBus + ), + p == cp + ) + ), + userMenu, + logo + ), + c + ) + ), + mods + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala new file mode 100644 index 0000000..9375398 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala @@ -0,0 +1,143 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.tailwind.list.IconText.ViewModel +import fiftyforms.ui.components.tailwind.Icons +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.Page + +import com.raquo.laminar.api.L.{*, given} +import io.laminext.syntax.core.{*, given} +import mdr.pdb.app.Action + +object Breadcrumbs: + + private def slash = + import svg.{*, given} + svg( + cls := "flex-shrink-0 h-5 w-5 text-gray-300", + xmlns := "http://www.w3.org/2000/svg", + fill := "currentColor", + viewBox := "0 0 20 20", + ariaHidden := true, + path( + d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" + ) + ) + + def backIcon = + Icons.solid + .`arrow-narrow-left`() + .amend( + svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" + ) + + object Link: + case class ViewModel( + page: Page, + icon: Option[SvgElement], + extraClasses: String, + text: String, + textClasses: Option[String] = None + ) + + val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" + + def shortHome(p: Page) = ViewModel( + p, + Some(backIcon), + "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", + "Zpět na úvodní stránku", + None + ) + + def fullHome(p: Page) = ViewModel( + p, + Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), + "text-gray-400 hover:text-gray-500", + "Domů", + Some("sr-only") + ) + + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + PageLink + .container($m.map(_.page), actionBus) + .amend( + cls <-- $m.map(_.extraClasses), + child.maybe <-- $m.map(_.icon), + span( + cls <-- $m.map(_.textClasses.getOrElse("")), + child.text <-- $m.map(_.text) + ) + ) + + object FullBreadcrumbs: + type ViewModel = Page + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + ol( + role := "list", + cls := "flex items-center space-x-4", + children <-- $m.map(_.path).split(_.id)((_, _, $p) => + li( + div( + cls := "flex items-center", + child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), + Link( + $p.map(p => + if (p.isRoot) then Link.fullHome(p) + else + Link.ViewModel( + p, + None, + s"ml-4 max-w-xs truncate ${Link.baseClasses}", + p.title + ) + ), + actionBus + ) + ) + ) + ) + ) + + object ShortBreadcrumbs: + type ViewModel = Page + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + Link( + $m.map { p => + val target = p.parent.getOrElse(p) + if target.isRoot then Link.shortHome(target) + else + Link.ViewModel( + target, + Some(backIcon), + "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", + s"Zpět na ${target.title}" + ) + }, + actionBus + ) + + def apply(actionBus: Observer[Action])(using + router: Router[Page] + ): HtmlElement = + nav( + cls := "flex", + aria.label := "Breadcrumb", + div( + cls := "flex sm:hidden", + ShortBreadcrumbs(router.$currentPage, actionBus) + ), + div( + cls := "hidden sm:block", + FullBreadcrumbs(router.$currentPage, actionBus) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala new file mode 100644 index 0000000..4bb8b8d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala @@ -0,0 +1,242 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent +import fiftyforms.ui.components.tailwind.Icons +import fiftyforms.ui.components.tailwind.Avatar +import mdr.pdb.UserInfo +import io.laminext.syntax.core.* + +object NavigationBar: + + case class Logo(img: String, name: String) + case class Link(a: () => Anchor, active: Boolean) + case class MenuItem(title: String) + + case class ViewModel( + userInfo: UserInfo, + pages: List[Link], + userMenu: List[MenuItem], + logo: Logo + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + val $userInfo = $m.map(_.userInfo) + val mobileMenuOpen = Var(false) + + val desktopOnly = cls("hidden md:block") + val mobileOnly = cls("md:hidden") + + inline def avatarImage(size: Int = 8) = + Avatar($userInfo.map(_.img)).avatarImage(size) + + def notificationButton = button( + tpe := "button", + cls := List( + "bg-indigo-600", + "focus:outline-none", + "focus:ring-2", + "focus:ring-offset-2", + "focus:ring-offset-indigo-600", + "focus:ring-white", + "hover:text-white", + "p-1", + "rounded-full", + "text-indigo-200" + ), + span(cls := "sr-only", "View notifications"), + Icons.outline.bell() + ) + + def userProfile: HtmlElement = + val menuOpen = Var(false) + + def menuItem(item: MenuItem, idx: Int): HtmlElement = + a( + href := "#", + cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", + role := "menuitem", + tabIndex := -1, + idAttr := s"user-menu-item-$idx", + item.title + ) + + div( + cls := "ml-3 relative", + div( + button( + tpe := "button", + cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", + idAttr := "user-menu-button", + aria.expanded <-- menuOpen.signal, + aria.hasPopup := true, + span(cls := "sr-only", "Open user menu"), + child <-- avatarImage(), + onClick.preventDefault.mapTo( + !menuOpen.now() + ) --> menuOpen.writer + ) + ), + /* + * */ + div( + cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", + cls <-- menuOpen.signal.map { o => + if (o) "md:block" else "md:hidden" + }, + role := "menu", + aria.orientation := "vertical", + aria.labelledBy := "user-menu-button", + tabIndex := -1, + // : keyboard navigation + children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) + ) + ) + + def mobileProfile = + def menuItem(item: MenuItem): HtmlElement = + a( + href := "#", + cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", + item.title + ) + + div( + cls := "pt-4 pb-3 border-t border-indigo-700", + div( + cls := "flex items-center px-5", + div( + cls := "flex-shrink-0", + child <-- avatarImage(10) + ), + div( + cls := "ml-3", + div( + cls := "text-base font-medium text-white", + child.text <-- $userInfo.map(_.name) + ), + child.maybe <-- $userInfo.map( + _.email.map(e => + div( + cls := "text-sm font-medium text-indigo-300", + e + ) + ) + ) + ), + notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) + ), + div( + cls := "mt-3 px-2 space-y-1", + children <-- $m.map(_.userMenu.map(menuItem)) + ) + ) + + def pageLink(page: Link): Anchor = + page + .a() + .amend( + cls := "text-white px-3 py-2 rounded-md text-sm font-medium", + cls := Seq( + "bg-indigo-700" -> page.active, + "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active + ), + ariaCurrent := (if page.active then "page" else "false") + ) + + def logoImg: Image = + img( + cls := "h-8 w-8", + src <-- $m.map(_.logo.img), + alt <-- $m.map(_.logo.name) + ) + + def mobileMenuButton = button( + tpe := "button", + cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", + aria.controls := "mobile-menu", + aria.expanded <-- mobileMenuOpen.signal, + span(cls := "sr-only", "Open main menu"), + Icons.outline + .menu() + .amend( + svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") + ), + Icons.outline + .x() + .amend( + svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") + ), + onClick.preventDefault.mapTo( + !mobileMenuOpen.now() + ) --> mobileMenuOpen.writer + ) + + def navBarLeft = + div( + cls := "flex items-center", + div(cls := "flex-shrink-0", logoImg), + div( + desktopOnly, + div( + cls := "ml-10 flex items-baseline space-x-4", + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) + ) + ) + ) + + def navBarRight = + div( + desktopOnly, + div( + cls := "ml-4 flex items-center md:ml-6", + notificationButton, + userProfile + ) + ) + + def navBarMobile = + div( + cls := "-mr-2 flex", + mobileOnly, + mobileMenuButton + ) + + def navBar = + div( + cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", + div( + cls := "flex items-center justify-between h-16", + navBarLeft, + navBarRight, + navBarMobile + ) + ) + + def mobileMenu = + div( + mobileOnly, + cls <-- mobileMenuOpen.signal.map { o => + if (o) "block" else "hidden" + }, + idAttr := "mobile-menu", + div( + cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) + ), + mobileProfile + ) + + nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala new file mode 100644 index 0000000..c4cb10d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala @@ -0,0 +1,19 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.Page +import com.raquo.waypoint.Router +import mdr.pdb.app.Action + +object PageHeader: + def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = + header( + cls := "bg-white shadow-sm", + div( + cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", + h1( + cls := "text-lg leading-6 font-semibold text-gray-900", + Breadcrumbs(actionBus) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala new file mode 100644 index 0000000..aae4539 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala @@ -0,0 +1,30 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.Page +import mdr.pdb.app.Action +import fiftyforms.ui.components.tailwind.Loading +import io.laminext.syntax.core.* + +object PageLayout: + case class ViewModel( + navigation: NavigationBar.ViewModel, + content: Option[HtmlElement] + ) + def apply(actionBus: Observer[Action])( + $m: Signal[ViewModel], + mods: Modifier[HtmlElement]* + )(using router: Router[Page]): HtmlElement = + val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) + div( + cls := "h-full flex flex-col", + NavigationBar($m.map(_.navigation)), + child.maybe <-- router.$currentPage.map(_.isRoot) + .switch(None, Some(PageHeader(actionBus))), + main( + cls := "flex-grow-1 overflow-y-auto", + mods, + child <-- $maybeContent.map(_.getOrElse(Loading)) + ) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala deleted file mode 100644 index a3b00af..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala +++ /dev/null @@ -1,103 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import scala.scalajs.js.annotation.JSExportTopLevel -import scala.scalajs.js.annotation.JSExport -import scala.scalajs.js.annotation.JSImport -import scala.scalajs.js -import org.scalajs.dom -import com.raquo.laminar.api.L.{*, given} -import scala.scalajs.js.Date -import com.raquo.waypoint.Router -import com.raquo.waypoint.SplitRender -import cz.e_bs.cmi.mdr.pdb.app.services.DataFetcher -import scala.scalajs.js.JSON -import zio.json._ -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.state.AppState - -@js.native -@JSImport("stylesheets/main.css", JSImport.Namespace) -object Css extends js.Any - -@js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native -@JSImport("params/pdb-params.json", JSImport.Default) -object pdbParams extends js.Object - -@JSExportTopLevel("app") -object Main: - - @JSExport - def main(args: Array[String]): Unit = - import Routes.given - onLoad { - setupAirstream() - val appContainer = dom.document.querySelector("#app") - val _ = - render( - appContainer, - renderPage(state.MockAppState(using unsafeWindowOwner, router)) - ) - } - - private def onLoad(f: => Unit): Unit = - documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) - - private def setupAirstream()(using router: Router[Page]): Unit = - AirstreamError.registerUnhandledErrorCallback(err => - router.forcePage( - Page.UnhandledError( - Some(err.getClass.getName), // TODO: Fill only in dev mode - Some(err.getMessage) - ) - ) - ) - - def renderPage(state: AppState)(using - router: Router[Page] - ): HtmlElement = - val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) - .collectSignal[Page.Detail]( - connectors - .DetailPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailParametru]( - connectors - .DetailParametruPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailKriteria]( - connectors - .DetailKriteriaPageConnector(state)(_) - .apply - ) - .collectSignal[Page.UpravDukazKriteria]( - pages.detail.UpravDukaz.Connector(state)(_).apply - ) - .collectStatic(Page.Dashboard)( - connectors.DashboardPageConnector(state.actionBus).apply - ) - .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) - ) - .collect[Page.UnhandledError](pg => - pages.errors - .UnhandledErrorPage( - pages.errors.UnhandledErrorPage - .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), - state.actionBus - ) - ) - .collectStatic(Page.Directory)( - connectors - .DirectoryPageConnector(state.users, state.actionBus) - .apply - ) - div(cls := "h-full", child <-- pageSplitter.$view) - - // Pull in the stylesheet - val css: Css.type = Css diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala deleted file mode 100644 index 3bbc8c0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ /dev/null @@ -1,198 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.* -import org.scalajs.dom -import zio.json.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import scala.scalajs.js -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.Page.Titled - -// enum is not working with Waypoints' SplitRender collectStatic -sealed abstract class Page( - val id: String, - val title: String, - val parent: Option[Page] -) { - val path: Vector[Page] = - parent match - case None => Vector(this) - case Some(p) => p.path :+ this - - val isRoot: Boolean = parent.isEmpty -} - -object Page: - - case class Titled[V](value: V, title: Option[String] = None): - val show: String = title.getOrElse(value.toString) - - case object Directory extends Page("directory", "Adresář", None) - - case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) - - case class Detail(osobniCislo: Titled[OsobniCislo]) - extends Page("user", osobniCislo.show, Some(Directory)) - - object Detail { - def apply(o: UserInfo): Detail = Detail( - Titled(o.personalNumber, Some(o.name)) - ) - } - - case class DetailParametru( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String] - ) extends Page( - "parameter", - parametr.show, - Some(Detail(osobniCislo)) - ) - - object DetailParametru { - def apply(o: UserInfo, p: Parameter): DetailParametru = - DetailParametru( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)) - ) - } - - case class DetailKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "criteria", - kriterium.show, - Some(DetailParametru(osobniCislo, parametr)) - ) - - object DetailKriteria { - def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = - DetailKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class UpravDukazKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "addProof", - "Důkaz", - Some(DetailKriteria(osobniCislo, parametr, kriterium)) - ) - - object UpravDukazKriteria { - def apply( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): UpravDukazKriteria = - UpravDukazKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class NotFound(url: String) extends Page("404", "404", Some(Directory)) - - case class UnhandledError( - errorName: Option[String], - errorMessage: Option[String] - ) extends Page("500", "Unexpected error", Some(Directory)) - -object Routes: - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) - given [V: JsonEncoder]: JsonEncoder[Titled[V]] = - DeriveJsonEncoder.gen[Titled[V]] - given [V: JsonDecoder]: JsonDecoder[Titled[V]] = - DeriveJsonDecoder.gen[Titled[V]] - given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] - given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] - - val base = - js.`import`.meta.env.BASE_URL - .asInstanceOf[String] + "app" - - val homePage: Page = Page.Directory - - given router: Router[Page] = Router[Page]( - routes = List( - Route.static(homePage, root / endOfSegments, basePath = base), - Route.static( - Page.Dashboard, - root / "dashboard" / endOfSegments, - basePath = base - ), - Route[Page.Detail, String]( - encode = _.osobniCislo.value.toString, - decode = osc => Page.Detail(Titled(OsobniCislo(osc))), - root / "osoba" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.DetailParametru, (String, String)]( - encode = p => (p.osobniCislo.value.toString, p.parametr.value), - decode = - p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / endOfSegments, - basePath = base - ), - Route[Page.DetailKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.DetailKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.UpravDukazKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.UpravDukazKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / "edit" / endOfSegments, - basePath = base - ) - ), - serializePage = _.toJson, - deserializePage = _.fromJson[Page] - .fold(s => throw IllegalStateException(s), identity), - getPageTitle = _.title, - routeFallback = url => Page.NotFound(url), - deserializeFallback = _ => Page.Dashboard - )( - $popStateEvent = windowEvents.onPopState, - owner = unsafeWindowOwner - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala deleted file mode 100644 index b24b82b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -sealed trait Action - -case object FetchDirectory extends Action -case class FetchUserDetails(osc: OsobniCislo) extends Action -case class FetchParameters(osc: OsobniCislo) extends Action -case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action -case class FetchParameterCriteria( - osc: OsobniCislo, - paramId: String, - critId: String, - page: (UserInfo, Parameter, ParameterCriteria) => Page -) extends Action -case class FetchAvailableFiles(osc: OsobniCislo) extends Action -case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala deleted file mode 100644 index c637d35..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala +++ /dev/null @@ -1,82 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.{UserProfile, UserInfo, OsobniCislo} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo -import cz.e_bs.cmi.mdr.pdb.UserFunction - -object AppPage: - // TODO: pages by logged in user - val pages: List[Page] = List(Page.Directory, Page.Dashboard) - - import NavigationBar.{Logo, MenuItem} - - val logo = Logo( - "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", - "Workflow" - ) - - // TODO: menu items by user profile - val userMenu = - List( - MenuItem("Your Profile"), - MenuItem("Settings"), - MenuItem("Sign out") - ) - - // TODO: load user profile - val $userProfile = Var( - UserProfile( - "tom", - UserInfo( - OsobniCislo("1031"), - "tom", - "Tom", - "Cook", - None, - None, - Some("tom@example.com"), - Some("+420 222 866 180"), - Some( - UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") - ), - Nil, - None - ) - ) - ) - - val $userInfo = $userProfile.signal.map(_.userInfo) - - type ViewModel = Option[HtmlElement] - def apply( - actionBus: Observer[Action] - )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using - router: Router[Page] - ): HtmlElement = - PageLayout(actionBus)( - $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => - PageLayout.ViewModel( - NavigationBar.ViewModel( - u, - pages.map(p => - NavigationBar.Link( - () => - PageLink( - p, - actionBus - ), - p == cp - ) - ), - userMenu, - logo - ), - c - ) - ), - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala deleted file mode 100644 index bf473c6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ /dev/null @@ -1,143 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden -import fiftyforms.ui.components.tailwind.list.IconText.ViewModel -import fiftyforms.ui.components.tailwind.Icons -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Action - -object Breadcrumbs: - - private def slash = - import svg.{*, given} - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-300", - xmlns := "http://www.w3.org/2000/svg", - fill := "currentColor", - viewBox := "0 0 20 20", - ariaHidden := true, - path( - d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" - ) - ) - - def backIcon = - Icons.solid - .`arrow-narrow-left`() - .amend( - svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" - ) - - object Link: - case class ViewModel( - page: Page, - icon: Option[SvgElement], - extraClasses: String, - text: String, - textClasses: Option[String] = None - ) - - val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" - - def shortHome(p: Page) = ViewModel( - p, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - "Zpět na úvodní stránku", - None - ) - - def fullHome(p: Page) = ViewModel( - p, - Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), - "text-gray-400 hover:text-gray-500", - "Domů", - Some("sr-only") - ) - - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - PageLink - .container($m.map(_.page), actionBus) - .amend( - cls <-- $m.map(_.extraClasses), - child.maybe <-- $m.map(_.icon), - span( - cls <-- $m.map(_.textClasses.getOrElse("")), - child.text <-- $m.map(_.text) - ) - ) - - object FullBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - ol( - role := "list", - cls := "flex items-center space-x-4", - children <-- $m.map(_.path).split(_.id)((_, _, $p) => - li( - div( - cls := "flex items-center", - child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), - Link( - $p.map(p => - if (p.isRoot) then Link.fullHome(p) - else - Link.ViewModel( - p, - None, - s"ml-4 max-w-xs truncate ${Link.baseClasses}", - p.title - ) - ), - actionBus - ) - ) - ) - ) - ) - - object ShortBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - Link( - $m.map { p => - val target = p.parent.getOrElse(p) - if target.isRoot then Link.shortHome(target) - else - Link.ViewModel( - target, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - s"Zpět na ${target.title}" - ) - }, - actionBus - ) - - def apply(actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - nav( - cls := "flex", - aria.label := "Breadcrumb", - div( - cls := "flex sm:hidden", - ShortBreadcrumbs(router.$currentPage, actionBus) - ), - div( - cls := "hidden sm:block", - FullBreadcrumbs(router.$currentPage, actionBus) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala deleted file mode 100644 index 0db2047..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ /dev/null @@ -1,242 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent -import fiftyforms.ui.components.tailwind.Icons -import fiftyforms.ui.components.tailwind.Avatar -import cz.e_bs.cmi.mdr.pdb.UserInfo -import io.laminext.syntax.core.* - -object NavigationBar: - - case class Logo(img: String, name: String) - case class Link(a: () => Anchor, active: Boolean) - case class MenuItem(title: String) - - case class ViewModel( - userInfo: UserInfo, - pages: List[Link], - userMenu: List[MenuItem], - logo: Logo - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - val $userInfo = $m.map(_.userInfo) - val mobileMenuOpen = Var(false) - - val desktopOnly = cls("hidden md:block") - val mobileOnly = cls("md:hidden") - - inline def avatarImage(size: Int = 8) = - Avatar($userInfo.map(_.img)).avatarImage(size) - - def notificationButton = button( - tpe := "button", - cls := List( - "bg-indigo-600", - "focus:outline-none", - "focus:ring-2", - "focus:ring-offset-2", - "focus:ring-offset-indigo-600", - "focus:ring-white", - "hover:text-white", - "p-1", - "rounded-full", - "text-indigo-200" - ), - span(cls := "sr-only", "View notifications"), - Icons.outline.bell() - ) - - def userProfile: HtmlElement = - val menuOpen = Var(false) - - def menuItem(item: MenuItem, idx: Int): HtmlElement = - a( - href := "#", - cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", - role := "menuitem", - tabIndex := -1, - idAttr := s"user-menu-item-$idx", - item.title - ) - - div( - cls := "ml-3 relative", - div( - button( - tpe := "button", - cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - idAttr := "user-menu-button", - aria.expanded <-- menuOpen.signal, - aria.hasPopup := true, - span(cls := "sr-only", "Open user menu"), - child <-- avatarImage(), - onClick.preventDefault.mapTo( - !menuOpen.now() - ) --> menuOpen.writer - ) - ), - /* - * */ - div( - cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", - cls <-- menuOpen.signal.map { o => - if (o) "md:block" else "md:hidden" - }, - role := "menu", - aria.orientation := "vertical", - aria.labelledBy := "user-menu-button", - tabIndex := -1, - // : keyboard navigation - children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) - ) - ) - - def mobileProfile = - def menuItem(item: MenuItem): HtmlElement = - a( - href := "#", - cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", - item.title - ) - - div( - cls := "pt-4 pb-3 border-t border-indigo-700", - div( - cls := "flex items-center px-5", - div( - cls := "flex-shrink-0", - child <-- avatarImage(10) - ), - div( - cls := "ml-3", - div( - cls := "text-base font-medium text-white", - child.text <-- $userInfo.map(_.name) - ), - child.maybe <-- $userInfo.map( - _.email.map(e => - div( - cls := "text-sm font-medium text-indigo-300", - e - ) - ) - ) - ), - notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) - ), - div( - cls := "mt-3 px-2 space-y-1", - children <-- $m.map(_.userMenu.map(menuItem)) - ) - ) - - def pageLink(page: Link): Anchor = - page - .a() - .amend( - cls := "text-white px-3 py-2 rounded-md text-sm font-medium", - cls := Seq( - "bg-indigo-700" -> page.active, - "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active - ), - ariaCurrent := (if page.active then "page" else "false") - ) - - def logoImg: Image = - img( - cls := "h-8 w-8", - src <-- $m.map(_.logo.img), - alt <-- $m.map(_.logo.name) - ) - - def mobileMenuButton = button( - tpe := "button", - cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - aria.controls := "mobile-menu", - aria.expanded <-- mobileMenuOpen.signal, - span(cls := "sr-only", "Open main menu"), - Icons.outline - .menu() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") - ), - Icons.outline - .x() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") - ), - onClick.preventDefault.mapTo( - !mobileMenuOpen.now() - ) --> mobileMenuOpen.writer - ) - - def navBarLeft = - div( - cls := "flex items-center", - div(cls := "flex-shrink-0", logoImg), - div( - desktopOnly, - div( - cls := "ml-10 flex items-baseline space-x-4", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ) - ) - ) - - def navBarRight = - div( - desktopOnly, - div( - cls := "ml-4 flex items-center md:ml-6", - notificationButton, - userProfile - ) - ) - - def navBarMobile = - div( - cls := "-mr-2 flex", - mobileOnly, - mobileMenuButton - ) - - def navBar = - div( - cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex items-center justify-between h-16", - navBarLeft, - navBarRight, - navBarMobile - ) - ) - - def mobileMenu = - div( - mobileOnly, - cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }, - idAttr := "mobile-menu", - div( - cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ), - mobileProfile - ) - - nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala deleted file mode 100644 index 75339df..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object PageHeader: - def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = - header( - cls := "bg-white shadow-sm", - div( - cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", - h1( - cls := "text-lg leading-6 font-semibold text-gray-900", - Breadcrumbs(actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala deleted file mode 100644 index 5096463..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ /dev/null @@ -1,30 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import fiftyforms.ui.components.tailwind.Loading -import io.laminext.syntax.core.* - -object PageLayout: - case class ViewModel( - navigation: NavigationBar.ViewModel, - content: Option[HtmlElement] - ) - def apply(actionBus: Observer[Action])( - $m: Signal[ViewModel], - mods: Modifier[HtmlElement]* - )(using router: Router[Page]): HtmlElement = - val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) - div( - cls := "h-full flex flex-col", - NavigationBar($m.map(_.navigation)), - child.maybe <-- router.$currentPage.map(_.isRoot) - .switch(None, Some(PageHeader(actionBus))), - main( - cls := "flex-grow-1 overflow-y-auto", - mods, - child <-- $maybeContent.map(_.getOrElse(Loading)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala deleted file mode 100644 index 62d820f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.LinkSupport.* -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo - -object PageLink: - type ViewModel = Page - def apply($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = - a(mods($m, actions), child.text <-- $m.map(_.title)) - - def apply(m: ViewModel, actions: Observer[Action])(using - router: Router[Page] - ): Anchor = apply(Val(m), actions) - - def container($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = a(mods($m, actions)) - - def container(m: ViewModel, actions: Observer[Action])(using - Router[Page] - ): Anchor = container(Val(m), actions) - - private def mods($m: Signal[Page], actions: Observer[Action])(using - router: Router[Page] - ): Modifier[Anchor] = - Seq( - href <-- $m.map(router.absoluteUrlForPage).recover { case _ => - Some("invalid url") - }, - composeEvents(onClick.noKeyMod.preventDefault)( - _.sample($m) - .map(NavigateTo.apply) - ) --> actions - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala deleted file mode 100644 index 87f825a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.pages.dashboard.DashboardPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -class DashboardPageConnector(actionBus: Observer[Action])(using - router: Router[Page] -): - def apply: HtmlElement = - AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala deleted file mode 100644 index e9b0f71..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala +++ /dev/null @@ -1,76 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailKriteriaPage -import pages.detail.components.DukazyKriteria -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -object DetailKriteriaPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailKriteriaPageConnector( - state: DetailKriteriaPageConnector.AppState -)( - $page: Signal[Page.DetailKriteria] -)(using Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => - (p.osobniCislo.value, p.parametr.value, p.kriterium.value) - )((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map( - FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) - ) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, s, $s) => - DetailKriteriaPage($s)(state.actionBus.contramap { - case DukazyKriteria.Add => - NavigateTo( - Page.UpravDukazKriteria( - Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), - Page.Titled(s.parametr.id, Some(s.parametr.nazev)), - Page.Titled(s.kriterium.id, Some(s.kriterium.id)) - ) - ) - }) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): DetailKriteriaPage.ViewModel = - DetailKriteriaPage.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala deleted file mode 100644 index 4d8024f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ /dev/null @@ -1,57 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import pages.detail.DetailPage -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import fiftyforms.ui.components.tailwind.Color - -object DetailPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailPageConnector(state: DetailPageConnector.AppState)( - $page: Signal[Page.Detail] -)(using router: Router[Page]): - val $oscChangeSignal = $page.splitOne(_.osobniCislo.value)((osc, _, _) => osc) - val $pageChangeSignal = - $oscChangeSignal.flatMap(osc => - EventStream.fromSeq(Seq(FetchUserDetails(osc), FetchParameters(osc))) - ) - // TODO: filter the value based on the current osc - // OSC change will fetch new data, but still - // - we need to be sure that what we got is really what we ought to display - // - we want to display stale data accordingly (at least with loading indicator) - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - def apply: HtmlElement = - AppPage(state.actionBus)( - $data.combineWithFn($params)(_ zip _) - .map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: List[Parameter] - ): DetailPage.ViewModel = - DetailPage.ViewModel( - o.toDetailOsoby, - p.map( - _.toParametr(param => - PageLink.container(Page.DetailParametru(o, param), state.actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala deleted file mode 100644 index fdaa7f8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailParametruPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink - -object DetailParametruPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailParametruPageConnector( - state: DetailParametruPageConnector.AppState -)( - $page: Signal[Page.DetailParametru] -)(using router: Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => (p.osobniCislo.value, p.parametr.value))((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map(FetchParameter(_, _)) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - } yield (da, pb) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailParametruPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter - ): DetailParametruPage.ViewModel = - DetailParametruPage.ViewModel( - o.toDetailOsoby, - p.toParametr(p => - PageLink.container(Page.DetailParametru(o, p), state.actionBus) - ), - p.criteria.map( - _.toKriterium { c => - PageLink.container( - Page.DetailKriteria(o, p, c), - state.actionBus - ) - } - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala deleted file mode 100644 index 049aa22..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -case class DirectoryPageConnector( - $input: EventStream[List[UserInfo]], - actionBus: Observer[Action] -)(using router: Router[Page]): - val $data = $input.startWithNone - val $actionSignal = EventStream.fromValue(FetchDirectory) - - def apply: HtmlElement = - AppPage(actionBus)( - $data.split(_ => ())((_, _, s) => - pages.directory.DirectoryPage( - s.map( - _.map( - _.toUserRow(u => - PageLink.container( - Page.Detail(Page.Titled(u.personalNumber)), - actionBus - ) - ) - ) - ) - ) - ), - $actionSignal --> actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala deleted file mode 100644 index 88e7450..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ /dev/null @@ -1,68 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii -import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria -import fiftyforms.ui.components.tailwind.Color - -extension (o: UserInfo) - def toDetailOsoby: DetailOsoby.ViewModel = - DetailOsoby.ViewModel( - o.personalNumber, - o.name, - o.email, - o.phone, - o.img, - o.mainFunction.map(f => - DetailOsoby.Funkce.ViewModel(f.name, f.dept, f.ou) - ), - o.userContracts.headOption.map(c => - DetailOsoby.PracovniPomer.ViewModel(c.rel, c.startDate, c.endDate) - ) - ) - -extension (param: Parameter) - def toParametr(container: Parameter => Anchor): DetailParametru.ViewModel = - DetailParametru.ViewModel( - id = param.id, - nazev = param.name, - popis = param.description, - status = "Nesplněno", - statusColor = Color.red, - a = container(param) - ) - -extension (crit: ParameterCriteria) - def toKriterium( - container: ParameterCriteria => Anchor - ): DetailKriteria.ViewModel = - DetailKriteria.ViewModel( - nazev = crit.criteriumText, - kapitola = crit.chapterId, - bod = crit.itemId, - status = "Nesplněno", - statusColor = Color.red, - splneno = false, - dukazy = Nil, - container = container(crit) - ) - -extension (user: UserInfo) - def toUserRow( - container: UserInfo => HtmlElement = (_: UserInfo) => div() - ): UserRow.ViewModel = - UserRow.ViewModel( - osobniCislo = user.personalNumber.toString, - celeJmeno = user.name, - prijmeni = user.surname, - hlavniFunkce = user.mainFunction.map(_.name), - img = user.img, - container = () => container(user) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala deleted file mode 100644 index 7d5b2c1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.dashboard - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.Page - -object DashboardPage: - - def render: HtmlElement = - div("Dashboard page") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala deleted file mode 100644 index 9ef0698..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala +++ /dev/null @@ -1,31 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailKriteriaPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply($m: Signal[ViewModel])( - action: Observer[DukazyKriteria.Action] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - DukazyKriteria($m.map(_.kriterium.dukazy))(action) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala deleted file mode 100644 index 3b316e1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object DetailPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametry: SeznamParametru.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby($m.map(_.osoba)), - SeznamParametru($m.map(_.parametry)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala deleted file mode 100644 index 17224cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala +++ /dev/null @@ -1,24 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailParametruPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriteria: SeznamKriterii.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby.header($m.map(_.osoba)), - DetailParametru($m.map(_.parametr)), - SeznamKriterii($m.map(_.kriteria)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala deleted file mode 100644 index dc912fa..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ /dev/null @@ -1,107 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb -package app -package pages.detail - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.UpravDukazForm -import fiftyforms.services.files.File - -object UpravDukaz: - - type ThisPage = Page.UpravDukazKriteria - type PageKey = (OsobniCislo, String, String) - - trait State { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def availableFiles: EventStream[List[File]] - def actionBus: Observer[Action] - } - - def keyOfPage(page: ThisPage): PageKey = - (page.osobniCislo.value, page.parametr.value, page.kriterium.value) - - def onChangeAction(key: PageKey): Action = - FetchParameterCriteria( - key._1, - key._2, - key._3, - Page.UpravDukazKriteria(_, _, _) - ) - - class Connector(state: State)($page: Signal[ThisPage])(using Router[Page]): - val $paramChangeSignal = $page.splitOne(keyOfPage)((x, _, _) => x) - val $pageChangeSignal = $paramChangeSignal.map(onChangeAction) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.split(_ => ())((_, s, $s) => - PageComponent( - $s.map(buildModel), - state.availableFiles, - state.actionBus.contramap { - case UpravDukazForm.Cancelled => - NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) - case UpravDukazForm.AvailableFilesRequested => - FetchAvailableFiles(s._1.personalNumber) - } - ) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): PageComponent.ViewModel = - import connectors.* - PageComponent.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) - - object PageComponent: - import components.* - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply( - $m: Signal[ViewModel], - availableFilesStream: EventStream[List[File]], - events: Observer[UpravDukazForm.Event] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - UpravDukazForm(availableFilesStream)(events) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala deleted file mode 100644 index ca311b0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.Icons -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Color - -object DetailKriteria: - case class ViewModel( - nazev: String, - kapitola: String, - bod: String, - status: String, - statusColor: Color, - splneno: Boolean, - dukazy: List[DukazKriteria.ViewModel], - container: HtmlElement = div() - ) { - val id = s"${kapitola}${bod}" - } - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h3( - cls := "text-xl leading-6 font-bold text-gray-900", - child.text <-- $m.map(_.nazev) - ), - h3( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - "Kapitola ", - child.text <-- $m.map(_.kapitola), - " bod ", - child <-- $m.map(_.bod) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala deleted file mode 100644 index ae90290..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala +++ /dev/null @@ -1,100 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Avatar - -object DetailOsoby: - - object Funkce: - case class ViewModel( - nazev: String, - stredisko: String, - voj: String - ) - - def render($m: Signal[ViewModel]): HtmlElement = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.nazev), - span( - cls := "hidden md:inline", - " @ ", - child.text <-- $m.map(_.stredisko), - ", ", - child.text <-- $m.map(_.voj) - ) - ) - - object PracovniPomer: - case class ViewModel( - druh: String, - pocatek: LocalDate, - konec: Option[LocalDate] - ) - - def render($m: Signal[ViewModel]): HtmlElement = - import fiftyforms.ui.components.tailwind.CustomAttrs.datetime - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.druh), - " od ", - time( - datetime <-- $m.map(_.pocatek.toString), - child.text <-- $m.map(_.pocatek.toString) - ) - ) - - case class ViewModel( - osobniCislo: OsobniCislo, - jmeno: String, - email: Option[String], - telefon: Option[String], - img: Option[String], - hlavniFunkce: Option[Funkce.ViewModel], - pracovniPomer: Option[PracovniPomer.ViewModel] - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-4", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(16) - ), - div( - h1( - cls := "text-2xl font-bold text-gray-900", - child.text <-- $m.map(_.jmeno) - ), - child.maybe <-- $m.map(_.hlavniFunkce).split(_ => ())((_, _, d) => - Funkce.render(d) - ), - child.maybe <-- $m.map(_.pracovniPomer).split(_ => ())((_, _, d) => - PracovniPomer.render(d) - ) - ) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-5", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(8) - ), - div( - h1( - cls := "text-lg font-medium text-gray-800", - child.text <-- $m.map(_.jmeno) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala deleted file mode 100644 index e11cb62..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color - -object DetailParametru: - case class ViewModel( - id: String, - nazev: String, - popis: String, - status: String, - statusColor: Color, - a: Anchor - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h2( - cls := "text-2xl leading-6 font-medium text-gray-900", - child.text <-- $m.map(_.nazev) - ), - p( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - child.text <-- $m.map(_.popis) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - h2( - cls := "text-lg leading-6 font-medium text-gray-600", - child.text <-- $m.map(_.nazev) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala deleted file mode 100644 index a00ede8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala +++ /dev/null @@ -1,177 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.CustomAttrs - -object DukazKriteria: - case class Osoba(osobniCislo: String, jmeno: String) - case class Udalost(osoba: Osoba, datum: LocalDate) - case class Dokument( - url: String, - nazev: String, - pridal: Udalost, - odebral: Option[Udalost] - ) - case class ViewModel( - dokumenty: List[Dokument], - autorizoval: Option[Udalost], - platiDo: Option[LocalDate], - poznámka: Option[String] - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-lg", - div( - cls := "px-4 py-5 sm:px-6", - h3( - cls := "text-lg leading-6 font-medium text-gray-900", - """Applicant Information""" - ), - p( - cls := "mt-1 max-w-2xl text-sm text-gray-500", - """Personal details and application.""" - ) - ), - div( - cls := "border-t border-gray-200 px-4 py-5 sm:px-6", - dl( - cls := "grid grid-cols-1 gap-x-4 gap-y-8 sm:grid-cols-2", - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Full name""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Margot Foster""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Application for""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Backend Developer""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Email address""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """margotfoster@example.com""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Salary expectation""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """$120,000""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """About""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Fugiat ipsum ipsum deserunt culpa aute sint do nostrud anim incididunt cillum culpa consequat. Excepteur qui ipsum aliquip consequat sint. Sit id mollit nulla mollit nostrud in ea officia proident. Irure nostrud pariatur mollit ad adipisicing reprehenderit deserunt qui eu.""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """Attachments""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - ul( - role := "list", - cls := "border border-gray-200 rounded-md divide-y divide-gray-200", - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """resume_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ), - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """coverletter_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala deleted file mode 100644 index 22f134f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala +++ /dev/null @@ -1,35 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object DukazyKriteria: - sealed trait Action - case object Add extends Action - - type ViewModel = List[DukazKriteria.ViewModel] - - def apply($m: Signal[ViewModel])(actions: Observer[Action]): HtmlElement = - div( - child.maybe <-- $m.map(m => - if (m.isEmpty) then Some(prazdnyDukaz(actions)) - else None - ), - children <-- $m.map(_.zipWithIndex).split(_._2)((_, _, $s) => - DukazKriteria($s.map(_._1)) - ) - ) - - private def prazdnyDukaz(actions: Observer[Action]): HtmlElement = - button( - tpe := "button", - onClick.preventDefault.mapTo(Add) --> actions, - cls := "relative block w-full border-2 border-gray-300 border-dashed rounded-lg p-12 text-center hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - Icons.outline - .`document-add`(12) - .amend(svg.cls := "mx-auto text-gray-400"), - span( - cls := "mt-2 block text-sm font-medium text-gray-900", - "Přidat důkaz" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala deleted file mode 100644 index f628697..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.list.StackedList -import fiftyforms.ui.components.tailwind.list.ListRow -import fiftyforms.ui.components.tailwind.list.RowTag -import fiftyforms.ui.components.tailwind.list.RowNext -import java.time.LocalDate - -object SeznamKriterii: - type ViewModel = List[DetailKriteria.ViewModel] - - private val kritList = new StackedList[DetailKriteria.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - kritList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = - p(cls := "text-sm text-gray-500", s"${i.kapitola}${i.bod}"), - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.container - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala deleted file mode 100644 index 2bfe4bc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.list.{ - StackedList, - ListRow, - RowTag, - PropList, - IconText, - RowNext -} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.LinkSupport.* - -object SeznamParametru: - type ViewModel = List[DetailParametru.ViewModel] - - private val parametrList = new StackedList[DetailParametru.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - parametrList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = emptyNode, - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.a - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala deleted file mode 100644 index f048e9d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ /dev/null @@ -1,101 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.form.* -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement -import fiftyforms.services.files.components.tailwind.FilePicker -import cz.e_bs.cmi.mdr.pdb.frontend.AutorizujDukaz -import cz.e_bs.cmi.mdr.pdb.frontend.DocumentRef -import fiftyforms.services.files.File - -object UpravDukazForm: - sealed trait Event - case object Cancelled extends Event - case object AvailableFilesRequested extends Event - def apply(availableFilesStream: EventStream[List[File]])( - updates: Observer[Event] - ): HtmlElement = - val (filesStream, filesObserver) = - EventStream.withObserver[FilePicker.Event] - val files = Var[List[File]](Nil) - def submitButtons: HtmlElement = - div( - cls := "pt-5", - div( - cls := "flex justify-end", - button( - tpe := "button", - cls := "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zrušit", - onClick.mapTo(Cancelled) --> updates - ), - button( - tpe := "submit", - cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - disabled <-- files.signal.map(_.isEmpty), - "Autorizovat" - ) - ) - ) - - div( - filesStream.collect { case FilePicker.SelectionUpdated(files) => - files.to(List) - } --> files.writer, - filesStream.collect { case FilePicker.AvailableFilesRequested => - AvailableFilesRequested - } --> updates, - cls := "bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6", - Form( - Form.Body( - Form.Section( - FormHeader( - FormHeader.ViewModel( - "Doložení kritéria", - "Sestavte doklady poskytující důkaz kritéria a potvrďte odesláním formuláře. Případné limitace či jiné relevantní údaje vepište do pole Komentář." - ) - ), - FormFields( - FormRow( - "dokumenty", - "Dokumenty", - FilePicker(files.signal, availableFilesStream)(filesObserver) - .amend(idAttr := "dokumenty", cls("max-w-lg")) - ).toHtml, - FormRow( - "platnost", - "Platnost", - input( - idAttr := "platnost", - name := "platnost", - tpe := "date", - autoComplete := "date", - cls := "block max-w-lg w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" - ) - ).toHtml, - FormRow( - "komentar", - "Komentář", - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - textArea( - idAttr := "komentar", - name := "about", - rows := 3, - cls := "max-w-lg shadow-sm block w-full focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border border-gray-300 rounded-md" - ), - p( - cls := "mt-2 text-sm text-gray-500", - "Doplňte prosím potřebné informace související s doložením kritéria, včetně případných limitací." - ) - ) - ).toHtml - ) - ) - ), - submitButtons - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala deleted file mode 100644 index 41033f6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -import components._ - -object DirectoryPage: - - type ViewModel = List[UserRow.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - val (actionsStream, actionObserver) = - EventStream.withObserver[SearchForm.Action] - val $filter = actionsStream - .collect { - case SearchForm.ClearFilter => None - case SearchForm.SetFilter(t) => Some(t) - } - .startWith(None) - val byLetter = for { - d <- $m - f <- $filter - } yield for { - (letter, users) <- d - .filter { user => - f match - case None => true - case Some(t) => user.search.contains(t) - } - .groupBy(_.prijmeni.head) - .to(List) - .sortBy(_._1) - } yield (letter.toString, users.sortBy(_.prijmeni)) - - div( - cls := "h-full max-w-7xl mx-auto order-first flex flex-col flex-shrink-0", - SearchForm(actionObserver), - Directory(byLetter) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala deleted file mode 100644 index 73f915d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -object Directory: - - object Header: - type ViewModel = String - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500", - h3(child.text <-- $m) - ) - - object UserList: - type ViewModel = List[UserRow.ViewModel] - def apply($m: Signal[ViewModel]): HtmlElement = - ul( - role := "list", - cls := "relative z-0 divide-y divide-gray-200", - children <-- $m.split(_.osobniCislo)((_, _, s) => UserRow(s)) - ) - - type ViewModel = List[(String, List[UserRow.ViewModel])] - def apply($m: Signal[ViewModel]): HtmlElement = - nav( - cls := "flex-1 min-h-0 overflow-y-auto", - aria.label := "Directory", - children <-- $m.split(_._1)((_, _, s) => - div( - cls := "relative", - Header(s.map(_._1)), - UserList(s.map(_._2)) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala deleted file mode 100644 index e182910..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala +++ /dev/null @@ -1,54 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object SearchForm: - sealed trait Action - case object SubmitFilter extends Action - sealed trait FilterAction extends Action - case object ClearFilter extends FilterAction - case class SetFilter(value: String) extends FilterAction - - def apply(actions: Observer[Action]): HtmlElement = - div( - cls := "px-6 pt-4 pb-4", - form( - cls := "flex space-x-4", - onSubmit.mapTo(SubmitFilter) --> actions, - div( - cls := "flex-1 min-w-0", - label( - forId := "search", - cls := "sr-only", - "Hledat" - ), - div( - cls := "relative rounded-md shadow-sm", - div( - cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", - Icons.solid.search().amend(svg.cls := "text-gray-400") - ), - input( - tpe := "search", - name := "search", - idAttr := "search", - cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", - placeholder := "Hledat", - composeEvents(onInput.mapToValue.setAsValue.map(SetFilter(_)))( - _.throttle(500) - ) --> actions - ) - ) - ), - button( - tpe := "submit", - cls := "inline-flex justify-center px-3.5 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500", - Icons.solid.filter().amend(svg.cls := "text-gray-400"), - span( - cls := "sr-only", - "Hledat" - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala deleted file mode 100644 index 82f7723..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala +++ /dev/null @@ -1,52 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.airstream.core.Signal -import fiftyforms.ui.components.tailwind.Avatar - -object UserRow: - case class ViewModel( - osobniCislo: String, - celeJmeno: String, - prijmeni: String, - hlavniFunkce: Option[String], - img: Option[String], - container: () => HtmlElement = () => div() - ) { - val search = prijmeni.toLowerCase - } - - def apply($m: Signal[ViewModel]): HtmlElement = - inline def avatarImage = - Avatar($m.map(_.img)).avatarImage(10) - - li( - div( - cls := "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500", - div( - cls := "flex-shrink-0", - child <-- avatarImage - ), - div( - cls := "flex-1 min-w-0", - child <-- $m.map { o => - o.container() - .amend( - cls := "focus:outline-none", - span( - cls := "absolute inset-0", - aria.hidden := true - ), - p( - cls := "text-sm font-medium text-gray-900", - o.celeJmeno - ), - p( - cls := "text-sm text-gray-500 truncate", - o.hlavniFunkce - ) - ) - } - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala deleted file mode 100644 index 467c5f5..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala +++ /dev/null @@ -1,99 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router - -object ErrorPage: - case class ViewModel( - homePage: Page, - errorName: String, - title: String, - subTitle: String - ) - - def apply(m: ViewModel, actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - val ViewModel(homePage, errorName, title, subTitle) = m - div( - cls := "min-h-full pt-16 pb-12 flex flex-col bg-white", - main( - cls := "flex-grow flex flex-col justify-center max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex-shrink-0 flex justify-center", - a( - href := "/", - cls := "inline-flex", - span( - cls := "sr-only", - """Workflow""" - ), - img( - cls := "h-12 w-auto", - src := "https://tailwindui.com/img/logos/workflow-mark.svg?color=indigo&shade=600", - alt := "" - ) - ) - ), - div( - cls := "py-16", - div( - cls := "text-center", - p( - cls := "text-sm font-semibold text-indigo-600 uppercase tracking-wide", - errorName - ), - h1( - cls := "mt-2 text-4xl font-extrabold text-gray-900 tracking-tight sm:text-5xl", - title - ), - p( - cls := "mt-2 text-base text-gray-500", - subTitle - ), - div( - cls := "mt-6", - PageLink - .container(homePage, actionBus) - .amend( - cls := "text-base font-medium text-indigo-600 hover:text-indigo-500", - """Go back home""" - ) - ) - ) - ) - ), - footer( - cls := "flex-shrink-0 max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", - nav( - cls := "flex justify-center space-x-4", - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Contact Support""" - ), - span( - cls := "inline-block border-l border-gray-300", - aria.hidden := true - ), - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Status""" - ), - span( - cls := "inline-block border-l border-gray-300", - aria.hidden := true - ), - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Twitter""" - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala deleted file mode 100644 index bd9baec..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action - -object NotFoundPage: - def apply(homePage: Page, url: String, actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - ErrorPage( - ErrorPage.ViewModel( - homePage, - "404 error", - "Page not found.", - s"Sorry, but page $url doesn't exist." - ), - actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala deleted file mode 100644 index 476fe3c..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action - -object UnhandledErrorPage: - - case class ViewModel( - homePage: Page, - errorName: Option[String], - errorMessage: Option[String] - ) - - def apply(m: ViewModel, actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - ErrorPage( - ErrorPage.ViewModel( - m.homePage, - "Unexpected error occurred", - m.errorName.getOrElse( - "Uh oh!" - ), // TODO: translations, better text than uh oh - m.errorMessage.getOrElse( - "This wasn't supposed to happen! Please try again." - ) - ), - actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala deleted file mode 100644 index 25f059f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala +++ /dev/null @@ -1,6 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.services - -import com.raquo.airstream.core.Observer - -trait DataFetcher[K, A]: - def fetch(id: K, sink: Observer[A]): Unit diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala deleted file mode 100644 index a469830..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala +++ /dev/null @@ -1,120 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package state - -import com.raquo.airstream.core.EventStream -import cz.e_bs.cmi.mdr.pdb.{UserInfo, OsobniCislo} -import com.raquo.airstream.core.Observer -import scala.scalajs.js -import scala.scalajs.js.JSON -import zio.json._ -import com.raquo.airstream.eventbus.EventBus -import com.raquo.airstream.ownership.Owner -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.UserFunction -import cz.e_bs.cmi.mdr.pdb.UserContract -import fiftyforms.services.files.File - -trait AppState - extends connectors.DetailPageConnector.AppState - with connectors.DetailParametruPageConnector.AppState - with connectors.DetailKriteriaPageConnector.AppState - with pages.detail.UpravDukaz.State: - def users: EventStream[List[UserInfo]] - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - -class MockAppState(implicit owner: Owner, router: Router[Page]) - extends AppState: - - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonDecoder[UserFunction] = DeriveJsonDecoder.gen - given JsonDecoder[UserContract] = DeriveJsonDecoder.gen - given JsonDecoder[UserInfo] = DeriveJsonDecoder.gen - - given JsonDecoder[ParameterCriteria] = DeriveJsonDecoder.gen - given JsonDecoder[Parameter] = DeriveJsonDecoder.gen - - private val actions = EventBus[Action]() - private val (parametersStream, pushParameters) = - EventStream.withCallback[List[Parameter]] - private val (usersStream, pushUsers) = - EventStream.withCallback[List[UserInfo]] - private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] - private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] - - private val mockData: List[UserInfo] = - mockUsers - .asInstanceOf[js.Dictionary[js.Object]] - .values - // TODO: is there a more efficient way to parse from JS object directly? - .map { o => - val parsed = JSON.stringify(o).fromJson[UserInfo] - parsed.left.foreach(org.scalajs.dom.console.log(_)) - parsed - } - .collect { case Right(u) => - u - } - .toList - - private val mockParameters: List[Parameter] = - pdbParams - .asInstanceOf[js.Dictionary[js.Object]] - .values - .map(o => JSON.stringify(o).fromJson[Parameter]) - .collect { case Right(p) => p } - .toList - - // TODO: Extract to separate event handler - actions.events.foreach { - case FetchDirectory => pushUsers(mockData) - case FetchUserDetails(osc) => - mockData.find(_.personalNumber == osc).foreach { o => - pushDetails(o) - router.replaceState(Page.Detail(o)) - } - case FetchParameters(osc) => - pushParameters(mockParameters) - case FetchParameter(osc, paramId) => - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(Page.DetailParametru(o, p)) - case FetchParameterCriteria(osc, paramId, critId, page) => - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - c <- p.criteria.find(_.id == critId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(page(o, p, c)) - case NavigateTo(page) => router.pushState(page) - case FetchAvailableFiles(osc) => - pushFiles( - List( - File("https://tc163.cmi.cz/here", "Example file") - ) - ) - } - - override def users: EventStream[List[UserInfo]] = - usersStream.debugWithName("users") - - override def details: EventStream[UserInfo] = - detailsStream.debugWithName("details") - - override def parameters: EventStream[List[Parameter]] = - parametersStream.debugWithName("parameters") - - override def availableFiles: EventStream[List[File]] = - filesStream.debugWithName("available files") - - override def actionBus: Observer[Action] = - actions.writer.debugWithName("actions writer") diff --git a/app/src/main/scala/mdr/pdb/app/Main.scala b/app/src/main/scala/mdr/pdb/app/Main.scala new file mode 100644 index 0000000..081b4ee --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/Main.scala @@ -0,0 +1,103 @@ +package mdr.pdb.app + +import scala.scalajs.js.annotation.JSExportTopLevel +import scala.scalajs.js.annotation.JSExport +import scala.scalajs.js.annotation.JSImport +import scala.scalajs.js +import org.scalajs.dom +import com.raquo.laminar.api.L.{*, given} +import scala.scalajs.js.Date +import com.raquo.waypoint.Router +import com.raquo.waypoint.SplitRender +import mdr.pdb.app.services.DataFetcher +import scala.scalajs.js.JSON +import zio.json._ +import mdr.pdb.UserInfo +import mdr.pdb.app.state.AppState + +@js.native +@JSImport("stylesheets/main.css", JSImport.Namespace) +object Css extends js.Any + +@js.native +@JSImport("data/users.json", JSImport.Default) +object mockUsers extends js.Object + +@js.native +@JSImport("params/pdb-params.json", JSImport.Default) +object pdbParams extends js.Object + +@JSExportTopLevel("app") +object Main: + + @JSExport + def main(args: Array[String]): Unit = + import Routes.given + onLoad { + setupAirstream() + val appContainer = dom.document.querySelector("#app") + val _ = + render( + appContainer, + renderPage(state.MockAppState(using unsafeWindowOwner, router)) + ) + } + + private def onLoad(f: => Unit): Unit = + documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) + + private def setupAirstream()(using router: Router[Page]): Unit = + AirstreamError.registerUnhandledErrorCallback(err => + router.forcePage( + Page.UnhandledError( + Some(err.getClass.getName), // TODO: Fill only in dev mode + Some(err.getMessage) + ) + ) + ) + + def renderPage(state: AppState)(using + router: Router[Page] + ): HtmlElement = + val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) + .collectSignal[Page.Detail]( + connectors + .DetailPageConnector(state)(_) + .apply + ) + .collectSignal[Page.DetailParametru]( + connectors + .DetailParametruPageConnector(state)(_) + .apply + ) + .collectSignal[Page.DetailKriteria]( + connectors + .DetailKriteriaPageConnector(state)(_) + .apply + ) + .collectSignal[Page.UpravDukazKriteria]( + pages.detail.UpravDukaz.Connector(state)(_).apply + ) + .collectStatic(Page.Dashboard)( + connectors.DashboardPageConnector(state.actionBus).apply + ) + .collect[Page.NotFound](pg => + pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) + ) + .collect[Page.UnhandledError](pg => + pages.errors + .UnhandledErrorPage( + pages.errors.UnhandledErrorPage + .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), + state.actionBus + ) + ) + .collectStatic(Page.Directory)( + connectors + .DirectoryPageConnector(state.users, state.actionBus) + .apply + ) + div(cls := "h-full", child <-- pageSplitter.$view) + + // Pull in the stylesheet + val css: Css.type = Css diff --git a/app/src/main/scala/mdr/pdb/app/Routes.scala b/app/src/main/scala/mdr/pdb/app/Routes.scala new file mode 100644 index 0000000..2cc860c --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/Routes.scala @@ -0,0 +1,198 @@ +package mdr.pdb.app + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.* +import org.scalajs.dom +import zio.json.{*, given} +import mdr.pdb.OsobniCislo + +import scala.scalajs.js +import mdr.pdb.UserInfo +import mdr.pdb.Parameter +import mdr.pdb.ParameterCriteria +import mdr.pdb.app.Page.Titled + +// enum is not working with Waypoints' SplitRender collectStatic +sealed abstract class Page( + val id: String, + val title: String, + val parent: Option[Page] +) { + val path: Vector[Page] = + parent match + case None => Vector(this) + case Some(p) => p.path :+ this + + val isRoot: Boolean = parent.isEmpty +} + +object Page: + + case class Titled[V](value: V, title: Option[String] = None): + val show: String = title.getOrElse(value.toString) + + case object Directory extends Page("directory", "Adresář", None) + + case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) + + case class Detail(osobniCislo: Titled[OsobniCislo]) + extends Page("user", osobniCislo.show, Some(Directory)) + + object Detail { + def apply(o: UserInfo): Detail = Detail( + Titled(o.personalNumber, Some(o.name)) + ) + } + + case class DetailParametru( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String] + ) extends Page( + "parameter", + parametr.show, + Some(Detail(osobniCislo)) + ) + + object DetailParametru { + def apply(o: UserInfo, p: Parameter): DetailParametru = + DetailParametru( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)) + ) + } + + case class DetailKriteria( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String], + kriterium: Titled[String] + ) extends Page( + "criteria", + kriterium.show, + Some(DetailParametru(osobniCislo, parametr)) + ) + + object DetailKriteria { + def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = + DetailKriteria( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)), + Titled(k.id, Some(k.id)) + ) + } + + case class UpravDukazKriteria( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String], + kriterium: Titled[String] + ) extends Page( + "addProof", + "Důkaz", + Some(DetailKriteria(osobniCislo, parametr, kriterium)) + ) + + object UpravDukazKriteria { + def apply( + o: UserInfo, + p: Parameter, + k: ParameterCriteria + ): UpravDukazKriteria = + UpravDukazKriteria( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)), + Titled(k.id, Some(k.id)) + ) + } + + case class NotFound(url: String) extends Page("404", "404", Some(Directory)) + + case class UnhandledError( + errorName: Option[String], + errorMessage: Option[String] + ) extends Page("500", "Unexpected error", Some(Directory)) + +object Routes: + given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) + given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) + given [V: JsonEncoder]: JsonEncoder[Titled[V]] = + DeriveJsonEncoder.gen[Titled[V]] + given [V: JsonDecoder]: JsonDecoder[Titled[V]] = + DeriveJsonDecoder.gen[Titled[V]] + given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] + given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] + + val base = + js.`import`.meta.env.BASE_URL + .asInstanceOf[String] + "app" + + val homePage: Page = Page.Directory + + given router: Router[Page] = Router[Page]( + routes = List( + Route.static(homePage, root / endOfSegments, basePath = base), + Route.static( + Page.Dashboard, + root / "dashboard" / endOfSegments, + basePath = base + ), + Route[Page.Detail, String]( + encode = _.osobniCislo.value.toString, + decode = osc => Page.Detail(Titled(OsobniCislo(osc))), + root / "osoba" / segment[String] / endOfSegments, + basePath = base + ), + Route[Page.DetailParametru, (String, String)]( + encode = p => (p.osobniCislo.value.toString, p.parametr.value), + decode = + p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / endOfSegments, + basePath = base + ), + Route[Page.DetailKriteria, (String, String, String)]( + encode = p => + ( + p.osobniCislo.value.toString, + p.parametr.value, + p.kriterium.value.replaceAll("\\.", "--") + ), + decode = p => + Page.DetailKriteria( + Titled(OsobniCislo(p._1)), + Titled(p._2), + Titled(p._3.replaceAll("--", ".")) + ), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / "kriterium" / segment[String] / endOfSegments, + basePath = base + ), + Route[Page.UpravDukazKriteria, (String, String, String)]( + encode = p => + ( + p.osobniCislo.value.toString, + p.parametr.value, + p.kriterium.value.replaceAll("\\.", "--") + ), + decode = p => + Page.UpravDukazKriteria( + Titled(OsobniCislo(p._1)), + Titled(p._2), + Titled(p._3.replaceAll("--", ".")) + ), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / "kriterium" / segment[String] / "edit" / endOfSegments, + basePath = base + ) + ), + serializePage = _.toJson, + deserializePage = _.fromJson[Page] + .fold(s => throw IllegalStateException(s), identity), + getPageTitle = _.title, + routeFallback = url => Page.NotFound(url), + deserializeFallback = _ => Page.Dashboard + )( + $popStateEvent = windowEvents.onPopState, + owner = unsafeWindowOwner + ) diff --git a/app/src/main/scala/mdr/pdb/app/actions.scala b/app/src/main/scala/mdr/pdb/app/actions.scala new file mode 100644 index 0000000..d078b3a --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/actions.scala @@ -0,0 +1,21 @@ +package mdr.pdb.app + +import mdr.pdb.OsobniCislo +import mdr.pdb.UserInfo +import mdr.pdb.Parameter +import mdr.pdb.ParameterCriteria + +sealed trait Action + +case object FetchDirectory extends Action +case class FetchUserDetails(osc: OsobniCislo) extends Action +case class FetchParameters(osc: OsobniCislo) extends Action +case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action +case class FetchParameterCriteria( + osc: OsobniCislo, + paramId: String, + critId: String, + page: (UserInfo, Parameter, ParameterCriteria) => Page +) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action +case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/mdr/pdb/app/components/AppPage.scala new file mode 100644 index 0000000..130c817 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/AppPage.scala @@ -0,0 +1,82 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.Page +import mdr.pdb.{UserProfile, UserInfo, OsobniCislo} +import com.raquo.waypoint.Router +import mdr.pdb.app.Action +import mdr.pdb.app.NavigateTo +import mdr.pdb.UserFunction + +object AppPage: + // TODO: pages by logged in user + val pages: List[Page] = List(Page.Directory, Page.Dashboard) + + import NavigationBar.{Logo, MenuItem} + + val logo = Logo( + "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", + "Workflow" + ) + + // TODO: menu items by user profile + val userMenu = + List( + MenuItem("Your Profile"), + MenuItem("Settings"), + MenuItem("Sign out") + ) + + // TODO: load user profile + val $userProfile = Var( + UserProfile( + "tom", + UserInfo( + OsobniCislo("1031"), + "tom", + "Tom", + "Cook", + None, + None, + Some("tom@example.com"), + Some("+420 222 866 180"), + Some( + UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") + ), + Nil, + None + ) + ) + ) + + val $userInfo = $userProfile.signal.map(_.userInfo) + + type ViewModel = Option[HtmlElement] + def apply( + actionBus: Observer[Action] + )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using + router: Router[Page] + ): HtmlElement = + PageLayout(actionBus)( + $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => + PageLayout.ViewModel( + NavigationBar.ViewModel( + u, + pages.map(p => + NavigationBar.Link( + () => + PageLink( + p, + actionBus + ), + p == cp + ) + ), + userMenu, + logo + ), + c + ) + ), + mods + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala new file mode 100644 index 0000000..9375398 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala @@ -0,0 +1,143 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.tailwind.list.IconText.ViewModel +import fiftyforms.ui.components.tailwind.Icons +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.Page + +import com.raquo.laminar.api.L.{*, given} +import io.laminext.syntax.core.{*, given} +import mdr.pdb.app.Action + +object Breadcrumbs: + + private def slash = + import svg.{*, given} + svg( + cls := "flex-shrink-0 h-5 w-5 text-gray-300", + xmlns := "http://www.w3.org/2000/svg", + fill := "currentColor", + viewBox := "0 0 20 20", + ariaHidden := true, + path( + d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" + ) + ) + + def backIcon = + Icons.solid + .`arrow-narrow-left`() + .amend( + svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" + ) + + object Link: + case class ViewModel( + page: Page, + icon: Option[SvgElement], + extraClasses: String, + text: String, + textClasses: Option[String] = None + ) + + val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" + + def shortHome(p: Page) = ViewModel( + p, + Some(backIcon), + "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", + "Zpět na úvodní stránku", + None + ) + + def fullHome(p: Page) = ViewModel( + p, + Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), + "text-gray-400 hover:text-gray-500", + "Domů", + Some("sr-only") + ) + + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + PageLink + .container($m.map(_.page), actionBus) + .amend( + cls <-- $m.map(_.extraClasses), + child.maybe <-- $m.map(_.icon), + span( + cls <-- $m.map(_.textClasses.getOrElse("")), + child.text <-- $m.map(_.text) + ) + ) + + object FullBreadcrumbs: + type ViewModel = Page + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + ol( + role := "list", + cls := "flex items-center space-x-4", + children <-- $m.map(_.path).split(_.id)((_, _, $p) => + li( + div( + cls := "flex items-center", + child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), + Link( + $p.map(p => + if (p.isRoot) then Link.fullHome(p) + else + Link.ViewModel( + p, + None, + s"ml-4 max-w-xs truncate ${Link.baseClasses}", + p.title + ) + ), + actionBus + ) + ) + ) + ) + ) + + object ShortBreadcrumbs: + type ViewModel = Page + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + Link( + $m.map { p => + val target = p.parent.getOrElse(p) + if target.isRoot then Link.shortHome(target) + else + Link.ViewModel( + target, + Some(backIcon), + "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", + s"Zpět na ${target.title}" + ) + }, + actionBus + ) + + def apply(actionBus: Observer[Action])(using + router: Router[Page] + ): HtmlElement = + nav( + cls := "flex", + aria.label := "Breadcrumb", + div( + cls := "flex sm:hidden", + ShortBreadcrumbs(router.$currentPage, actionBus) + ), + div( + cls := "hidden sm:block", + FullBreadcrumbs(router.$currentPage, actionBus) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala new file mode 100644 index 0000000..4bb8b8d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala @@ -0,0 +1,242 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent +import fiftyforms.ui.components.tailwind.Icons +import fiftyforms.ui.components.tailwind.Avatar +import mdr.pdb.UserInfo +import io.laminext.syntax.core.* + +object NavigationBar: + + case class Logo(img: String, name: String) + case class Link(a: () => Anchor, active: Boolean) + case class MenuItem(title: String) + + case class ViewModel( + userInfo: UserInfo, + pages: List[Link], + userMenu: List[MenuItem], + logo: Logo + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + val $userInfo = $m.map(_.userInfo) + val mobileMenuOpen = Var(false) + + val desktopOnly = cls("hidden md:block") + val mobileOnly = cls("md:hidden") + + inline def avatarImage(size: Int = 8) = + Avatar($userInfo.map(_.img)).avatarImage(size) + + def notificationButton = button( + tpe := "button", + cls := List( + "bg-indigo-600", + "focus:outline-none", + "focus:ring-2", + "focus:ring-offset-2", + "focus:ring-offset-indigo-600", + "focus:ring-white", + "hover:text-white", + "p-1", + "rounded-full", + "text-indigo-200" + ), + span(cls := "sr-only", "View notifications"), + Icons.outline.bell() + ) + + def userProfile: HtmlElement = + val menuOpen = Var(false) + + def menuItem(item: MenuItem, idx: Int): HtmlElement = + a( + href := "#", + cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", + role := "menuitem", + tabIndex := -1, + idAttr := s"user-menu-item-$idx", + item.title + ) + + div( + cls := "ml-3 relative", + div( + button( + tpe := "button", + cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", + idAttr := "user-menu-button", + aria.expanded <-- menuOpen.signal, + aria.hasPopup := true, + span(cls := "sr-only", "Open user menu"), + child <-- avatarImage(), + onClick.preventDefault.mapTo( + !menuOpen.now() + ) --> menuOpen.writer + ) + ), + /* + * */ + div( + cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", + cls <-- menuOpen.signal.map { o => + if (o) "md:block" else "md:hidden" + }, + role := "menu", + aria.orientation := "vertical", + aria.labelledBy := "user-menu-button", + tabIndex := -1, + // : keyboard navigation + children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) + ) + ) + + def mobileProfile = + def menuItem(item: MenuItem): HtmlElement = + a( + href := "#", + cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", + item.title + ) + + div( + cls := "pt-4 pb-3 border-t border-indigo-700", + div( + cls := "flex items-center px-5", + div( + cls := "flex-shrink-0", + child <-- avatarImage(10) + ), + div( + cls := "ml-3", + div( + cls := "text-base font-medium text-white", + child.text <-- $userInfo.map(_.name) + ), + child.maybe <-- $userInfo.map( + _.email.map(e => + div( + cls := "text-sm font-medium text-indigo-300", + e + ) + ) + ) + ), + notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) + ), + div( + cls := "mt-3 px-2 space-y-1", + children <-- $m.map(_.userMenu.map(menuItem)) + ) + ) + + def pageLink(page: Link): Anchor = + page + .a() + .amend( + cls := "text-white px-3 py-2 rounded-md text-sm font-medium", + cls := Seq( + "bg-indigo-700" -> page.active, + "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active + ), + ariaCurrent := (if page.active then "page" else "false") + ) + + def logoImg: Image = + img( + cls := "h-8 w-8", + src <-- $m.map(_.logo.img), + alt <-- $m.map(_.logo.name) + ) + + def mobileMenuButton = button( + tpe := "button", + cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", + aria.controls := "mobile-menu", + aria.expanded <-- mobileMenuOpen.signal, + span(cls := "sr-only", "Open main menu"), + Icons.outline + .menu() + .amend( + svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") + ), + Icons.outline + .x() + .amend( + svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") + ), + onClick.preventDefault.mapTo( + !mobileMenuOpen.now() + ) --> mobileMenuOpen.writer + ) + + def navBarLeft = + div( + cls := "flex items-center", + div(cls := "flex-shrink-0", logoImg), + div( + desktopOnly, + div( + cls := "ml-10 flex items-baseline space-x-4", + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) + ) + ) + ) + + def navBarRight = + div( + desktopOnly, + div( + cls := "ml-4 flex items-center md:ml-6", + notificationButton, + userProfile + ) + ) + + def navBarMobile = + div( + cls := "-mr-2 flex", + mobileOnly, + mobileMenuButton + ) + + def navBar = + div( + cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", + div( + cls := "flex items-center justify-between h-16", + navBarLeft, + navBarRight, + navBarMobile + ) + ) + + def mobileMenu = + div( + mobileOnly, + cls <-- mobileMenuOpen.signal.map { o => + if (o) "block" else "hidden" + }, + idAttr := "mobile-menu", + div( + cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) + ), + mobileProfile + ) + + nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala new file mode 100644 index 0000000..c4cb10d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala @@ -0,0 +1,19 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.Page +import com.raquo.waypoint.Router +import mdr.pdb.app.Action + +object PageHeader: + def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = + header( + cls := "bg-white shadow-sm", + div( + cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", + h1( + cls := "text-lg leading-6 font-semibold text-gray-900", + Breadcrumbs(actionBus) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala new file mode 100644 index 0000000..aae4539 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala @@ -0,0 +1,30 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.Page +import mdr.pdb.app.Action +import fiftyforms.ui.components.tailwind.Loading +import io.laminext.syntax.core.* + +object PageLayout: + case class ViewModel( + navigation: NavigationBar.ViewModel, + content: Option[HtmlElement] + ) + def apply(actionBus: Observer[Action])( + $m: Signal[ViewModel], + mods: Modifier[HtmlElement]* + )(using router: Router[Page]): HtmlElement = + val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) + div( + cls := "h-full flex flex-col", + NavigationBar($m.map(_.navigation)), + child.maybe <-- router.$currentPage.map(_.isRoot) + .switch(None, Some(PageHeader(actionBus))), + main( + cls := "flex-grow-1 overflow-y-auto", + mods, + child <-- $maybeContent.map(_.getOrElse(Loading)) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/mdr/pdb/app/components/PageLink.scala new file mode 100644 index 0000000..e6e7052 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageLink.scala @@ -0,0 +1,40 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.LinkSupport.* +import mdr.pdb.app.Page +import mdr.pdb.app.Action +import com.raquo.waypoint.Router +import mdr.pdb.app.NavigateTo + +object PageLink: + type ViewModel = Page + def apply($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = + a(mods($m, actions), child.text <-- $m.map(_.title)) + + def apply(m: ViewModel, actions: Observer[Action])(using + router: Router[Page] + ): Anchor = apply(Val(m), actions) + + def container($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = a(mods($m, actions)) + + def container(m: ViewModel, actions: Observer[Action])(using + Router[Page] + ): Anchor = container(Val(m), actions) + + private def mods($m: Signal[Page], actions: Observer[Action])(using + router: Router[Page] + ): Modifier[Anchor] = + Seq( + href <-- $m.map(router.absoluteUrlForPage).recover { case _ => + Some("invalid url") + }, + composeEvents(onClick.noKeyMod.preventDefault)( + _.sample($m) + .map(NavigateTo.apply) + ) --> actions + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala deleted file mode 100644 index a3b00af..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala +++ /dev/null @@ -1,103 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import scala.scalajs.js.annotation.JSExportTopLevel -import scala.scalajs.js.annotation.JSExport -import scala.scalajs.js.annotation.JSImport -import scala.scalajs.js -import org.scalajs.dom -import com.raquo.laminar.api.L.{*, given} -import scala.scalajs.js.Date -import com.raquo.waypoint.Router -import com.raquo.waypoint.SplitRender -import cz.e_bs.cmi.mdr.pdb.app.services.DataFetcher -import scala.scalajs.js.JSON -import zio.json._ -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.state.AppState - -@js.native -@JSImport("stylesheets/main.css", JSImport.Namespace) -object Css extends js.Any - -@js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native -@JSImport("params/pdb-params.json", JSImport.Default) -object pdbParams extends js.Object - -@JSExportTopLevel("app") -object Main: - - @JSExport - def main(args: Array[String]): Unit = - import Routes.given - onLoad { - setupAirstream() - val appContainer = dom.document.querySelector("#app") - val _ = - render( - appContainer, - renderPage(state.MockAppState(using unsafeWindowOwner, router)) - ) - } - - private def onLoad(f: => Unit): Unit = - documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) - - private def setupAirstream()(using router: Router[Page]): Unit = - AirstreamError.registerUnhandledErrorCallback(err => - router.forcePage( - Page.UnhandledError( - Some(err.getClass.getName), // TODO: Fill only in dev mode - Some(err.getMessage) - ) - ) - ) - - def renderPage(state: AppState)(using - router: Router[Page] - ): HtmlElement = - val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) - .collectSignal[Page.Detail]( - connectors - .DetailPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailParametru]( - connectors - .DetailParametruPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailKriteria]( - connectors - .DetailKriteriaPageConnector(state)(_) - .apply - ) - .collectSignal[Page.UpravDukazKriteria]( - pages.detail.UpravDukaz.Connector(state)(_).apply - ) - .collectStatic(Page.Dashboard)( - connectors.DashboardPageConnector(state.actionBus).apply - ) - .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) - ) - .collect[Page.UnhandledError](pg => - pages.errors - .UnhandledErrorPage( - pages.errors.UnhandledErrorPage - .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), - state.actionBus - ) - ) - .collectStatic(Page.Directory)( - connectors - .DirectoryPageConnector(state.users, state.actionBus) - .apply - ) - div(cls := "h-full", child <-- pageSplitter.$view) - - // Pull in the stylesheet - val css: Css.type = Css diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala deleted file mode 100644 index 3bbc8c0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ /dev/null @@ -1,198 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.* -import org.scalajs.dom -import zio.json.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import scala.scalajs.js -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.Page.Titled - -// enum is not working with Waypoints' SplitRender collectStatic -sealed abstract class Page( - val id: String, - val title: String, - val parent: Option[Page] -) { - val path: Vector[Page] = - parent match - case None => Vector(this) - case Some(p) => p.path :+ this - - val isRoot: Boolean = parent.isEmpty -} - -object Page: - - case class Titled[V](value: V, title: Option[String] = None): - val show: String = title.getOrElse(value.toString) - - case object Directory extends Page("directory", "Adresář", None) - - case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) - - case class Detail(osobniCislo: Titled[OsobniCislo]) - extends Page("user", osobniCislo.show, Some(Directory)) - - object Detail { - def apply(o: UserInfo): Detail = Detail( - Titled(o.personalNumber, Some(o.name)) - ) - } - - case class DetailParametru( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String] - ) extends Page( - "parameter", - parametr.show, - Some(Detail(osobniCislo)) - ) - - object DetailParametru { - def apply(o: UserInfo, p: Parameter): DetailParametru = - DetailParametru( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)) - ) - } - - case class DetailKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "criteria", - kriterium.show, - Some(DetailParametru(osobniCislo, parametr)) - ) - - object DetailKriteria { - def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = - DetailKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class UpravDukazKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "addProof", - "Důkaz", - Some(DetailKriteria(osobniCislo, parametr, kriterium)) - ) - - object UpravDukazKriteria { - def apply( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): UpravDukazKriteria = - UpravDukazKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class NotFound(url: String) extends Page("404", "404", Some(Directory)) - - case class UnhandledError( - errorName: Option[String], - errorMessage: Option[String] - ) extends Page("500", "Unexpected error", Some(Directory)) - -object Routes: - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) - given [V: JsonEncoder]: JsonEncoder[Titled[V]] = - DeriveJsonEncoder.gen[Titled[V]] - given [V: JsonDecoder]: JsonDecoder[Titled[V]] = - DeriveJsonDecoder.gen[Titled[V]] - given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] - given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] - - val base = - js.`import`.meta.env.BASE_URL - .asInstanceOf[String] + "app" - - val homePage: Page = Page.Directory - - given router: Router[Page] = Router[Page]( - routes = List( - Route.static(homePage, root / endOfSegments, basePath = base), - Route.static( - Page.Dashboard, - root / "dashboard" / endOfSegments, - basePath = base - ), - Route[Page.Detail, String]( - encode = _.osobniCislo.value.toString, - decode = osc => Page.Detail(Titled(OsobniCislo(osc))), - root / "osoba" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.DetailParametru, (String, String)]( - encode = p => (p.osobniCislo.value.toString, p.parametr.value), - decode = - p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / endOfSegments, - basePath = base - ), - Route[Page.DetailKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.DetailKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.UpravDukazKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.UpravDukazKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / "edit" / endOfSegments, - basePath = base - ) - ), - serializePage = _.toJson, - deserializePage = _.fromJson[Page] - .fold(s => throw IllegalStateException(s), identity), - getPageTitle = _.title, - routeFallback = url => Page.NotFound(url), - deserializeFallback = _ => Page.Dashboard - )( - $popStateEvent = windowEvents.onPopState, - owner = unsafeWindowOwner - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala deleted file mode 100644 index b24b82b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -sealed trait Action - -case object FetchDirectory extends Action -case class FetchUserDetails(osc: OsobniCislo) extends Action -case class FetchParameters(osc: OsobniCislo) extends Action -case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action -case class FetchParameterCriteria( - osc: OsobniCislo, - paramId: String, - critId: String, - page: (UserInfo, Parameter, ParameterCriteria) => Page -) extends Action -case class FetchAvailableFiles(osc: OsobniCislo) extends Action -case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala deleted file mode 100644 index c637d35..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala +++ /dev/null @@ -1,82 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.{UserProfile, UserInfo, OsobniCislo} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo -import cz.e_bs.cmi.mdr.pdb.UserFunction - -object AppPage: - // TODO: pages by logged in user - val pages: List[Page] = List(Page.Directory, Page.Dashboard) - - import NavigationBar.{Logo, MenuItem} - - val logo = Logo( - "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", - "Workflow" - ) - - // TODO: menu items by user profile - val userMenu = - List( - MenuItem("Your Profile"), - MenuItem("Settings"), - MenuItem("Sign out") - ) - - // TODO: load user profile - val $userProfile = Var( - UserProfile( - "tom", - UserInfo( - OsobniCislo("1031"), - "tom", - "Tom", - "Cook", - None, - None, - Some("tom@example.com"), - Some("+420 222 866 180"), - Some( - UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") - ), - Nil, - None - ) - ) - ) - - val $userInfo = $userProfile.signal.map(_.userInfo) - - type ViewModel = Option[HtmlElement] - def apply( - actionBus: Observer[Action] - )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using - router: Router[Page] - ): HtmlElement = - PageLayout(actionBus)( - $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => - PageLayout.ViewModel( - NavigationBar.ViewModel( - u, - pages.map(p => - NavigationBar.Link( - () => - PageLink( - p, - actionBus - ), - p == cp - ) - ), - userMenu, - logo - ), - c - ) - ), - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala deleted file mode 100644 index bf473c6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ /dev/null @@ -1,143 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden -import fiftyforms.ui.components.tailwind.list.IconText.ViewModel -import fiftyforms.ui.components.tailwind.Icons -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Action - -object Breadcrumbs: - - private def slash = - import svg.{*, given} - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-300", - xmlns := "http://www.w3.org/2000/svg", - fill := "currentColor", - viewBox := "0 0 20 20", - ariaHidden := true, - path( - d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" - ) - ) - - def backIcon = - Icons.solid - .`arrow-narrow-left`() - .amend( - svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" - ) - - object Link: - case class ViewModel( - page: Page, - icon: Option[SvgElement], - extraClasses: String, - text: String, - textClasses: Option[String] = None - ) - - val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" - - def shortHome(p: Page) = ViewModel( - p, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - "Zpět na úvodní stránku", - None - ) - - def fullHome(p: Page) = ViewModel( - p, - Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), - "text-gray-400 hover:text-gray-500", - "Domů", - Some("sr-only") - ) - - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - PageLink - .container($m.map(_.page), actionBus) - .amend( - cls <-- $m.map(_.extraClasses), - child.maybe <-- $m.map(_.icon), - span( - cls <-- $m.map(_.textClasses.getOrElse("")), - child.text <-- $m.map(_.text) - ) - ) - - object FullBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - ol( - role := "list", - cls := "flex items-center space-x-4", - children <-- $m.map(_.path).split(_.id)((_, _, $p) => - li( - div( - cls := "flex items-center", - child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), - Link( - $p.map(p => - if (p.isRoot) then Link.fullHome(p) - else - Link.ViewModel( - p, - None, - s"ml-4 max-w-xs truncate ${Link.baseClasses}", - p.title - ) - ), - actionBus - ) - ) - ) - ) - ) - - object ShortBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - Link( - $m.map { p => - val target = p.parent.getOrElse(p) - if target.isRoot then Link.shortHome(target) - else - Link.ViewModel( - target, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - s"Zpět na ${target.title}" - ) - }, - actionBus - ) - - def apply(actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - nav( - cls := "flex", - aria.label := "Breadcrumb", - div( - cls := "flex sm:hidden", - ShortBreadcrumbs(router.$currentPage, actionBus) - ), - div( - cls := "hidden sm:block", - FullBreadcrumbs(router.$currentPage, actionBus) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala deleted file mode 100644 index 0db2047..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ /dev/null @@ -1,242 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent -import fiftyforms.ui.components.tailwind.Icons -import fiftyforms.ui.components.tailwind.Avatar -import cz.e_bs.cmi.mdr.pdb.UserInfo -import io.laminext.syntax.core.* - -object NavigationBar: - - case class Logo(img: String, name: String) - case class Link(a: () => Anchor, active: Boolean) - case class MenuItem(title: String) - - case class ViewModel( - userInfo: UserInfo, - pages: List[Link], - userMenu: List[MenuItem], - logo: Logo - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - val $userInfo = $m.map(_.userInfo) - val mobileMenuOpen = Var(false) - - val desktopOnly = cls("hidden md:block") - val mobileOnly = cls("md:hidden") - - inline def avatarImage(size: Int = 8) = - Avatar($userInfo.map(_.img)).avatarImage(size) - - def notificationButton = button( - tpe := "button", - cls := List( - "bg-indigo-600", - "focus:outline-none", - "focus:ring-2", - "focus:ring-offset-2", - "focus:ring-offset-indigo-600", - "focus:ring-white", - "hover:text-white", - "p-1", - "rounded-full", - "text-indigo-200" - ), - span(cls := "sr-only", "View notifications"), - Icons.outline.bell() - ) - - def userProfile: HtmlElement = - val menuOpen = Var(false) - - def menuItem(item: MenuItem, idx: Int): HtmlElement = - a( - href := "#", - cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", - role := "menuitem", - tabIndex := -1, - idAttr := s"user-menu-item-$idx", - item.title - ) - - div( - cls := "ml-3 relative", - div( - button( - tpe := "button", - cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - idAttr := "user-menu-button", - aria.expanded <-- menuOpen.signal, - aria.hasPopup := true, - span(cls := "sr-only", "Open user menu"), - child <-- avatarImage(), - onClick.preventDefault.mapTo( - !menuOpen.now() - ) --> menuOpen.writer - ) - ), - /* - * */ - div( - cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", - cls <-- menuOpen.signal.map { o => - if (o) "md:block" else "md:hidden" - }, - role := "menu", - aria.orientation := "vertical", - aria.labelledBy := "user-menu-button", - tabIndex := -1, - // : keyboard navigation - children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) - ) - ) - - def mobileProfile = - def menuItem(item: MenuItem): HtmlElement = - a( - href := "#", - cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", - item.title - ) - - div( - cls := "pt-4 pb-3 border-t border-indigo-700", - div( - cls := "flex items-center px-5", - div( - cls := "flex-shrink-0", - child <-- avatarImage(10) - ), - div( - cls := "ml-3", - div( - cls := "text-base font-medium text-white", - child.text <-- $userInfo.map(_.name) - ), - child.maybe <-- $userInfo.map( - _.email.map(e => - div( - cls := "text-sm font-medium text-indigo-300", - e - ) - ) - ) - ), - notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) - ), - div( - cls := "mt-3 px-2 space-y-1", - children <-- $m.map(_.userMenu.map(menuItem)) - ) - ) - - def pageLink(page: Link): Anchor = - page - .a() - .amend( - cls := "text-white px-3 py-2 rounded-md text-sm font-medium", - cls := Seq( - "bg-indigo-700" -> page.active, - "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active - ), - ariaCurrent := (if page.active then "page" else "false") - ) - - def logoImg: Image = - img( - cls := "h-8 w-8", - src <-- $m.map(_.logo.img), - alt <-- $m.map(_.logo.name) - ) - - def mobileMenuButton = button( - tpe := "button", - cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - aria.controls := "mobile-menu", - aria.expanded <-- mobileMenuOpen.signal, - span(cls := "sr-only", "Open main menu"), - Icons.outline - .menu() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") - ), - Icons.outline - .x() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") - ), - onClick.preventDefault.mapTo( - !mobileMenuOpen.now() - ) --> mobileMenuOpen.writer - ) - - def navBarLeft = - div( - cls := "flex items-center", - div(cls := "flex-shrink-0", logoImg), - div( - desktopOnly, - div( - cls := "ml-10 flex items-baseline space-x-4", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ) - ) - ) - - def navBarRight = - div( - desktopOnly, - div( - cls := "ml-4 flex items-center md:ml-6", - notificationButton, - userProfile - ) - ) - - def navBarMobile = - div( - cls := "-mr-2 flex", - mobileOnly, - mobileMenuButton - ) - - def navBar = - div( - cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex items-center justify-between h-16", - navBarLeft, - navBarRight, - navBarMobile - ) - ) - - def mobileMenu = - div( - mobileOnly, - cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }, - idAttr := "mobile-menu", - div( - cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ), - mobileProfile - ) - - nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala deleted file mode 100644 index 75339df..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object PageHeader: - def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = - header( - cls := "bg-white shadow-sm", - div( - cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", - h1( - cls := "text-lg leading-6 font-semibold text-gray-900", - Breadcrumbs(actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala deleted file mode 100644 index 5096463..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ /dev/null @@ -1,30 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import fiftyforms.ui.components.tailwind.Loading -import io.laminext.syntax.core.* - -object PageLayout: - case class ViewModel( - navigation: NavigationBar.ViewModel, - content: Option[HtmlElement] - ) - def apply(actionBus: Observer[Action])( - $m: Signal[ViewModel], - mods: Modifier[HtmlElement]* - )(using router: Router[Page]): HtmlElement = - val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) - div( - cls := "h-full flex flex-col", - NavigationBar($m.map(_.navigation)), - child.maybe <-- router.$currentPage.map(_.isRoot) - .switch(None, Some(PageHeader(actionBus))), - main( - cls := "flex-grow-1 overflow-y-auto", - mods, - child <-- $maybeContent.map(_.getOrElse(Loading)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala deleted file mode 100644 index 62d820f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.LinkSupport.* -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo - -object PageLink: - type ViewModel = Page - def apply($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = - a(mods($m, actions), child.text <-- $m.map(_.title)) - - def apply(m: ViewModel, actions: Observer[Action])(using - router: Router[Page] - ): Anchor = apply(Val(m), actions) - - def container($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = a(mods($m, actions)) - - def container(m: ViewModel, actions: Observer[Action])(using - Router[Page] - ): Anchor = container(Val(m), actions) - - private def mods($m: Signal[Page], actions: Observer[Action])(using - router: Router[Page] - ): Modifier[Anchor] = - Seq( - href <-- $m.map(router.absoluteUrlForPage).recover { case _ => - Some("invalid url") - }, - composeEvents(onClick.noKeyMod.preventDefault)( - _.sample($m) - .map(NavigateTo.apply) - ) --> actions - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala deleted file mode 100644 index 87f825a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.pages.dashboard.DashboardPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -class DashboardPageConnector(actionBus: Observer[Action])(using - router: Router[Page] -): - def apply: HtmlElement = - AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala deleted file mode 100644 index e9b0f71..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala +++ /dev/null @@ -1,76 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailKriteriaPage -import pages.detail.components.DukazyKriteria -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -object DetailKriteriaPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailKriteriaPageConnector( - state: DetailKriteriaPageConnector.AppState -)( - $page: Signal[Page.DetailKriteria] -)(using Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => - (p.osobniCislo.value, p.parametr.value, p.kriterium.value) - )((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map( - FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) - ) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, s, $s) => - DetailKriteriaPage($s)(state.actionBus.contramap { - case DukazyKriteria.Add => - NavigateTo( - Page.UpravDukazKriteria( - Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), - Page.Titled(s.parametr.id, Some(s.parametr.nazev)), - Page.Titled(s.kriterium.id, Some(s.kriterium.id)) - ) - ) - }) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): DetailKriteriaPage.ViewModel = - DetailKriteriaPage.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala deleted file mode 100644 index 4d8024f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ /dev/null @@ -1,57 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import pages.detail.DetailPage -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import fiftyforms.ui.components.tailwind.Color - -object DetailPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailPageConnector(state: DetailPageConnector.AppState)( - $page: Signal[Page.Detail] -)(using router: Router[Page]): - val $oscChangeSignal = $page.splitOne(_.osobniCislo.value)((osc, _, _) => osc) - val $pageChangeSignal = - $oscChangeSignal.flatMap(osc => - EventStream.fromSeq(Seq(FetchUserDetails(osc), FetchParameters(osc))) - ) - // TODO: filter the value based on the current osc - // OSC change will fetch new data, but still - // - we need to be sure that what we got is really what we ought to display - // - we want to display stale data accordingly (at least with loading indicator) - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - def apply: HtmlElement = - AppPage(state.actionBus)( - $data.combineWithFn($params)(_ zip _) - .map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: List[Parameter] - ): DetailPage.ViewModel = - DetailPage.ViewModel( - o.toDetailOsoby, - p.map( - _.toParametr(param => - PageLink.container(Page.DetailParametru(o, param), state.actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala deleted file mode 100644 index fdaa7f8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailParametruPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink - -object DetailParametruPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailParametruPageConnector( - state: DetailParametruPageConnector.AppState -)( - $page: Signal[Page.DetailParametru] -)(using router: Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => (p.osobniCislo.value, p.parametr.value))((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map(FetchParameter(_, _)) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - } yield (da, pb) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailParametruPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter - ): DetailParametruPage.ViewModel = - DetailParametruPage.ViewModel( - o.toDetailOsoby, - p.toParametr(p => - PageLink.container(Page.DetailParametru(o, p), state.actionBus) - ), - p.criteria.map( - _.toKriterium { c => - PageLink.container( - Page.DetailKriteria(o, p, c), - state.actionBus - ) - } - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala deleted file mode 100644 index 049aa22..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -case class DirectoryPageConnector( - $input: EventStream[List[UserInfo]], - actionBus: Observer[Action] -)(using router: Router[Page]): - val $data = $input.startWithNone - val $actionSignal = EventStream.fromValue(FetchDirectory) - - def apply: HtmlElement = - AppPage(actionBus)( - $data.split(_ => ())((_, _, s) => - pages.directory.DirectoryPage( - s.map( - _.map( - _.toUserRow(u => - PageLink.container( - Page.Detail(Page.Titled(u.personalNumber)), - actionBus - ) - ) - ) - ) - ) - ), - $actionSignal --> actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala deleted file mode 100644 index 88e7450..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ /dev/null @@ -1,68 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii -import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria -import fiftyforms.ui.components.tailwind.Color - -extension (o: UserInfo) - def toDetailOsoby: DetailOsoby.ViewModel = - DetailOsoby.ViewModel( - o.personalNumber, - o.name, - o.email, - o.phone, - o.img, - o.mainFunction.map(f => - DetailOsoby.Funkce.ViewModel(f.name, f.dept, f.ou) - ), - o.userContracts.headOption.map(c => - DetailOsoby.PracovniPomer.ViewModel(c.rel, c.startDate, c.endDate) - ) - ) - -extension (param: Parameter) - def toParametr(container: Parameter => Anchor): DetailParametru.ViewModel = - DetailParametru.ViewModel( - id = param.id, - nazev = param.name, - popis = param.description, - status = "Nesplněno", - statusColor = Color.red, - a = container(param) - ) - -extension (crit: ParameterCriteria) - def toKriterium( - container: ParameterCriteria => Anchor - ): DetailKriteria.ViewModel = - DetailKriteria.ViewModel( - nazev = crit.criteriumText, - kapitola = crit.chapterId, - bod = crit.itemId, - status = "Nesplněno", - statusColor = Color.red, - splneno = false, - dukazy = Nil, - container = container(crit) - ) - -extension (user: UserInfo) - def toUserRow( - container: UserInfo => HtmlElement = (_: UserInfo) => div() - ): UserRow.ViewModel = - UserRow.ViewModel( - osobniCislo = user.personalNumber.toString, - celeJmeno = user.name, - prijmeni = user.surname, - hlavniFunkce = user.mainFunction.map(_.name), - img = user.img, - container = () => container(user) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala deleted file mode 100644 index 7d5b2c1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.dashboard - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.Page - -object DashboardPage: - - def render: HtmlElement = - div("Dashboard page") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala deleted file mode 100644 index 9ef0698..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala +++ /dev/null @@ -1,31 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailKriteriaPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply($m: Signal[ViewModel])( - action: Observer[DukazyKriteria.Action] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - DukazyKriteria($m.map(_.kriterium.dukazy))(action) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala deleted file mode 100644 index 3b316e1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object DetailPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametry: SeznamParametru.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby($m.map(_.osoba)), - SeznamParametru($m.map(_.parametry)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala deleted file mode 100644 index 17224cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala +++ /dev/null @@ -1,24 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailParametruPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriteria: SeznamKriterii.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby.header($m.map(_.osoba)), - DetailParametru($m.map(_.parametr)), - SeznamKriterii($m.map(_.kriteria)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala deleted file mode 100644 index dc912fa..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ /dev/null @@ -1,107 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb -package app -package pages.detail - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.UpravDukazForm -import fiftyforms.services.files.File - -object UpravDukaz: - - type ThisPage = Page.UpravDukazKriteria - type PageKey = (OsobniCislo, String, String) - - trait State { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def availableFiles: EventStream[List[File]] - def actionBus: Observer[Action] - } - - def keyOfPage(page: ThisPage): PageKey = - (page.osobniCislo.value, page.parametr.value, page.kriterium.value) - - def onChangeAction(key: PageKey): Action = - FetchParameterCriteria( - key._1, - key._2, - key._3, - Page.UpravDukazKriteria(_, _, _) - ) - - class Connector(state: State)($page: Signal[ThisPage])(using Router[Page]): - val $paramChangeSignal = $page.splitOne(keyOfPage)((x, _, _) => x) - val $pageChangeSignal = $paramChangeSignal.map(onChangeAction) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.split(_ => ())((_, s, $s) => - PageComponent( - $s.map(buildModel), - state.availableFiles, - state.actionBus.contramap { - case UpravDukazForm.Cancelled => - NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) - case UpravDukazForm.AvailableFilesRequested => - FetchAvailableFiles(s._1.personalNumber) - } - ) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): PageComponent.ViewModel = - import connectors.* - PageComponent.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) - - object PageComponent: - import components.* - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply( - $m: Signal[ViewModel], - availableFilesStream: EventStream[List[File]], - events: Observer[UpravDukazForm.Event] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - UpravDukazForm(availableFilesStream)(events) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala deleted file mode 100644 index ca311b0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.Icons -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Color - -object DetailKriteria: - case class ViewModel( - nazev: String, - kapitola: String, - bod: String, - status: String, - statusColor: Color, - splneno: Boolean, - dukazy: List[DukazKriteria.ViewModel], - container: HtmlElement = div() - ) { - val id = s"${kapitola}${bod}" - } - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h3( - cls := "text-xl leading-6 font-bold text-gray-900", - child.text <-- $m.map(_.nazev) - ), - h3( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - "Kapitola ", - child.text <-- $m.map(_.kapitola), - " bod ", - child <-- $m.map(_.bod) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala deleted file mode 100644 index ae90290..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala +++ /dev/null @@ -1,100 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Avatar - -object DetailOsoby: - - object Funkce: - case class ViewModel( - nazev: String, - stredisko: String, - voj: String - ) - - def render($m: Signal[ViewModel]): HtmlElement = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.nazev), - span( - cls := "hidden md:inline", - " @ ", - child.text <-- $m.map(_.stredisko), - ", ", - child.text <-- $m.map(_.voj) - ) - ) - - object PracovniPomer: - case class ViewModel( - druh: String, - pocatek: LocalDate, - konec: Option[LocalDate] - ) - - def render($m: Signal[ViewModel]): HtmlElement = - import fiftyforms.ui.components.tailwind.CustomAttrs.datetime - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.druh), - " od ", - time( - datetime <-- $m.map(_.pocatek.toString), - child.text <-- $m.map(_.pocatek.toString) - ) - ) - - case class ViewModel( - osobniCislo: OsobniCislo, - jmeno: String, - email: Option[String], - telefon: Option[String], - img: Option[String], - hlavniFunkce: Option[Funkce.ViewModel], - pracovniPomer: Option[PracovniPomer.ViewModel] - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-4", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(16) - ), - div( - h1( - cls := "text-2xl font-bold text-gray-900", - child.text <-- $m.map(_.jmeno) - ), - child.maybe <-- $m.map(_.hlavniFunkce).split(_ => ())((_, _, d) => - Funkce.render(d) - ), - child.maybe <-- $m.map(_.pracovniPomer).split(_ => ())((_, _, d) => - PracovniPomer.render(d) - ) - ) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-5", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(8) - ), - div( - h1( - cls := "text-lg font-medium text-gray-800", - child.text <-- $m.map(_.jmeno) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala deleted file mode 100644 index e11cb62..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color - -object DetailParametru: - case class ViewModel( - id: String, - nazev: String, - popis: String, - status: String, - statusColor: Color, - a: Anchor - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h2( - cls := "text-2xl leading-6 font-medium text-gray-900", - child.text <-- $m.map(_.nazev) - ), - p( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - child.text <-- $m.map(_.popis) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - h2( - cls := "text-lg leading-6 font-medium text-gray-600", - child.text <-- $m.map(_.nazev) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala deleted file mode 100644 index a00ede8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala +++ /dev/null @@ -1,177 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.CustomAttrs - -object DukazKriteria: - case class Osoba(osobniCislo: String, jmeno: String) - case class Udalost(osoba: Osoba, datum: LocalDate) - case class Dokument( - url: String, - nazev: String, - pridal: Udalost, - odebral: Option[Udalost] - ) - case class ViewModel( - dokumenty: List[Dokument], - autorizoval: Option[Udalost], - platiDo: Option[LocalDate], - poznámka: Option[String] - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-lg", - div( - cls := "px-4 py-5 sm:px-6", - h3( - cls := "text-lg leading-6 font-medium text-gray-900", - """Applicant Information""" - ), - p( - cls := "mt-1 max-w-2xl text-sm text-gray-500", - """Personal details and application.""" - ) - ), - div( - cls := "border-t border-gray-200 px-4 py-5 sm:px-6", - dl( - cls := "grid grid-cols-1 gap-x-4 gap-y-8 sm:grid-cols-2", - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Full name""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Margot Foster""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Application for""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Backend Developer""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Email address""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """margotfoster@example.com""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Salary expectation""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """$120,000""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """About""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Fugiat ipsum ipsum deserunt culpa aute sint do nostrud anim incididunt cillum culpa consequat. Excepteur qui ipsum aliquip consequat sint. Sit id mollit nulla mollit nostrud in ea officia proident. Irure nostrud pariatur mollit ad adipisicing reprehenderit deserunt qui eu.""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """Attachments""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - ul( - role := "list", - cls := "border border-gray-200 rounded-md divide-y divide-gray-200", - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """resume_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ), - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """coverletter_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala deleted file mode 100644 index 22f134f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala +++ /dev/null @@ -1,35 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object DukazyKriteria: - sealed trait Action - case object Add extends Action - - type ViewModel = List[DukazKriteria.ViewModel] - - def apply($m: Signal[ViewModel])(actions: Observer[Action]): HtmlElement = - div( - child.maybe <-- $m.map(m => - if (m.isEmpty) then Some(prazdnyDukaz(actions)) - else None - ), - children <-- $m.map(_.zipWithIndex).split(_._2)((_, _, $s) => - DukazKriteria($s.map(_._1)) - ) - ) - - private def prazdnyDukaz(actions: Observer[Action]): HtmlElement = - button( - tpe := "button", - onClick.preventDefault.mapTo(Add) --> actions, - cls := "relative block w-full border-2 border-gray-300 border-dashed rounded-lg p-12 text-center hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - Icons.outline - .`document-add`(12) - .amend(svg.cls := "mx-auto text-gray-400"), - span( - cls := "mt-2 block text-sm font-medium text-gray-900", - "Přidat důkaz" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala deleted file mode 100644 index f628697..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.list.StackedList -import fiftyforms.ui.components.tailwind.list.ListRow -import fiftyforms.ui.components.tailwind.list.RowTag -import fiftyforms.ui.components.tailwind.list.RowNext -import java.time.LocalDate - -object SeznamKriterii: - type ViewModel = List[DetailKriteria.ViewModel] - - private val kritList = new StackedList[DetailKriteria.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - kritList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = - p(cls := "text-sm text-gray-500", s"${i.kapitola}${i.bod}"), - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.container - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala deleted file mode 100644 index 2bfe4bc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.list.{ - StackedList, - ListRow, - RowTag, - PropList, - IconText, - RowNext -} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.LinkSupport.* - -object SeznamParametru: - type ViewModel = List[DetailParametru.ViewModel] - - private val parametrList = new StackedList[DetailParametru.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - parametrList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = emptyNode, - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.a - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala deleted file mode 100644 index f048e9d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ /dev/null @@ -1,101 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.form.* -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement -import fiftyforms.services.files.components.tailwind.FilePicker -import cz.e_bs.cmi.mdr.pdb.frontend.AutorizujDukaz -import cz.e_bs.cmi.mdr.pdb.frontend.DocumentRef -import fiftyforms.services.files.File - -object UpravDukazForm: - sealed trait Event - case object Cancelled extends Event - case object AvailableFilesRequested extends Event - def apply(availableFilesStream: EventStream[List[File]])( - updates: Observer[Event] - ): HtmlElement = - val (filesStream, filesObserver) = - EventStream.withObserver[FilePicker.Event] - val files = Var[List[File]](Nil) - def submitButtons: HtmlElement = - div( - cls := "pt-5", - div( - cls := "flex justify-end", - button( - tpe := "button", - cls := "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zrušit", - onClick.mapTo(Cancelled) --> updates - ), - button( - tpe := "submit", - cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - disabled <-- files.signal.map(_.isEmpty), - "Autorizovat" - ) - ) - ) - - div( - filesStream.collect { case FilePicker.SelectionUpdated(files) => - files.to(List) - } --> files.writer, - filesStream.collect { case FilePicker.AvailableFilesRequested => - AvailableFilesRequested - } --> updates, - cls := "bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6", - Form( - Form.Body( - Form.Section( - FormHeader( - FormHeader.ViewModel( - "Doložení kritéria", - "Sestavte doklady poskytující důkaz kritéria a potvrďte odesláním formuláře. Případné limitace či jiné relevantní údaje vepište do pole Komentář." - ) - ), - FormFields( - FormRow( - "dokumenty", - "Dokumenty", - FilePicker(files.signal, availableFilesStream)(filesObserver) - .amend(idAttr := "dokumenty", cls("max-w-lg")) - ).toHtml, - FormRow( - "platnost", - "Platnost", - input( - idAttr := "platnost", - name := "platnost", - tpe := "date", - autoComplete := "date", - cls := "block max-w-lg w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" - ) - ).toHtml, - FormRow( - "komentar", - "Komentář", - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - textArea( - idAttr := "komentar", - name := "about", - rows := 3, - cls := "max-w-lg shadow-sm block w-full focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border border-gray-300 rounded-md" - ), - p( - cls := "mt-2 text-sm text-gray-500", - "Doplňte prosím potřebné informace související s doložením kritéria, včetně případných limitací." - ) - ) - ).toHtml - ) - ) - ), - submitButtons - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala deleted file mode 100644 index 41033f6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -import components._ - -object DirectoryPage: - - type ViewModel = List[UserRow.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - val (actionsStream, actionObserver) = - EventStream.withObserver[SearchForm.Action] - val $filter = actionsStream - .collect { - case SearchForm.ClearFilter => None - case SearchForm.SetFilter(t) => Some(t) - } - .startWith(None) - val byLetter = for { - d <- $m - f <- $filter - } yield for { - (letter, users) <- d - .filter { user => - f match - case None => true - case Some(t) => user.search.contains(t) - } - .groupBy(_.prijmeni.head) - .to(List) - .sortBy(_._1) - } yield (letter.toString, users.sortBy(_.prijmeni)) - - div( - cls := "h-full max-w-7xl mx-auto order-first flex flex-col flex-shrink-0", - SearchForm(actionObserver), - Directory(byLetter) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala deleted file mode 100644 index 73f915d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -object Directory: - - object Header: - type ViewModel = String - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500", - h3(child.text <-- $m) - ) - - object UserList: - type ViewModel = List[UserRow.ViewModel] - def apply($m: Signal[ViewModel]): HtmlElement = - ul( - role := "list", - cls := "relative z-0 divide-y divide-gray-200", - children <-- $m.split(_.osobniCislo)((_, _, s) => UserRow(s)) - ) - - type ViewModel = List[(String, List[UserRow.ViewModel])] - def apply($m: Signal[ViewModel]): HtmlElement = - nav( - cls := "flex-1 min-h-0 overflow-y-auto", - aria.label := "Directory", - children <-- $m.split(_._1)((_, _, s) => - div( - cls := "relative", - Header(s.map(_._1)), - UserList(s.map(_._2)) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala deleted file mode 100644 index e182910..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala +++ /dev/null @@ -1,54 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object SearchForm: - sealed trait Action - case object SubmitFilter extends Action - sealed trait FilterAction extends Action - case object ClearFilter extends FilterAction - case class SetFilter(value: String) extends FilterAction - - def apply(actions: Observer[Action]): HtmlElement = - div( - cls := "px-6 pt-4 pb-4", - form( - cls := "flex space-x-4", - onSubmit.mapTo(SubmitFilter) --> actions, - div( - cls := "flex-1 min-w-0", - label( - forId := "search", - cls := "sr-only", - "Hledat" - ), - div( - cls := "relative rounded-md shadow-sm", - div( - cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", - Icons.solid.search().amend(svg.cls := "text-gray-400") - ), - input( - tpe := "search", - name := "search", - idAttr := "search", - cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", - placeholder := "Hledat", - composeEvents(onInput.mapToValue.setAsValue.map(SetFilter(_)))( - _.throttle(500) - ) --> actions - ) - ) - ), - button( - tpe := "submit", - cls := "inline-flex justify-center px-3.5 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500", - Icons.solid.filter().amend(svg.cls := "text-gray-400"), - span( - cls := "sr-only", - "Hledat" - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala deleted file mode 100644 index 82f7723..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala +++ /dev/null @@ -1,52 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.airstream.core.Signal -import fiftyforms.ui.components.tailwind.Avatar - -object UserRow: - case class ViewModel( - osobniCislo: String, - celeJmeno: String, - prijmeni: String, - hlavniFunkce: Option[String], - img: Option[String], - container: () => HtmlElement = () => div() - ) { - val search = prijmeni.toLowerCase - } - - def apply($m: Signal[ViewModel]): HtmlElement = - inline def avatarImage = - Avatar($m.map(_.img)).avatarImage(10) - - li( - div( - cls := "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500", - div( - cls := "flex-shrink-0", - child <-- avatarImage - ), - div( - cls := "flex-1 min-w-0", - child <-- $m.map { o => - o.container() - .amend( - cls := "focus:outline-none", - span( - cls := "absolute inset-0", - aria.hidden := true - ), - p( - cls := "text-sm font-medium text-gray-900", - o.celeJmeno - ), - p( - cls := "text-sm text-gray-500 truncate", - o.hlavniFunkce - ) - ) - } - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala deleted file mode 100644 index 467c5f5..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala +++ /dev/null @@ -1,99 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router - -object ErrorPage: - case class ViewModel( - homePage: Page, - errorName: String, - title: String, - subTitle: String - ) - - def apply(m: ViewModel, actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - val ViewModel(homePage, errorName, title, subTitle) = m - div( - cls := "min-h-full pt-16 pb-12 flex flex-col bg-white", - main( - cls := "flex-grow flex flex-col justify-center max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex-shrink-0 flex justify-center", - a( - href := "/", - cls := "inline-flex", - span( - cls := "sr-only", - """Workflow""" - ), - img( - cls := "h-12 w-auto", - src := "https://tailwindui.com/img/logos/workflow-mark.svg?color=indigo&shade=600", - alt := "" - ) - ) - ), - div( - cls := "py-16", - div( - cls := "text-center", - p( - cls := "text-sm font-semibold text-indigo-600 uppercase tracking-wide", - errorName - ), - h1( - cls := "mt-2 text-4xl font-extrabold text-gray-900 tracking-tight sm:text-5xl", - title - ), - p( - cls := "mt-2 text-base text-gray-500", - subTitle - ), - div( - cls := "mt-6", - PageLink - .container(homePage, actionBus) - .amend( - cls := "text-base font-medium text-indigo-600 hover:text-indigo-500", - """Go back home""" - ) - ) - ) - ) - ), - footer( - cls := "flex-shrink-0 max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", - nav( - cls := "flex justify-center space-x-4", - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Contact Support""" - ), - span( - cls := "inline-block border-l border-gray-300", - aria.hidden := true - ), - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Status""" - ), - span( - cls := "inline-block border-l border-gray-300", - aria.hidden := true - ), - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Twitter""" - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala deleted file mode 100644 index bd9baec..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action - -object NotFoundPage: - def apply(homePage: Page, url: String, actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - ErrorPage( - ErrorPage.ViewModel( - homePage, - "404 error", - "Page not found.", - s"Sorry, but page $url doesn't exist." - ), - actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala deleted file mode 100644 index 476fe3c..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action - -object UnhandledErrorPage: - - case class ViewModel( - homePage: Page, - errorName: Option[String], - errorMessage: Option[String] - ) - - def apply(m: ViewModel, actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - ErrorPage( - ErrorPage.ViewModel( - m.homePage, - "Unexpected error occurred", - m.errorName.getOrElse( - "Uh oh!" - ), // TODO: translations, better text than uh oh - m.errorMessage.getOrElse( - "This wasn't supposed to happen! Please try again." - ) - ), - actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala deleted file mode 100644 index 25f059f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala +++ /dev/null @@ -1,6 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.services - -import com.raquo.airstream.core.Observer - -trait DataFetcher[K, A]: - def fetch(id: K, sink: Observer[A]): Unit diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala deleted file mode 100644 index a469830..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala +++ /dev/null @@ -1,120 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package state - -import com.raquo.airstream.core.EventStream -import cz.e_bs.cmi.mdr.pdb.{UserInfo, OsobniCislo} -import com.raquo.airstream.core.Observer -import scala.scalajs.js -import scala.scalajs.js.JSON -import zio.json._ -import com.raquo.airstream.eventbus.EventBus -import com.raquo.airstream.ownership.Owner -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.UserFunction -import cz.e_bs.cmi.mdr.pdb.UserContract -import fiftyforms.services.files.File - -trait AppState - extends connectors.DetailPageConnector.AppState - with connectors.DetailParametruPageConnector.AppState - with connectors.DetailKriteriaPageConnector.AppState - with pages.detail.UpravDukaz.State: - def users: EventStream[List[UserInfo]] - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - -class MockAppState(implicit owner: Owner, router: Router[Page]) - extends AppState: - - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonDecoder[UserFunction] = DeriveJsonDecoder.gen - given JsonDecoder[UserContract] = DeriveJsonDecoder.gen - given JsonDecoder[UserInfo] = DeriveJsonDecoder.gen - - given JsonDecoder[ParameterCriteria] = DeriveJsonDecoder.gen - given JsonDecoder[Parameter] = DeriveJsonDecoder.gen - - private val actions = EventBus[Action]() - private val (parametersStream, pushParameters) = - EventStream.withCallback[List[Parameter]] - private val (usersStream, pushUsers) = - EventStream.withCallback[List[UserInfo]] - private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] - private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] - - private val mockData: List[UserInfo] = - mockUsers - .asInstanceOf[js.Dictionary[js.Object]] - .values - // TODO: is there a more efficient way to parse from JS object directly? - .map { o => - val parsed = JSON.stringify(o).fromJson[UserInfo] - parsed.left.foreach(org.scalajs.dom.console.log(_)) - parsed - } - .collect { case Right(u) => - u - } - .toList - - private val mockParameters: List[Parameter] = - pdbParams - .asInstanceOf[js.Dictionary[js.Object]] - .values - .map(o => JSON.stringify(o).fromJson[Parameter]) - .collect { case Right(p) => p } - .toList - - // TODO: Extract to separate event handler - actions.events.foreach { - case FetchDirectory => pushUsers(mockData) - case FetchUserDetails(osc) => - mockData.find(_.personalNumber == osc).foreach { o => - pushDetails(o) - router.replaceState(Page.Detail(o)) - } - case FetchParameters(osc) => - pushParameters(mockParameters) - case FetchParameter(osc, paramId) => - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(Page.DetailParametru(o, p)) - case FetchParameterCriteria(osc, paramId, critId, page) => - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - c <- p.criteria.find(_.id == critId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(page(o, p, c)) - case NavigateTo(page) => router.pushState(page) - case FetchAvailableFiles(osc) => - pushFiles( - List( - File("https://tc163.cmi.cz/here", "Example file") - ) - ) - } - - override def users: EventStream[List[UserInfo]] = - usersStream.debugWithName("users") - - override def details: EventStream[UserInfo] = - detailsStream.debugWithName("details") - - override def parameters: EventStream[List[Parameter]] = - parametersStream.debugWithName("parameters") - - override def availableFiles: EventStream[List[File]] = - filesStream.debugWithName("available files") - - override def actionBus: Observer[Action] = - actions.writer.debugWithName("actions writer") diff --git a/app/src/main/scala/mdr/pdb/app/Main.scala b/app/src/main/scala/mdr/pdb/app/Main.scala new file mode 100644 index 0000000..081b4ee --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/Main.scala @@ -0,0 +1,103 @@ +package mdr.pdb.app + +import scala.scalajs.js.annotation.JSExportTopLevel +import scala.scalajs.js.annotation.JSExport +import scala.scalajs.js.annotation.JSImport +import scala.scalajs.js +import org.scalajs.dom +import com.raquo.laminar.api.L.{*, given} +import scala.scalajs.js.Date +import com.raquo.waypoint.Router +import com.raquo.waypoint.SplitRender +import mdr.pdb.app.services.DataFetcher +import scala.scalajs.js.JSON +import zio.json._ +import mdr.pdb.UserInfo +import mdr.pdb.app.state.AppState + +@js.native +@JSImport("stylesheets/main.css", JSImport.Namespace) +object Css extends js.Any + +@js.native +@JSImport("data/users.json", JSImport.Default) +object mockUsers extends js.Object + +@js.native +@JSImport("params/pdb-params.json", JSImport.Default) +object pdbParams extends js.Object + +@JSExportTopLevel("app") +object Main: + + @JSExport + def main(args: Array[String]): Unit = + import Routes.given + onLoad { + setupAirstream() + val appContainer = dom.document.querySelector("#app") + val _ = + render( + appContainer, + renderPage(state.MockAppState(using unsafeWindowOwner, router)) + ) + } + + private def onLoad(f: => Unit): Unit = + documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) + + private def setupAirstream()(using router: Router[Page]): Unit = + AirstreamError.registerUnhandledErrorCallback(err => + router.forcePage( + Page.UnhandledError( + Some(err.getClass.getName), // TODO: Fill only in dev mode + Some(err.getMessage) + ) + ) + ) + + def renderPage(state: AppState)(using + router: Router[Page] + ): HtmlElement = + val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) + .collectSignal[Page.Detail]( + connectors + .DetailPageConnector(state)(_) + .apply + ) + .collectSignal[Page.DetailParametru]( + connectors + .DetailParametruPageConnector(state)(_) + .apply + ) + .collectSignal[Page.DetailKriteria]( + connectors + .DetailKriteriaPageConnector(state)(_) + .apply + ) + .collectSignal[Page.UpravDukazKriteria]( + pages.detail.UpravDukaz.Connector(state)(_).apply + ) + .collectStatic(Page.Dashboard)( + connectors.DashboardPageConnector(state.actionBus).apply + ) + .collect[Page.NotFound](pg => + pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) + ) + .collect[Page.UnhandledError](pg => + pages.errors + .UnhandledErrorPage( + pages.errors.UnhandledErrorPage + .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), + state.actionBus + ) + ) + .collectStatic(Page.Directory)( + connectors + .DirectoryPageConnector(state.users, state.actionBus) + .apply + ) + div(cls := "h-full", child <-- pageSplitter.$view) + + // Pull in the stylesheet + val css: Css.type = Css diff --git a/app/src/main/scala/mdr/pdb/app/Routes.scala b/app/src/main/scala/mdr/pdb/app/Routes.scala new file mode 100644 index 0000000..2cc860c --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/Routes.scala @@ -0,0 +1,198 @@ +package mdr.pdb.app + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.* +import org.scalajs.dom +import zio.json.{*, given} +import mdr.pdb.OsobniCislo + +import scala.scalajs.js +import mdr.pdb.UserInfo +import mdr.pdb.Parameter +import mdr.pdb.ParameterCriteria +import mdr.pdb.app.Page.Titled + +// enum is not working with Waypoints' SplitRender collectStatic +sealed abstract class Page( + val id: String, + val title: String, + val parent: Option[Page] +) { + val path: Vector[Page] = + parent match + case None => Vector(this) + case Some(p) => p.path :+ this + + val isRoot: Boolean = parent.isEmpty +} + +object Page: + + case class Titled[V](value: V, title: Option[String] = None): + val show: String = title.getOrElse(value.toString) + + case object Directory extends Page("directory", "Adresář", None) + + case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) + + case class Detail(osobniCislo: Titled[OsobniCislo]) + extends Page("user", osobniCislo.show, Some(Directory)) + + object Detail { + def apply(o: UserInfo): Detail = Detail( + Titled(o.personalNumber, Some(o.name)) + ) + } + + case class DetailParametru( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String] + ) extends Page( + "parameter", + parametr.show, + Some(Detail(osobniCislo)) + ) + + object DetailParametru { + def apply(o: UserInfo, p: Parameter): DetailParametru = + DetailParametru( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)) + ) + } + + case class DetailKriteria( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String], + kriterium: Titled[String] + ) extends Page( + "criteria", + kriterium.show, + Some(DetailParametru(osobniCislo, parametr)) + ) + + object DetailKriteria { + def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = + DetailKriteria( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)), + Titled(k.id, Some(k.id)) + ) + } + + case class UpravDukazKriteria( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String], + kriterium: Titled[String] + ) extends Page( + "addProof", + "Důkaz", + Some(DetailKriteria(osobniCislo, parametr, kriterium)) + ) + + object UpravDukazKriteria { + def apply( + o: UserInfo, + p: Parameter, + k: ParameterCriteria + ): UpravDukazKriteria = + UpravDukazKriteria( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)), + Titled(k.id, Some(k.id)) + ) + } + + case class NotFound(url: String) extends Page("404", "404", Some(Directory)) + + case class UnhandledError( + errorName: Option[String], + errorMessage: Option[String] + ) extends Page("500", "Unexpected error", Some(Directory)) + +object Routes: + given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) + given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) + given [V: JsonEncoder]: JsonEncoder[Titled[V]] = + DeriveJsonEncoder.gen[Titled[V]] + given [V: JsonDecoder]: JsonDecoder[Titled[V]] = + DeriveJsonDecoder.gen[Titled[V]] + given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] + given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] + + val base = + js.`import`.meta.env.BASE_URL + .asInstanceOf[String] + "app" + + val homePage: Page = Page.Directory + + given router: Router[Page] = Router[Page]( + routes = List( + Route.static(homePage, root / endOfSegments, basePath = base), + Route.static( + Page.Dashboard, + root / "dashboard" / endOfSegments, + basePath = base + ), + Route[Page.Detail, String]( + encode = _.osobniCislo.value.toString, + decode = osc => Page.Detail(Titled(OsobniCislo(osc))), + root / "osoba" / segment[String] / endOfSegments, + basePath = base + ), + Route[Page.DetailParametru, (String, String)]( + encode = p => (p.osobniCislo.value.toString, p.parametr.value), + decode = + p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / endOfSegments, + basePath = base + ), + Route[Page.DetailKriteria, (String, String, String)]( + encode = p => + ( + p.osobniCislo.value.toString, + p.parametr.value, + p.kriterium.value.replaceAll("\\.", "--") + ), + decode = p => + Page.DetailKriteria( + Titled(OsobniCislo(p._1)), + Titled(p._2), + Titled(p._3.replaceAll("--", ".")) + ), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / "kriterium" / segment[String] / endOfSegments, + basePath = base + ), + Route[Page.UpravDukazKriteria, (String, String, String)]( + encode = p => + ( + p.osobniCislo.value.toString, + p.parametr.value, + p.kriterium.value.replaceAll("\\.", "--") + ), + decode = p => + Page.UpravDukazKriteria( + Titled(OsobniCislo(p._1)), + Titled(p._2), + Titled(p._3.replaceAll("--", ".")) + ), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / "kriterium" / segment[String] / "edit" / endOfSegments, + basePath = base + ) + ), + serializePage = _.toJson, + deserializePage = _.fromJson[Page] + .fold(s => throw IllegalStateException(s), identity), + getPageTitle = _.title, + routeFallback = url => Page.NotFound(url), + deserializeFallback = _ => Page.Dashboard + )( + $popStateEvent = windowEvents.onPopState, + owner = unsafeWindowOwner + ) diff --git a/app/src/main/scala/mdr/pdb/app/actions.scala b/app/src/main/scala/mdr/pdb/app/actions.scala new file mode 100644 index 0000000..d078b3a --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/actions.scala @@ -0,0 +1,21 @@ +package mdr.pdb.app + +import mdr.pdb.OsobniCislo +import mdr.pdb.UserInfo +import mdr.pdb.Parameter +import mdr.pdb.ParameterCriteria + +sealed trait Action + +case object FetchDirectory extends Action +case class FetchUserDetails(osc: OsobniCislo) extends Action +case class FetchParameters(osc: OsobniCislo) extends Action +case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action +case class FetchParameterCriteria( + osc: OsobniCislo, + paramId: String, + critId: String, + page: (UserInfo, Parameter, ParameterCriteria) => Page +) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action +case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/mdr/pdb/app/components/AppPage.scala new file mode 100644 index 0000000..130c817 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/AppPage.scala @@ -0,0 +1,82 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.Page +import mdr.pdb.{UserProfile, UserInfo, OsobniCislo} +import com.raquo.waypoint.Router +import mdr.pdb.app.Action +import mdr.pdb.app.NavigateTo +import mdr.pdb.UserFunction + +object AppPage: + // TODO: pages by logged in user + val pages: List[Page] = List(Page.Directory, Page.Dashboard) + + import NavigationBar.{Logo, MenuItem} + + val logo = Logo( + "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", + "Workflow" + ) + + // TODO: menu items by user profile + val userMenu = + List( + MenuItem("Your Profile"), + MenuItem("Settings"), + MenuItem("Sign out") + ) + + // TODO: load user profile + val $userProfile = Var( + UserProfile( + "tom", + UserInfo( + OsobniCislo("1031"), + "tom", + "Tom", + "Cook", + None, + None, + Some("tom@example.com"), + Some("+420 222 866 180"), + Some( + UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") + ), + Nil, + None + ) + ) + ) + + val $userInfo = $userProfile.signal.map(_.userInfo) + + type ViewModel = Option[HtmlElement] + def apply( + actionBus: Observer[Action] + )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using + router: Router[Page] + ): HtmlElement = + PageLayout(actionBus)( + $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => + PageLayout.ViewModel( + NavigationBar.ViewModel( + u, + pages.map(p => + NavigationBar.Link( + () => + PageLink( + p, + actionBus + ), + p == cp + ) + ), + userMenu, + logo + ), + c + ) + ), + mods + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala new file mode 100644 index 0000000..9375398 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala @@ -0,0 +1,143 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.tailwind.list.IconText.ViewModel +import fiftyforms.ui.components.tailwind.Icons +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.Page + +import com.raquo.laminar.api.L.{*, given} +import io.laminext.syntax.core.{*, given} +import mdr.pdb.app.Action + +object Breadcrumbs: + + private def slash = + import svg.{*, given} + svg( + cls := "flex-shrink-0 h-5 w-5 text-gray-300", + xmlns := "http://www.w3.org/2000/svg", + fill := "currentColor", + viewBox := "0 0 20 20", + ariaHidden := true, + path( + d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" + ) + ) + + def backIcon = + Icons.solid + .`arrow-narrow-left`() + .amend( + svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" + ) + + object Link: + case class ViewModel( + page: Page, + icon: Option[SvgElement], + extraClasses: String, + text: String, + textClasses: Option[String] = None + ) + + val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" + + def shortHome(p: Page) = ViewModel( + p, + Some(backIcon), + "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", + "Zpět na úvodní stránku", + None + ) + + def fullHome(p: Page) = ViewModel( + p, + Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), + "text-gray-400 hover:text-gray-500", + "Domů", + Some("sr-only") + ) + + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + PageLink + .container($m.map(_.page), actionBus) + .amend( + cls <-- $m.map(_.extraClasses), + child.maybe <-- $m.map(_.icon), + span( + cls <-- $m.map(_.textClasses.getOrElse("")), + child.text <-- $m.map(_.text) + ) + ) + + object FullBreadcrumbs: + type ViewModel = Page + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + ol( + role := "list", + cls := "flex items-center space-x-4", + children <-- $m.map(_.path).split(_.id)((_, _, $p) => + li( + div( + cls := "flex items-center", + child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), + Link( + $p.map(p => + if (p.isRoot) then Link.fullHome(p) + else + Link.ViewModel( + p, + None, + s"ml-4 max-w-xs truncate ${Link.baseClasses}", + p.title + ) + ), + actionBus + ) + ) + ) + ) + ) + + object ShortBreadcrumbs: + type ViewModel = Page + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + Link( + $m.map { p => + val target = p.parent.getOrElse(p) + if target.isRoot then Link.shortHome(target) + else + Link.ViewModel( + target, + Some(backIcon), + "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", + s"Zpět na ${target.title}" + ) + }, + actionBus + ) + + def apply(actionBus: Observer[Action])(using + router: Router[Page] + ): HtmlElement = + nav( + cls := "flex", + aria.label := "Breadcrumb", + div( + cls := "flex sm:hidden", + ShortBreadcrumbs(router.$currentPage, actionBus) + ), + div( + cls := "hidden sm:block", + FullBreadcrumbs(router.$currentPage, actionBus) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala new file mode 100644 index 0000000..4bb8b8d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala @@ -0,0 +1,242 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent +import fiftyforms.ui.components.tailwind.Icons +import fiftyforms.ui.components.tailwind.Avatar +import mdr.pdb.UserInfo +import io.laminext.syntax.core.* + +object NavigationBar: + + case class Logo(img: String, name: String) + case class Link(a: () => Anchor, active: Boolean) + case class MenuItem(title: String) + + case class ViewModel( + userInfo: UserInfo, + pages: List[Link], + userMenu: List[MenuItem], + logo: Logo + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + val $userInfo = $m.map(_.userInfo) + val mobileMenuOpen = Var(false) + + val desktopOnly = cls("hidden md:block") + val mobileOnly = cls("md:hidden") + + inline def avatarImage(size: Int = 8) = + Avatar($userInfo.map(_.img)).avatarImage(size) + + def notificationButton = button( + tpe := "button", + cls := List( + "bg-indigo-600", + "focus:outline-none", + "focus:ring-2", + "focus:ring-offset-2", + "focus:ring-offset-indigo-600", + "focus:ring-white", + "hover:text-white", + "p-1", + "rounded-full", + "text-indigo-200" + ), + span(cls := "sr-only", "View notifications"), + Icons.outline.bell() + ) + + def userProfile: HtmlElement = + val menuOpen = Var(false) + + def menuItem(item: MenuItem, idx: Int): HtmlElement = + a( + href := "#", + cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", + role := "menuitem", + tabIndex := -1, + idAttr := s"user-menu-item-$idx", + item.title + ) + + div( + cls := "ml-3 relative", + div( + button( + tpe := "button", + cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", + idAttr := "user-menu-button", + aria.expanded <-- menuOpen.signal, + aria.hasPopup := true, + span(cls := "sr-only", "Open user menu"), + child <-- avatarImage(), + onClick.preventDefault.mapTo( + !menuOpen.now() + ) --> menuOpen.writer + ) + ), + /* + * */ + div( + cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", + cls <-- menuOpen.signal.map { o => + if (o) "md:block" else "md:hidden" + }, + role := "menu", + aria.orientation := "vertical", + aria.labelledBy := "user-menu-button", + tabIndex := -1, + // : keyboard navigation + children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) + ) + ) + + def mobileProfile = + def menuItem(item: MenuItem): HtmlElement = + a( + href := "#", + cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", + item.title + ) + + div( + cls := "pt-4 pb-3 border-t border-indigo-700", + div( + cls := "flex items-center px-5", + div( + cls := "flex-shrink-0", + child <-- avatarImage(10) + ), + div( + cls := "ml-3", + div( + cls := "text-base font-medium text-white", + child.text <-- $userInfo.map(_.name) + ), + child.maybe <-- $userInfo.map( + _.email.map(e => + div( + cls := "text-sm font-medium text-indigo-300", + e + ) + ) + ) + ), + notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) + ), + div( + cls := "mt-3 px-2 space-y-1", + children <-- $m.map(_.userMenu.map(menuItem)) + ) + ) + + def pageLink(page: Link): Anchor = + page + .a() + .amend( + cls := "text-white px-3 py-2 rounded-md text-sm font-medium", + cls := Seq( + "bg-indigo-700" -> page.active, + "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active + ), + ariaCurrent := (if page.active then "page" else "false") + ) + + def logoImg: Image = + img( + cls := "h-8 w-8", + src <-- $m.map(_.logo.img), + alt <-- $m.map(_.logo.name) + ) + + def mobileMenuButton = button( + tpe := "button", + cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", + aria.controls := "mobile-menu", + aria.expanded <-- mobileMenuOpen.signal, + span(cls := "sr-only", "Open main menu"), + Icons.outline + .menu() + .amend( + svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") + ), + Icons.outline + .x() + .amend( + svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") + ), + onClick.preventDefault.mapTo( + !mobileMenuOpen.now() + ) --> mobileMenuOpen.writer + ) + + def navBarLeft = + div( + cls := "flex items-center", + div(cls := "flex-shrink-0", logoImg), + div( + desktopOnly, + div( + cls := "ml-10 flex items-baseline space-x-4", + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) + ) + ) + ) + + def navBarRight = + div( + desktopOnly, + div( + cls := "ml-4 flex items-center md:ml-6", + notificationButton, + userProfile + ) + ) + + def navBarMobile = + div( + cls := "-mr-2 flex", + mobileOnly, + mobileMenuButton + ) + + def navBar = + div( + cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", + div( + cls := "flex items-center justify-between h-16", + navBarLeft, + navBarRight, + navBarMobile + ) + ) + + def mobileMenu = + div( + mobileOnly, + cls <-- mobileMenuOpen.signal.map { o => + if (o) "block" else "hidden" + }, + idAttr := "mobile-menu", + div( + cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) + ), + mobileProfile + ) + + nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala new file mode 100644 index 0000000..c4cb10d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala @@ -0,0 +1,19 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.Page +import com.raquo.waypoint.Router +import mdr.pdb.app.Action + +object PageHeader: + def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = + header( + cls := "bg-white shadow-sm", + div( + cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", + h1( + cls := "text-lg leading-6 font-semibold text-gray-900", + Breadcrumbs(actionBus) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala new file mode 100644 index 0000000..aae4539 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala @@ -0,0 +1,30 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.Page +import mdr.pdb.app.Action +import fiftyforms.ui.components.tailwind.Loading +import io.laminext.syntax.core.* + +object PageLayout: + case class ViewModel( + navigation: NavigationBar.ViewModel, + content: Option[HtmlElement] + ) + def apply(actionBus: Observer[Action])( + $m: Signal[ViewModel], + mods: Modifier[HtmlElement]* + )(using router: Router[Page]): HtmlElement = + val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) + div( + cls := "h-full flex flex-col", + NavigationBar($m.map(_.navigation)), + child.maybe <-- router.$currentPage.map(_.isRoot) + .switch(None, Some(PageHeader(actionBus))), + main( + cls := "flex-grow-1 overflow-y-auto", + mods, + child <-- $maybeContent.map(_.getOrElse(Loading)) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/mdr/pdb/app/components/PageLink.scala new file mode 100644 index 0000000..e6e7052 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageLink.scala @@ -0,0 +1,40 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.LinkSupport.* +import mdr.pdb.app.Page +import mdr.pdb.app.Action +import com.raquo.waypoint.Router +import mdr.pdb.app.NavigateTo + +object PageLink: + type ViewModel = Page + def apply($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = + a(mods($m, actions), child.text <-- $m.map(_.title)) + + def apply(m: ViewModel, actions: Observer[Action])(using + router: Router[Page] + ): Anchor = apply(Val(m), actions) + + def container($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = a(mods($m, actions)) + + def container(m: ViewModel, actions: Observer[Action])(using + Router[Page] + ): Anchor = container(Val(m), actions) + + private def mods($m: Signal[Page], actions: Observer[Action])(using + router: Router[Page] + ): Modifier[Anchor] = + Seq( + href <-- $m.map(router.absoluteUrlForPage).recover { case _ => + Some("invalid url") + }, + composeEvents(onClick.noKeyMod.preventDefault)( + _.sample($m) + .map(NavigateTo.apply) + ) --> actions + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala new file mode 100644 index 0000000..7682772 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala @@ -0,0 +1,13 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.pages.dashboard.DashboardPage +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage + +class DashboardPageConnector(actionBus: Observer[Action])(using + router: Router[Page] +): + def apply: HtmlElement = + AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala deleted file mode 100644 index a3b00af..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala +++ /dev/null @@ -1,103 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import scala.scalajs.js.annotation.JSExportTopLevel -import scala.scalajs.js.annotation.JSExport -import scala.scalajs.js.annotation.JSImport -import scala.scalajs.js -import org.scalajs.dom -import com.raquo.laminar.api.L.{*, given} -import scala.scalajs.js.Date -import com.raquo.waypoint.Router -import com.raquo.waypoint.SplitRender -import cz.e_bs.cmi.mdr.pdb.app.services.DataFetcher -import scala.scalajs.js.JSON -import zio.json._ -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.state.AppState - -@js.native -@JSImport("stylesheets/main.css", JSImport.Namespace) -object Css extends js.Any - -@js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native -@JSImport("params/pdb-params.json", JSImport.Default) -object pdbParams extends js.Object - -@JSExportTopLevel("app") -object Main: - - @JSExport - def main(args: Array[String]): Unit = - import Routes.given - onLoad { - setupAirstream() - val appContainer = dom.document.querySelector("#app") - val _ = - render( - appContainer, - renderPage(state.MockAppState(using unsafeWindowOwner, router)) - ) - } - - private def onLoad(f: => Unit): Unit = - documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) - - private def setupAirstream()(using router: Router[Page]): Unit = - AirstreamError.registerUnhandledErrorCallback(err => - router.forcePage( - Page.UnhandledError( - Some(err.getClass.getName), // TODO: Fill only in dev mode - Some(err.getMessage) - ) - ) - ) - - def renderPage(state: AppState)(using - router: Router[Page] - ): HtmlElement = - val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) - .collectSignal[Page.Detail]( - connectors - .DetailPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailParametru]( - connectors - .DetailParametruPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailKriteria]( - connectors - .DetailKriteriaPageConnector(state)(_) - .apply - ) - .collectSignal[Page.UpravDukazKriteria]( - pages.detail.UpravDukaz.Connector(state)(_).apply - ) - .collectStatic(Page.Dashboard)( - connectors.DashboardPageConnector(state.actionBus).apply - ) - .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) - ) - .collect[Page.UnhandledError](pg => - pages.errors - .UnhandledErrorPage( - pages.errors.UnhandledErrorPage - .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), - state.actionBus - ) - ) - .collectStatic(Page.Directory)( - connectors - .DirectoryPageConnector(state.users, state.actionBus) - .apply - ) - div(cls := "h-full", child <-- pageSplitter.$view) - - // Pull in the stylesheet - val css: Css.type = Css diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala deleted file mode 100644 index 3bbc8c0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ /dev/null @@ -1,198 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.* -import org.scalajs.dom -import zio.json.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import scala.scalajs.js -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.Page.Titled - -// enum is not working with Waypoints' SplitRender collectStatic -sealed abstract class Page( - val id: String, - val title: String, - val parent: Option[Page] -) { - val path: Vector[Page] = - parent match - case None => Vector(this) - case Some(p) => p.path :+ this - - val isRoot: Boolean = parent.isEmpty -} - -object Page: - - case class Titled[V](value: V, title: Option[String] = None): - val show: String = title.getOrElse(value.toString) - - case object Directory extends Page("directory", "Adresář", None) - - case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) - - case class Detail(osobniCislo: Titled[OsobniCislo]) - extends Page("user", osobniCislo.show, Some(Directory)) - - object Detail { - def apply(o: UserInfo): Detail = Detail( - Titled(o.personalNumber, Some(o.name)) - ) - } - - case class DetailParametru( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String] - ) extends Page( - "parameter", - parametr.show, - Some(Detail(osobniCislo)) - ) - - object DetailParametru { - def apply(o: UserInfo, p: Parameter): DetailParametru = - DetailParametru( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)) - ) - } - - case class DetailKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "criteria", - kriterium.show, - Some(DetailParametru(osobniCislo, parametr)) - ) - - object DetailKriteria { - def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = - DetailKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class UpravDukazKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "addProof", - "Důkaz", - Some(DetailKriteria(osobniCislo, parametr, kriterium)) - ) - - object UpravDukazKriteria { - def apply( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): UpravDukazKriteria = - UpravDukazKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class NotFound(url: String) extends Page("404", "404", Some(Directory)) - - case class UnhandledError( - errorName: Option[String], - errorMessage: Option[String] - ) extends Page("500", "Unexpected error", Some(Directory)) - -object Routes: - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) - given [V: JsonEncoder]: JsonEncoder[Titled[V]] = - DeriveJsonEncoder.gen[Titled[V]] - given [V: JsonDecoder]: JsonDecoder[Titled[V]] = - DeriveJsonDecoder.gen[Titled[V]] - given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] - given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] - - val base = - js.`import`.meta.env.BASE_URL - .asInstanceOf[String] + "app" - - val homePage: Page = Page.Directory - - given router: Router[Page] = Router[Page]( - routes = List( - Route.static(homePage, root / endOfSegments, basePath = base), - Route.static( - Page.Dashboard, - root / "dashboard" / endOfSegments, - basePath = base - ), - Route[Page.Detail, String]( - encode = _.osobniCislo.value.toString, - decode = osc => Page.Detail(Titled(OsobniCislo(osc))), - root / "osoba" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.DetailParametru, (String, String)]( - encode = p => (p.osobniCislo.value.toString, p.parametr.value), - decode = - p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / endOfSegments, - basePath = base - ), - Route[Page.DetailKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.DetailKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.UpravDukazKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.UpravDukazKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / "edit" / endOfSegments, - basePath = base - ) - ), - serializePage = _.toJson, - deserializePage = _.fromJson[Page] - .fold(s => throw IllegalStateException(s), identity), - getPageTitle = _.title, - routeFallback = url => Page.NotFound(url), - deserializeFallback = _ => Page.Dashboard - )( - $popStateEvent = windowEvents.onPopState, - owner = unsafeWindowOwner - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala deleted file mode 100644 index b24b82b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -sealed trait Action - -case object FetchDirectory extends Action -case class FetchUserDetails(osc: OsobniCislo) extends Action -case class FetchParameters(osc: OsobniCislo) extends Action -case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action -case class FetchParameterCriteria( - osc: OsobniCislo, - paramId: String, - critId: String, - page: (UserInfo, Parameter, ParameterCriteria) => Page -) extends Action -case class FetchAvailableFiles(osc: OsobniCislo) extends Action -case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala deleted file mode 100644 index c637d35..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala +++ /dev/null @@ -1,82 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.{UserProfile, UserInfo, OsobniCislo} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo -import cz.e_bs.cmi.mdr.pdb.UserFunction - -object AppPage: - // TODO: pages by logged in user - val pages: List[Page] = List(Page.Directory, Page.Dashboard) - - import NavigationBar.{Logo, MenuItem} - - val logo = Logo( - "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", - "Workflow" - ) - - // TODO: menu items by user profile - val userMenu = - List( - MenuItem("Your Profile"), - MenuItem("Settings"), - MenuItem("Sign out") - ) - - // TODO: load user profile - val $userProfile = Var( - UserProfile( - "tom", - UserInfo( - OsobniCislo("1031"), - "tom", - "Tom", - "Cook", - None, - None, - Some("tom@example.com"), - Some("+420 222 866 180"), - Some( - UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") - ), - Nil, - None - ) - ) - ) - - val $userInfo = $userProfile.signal.map(_.userInfo) - - type ViewModel = Option[HtmlElement] - def apply( - actionBus: Observer[Action] - )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using - router: Router[Page] - ): HtmlElement = - PageLayout(actionBus)( - $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => - PageLayout.ViewModel( - NavigationBar.ViewModel( - u, - pages.map(p => - NavigationBar.Link( - () => - PageLink( - p, - actionBus - ), - p == cp - ) - ), - userMenu, - logo - ), - c - ) - ), - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala deleted file mode 100644 index bf473c6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ /dev/null @@ -1,143 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden -import fiftyforms.ui.components.tailwind.list.IconText.ViewModel -import fiftyforms.ui.components.tailwind.Icons -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Action - -object Breadcrumbs: - - private def slash = - import svg.{*, given} - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-300", - xmlns := "http://www.w3.org/2000/svg", - fill := "currentColor", - viewBox := "0 0 20 20", - ariaHidden := true, - path( - d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" - ) - ) - - def backIcon = - Icons.solid - .`arrow-narrow-left`() - .amend( - svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" - ) - - object Link: - case class ViewModel( - page: Page, - icon: Option[SvgElement], - extraClasses: String, - text: String, - textClasses: Option[String] = None - ) - - val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" - - def shortHome(p: Page) = ViewModel( - p, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - "Zpět na úvodní stránku", - None - ) - - def fullHome(p: Page) = ViewModel( - p, - Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), - "text-gray-400 hover:text-gray-500", - "Domů", - Some("sr-only") - ) - - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - PageLink - .container($m.map(_.page), actionBus) - .amend( - cls <-- $m.map(_.extraClasses), - child.maybe <-- $m.map(_.icon), - span( - cls <-- $m.map(_.textClasses.getOrElse("")), - child.text <-- $m.map(_.text) - ) - ) - - object FullBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - ol( - role := "list", - cls := "flex items-center space-x-4", - children <-- $m.map(_.path).split(_.id)((_, _, $p) => - li( - div( - cls := "flex items-center", - child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), - Link( - $p.map(p => - if (p.isRoot) then Link.fullHome(p) - else - Link.ViewModel( - p, - None, - s"ml-4 max-w-xs truncate ${Link.baseClasses}", - p.title - ) - ), - actionBus - ) - ) - ) - ) - ) - - object ShortBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - Link( - $m.map { p => - val target = p.parent.getOrElse(p) - if target.isRoot then Link.shortHome(target) - else - Link.ViewModel( - target, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - s"Zpět na ${target.title}" - ) - }, - actionBus - ) - - def apply(actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - nav( - cls := "flex", - aria.label := "Breadcrumb", - div( - cls := "flex sm:hidden", - ShortBreadcrumbs(router.$currentPage, actionBus) - ), - div( - cls := "hidden sm:block", - FullBreadcrumbs(router.$currentPage, actionBus) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala deleted file mode 100644 index 0db2047..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ /dev/null @@ -1,242 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent -import fiftyforms.ui.components.tailwind.Icons -import fiftyforms.ui.components.tailwind.Avatar -import cz.e_bs.cmi.mdr.pdb.UserInfo -import io.laminext.syntax.core.* - -object NavigationBar: - - case class Logo(img: String, name: String) - case class Link(a: () => Anchor, active: Boolean) - case class MenuItem(title: String) - - case class ViewModel( - userInfo: UserInfo, - pages: List[Link], - userMenu: List[MenuItem], - logo: Logo - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - val $userInfo = $m.map(_.userInfo) - val mobileMenuOpen = Var(false) - - val desktopOnly = cls("hidden md:block") - val mobileOnly = cls("md:hidden") - - inline def avatarImage(size: Int = 8) = - Avatar($userInfo.map(_.img)).avatarImage(size) - - def notificationButton = button( - tpe := "button", - cls := List( - "bg-indigo-600", - "focus:outline-none", - "focus:ring-2", - "focus:ring-offset-2", - "focus:ring-offset-indigo-600", - "focus:ring-white", - "hover:text-white", - "p-1", - "rounded-full", - "text-indigo-200" - ), - span(cls := "sr-only", "View notifications"), - Icons.outline.bell() - ) - - def userProfile: HtmlElement = - val menuOpen = Var(false) - - def menuItem(item: MenuItem, idx: Int): HtmlElement = - a( - href := "#", - cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", - role := "menuitem", - tabIndex := -1, - idAttr := s"user-menu-item-$idx", - item.title - ) - - div( - cls := "ml-3 relative", - div( - button( - tpe := "button", - cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - idAttr := "user-menu-button", - aria.expanded <-- menuOpen.signal, - aria.hasPopup := true, - span(cls := "sr-only", "Open user menu"), - child <-- avatarImage(), - onClick.preventDefault.mapTo( - !menuOpen.now() - ) --> menuOpen.writer - ) - ), - /* - * */ - div( - cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", - cls <-- menuOpen.signal.map { o => - if (o) "md:block" else "md:hidden" - }, - role := "menu", - aria.orientation := "vertical", - aria.labelledBy := "user-menu-button", - tabIndex := -1, - // : keyboard navigation - children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) - ) - ) - - def mobileProfile = - def menuItem(item: MenuItem): HtmlElement = - a( - href := "#", - cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", - item.title - ) - - div( - cls := "pt-4 pb-3 border-t border-indigo-700", - div( - cls := "flex items-center px-5", - div( - cls := "flex-shrink-0", - child <-- avatarImage(10) - ), - div( - cls := "ml-3", - div( - cls := "text-base font-medium text-white", - child.text <-- $userInfo.map(_.name) - ), - child.maybe <-- $userInfo.map( - _.email.map(e => - div( - cls := "text-sm font-medium text-indigo-300", - e - ) - ) - ) - ), - notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) - ), - div( - cls := "mt-3 px-2 space-y-1", - children <-- $m.map(_.userMenu.map(menuItem)) - ) - ) - - def pageLink(page: Link): Anchor = - page - .a() - .amend( - cls := "text-white px-3 py-2 rounded-md text-sm font-medium", - cls := Seq( - "bg-indigo-700" -> page.active, - "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active - ), - ariaCurrent := (if page.active then "page" else "false") - ) - - def logoImg: Image = - img( - cls := "h-8 w-8", - src <-- $m.map(_.logo.img), - alt <-- $m.map(_.logo.name) - ) - - def mobileMenuButton = button( - tpe := "button", - cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - aria.controls := "mobile-menu", - aria.expanded <-- mobileMenuOpen.signal, - span(cls := "sr-only", "Open main menu"), - Icons.outline - .menu() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") - ), - Icons.outline - .x() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") - ), - onClick.preventDefault.mapTo( - !mobileMenuOpen.now() - ) --> mobileMenuOpen.writer - ) - - def navBarLeft = - div( - cls := "flex items-center", - div(cls := "flex-shrink-0", logoImg), - div( - desktopOnly, - div( - cls := "ml-10 flex items-baseline space-x-4", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ) - ) - ) - - def navBarRight = - div( - desktopOnly, - div( - cls := "ml-4 flex items-center md:ml-6", - notificationButton, - userProfile - ) - ) - - def navBarMobile = - div( - cls := "-mr-2 flex", - mobileOnly, - mobileMenuButton - ) - - def navBar = - div( - cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex items-center justify-between h-16", - navBarLeft, - navBarRight, - navBarMobile - ) - ) - - def mobileMenu = - div( - mobileOnly, - cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }, - idAttr := "mobile-menu", - div( - cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ), - mobileProfile - ) - - nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala deleted file mode 100644 index 75339df..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object PageHeader: - def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = - header( - cls := "bg-white shadow-sm", - div( - cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", - h1( - cls := "text-lg leading-6 font-semibold text-gray-900", - Breadcrumbs(actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala deleted file mode 100644 index 5096463..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ /dev/null @@ -1,30 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import fiftyforms.ui.components.tailwind.Loading -import io.laminext.syntax.core.* - -object PageLayout: - case class ViewModel( - navigation: NavigationBar.ViewModel, - content: Option[HtmlElement] - ) - def apply(actionBus: Observer[Action])( - $m: Signal[ViewModel], - mods: Modifier[HtmlElement]* - )(using router: Router[Page]): HtmlElement = - val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) - div( - cls := "h-full flex flex-col", - NavigationBar($m.map(_.navigation)), - child.maybe <-- router.$currentPage.map(_.isRoot) - .switch(None, Some(PageHeader(actionBus))), - main( - cls := "flex-grow-1 overflow-y-auto", - mods, - child <-- $maybeContent.map(_.getOrElse(Loading)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala deleted file mode 100644 index 62d820f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.LinkSupport.* -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo - -object PageLink: - type ViewModel = Page - def apply($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = - a(mods($m, actions), child.text <-- $m.map(_.title)) - - def apply(m: ViewModel, actions: Observer[Action])(using - router: Router[Page] - ): Anchor = apply(Val(m), actions) - - def container($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = a(mods($m, actions)) - - def container(m: ViewModel, actions: Observer[Action])(using - Router[Page] - ): Anchor = container(Val(m), actions) - - private def mods($m: Signal[Page], actions: Observer[Action])(using - router: Router[Page] - ): Modifier[Anchor] = - Seq( - href <-- $m.map(router.absoluteUrlForPage).recover { case _ => - Some("invalid url") - }, - composeEvents(onClick.noKeyMod.preventDefault)( - _.sample($m) - .map(NavigateTo.apply) - ) --> actions - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala deleted file mode 100644 index 87f825a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.pages.dashboard.DashboardPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -class DashboardPageConnector(actionBus: Observer[Action])(using - router: Router[Page] -): - def apply: HtmlElement = - AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala deleted file mode 100644 index e9b0f71..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala +++ /dev/null @@ -1,76 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailKriteriaPage -import pages.detail.components.DukazyKriteria -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -object DetailKriteriaPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailKriteriaPageConnector( - state: DetailKriteriaPageConnector.AppState -)( - $page: Signal[Page.DetailKriteria] -)(using Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => - (p.osobniCislo.value, p.parametr.value, p.kriterium.value) - )((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map( - FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) - ) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, s, $s) => - DetailKriteriaPage($s)(state.actionBus.contramap { - case DukazyKriteria.Add => - NavigateTo( - Page.UpravDukazKriteria( - Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), - Page.Titled(s.parametr.id, Some(s.parametr.nazev)), - Page.Titled(s.kriterium.id, Some(s.kriterium.id)) - ) - ) - }) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): DetailKriteriaPage.ViewModel = - DetailKriteriaPage.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala deleted file mode 100644 index 4d8024f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ /dev/null @@ -1,57 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import pages.detail.DetailPage -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import fiftyforms.ui.components.tailwind.Color - -object DetailPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailPageConnector(state: DetailPageConnector.AppState)( - $page: Signal[Page.Detail] -)(using router: Router[Page]): - val $oscChangeSignal = $page.splitOne(_.osobniCislo.value)((osc, _, _) => osc) - val $pageChangeSignal = - $oscChangeSignal.flatMap(osc => - EventStream.fromSeq(Seq(FetchUserDetails(osc), FetchParameters(osc))) - ) - // TODO: filter the value based on the current osc - // OSC change will fetch new data, but still - // - we need to be sure that what we got is really what we ought to display - // - we want to display stale data accordingly (at least with loading indicator) - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - def apply: HtmlElement = - AppPage(state.actionBus)( - $data.combineWithFn($params)(_ zip _) - .map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: List[Parameter] - ): DetailPage.ViewModel = - DetailPage.ViewModel( - o.toDetailOsoby, - p.map( - _.toParametr(param => - PageLink.container(Page.DetailParametru(o, param), state.actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala deleted file mode 100644 index fdaa7f8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailParametruPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink - -object DetailParametruPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailParametruPageConnector( - state: DetailParametruPageConnector.AppState -)( - $page: Signal[Page.DetailParametru] -)(using router: Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => (p.osobniCislo.value, p.parametr.value))((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map(FetchParameter(_, _)) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - } yield (da, pb) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailParametruPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter - ): DetailParametruPage.ViewModel = - DetailParametruPage.ViewModel( - o.toDetailOsoby, - p.toParametr(p => - PageLink.container(Page.DetailParametru(o, p), state.actionBus) - ), - p.criteria.map( - _.toKriterium { c => - PageLink.container( - Page.DetailKriteria(o, p, c), - state.actionBus - ) - } - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala deleted file mode 100644 index 049aa22..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -case class DirectoryPageConnector( - $input: EventStream[List[UserInfo]], - actionBus: Observer[Action] -)(using router: Router[Page]): - val $data = $input.startWithNone - val $actionSignal = EventStream.fromValue(FetchDirectory) - - def apply: HtmlElement = - AppPage(actionBus)( - $data.split(_ => ())((_, _, s) => - pages.directory.DirectoryPage( - s.map( - _.map( - _.toUserRow(u => - PageLink.container( - Page.Detail(Page.Titled(u.personalNumber)), - actionBus - ) - ) - ) - ) - ) - ), - $actionSignal --> actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala deleted file mode 100644 index 88e7450..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ /dev/null @@ -1,68 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii -import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria -import fiftyforms.ui.components.tailwind.Color - -extension (o: UserInfo) - def toDetailOsoby: DetailOsoby.ViewModel = - DetailOsoby.ViewModel( - o.personalNumber, - o.name, - o.email, - o.phone, - o.img, - o.mainFunction.map(f => - DetailOsoby.Funkce.ViewModel(f.name, f.dept, f.ou) - ), - o.userContracts.headOption.map(c => - DetailOsoby.PracovniPomer.ViewModel(c.rel, c.startDate, c.endDate) - ) - ) - -extension (param: Parameter) - def toParametr(container: Parameter => Anchor): DetailParametru.ViewModel = - DetailParametru.ViewModel( - id = param.id, - nazev = param.name, - popis = param.description, - status = "Nesplněno", - statusColor = Color.red, - a = container(param) - ) - -extension (crit: ParameterCriteria) - def toKriterium( - container: ParameterCriteria => Anchor - ): DetailKriteria.ViewModel = - DetailKriteria.ViewModel( - nazev = crit.criteriumText, - kapitola = crit.chapterId, - bod = crit.itemId, - status = "Nesplněno", - statusColor = Color.red, - splneno = false, - dukazy = Nil, - container = container(crit) - ) - -extension (user: UserInfo) - def toUserRow( - container: UserInfo => HtmlElement = (_: UserInfo) => div() - ): UserRow.ViewModel = - UserRow.ViewModel( - osobniCislo = user.personalNumber.toString, - celeJmeno = user.name, - prijmeni = user.surname, - hlavniFunkce = user.mainFunction.map(_.name), - img = user.img, - container = () => container(user) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala deleted file mode 100644 index 7d5b2c1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.dashboard - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.Page - -object DashboardPage: - - def render: HtmlElement = - div("Dashboard page") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala deleted file mode 100644 index 9ef0698..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala +++ /dev/null @@ -1,31 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailKriteriaPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply($m: Signal[ViewModel])( - action: Observer[DukazyKriteria.Action] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - DukazyKriteria($m.map(_.kriterium.dukazy))(action) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala deleted file mode 100644 index 3b316e1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object DetailPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametry: SeznamParametru.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby($m.map(_.osoba)), - SeznamParametru($m.map(_.parametry)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala deleted file mode 100644 index 17224cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala +++ /dev/null @@ -1,24 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailParametruPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriteria: SeznamKriterii.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby.header($m.map(_.osoba)), - DetailParametru($m.map(_.parametr)), - SeznamKriterii($m.map(_.kriteria)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala deleted file mode 100644 index dc912fa..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ /dev/null @@ -1,107 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb -package app -package pages.detail - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.UpravDukazForm -import fiftyforms.services.files.File - -object UpravDukaz: - - type ThisPage = Page.UpravDukazKriteria - type PageKey = (OsobniCislo, String, String) - - trait State { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def availableFiles: EventStream[List[File]] - def actionBus: Observer[Action] - } - - def keyOfPage(page: ThisPage): PageKey = - (page.osobniCislo.value, page.parametr.value, page.kriterium.value) - - def onChangeAction(key: PageKey): Action = - FetchParameterCriteria( - key._1, - key._2, - key._3, - Page.UpravDukazKriteria(_, _, _) - ) - - class Connector(state: State)($page: Signal[ThisPage])(using Router[Page]): - val $paramChangeSignal = $page.splitOne(keyOfPage)((x, _, _) => x) - val $pageChangeSignal = $paramChangeSignal.map(onChangeAction) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.split(_ => ())((_, s, $s) => - PageComponent( - $s.map(buildModel), - state.availableFiles, - state.actionBus.contramap { - case UpravDukazForm.Cancelled => - NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) - case UpravDukazForm.AvailableFilesRequested => - FetchAvailableFiles(s._1.personalNumber) - } - ) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): PageComponent.ViewModel = - import connectors.* - PageComponent.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) - - object PageComponent: - import components.* - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply( - $m: Signal[ViewModel], - availableFilesStream: EventStream[List[File]], - events: Observer[UpravDukazForm.Event] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - UpravDukazForm(availableFilesStream)(events) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala deleted file mode 100644 index ca311b0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.Icons -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Color - -object DetailKriteria: - case class ViewModel( - nazev: String, - kapitola: String, - bod: String, - status: String, - statusColor: Color, - splneno: Boolean, - dukazy: List[DukazKriteria.ViewModel], - container: HtmlElement = div() - ) { - val id = s"${kapitola}${bod}" - } - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h3( - cls := "text-xl leading-6 font-bold text-gray-900", - child.text <-- $m.map(_.nazev) - ), - h3( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - "Kapitola ", - child.text <-- $m.map(_.kapitola), - " bod ", - child <-- $m.map(_.bod) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala deleted file mode 100644 index ae90290..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala +++ /dev/null @@ -1,100 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Avatar - -object DetailOsoby: - - object Funkce: - case class ViewModel( - nazev: String, - stredisko: String, - voj: String - ) - - def render($m: Signal[ViewModel]): HtmlElement = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.nazev), - span( - cls := "hidden md:inline", - " @ ", - child.text <-- $m.map(_.stredisko), - ", ", - child.text <-- $m.map(_.voj) - ) - ) - - object PracovniPomer: - case class ViewModel( - druh: String, - pocatek: LocalDate, - konec: Option[LocalDate] - ) - - def render($m: Signal[ViewModel]): HtmlElement = - import fiftyforms.ui.components.tailwind.CustomAttrs.datetime - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.druh), - " od ", - time( - datetime <-- $m.map(_.pocatek.toString), - child.text <-- $m.map(_.pocatek.toString) - ) - ) - - case class ViewModel( - osobniCislo: OsobniCislo, - jmeno: String, - email: Option[String], - telefon: Option[String], - img: Option[String], - hlavniFunkce: Option[Funkce.ViewModel], - pracovniPomer: Option[PracovniPomer.ViewModel] - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-4", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(16) - ), - div( - h1( - cls := "text-2xl font-bold text-gray-900", - child.text <-- $m.map(_.jmeno) - ), - child.maybe <-- $m.map(_.hlavniFunkce).split(_ => ())((_, _, d) => - Funkce.render(d) - ), - child.maybe <-- $m.map(_.pracovniPomer).split(_ => ())((_, _, d) => - PracovniPomer.render(d) - ) - ) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-5", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(8) - ), - div( - h1( - cls := "text-lg font-medium text-gray-800", - child.text <-- $m.map(_.jmeno) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala deleted file mode 100644 index e11cb62..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color - -object DetailParametru: - case class ViewModel( - id: String, - nazev: String, - popis: String, - status: String, - statusColor: Color, - a: Anchor - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h2( - cls := "text-2xl leading-6 font-medium text-gray-900", - child.text <-- $m.map(_.nazev) - ), - p( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - child.text <-- $m.map(_.popis) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - h2( - cls := "text-lg leading-6 font-medium text-gray-600", - child.text <-- $m.map(_.nazev) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala deleted file mode 100644 index a00ede8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala +++ /dev/null @@ -1,177 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.CustomAttrs - -object DukazKriteria: - case class Osoba(osobniCislo: String, jmeno: String) - case class Udalost(osoba: Osoba, datum: LocalDate) - case class Dokument( - url: String, - nazev: String, - pridal: Udalost, - odebral: Option[Udalost] - ) - case class ViewModel( - dokumenty: List[Dokument], - autorizoval: Option[Udalost], - platiDo: Option[LocalDate], - poznámka: Option[String] - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-lg", - div( - cls := "px-4 py-5 sm:px-6", - h3( - cls := "text-lg leading-6 font-medium text-gray-900", - """Applicant Information""" - ), - p( - cls := "mt-1 max-w-2xl text-sm text-gray-500", - """Personal details and application.""" - ) - ), - div( - cls := "border-t border-gray-200 px-4 py-5 sm:px-6", - dl( - cls := "grid grid-cols-1 gap-x-4 gap-y-8 sm:grid-cols-2", - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Full name""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Margot Foster""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Application for""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Backend Developer""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Email address""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """margotfoster@example.com""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Salary expectation""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """$120,000""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """About""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Fugiat ipsum ipsum deserunt culpa aute sint do nostrud anim incididunt cillum culpa consequat. Excepteur qui ipsum aliquip consequat sint. Sit id mollit nulla mollit nostrud in ea officia proident. Irure nostrud pariatur mollit ad adipisicing reprehenderit deserunt qui eu.""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """Attachments""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - ul( - role := "list", - cls := "border border-gray-200 rounded-md divide-y divide-gray-200", - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """resume_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ), - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """coverletter_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala deleted file mode 100644 index 22f134f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala +++ /dev/null @@ -1,35 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object DukazyKriteria: - sealed trait Action - case object Add extends Action - - type ViewModel = List[DukazKriteria.ViewModel] - - def apply($m: Signal[ViewModel])(actions: Observer[Action]): HtmlElement = - div( - child.maybe <-- $m.map(m => - if (m.isEmpty) then Some(prazdnyDukaz(actions)) - else None - ), - children <-- $m.map(_.zipWithIndex).split(_._2)((_, _, $s) => - DukazKriteria($s.map(_._1)) - ) - ) - - private def prazdnyDukaz(actions: Observer[Action]): HtmlElement = - button( - tpe := "button", - onClick.preventDefault.mapTo(Add) --> actions, - cls := "relative block w-full border-2 border-gray-300 border-dashed rounded-lg p-12 text-center hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - Icons.outline - .`document-add`(12) - .amend(svg.cls := "mx-auto text-gray-400"), - span( - cls := "mt-2 block text-sm font-medium text-gray-900", - "Přidat důkaz" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala deleted file mode 100644 index f628697..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.list.StackedList -import fiftyforms.ui.components.tailwind.list.ListRow -import fiftyforms.ui.components.tailwind.list.RowTag -import fiftyforms.ui.components.tailwind.list.RowNext -import java.time.LocalDate - -object SeznamKriterii: - type ViewModel = List[DetailKriteria.ViewModel] - - private val kritList = new StackedList[DetailKriteria.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - kritList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = - p(cls := "text-sm text-gray-500", s"${i.kapitola}${i.bod}"), - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.container - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala deleted file mode 100644 index 2bfe4bc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.list.{ - StackedList, - ListRow, - RowTag, - PropList, - IconText, - RowNext -} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.LinkSupport.* - -object SeznamParametru: - type ViewModel = List[DetailParametru.ViewModel] - - private val parametrList = new StackedList[DetailParametru.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - parametrList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = emptyNode, - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.a - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala deleted file mode 100644 index f048e9d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ /dev/null @@ -1,101 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.form.* -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement -import fiftyforms.services.files.components.tailwind.FilePicker -import cz.e_bs.cmi.mdr.pdb.frontend.AutorizujDukaz -import cz.e_bs.cmi.mdr.pdb.frontend.DocumentRef -import fiftyforms.services.files.File - -object UpravDukazForm: - sealed trait Event - case object Cancelled extends Event - case object AvailableFilesRequested extends Event - def apply(availableFilesStream: EventStream[List[File]])( - updates: Observer[Event] - ): HtmlElement = - val (filesStream, filesObserver) = - EventStream.withObserver[FilePicker.Event] - val files = Var[List[File]](Nil) - def submitButtons: HtmlElement = - div( - cls := "pt-5", - div( - cls := "flex justify-end", - button( - tpe := "button", - cls := "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zrušit", - onClick.mapTo(Cancelled) --> updates - ), - button( - tpe := "submit", - cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - disabled <-- files.signal.map(_.isEmpty), - "Autorizovat" - ) - ) - ) - - div( - filesStream.collect { case FilePicker.SelectionUpdated(files) => - files.to(List) - } --> files.writer, - filesStream.collect { case FilePicker.AvailableFilesRequested => - AvailableFilesRequested - } --> updates, - cls := "bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6", - Form( - Form.Body( - Form.Section( - FormHeader( - FormHeader.ViewModel( - "Doložení kritéria", - "Sestavte doklady poskytující důkaz kritéria a potvrďte odesláním formuláře. Případné limitace či jiné relevantní údaje vepište do pole Komentář." - ) - ), - FormFields( - FormRow( - "dokumenty", - "Dokumenty", - FilePicker(files.signal, availableFilesStream)(filesObserver) - .amend(idAttr := "dokumenty", cls("max-w-lg")) - ).toHtml, - FormRow( - "platnost", - "Platnost", - input( - idAttr := "platnost", - name := "platnost", - tpe := "date", - autoComplete := "date", - cls := "block max-w-lg w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" - ) - ).toHtml, - FormRow( - "komentar", - "Komentář", - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - textArea( - idAttr := "komentar", - name := "about", - rows := 3, - cls := "max-w-lg shadow-sm block w-full focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border border-gray-300 rounded-md" - ), - p( - cls := "mt-2 text-sm text-gray-500", - "Doplňte prosím potřebné informace související s doložením kritéria, včetně případných limitací." - ) - ) - ).toHtml - ) - ) - ), - submitButtons - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala deleted file mode 100644 index 41033f6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -import components._ - -object DirectoryPage: - - type ViewModel = List[UserRow.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - val (actionsStream, actionObserver) = - EventStream.withObserver[SearchForm.Action] - val $filter = actionsStream - .collect { - case SearchForm.ClearFilter => None - case SearchForm.SetFilter(t) => Some(t) - } - .startWith(None) - val byLetter = for { - d <- $m - f <- $filter - } yield for { - (letter, users) <- d - .filter { user => - f match - case None => true - case Some(t) => user.search.contains(t) - } - .groupBy(_.prijmeni.head) - .to(List) - .sortBy(_._1) - } yield (letter.toString, users.sortBy(_.prijmeni)) - - div( - cls := "h-full max-w-7xl mx-auto order-first flex flex-col flex-shrink-0", - SearchForm(actionObserver), - Directory(byLetter) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala deleted file mode 100644 index 73f915d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -object Directory: - - object Header: - type ViewModel = String - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500", - h3(child.text <-- $m) - ) - - object UserList: - type ViewModel = List[UserRow.ViewModel] - def apply($m: Signal[ViewModel]): HtmlElement = - ul( - role := "list", - cls := "relative z-0 divide-y divide-gray-200", - children <-- $m.split(_.osobniCislo)((_, _, s) => UserRow(s)) - ) - - type ViewModel = List[(String, List[UserRow.ViewModel])] - def apply($m: Signal[ViewModel]): HtmlElement = - nav( - cls := "flex-1 min-h-0 overflow-y-auto", - aria.label := "Directory", - children <-- $m.split(_._1)((_, _, s) => - div( - cls := "relative", - Header(s.map(_._1)), - UserList(s.map(_._2)) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala deleted file mode 100644 index e182910..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala +++ /dev/null @@ -1,54 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object SearchForm: - sealed trait Action - case object SubmitFilter extends Action - sealed trait FilterAction extends Action - case object ClearFilter extends FilterAction - case class SetFilter(value: String) extends FilterAction - - def apply(actions: Observer[Action]): HtmlElement = - div( - cls := "px-6 pt-4 pb-4", - form( - cls := "flex space-x-4", - onSubmit.mapTo(SubmitFilter) --> actions, - div( - cls := "flex-1 min-w-0", - label( - forId := "search", - cls := "sr-only", - "Hledat" - ), - div( - cls := "relative rounded-md shadow-sm", - div( - cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", - Icons.solid.search().amend(svg.cls := "text-gray-400") - ), - input( - tpe := "search", - name := "search", - idAttr := "search", - cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", - placeholder := "Hledat", - composeEvents(onInput.mapToValue.setAsValue.map(SetFilter(_)))( - _.throttle(500) - ) --> actions - ) - ) - ), - button( - tpe := "submit", - cls := "inline-flex justify-center px-3.5 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500", - Icons.solid.filter().amend(svg.cls := "text-gray-400"), - span( - cls := "sr-only", - "Hledat" - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala deleted file mode 100644 index 82f7723..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala +++ /dev/null @@ -1,52 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.airstream.core.Signal -import fiftyforms.ui.components.tailwind.Avatar - -object UserRow: - case class ViewModel( - osobniCislo: String, - celeJmeno: String, - prijmeni: String, - hlavniFunkce: Option[String], - img: Option[String], - container: () => HtmlElement = () => div() - ) { - val search = prijmeni.toLowerCase - } - - def apply($m: Signal[ViewModel]): HtmlElement = - inline def avatarImage = - Avatar($m.map(_.img)).avatarImage(10) - - li( - div( - cls := "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500", - div( - cls := "flex-shrink-0", - child <-- avatarImage - ), - div( - cls := "flex-1 min-w-0", - child <-- $m.map { o => - o.container() - .amend( - cls := "focus:outline-none", - span( - cls := "absolute inset-0", - aria.hidden := true - ), - p( - cls := "text-sm font-medium text-gray-900", - o.celeJmeno - ), - p( - cls := "text-sm text-gray-500 truncate", - o.hlavniFunkce - ) - ) - } - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala deleted file mode 100644 index 467c5f5..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala +++ /dev/null @@ -1,99 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router - -object ErrorPage: - case class ViewModel( - homePage: Page, - errorName: String, - title: String, - subTitle: String - ) - - def apply(m: ViewModel, actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - val ViewModel(homePage, errorName, title, subTitle) = m - div( - cls := "min-h-full pt-16 pb-12 flex flex-col bg-white", - main( - cls := "flex-grow flex flex-col justify-center max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex-shrink-0 flex justify-center", - a( - href := "/", - cls := "inline-flex", - span( - cls := "sr-only", - """Workflow""" - ), - img( - cls := "h-12 w-auto", - src := "https://tailwindui.com/img/logos/workflow-mark.svg?color=indigo&shade=600", - alt := "" - ) - ) - ), - div( - cls := "py-16", - div( - cls := "text-center", - p( - cls := "text-sm font-semibold text-indigo-600 uppercase tracking-wide", - errorName - ), - h1( - cls := "mt-2 text-4xl font-extrabold text-gray-900 tracking-tight sm:text-5xl", - title - ), - p( - cls := "mt-2 text-base text-gray-500", - subTitle - ), - div( - cls := "mt-6", - PageLink - .container(homePage, actionBus) - .amend( - cls := "text-base font-medium text-indigo-600 hover:text-indigo-500", - """Go back home""" - ) - ) - ) - ) - ), - footer( - cls := "flex-shrink-0 max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", - nav( - cls := "flex justify-center space-x-4", - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Contact Support""" - ), - span( - cls := "inline-block border-l border-gray-300", - aria.hidden := true - ), - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Status""" - ), - span( - cls := "inline-block border-l border-gray-300", - aria.hidden := true - ), - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Twitter""" - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala deleted file mode 100644 index bd9baec..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action - -object NotFoundPage: - def apply(homePage: Page, url: String, actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - ErrorPage( - ErrorPage.ViewModel( - homePage, - "404 error", - "Page not found.", - s"Sorry, but page $url doesn't exist." - ), - actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala deleted file mode 100644 index 476fe3c..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action - -object UnhandledErrorPage: - - case class ViewModel( - homePage: Page, - errorName: Option[String], - errorMessage: Option[String] - ) - - def apply(m: ViewModel, actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - ErrorPage( - ErrorPage.ViewModel( - m.homePage, - "Unexpected error occurred", - m.errorName.getOrElse( - "Uh oh!" - ), // TODO: translations, better text than uh oh - m.errorMessage.getOrElse( - "This wasn't supposed to happen! Please try again." - ) - ), - actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala deleted file mode 100644 index 25f059f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala +++ /dev/null @@ -1,6 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.services - -import com.raquo.airstream.core.Observer - -trait DataFetcher[K, A]: - def fetch(id: K, sink: Observer[A]): Unit diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala deleted file mode 100644 index a469830..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala +++ /dev/null @@ -1,120 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package state - -import com.raquo.airstream.core.EventStream -import cz.e_bs.cmi.mdr.pdb.{UserInfo, OsobniCislo} -import com.raquo.airstream.core.Observer -import scala.scalajs.js -import scala.scalajs.js.JSON -import zio.json._ -import com.raquo.airstream.eventbus.EventBus -import com.raquo.airstream.ownership.Owner -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.UserFunction -import cz.e_bs.cmi.mdr.pdb.UserContract -import fiftyforms.services.files.File - -trait AppState - extends connectors.DetailPageConnector.AppState - with connectors.DetailParametruPageConnector.AppState - with connectors.DetailKriteriaPageConnector.AppState - with pages.detail.UpravDukaz.State: - def users: EventStream[List[UserInfo]] - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - -class MockAppState(implicit owner: Owner, router: Router[Page]) - extends AppState: - - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonDecoder[UserFunction] = DeriveJsonDecoder.gen - given JsonDecoder[UserContract] = DeriveJsonDecoder.gen - given JsonDecoder[UserInfo] = DeriveJsonDecoder.gen - - given JsonDecoder[ParameterCriteria] = DeriveJsonDecoder.gen - given JsonDecoder[Parameter] = DeriveJsonDecoder.gen - - private val actions = EventBus[Action]() - private val (parametersStream, pushParameters) = - EventStream.withCallback[List[Parameter]] - private val (usersStream, pushUsers) = - EventStream.withCallback[List[UserInfo]] - private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] - private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] - - private val mockData: List[UserInfo] = - mockUsers - .asInstanceOf[js.Dictionary[js.Object]] - .values - // TODO: is there a more efficient way to parse from JS object directly? - .map { o => - val parsed = JSON.stringify(o).fromJson[UserInfo] - parsed.left.foreach(org.scalajs.dom.console.log(_)) - parsed - } - .collect { case Right(u) => - u - } - .toList - - private val mockParameters: List[Parameter] = - pdbParams - .asInstanceOf[js.Dictionary[js.Object]] - .values - .map(o => JSON.stringify(o).fromJson[Parameter]) - .collect { case Right(p) => p } - .toList - - // TODO: Extract to separate event handler - actions.events.foreach { - case FetchDirectory => pushUsers(mockData) - case FetchUserDetails(osc) => - mockData.find(_.personalNumber == osc).foreach { o => - pushDetails(o) - router.replaceState(Page.Detail(o)) - } - case FetchParameters(osc) => - pushParameters(mockParameters) - case FetchParameter(osc, paramId) => - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(Page.DetailParametru(o, p)) - case FetchParameterCriteria(osc, paramId, critId, page) => - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - c <- p.criteria.find(_.id == critId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(page(o, p, c)) - case NavigateTo(page) => router.pushState(page) - case FetchAvailableFiles(osc) => - pushFiles( - List( - File("https://tc163.cmi.cz/here", "Example file") - ) - ) - } - - override def users: EventStream[List[UserInfo]] = - usersStream.debugWithName("users") - - override def details: EventStream[UserInfo] = - detailsStream.debugWithName("details") - - override def parameters: EventStream[List[Parameter]] = - parametersStream.debugWithName("parameters") - - override def availableFiles: EventStream[List[File]] = - filesStream.debugWithName("available files") - - override def actionBus: Observer[Action] = - actions.writer.debugWithName("actions writer") diff --git a/app/src/main/scala/mdr/pdb/app/Main.scala b/app/src/main/scala/mdr/pdb/app/Main.scala new file mode 100644 index 0000000..081b4ee --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/Main.scala @@ -0,0 +1,103 @@ +package mdr.pdb.app + +import scala.scalajs.js.annotation.JSExportTopLevel +import scala.scalajs.js.annotation.JSExport +import scala.scalajs.js.annotation.JSImport +import scala.scalajs.js +import org.scalajs.dom +import com.raquo.laminar.api.L.{*, given} +import scala.scalajs.js.Date +import com.raquo.waypoint.Router +import com.raquo.waypoint.SplitRender +import mdr.pdb.app.services.DataFetcher +import scala.scalajs.js.JSON +import zio.json._ +import mdr.pdb.UserInfo +import mdr.pdb.app.state.AppState + +@js.native +@JSImport("stylesheets/main.css", JSImport.Namespace) +object Css extends js.Any + +@js.native +@JSImport("data/users.json", JSImport.Default) +object mockUsers extends js.Object + +@js.native +@JSImport("params/pdb-params.json", JSImport.Default) +object pdbParams extends js.Object + +@JSExportTopLevel("app") +object Main: + + @JSExport + def main(args: Array[String]): Unit = + import Routes.given + onLoad { + setupAirstream() + val appContainer = dom.document.querySelector("#app") + val _ = + render( + appContainer, + renderPage(state.MockAppState(using unsafeWindowOwner, router)) + ) + } + + private def onLoad(f: => Unit): Unit = + documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) + + private def setupAirstream()(using router: Router[Page]): Unit = + AirstreamError.registerUnhandledErrorCallback(err => + router.forcePage( + Page.UnhandledError( + Some(err.getClass.getName), // TODO: Fill only in dev mode + Some(err.getMessage) + ) + ) + ) + + def renderPage(state: AppState)(using + router: Router[Page] + ): HtmlElement = + val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) + .collectSignal[Page.Detail]( + connectors + .DetailPageConnector(state)(_) + .apply + ) + .collectSignal[Page.DetailParametru]( + connectors + .DetailParametruPageConnector(state)(_) + .apply + ) + .collectSignal[Page.DetailKriteria]( + connectors + .DetailKriteriaPageConnector(state)(_) + .apply + ) + .collectSignal[Page.UpravDukazKriteria]( + pages.detail.UpravDukaz.Connector(state)(_).apply + ) + .collectStatic(Page.Dashboard)( + connectors.DashboardPageConnector(state.actionBus).apply + ) + .collect[Page.NotFound](pg => + pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) + ) + .collect[Page.UnhandledError](pg => + pages.errors + .UnhandledErrorPage( + pages.errors.UnhandledErrorPage + .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), + state.actionBus + ) + ) + .collectStatic(Page.Directory)( + connectors + .DirectoryPageConnector(state.users, state.actionBus) + .apply + ) + div(cls := "h-full", child <-- pageSplitter.$view) + + // Pull in the stylesheet + val css: Css.type = Css diff --git a/app/src/main/scala/mdr/pdb/app/Routes.scala b/app/src/main/scala/mdr/pdb/app/Routes.scala new file mode 100644 index 0000000..2cc860c --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/Routes.scala @@ -0,0 +1,198 @@ +package mdr.pdb.app + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.* +import org.scalajs.dom +import zio.json.{*, given} +import mdr.pdb.OsobniCislo + +import scala.scalajs.js +import mdr.pdb.UserInfo +import mdr.pdb.Parameter +import mdr.pdb.ParameterCriteria +import mdr.pdb.app.Page.Titled + +// enum is not working with Waypoints' SplitRender collectStatic +sealed abstract class Page( + val id: String, + val title: String, + val parent: Option[Page] +) { + val path: Vector[Page] = + parent match + case None => Vector(this) + case Some(p) => p.path :+ this + + val isRoot: Boolean = parent.isEmpty +} + +object Page: + + case class Titled[V](value: V, title: Option[String] = None): + val show: String = title.getOrElse(value.toString) + + case object Directory extends Page("directory", "Adresář", None) + + case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) + + case class Detail(osobniCislo: Titled[OsobniCislo]) + extends Page("user", osobniCislo.show, Some(Directory)) + + object Detail { + def apply(o: UserInfo): Detail = Detail( + Titled(o.personalNumber, Some(o.name)) + ) + } + + case class DetailParametru( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String] + ) extends Page( + "parameter", + parametr.show, + Some(Detail(osobniCislo)) + ) + + object DetailParametru { + def apply(o: UserInfo, p: Parameter): DetailParametru = + DetailParametru( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)) + ) + } + + case class DetailKriteria( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String], + kriterium: Titled[String] + ) extends Page( + "criteria", + kriterium.show, + Some(DetailParametru(osobniCislo, parametr)) + ) + + object DetailKriteria { + def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = + DetailKriteria( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)), + Titled(k.id, Some(k.id)) + ) + } + + case class UpravDukazKriteria( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String], + kriterium: Titled[String] + ) extends Page( + "addProof", + "Důkaz", + Some(DetailKriteria(osobniCislo, parametr, kriterium)) + ) + + object UpravDukazKriteria { + def apply( + o: UserInfo, + p: Parameter, + k: ParameterCriteria + ): UpravDukazKriteria = + UpravDukazKriteria( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)), + Titled(k.id, Some(k.id)) + ) + } + + case class NotFound(url: String) extends Page("404", "404", Some(Directory)) + + case class UnhandledError( + errorName: Option[String], + errorMessage: Option[String] + ) extends Page("500", "Unexpected error", Some(Directory)) + +object Routes: + given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) + given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) + given [V: JsonEncoder]: JsonEncoder[Titled[V]] = + DeriveJsonEncoder.gen[Titled[V]] + given [V: JsonDecoder]: JsonDecoder[Titled[V]] = + DeriveJsonDecoder.gen[Titled[V]] + given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] + given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] + + val base = + js.`import`.meta.env.BASE_URL + .asInstanceOf[String] + "app" + + val homePage: Page = Page.Directory + + given router: Router[Page] = Router[Page]( + routes = List( + Route.static(homePage, root / endOfSegments, basePath = base), + Route.static( + Page.Dashboard, + root / "dashboard" / endOfSegments, + basePath = base + ), + Route[Page.Detail, String]( + encode = _.osobniCislo.value.toString, + decode = osc => Page.Detail(Titled(OsobniCislo(osc))), + root / "osoba" / segment[String] / endOfSegments, + basePath = base + ), + Route[Page.DetailParametru, (String, String)]( + encode = p => (p.osobniCislo.value.toString, p.parametr.value), + decode = + p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / endOfSegments, + basePath = base + ), + Route[Page.DetailKriteria, (String, String, String)]( + encode = p => + ( + p.osobniCislo.value.toString, + p.parametr.value, + p.kriterium.value.replaceAll("\\.", "--") + ), + decode = p => + Page.DetailKriteria( + Titled(OsobniCislo(p._1)), + Titled(p._2), + Titled(p._3.replaceAll("--", ".")) + ), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / "kriterium" / segment[String] / endOfSegments, + basePath = base + ), + Route[Page.UpravDukazKriteria, (String, String, String)]( + encode = p => + ( + p.osobniCislo.value.toString, + p.parametr.value, + p.kriterium.value.replaceAll("\\.", "--") + ), + decode = p => + Page.UpravDukazKriteria( + Titled(OsobniCislo(p._1)), + Titled(p._2), + Titled(p._3.replaceAll("--", ".")) + ), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / "kriterium" / segment[String] / "edit" / endOfSegments, + basePath = base + ) + ), + serializePage = _.toJson, + deserializePage = _.fromJson[Page] + .fold(s => throw IllegalStateException(s), identity), + getPageTitle = _.title, + routeFallback = url => Page.NotFound(url), + deserializeFallback = _ => Page.Dashboard + )( + $popStateEvent = windowEvents.onPopState, + owner = unsafeWindowOwner + ) diff --git a/app/src/main/scala/mdr/pdb/app/actions.scala b/app/src/main/scala/mdr/pdb/app/actions.scala new file mode 100644 index 0000000..d078b3a --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/actions.scala @@ -0,0 +1,21 @@ +package mdr.pdb.app + +import mdr.pdb.OsobniCislo +import mdr.pdb.UserInfo +import mdr.pdb.Parameter +import mdr.pdb.ParameterCriteria + +sealed trait Action + +case object FetchDirectory extends Action +case class FetchUserDetails(osc: OsobniCislo) extends Action +case class FetchParameters(osc: OsobniCislo) extends Action +case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action +case class FetchParameterCriteria( + osc: OsobniCislo, + paramId: String, + critId: String, + page: (UserInfo, Parameter, ParameterCriteria) => Page +) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action +case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/mdr/pdb/app/components/AppPage.scala new file mode 100644 index 0000000..130c817 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/AppPage.scala @@ -0,0 +1,82 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.Page +import mdr.pdb.{UserProfile, UserInfo, OsobniCislo} +import com.raquo.waypoint.Router +import mdr.pdb.app.Action +import mdr.pdb.app.NavigateTo +import mdr.pdb.UserFunction + +object AppPage: + // TODO: pages by logged in user + val pages: List[Page] = List(Page.Directory, Page.Dashboard) + + import NavigationBar.{Logo, MenuItem} + + val logo = Logo( + "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", + "Workflow" + ) + + // TODO: menu items by user profile + val userMenu = + List( + MenuItem("Your Profile"), + MenuItem("Settings"), + MenuItem("Sign out") + ) + + // TODO: load user profile + val $userProfile = Var( + UserProfile( + "tom", + UserInfo( + OsobniCislo("1031"), + "tom", + "Tom", + "Cook", + None, + None, + Some("tom@example.com"), + Some("+420 222 866 180"), + Some( + UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") + ), + Nil, + None + ) + ) + ) + + val $userInfo = $userProfile.signal.map(_.userInfo) + + type ViewModel = Option[HtmlElement] + def apply( + actionBus: Observer[Action] + )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using + router: Router[Page] + ): HtmlElement = + PageLayout(actionBus)( + $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => + PageLayout.ViewModel( + NavigationBar.ViewModel( + u, + pages.map(p => + NavigationBar.Link( + () => + PageLink( + p, + actionBus + ), + p == cp + ) + ), + userMenu, + logo + ), + c + ) + ), + mods + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala new file mode 100644 index 0000000..9375398 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala @@ -0,0 +1,143 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.tailwind.list.IconText.ViewModel +import fiftyforms.ui.components.tailwind.Icons +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.Page + +import com.raquo.laminar.api.L.{*, given} +import io.laminext.syntax.core.{*, given} +import mdr.pdb.app.Action + +object Breadcrumbs: + + private def slash = + import svg.{*, given} + svg( + cls := "flex-shrink-0 h-5 w-5 text-gray-300", + xmlns := "http://www.w3.org/2000/svg", + fill := "currentColor", + viewBox := "0 0 20 20", + ariaHidden := true, + path( + d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" + ) + ) + + def backIcon = + Icons.solid + .`arrow-narrow-left`() + .amend( + svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" + ) + + object Link: + case class ViewModel( + page: Page, + icon: Option[SvgElement], + extraClasses: String, + text: String, + textClasses: Option[String] = None + ) + + val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" + + def shortHome(p: Page) = ViewModel( + p, + Some(backIcon), + "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", + "Zpět na úvodní stránku", + None + ) + + def fullHome(p: Page) = ViewModel( + p, + Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), + "text-gray-400 hover:text-gray-500", + "Domů", + Some("sr-only") + ) + + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + PageLink + .container($m.map(_.page), actionBus) + .amend( + cls <-- $m.map(_.extraClasses), + child.maybe <-- $m.map(_.icon), + span( + cls <-- $m.map(_.textClasses.getOrElse("")), + child.text <-- $m.map(_.text) + ) + ) + + object FullBreadcrumbs: + type ViewModel = Page + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + ol( + role := "list", + cls := "flex items-center space-x-4", + children <-- $m.map(_.path).split(_.id)((_, _, $p) => + li( + div( + cls := "flex items-center", + child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), + Link( + $p.map(p => + if (p.isRoot) then Link.fullHome(p) + else + Link.ViewModel( + p, + None, + s"ml-4 max-w-xs truncate ${Link.baseClasses}", + p.title + ) + ), + actionBus + ) + ) + ) + ) + ) + + object ShortBreadcrumbs: + type ViewModel = Page + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + Link( + $m.map { p => + val target = p.parent.getOrElse(p) + if target.isRoot then Link.shortHome(target) + else + Link.ViewModel( + target, + Some(backIcon), + "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", + s"Zpět na ${target.title}" + ) + }, + actionBus + ) + + def apply(actionBus: Observer[Action])(using + router: Router[Page] + ): HtmlElement = + nav( + cls := "flex", + aria.label := "Breadcrumb", + div( + cls := "flex sm:hidden", + ShortBreadcrumbs(router.$currentPage, actionBus) + ), + div( + cls := "hidden sm:block", + FullBreadcrumbs(router.$currentPage, actionBus) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala new file mode 100644 index 0000000..4bb8b8d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala @@ -0,0 +1,242 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent +import fiftyforms.ui.components.tailwind.Icons +import fiftyforms.ui.components.tailwind.Avatar +import mdr.pdb.UserInfo +import io.laminext.syntax.core.* + +object NavigationBar: + + case class Logo(img: String, name: String) + case class Link(a: () => Anchor, active: Boolean) + case class MenuItem(title: String) + + case class ViewModel( + userInfo: UserInfo, + pages: List[Link], + userMenu: List[MenuItem], + logo: Logo + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + val $userInfo = $m.map(_.userInfo) + val mobileMenuOpen = Var(false) + + val desktopOnly = cls("hidden md:block") + val mobileOnly = cls("md:hidden") + + inline def avatarImage(size: Int = 8) = + Avatar($userInfo.map(_.img)).avatarImage(size) + + def notificationButton = button( + tpe := "button", + cls := List( + "bg-indigo-600", + "focus:outline-none", + "focus:ring-2", + "focus:ring-offset-2", + "focus:ring-offset-indigo-600", + "focus:ring-white", + "hover:text-white", + "p-1", + "rounded-full", + "text-indigo-200" + ), + span(cls := "sr-only", "View notifications"), + Icons.outline.bell() + ) + + def userProfile: HtmlElement = + val menuOpen = Var(false) + + def menuItem(item: MenuItem, idx: Int): HtmlElement = + a( + href := "#", + cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", + role := "menuitem", + tabIndex := -1, + idAttr := s"user-menu-item-$idx", + item.title + ) + + div( + cls := "ml-3 relative", + div( + button( + tpe := "button", + cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", + idAttr := "user-menu-button", + aria.expanded <-- menuOpen.signal, + aria.hasPopup := true, + span(cls := "sr-only", "Open user menu"), + child <-- avatarImage(), + onClick.preventDefault.mapTo( + !menuOpen.now() + ) --> menuOpen.writer + ) + ), + /* + * */ + div( + cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", + cls <-- menuOpen.signal.map { o => + if (o) "md:block" else "md:hidden" + }, + role := "menu", + aria.orientation := "vertical", + aria.labelledBy := "user-menu-button", + tabIndex := -1, + // : keyboard navigation + children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) + ) + ) + + def mobileProfile = + def menuItem(item: MenuItem): HtmlElement = + a( + href := "#", + cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", + item.title + ) + + div( + cls := "pt-4 pb-3 border-t border-indigo-700", + div( + cls := "flex items-center px-5", + div( + cls := "flex-shrink-0", + child <-- avatarImage(10) + ), + div( + cls := "ml-3", + div( + cls := "text-base font-medium text-white", + child.text <-- $userInfo.map(_.name) + ), + child.maybe <-- $userInfo.map( + _.email.map(e => + div( + cls := "text-sm font-medium text-indigo-300", + e + ) + ) + ) + ), + notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) + ), + div( + cls := "mt-3 px-2 space-y-1", + children <-- $m.map(_.userMenu.map(menuItem)) + ) + ) + + def pageLink(page: Link): Anchor = + page + .a() + .amend( + cls := "text-white px-3 py-2 rounded-md text-sm font-medium", + cls := Seq( + "bg-indigo-700" -> page.active, + "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active + ), + ariaCurrent := (if page.active then "page" else "false") + ) + + def logoImg: Image = + img( + cls := "h-8 w-8", + src <-- $m.map(_.logo.img), + alt <-- $m.map(_.logo.name) + ) + + def mobileMenuButton = button( + tpe := "button", + cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", + aria.controls := "mobile-menu", + aria.expanded <-- mobileMenuOpen.signal, + span(cls := "sr-only", "Open main menu"), + Icons.outline + .menu() + .amend( + svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") + ), + Icons.outline + .x() + .amend( + svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") + ), + onClick.preventDefault.mapTo( + !mobileMenuOpen.now() + ) --> mobileMenuOpen.writer + ) + + def navBarLeft = + div( + cls := "flex items-center", + div(cls := "flex-shrink-0", logoImg), + div( + desktopOnly, + div( + cls := "ml-10 flex items-baseline space-x-4", + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) + ) + ) + ) + + def navBarRight = + div( + desktopOnly, + div( + cls := "ml-4 flex items-center md:ml-6", + notificationButton, + userProfile + ) + ) + + def navBarMobile = + div( + cls := "-mr-2 flex", + mobileOnly, + mobileMenuButton + ) + + def navBar = + div( + cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", + div( + cls := "flex items-center justify-between h-16", + navBarLeft, + navBarRight, + navBarMobile + ) + ) + + def mobileMenu = + div( + mobileOnly, + cls <-- mobileMenuOpen.signal.map { o => + if (o) "block" else "hidden" + }, + idAttr := "mobile-menu", + div( + cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) + ), + mobileProfile + ) + + nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala new file mode 100644 index 0000000..c4cb10d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala @@ -0,0 +1,19 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.Page +import com.raquo.waypoint.Router +import mdr.pdb.app.Action + +object PageHeader: + def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = + header( + cls := "bg-white shadow-sm", + div( + cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", + h1( + cls := "text-lg leading-6 font-semibold text-gray-900", + Breadcrumbs(actionBus) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala new file mode 100644 index 0000000..aae4539 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala @@ -0,0 +1,30 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.Page +import mdr.pdb.app.Action +import fiftyforms.ui.components.tailwind.Loading +import io.laminext.syntax.core.* + +object PageLayout: + case class ViewModel( + navigation: NavigationBar.ViewModel, + content: Option[HtmlElement] + ) + def apply(actionBus: Observer[Action])( + $m: Signal[ViewModel], + mods: Modifier[HtmlElement]* + )(using router: Router[Page]): HtmlElement = + val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) + div( + cls := "h-full flex flex-col", + NavigationBar($m.map(_.navigation)), + child.maybe <-- router.$currentPage.map(_.isRoot) + .switch(None, Some(PageHeader(actionBus))), + main( + cls := "flex-grow-1 overflow-y-auto", + mods, + child <-- $maybeContent.map(_.getOrElse(Loading)) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/mdr/pdb/app/components/PageLink.scala new file mode 100644 index 0000000..e6e7052 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageLink.scala @@ -0,0 +1,40 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.LinkSupport.* +import mdr.pdb.app.Page +import mdr.pdb.app.Action +import com.raquo.waypoint.Router +import mdr.pdb.app.NavigateTo + +object PageLink: + type ViewModel = Page + def apply($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = + a(mods($m, actions), child.text <-- $m.map(_.title)) + + def apply(m: ViewModel, actions: Observer[Action])(using + router: Router[Page] + ): Anchor = apply(Val(m), actions) + + def container($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = a(mods($m, actions)) + + def container(m: ViewModel, actions: Observer[Action])(using + Router[Page] + ): Anchor = container(Val(m), actions) + + private def mods($m: Signal[Page], actions: Observer[Action])(using + router: Router[Page] + ): Modifier[Anchor] = + Seq( + href <-- $m.map(router.absoluteUrlForPage).recover { case _ => + Some("invalid url") + }, + composeEvents(onClick.noKeyMod.preventDefault)( + _.sample($m) + .map(NavigateTo.apply) + ) --> actions + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala new file mode 100644 index 0000000..7682772 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala @@ -0,0 +1,13 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.pages.dashboard.DashboardPage +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage + +class DashboardPageConnector(actionBus: Observer[Action])(using + router: Router[Page] +): + def apply: HtmlElement = + AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala new file mode 100644 index 0000000..76b9133 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala @@ -0,0 +1,76 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.Parameter +import mdr.pdb.UserInfo +import mdr.pdb.app.pages.detail.DetailParametruPage +import pages.detail.DetailKriteriaPage +import pages.detail.components.DukazyKriteria +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.ParameterCriteria + +object DetailKriteriaPageConnector { + trait AppState { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + } +} + +case class DetailKriteriaPageConnector( + state: DetailKriteriaPageConnector.AppState +)( + $page: Signal[Page.DetailKriteria] +)(using Router[Page]): + val $paramChangeSignal = + $page.splitOne(p => + (p.osobniCislo.value, p.parametr.value, p.kriterium.value) + )((x, _, _) => x) + val $pageChangeSignal = + $paramChangeSignal.map( + FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) + ) + + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + val $merged = + $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => + for { + da <- d + pa <- p + pb <- pa.find(_.id == pc._2) + ka <- pb.criteria.find(_.id == pc._3) + } yield (da, pb, ka) + ) + + def apply: HtmlElement = + AppPage(state.actionBus)( + $merged.map(_.map(buildModel)) + .split(_ => ())((_, s, $s) => + DetailKriteriaPage($s)(state.actionBus.contramap { + case DukazyKriteria.Add => + NavigateTo( + Page.UpravDukazKriteria( + Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), + Page.Titled(s.parametr.id, Some(s.parametr.nazev)), + Page.Titled(s.kriterium.id, Some(s.kriterium.id)) + ) + ) + }) + ), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: Parameter, + k: ParameterCriteria + ): DetailKriteriaPage.ViewModel = + DetailKriteriaPage.ViewModel( + o.toDetailOsoby, + p.toParametr(_ => a()), + k.toKriterium(_ => a()) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala deleted file mode 100644 index a3b00af..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala +++ /dev/null @@ -1,103 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import scala.scalajs.js.annotation.JSExportTopLevel -import scala.scalajs.js.annotation.JSExport -import scala.scalajs.js.annotation.JSImport -import scala.scalajs.js -import org.scalajs.dom -import com.raquo.laminar.api.L.{*, given} -import scala.scalajs.js.Date -import com.raquo.waypoint.Router -import com.raquo.waypoint.SplitRender -import cz.e_bs.cmi.mdr.pdb.app.services.DataFetcher -import scala.scalajs.js.JSON -import zio.json._ -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.state.AppState - -@js.native -@JSImport("stylesheets/main.css", JSImport.Namespace) -object Css extends js.Any - -@js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native -@JSImport("params/pdb-params.json", JSImport.Default) -object pdbParams extends js.Object - -@JSExportTopLevel("app") -object Main: - - @JSExport - def main(args: Array[String]): Unit = - import Routes.given - onLoad { - setupAirstream() - val appContainer = dom.document.querySelector("#app") - val _ = - render( - appContainer, - renderPage(state.MockAppState(using unsafeWindowOwner, router)) - ) - } - - private def onLoad(f: => Unit): Unit = - documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) - - private def setupAirstream()(using router: Router[Page]): Unit = - AirstreamError.registerUnhandledErrorCallback(err => - router.forcePage( - Page.UnhandledError( - Some(err.getClass.getName), // TODO: Fill only in dev mode - Some(err.getMessage) - ) - ) - ) - - def renderPage(state: AppState)(using - router: Router[Page] - ): HtmlElement = - val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) - .collectSignal[Page.Detail]( - connectors - .DetailPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailParametru]( - connectors - .DetailParametruPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailKriteria]( - connectors - .DetailKriteriaPageConnector(state)(_) - .apply - ) - .collectSignal[Page.UpravDukazKriteria]( - pages.detail.UpravDukaz.Connector(state)(_).apply - ) - .collectStatic(Page.Dashboard)( - connectors.DashboardPageConnector(state.actionBus).apply - ) - .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) - ) - .collect[Page.UnhandledError](pg => - pages.errors - .UnhandledErrorPage( - pages.errors.UnhandledErrorPage - .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), - state.actionBus - ) - ) - .collectStatic(Page.Directory)( - connectors - .DirectoryPageConnector(state.users, state.actionBus) - .apply - ) - div(cls := "h-full", child <-- pageSplitter.$view) - - // Pull in the stylesheet - val css: Css.type = Css diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala deleted file mode 100644 index 3bbc8c0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ /dev/null @@ -1,198 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.* -import org.scalajs.dom -import zio.json.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import scala.scalajs.js -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.Page.Titled - -// enum is not working with Waypoints' SplitRender collectStatic -sealed abstract class Page( - val id: String, - val title: String, - val parent: Option[Page] -) { - val path: Vector[Page] = - parent match - case None => Vector(this) - case Some(p) => p.path :+ this - - val isRoot: Boolean = parent.isEmpty -} - -object Page: - - case class Titled[V](value: V, title: Option[String] = None): - val show: String = title.getOrElse(value.toString) - - case object Directory extends Page("directory", "Adresář", None) - - case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) - - case class Detail(osobniCislo: Titled[OsobniCislo]) - extends Page("user", osobniCislo.show, Some(Directory)) - - object Detail { - def apply(o: UserInfo): Detail = Detail( - Titled(o.personalNumber, Some(o.name)) - ) - } - - case class DetailParametru( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String] - ) extends Page( - "parameter", - parametr.show, - Some(Detail(osobniCislo)) - ) - - object DetailParametru { - def apply(o: UserInfo, p: Parameter): DetailParametru = - DetailParametru( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)) - ) - } - - case class DetailKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "criteria", - kriterium.show, - Some(DetailParametru(osobniCislo, parametr)) - ) - - object DetailKriteria { - def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = - DetailKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class UpravDukazKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "addProof", - "Důkaz", - Some(DetailKriteria(osobniCislo, parametr, kriterium)) - ) - - object UpravDukazKriteria { - def apply( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): UpravDukazKriteria = - UpravDukazKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class NotFound(url: String) extends Page("404", "404", Some(Directory)) - - case class UnhandledError( - errorName: Option[String], - errorMessage: Option[String] - ) extends Page("500", "Unexpected error", Some(Directory)) - -object Routes: - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) - given [V: JsonEncoder]: JsonEncoder[Titled[V]] = - DeriveJsonEncoder.gen[Titled[V]] - given [V: JsonDecoder]: JsonDecoder[Titled[V]] = - DeriveJsonDecoder.gen[Titled[V]] - given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] - given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] - - val base = - js.`import`.meta.env.BASE_URL - .asInstanceOf[String] + "app" - - val homePage: Page = Page.Directory - - given router: Router[Page] = Router[Page]( - routes = List( - Route.static(homePage, root / endOfSegments, basePath = base), - Route.static( - Page.Dashboard, - root / "dashboard" / endOfSegments, - basePath = base - ), - Route[Page.Detail, String]( - encode = _.osobniCislo.value.toString, - decode = osc => Page.Detail(Titled(OsobniCislo(osc))), - root / "osoba" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.DetailParametru, (String, String)]( - encode = p => (p.osobniCislo.value.toString, p.parametr.value), - decode = - p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / endOfSegments, - basePath = base - ), - Route[Page.DetailKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.DetailKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.UpravDukazKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.UpravDukazKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / "edit" / endOfSegments, - basePath = base - ) - ), - serializePage = _.toJson, - deserializePage = _.fromJson[Page] - .fold(s => throw IllegalStateException(s), identity), - getPageTitle = _.title, - routeFallback = url => Page.NotFound(url), - deserializeFallback = _ => Page.Dashboard - )( - $popStateEvent = windowEvents.onPopState, - owner = unsafeWindowOwner - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala deleted file mode 100644 index b24b82b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -sealed trait Action - -case object FetchDirectory extends Action -case class FetchUserDetails(osc: OsobniCislo) extends Action -case class FetchParameters(osc: OsobniCislo) extends Action -case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action -case class FetchParameterCriteria( - osc: OsobniCislo, - paramId: String, - critId: String, - page: (UserInfo, Parameter, ParameterCriteria) => Page -) extends Action -case class FetchAvailableFiles(osc: OsobniCislo) extends Action -case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala deleted file mode 100644 index c637d35..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala +++ /dev/null @@ -1,82 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.{UserProfile, UserInfo, OsobniCislo} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo -import cz.e_bs.cmi.mdr.pdb.UserFunction - -object AppPage: - // TODO: pages by logged in user - val pages: List[Page] = List(Page.Directory, Page.Dashboard) - - import NavigationBar.{Logo, MenuItem} - - val logo = Logo( - "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", - "Workflow" - ) - - // TODO: menu items by user profile - val userMenu = - List( - MenuItem("Your Profile"), - MenuItem("Settings"), - MenuItem("Sign out") - ) - - // TODO: load user profile - val $userProfile = Var( - UserProfile( - "tom", - UserInfo( - OsobniCislo("1031"), - "tom", - "Tom", - "Cook", - None, - None, - Some("tom@example.com"), - Some("+420 222 866 180"), - Some( - UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") - ), - Nil, - None - ) - ) - ) - - val $userInfo = $userProfile.signal.map(_.userInfo) - - type ViewModel = Option[HtmlElement] - def apply( - actionBus: Observer[Action] - )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using - router: Router[Page] - ): HtmlElement = - PageLayout(actionBus)( - $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => - PageLayout.ViewModel( - NavigationBar.ViewModel( - u, - pages.map(p => - NavigationBar.Link( - () => - PageLink( - p, - actionBus - ), - p == cp - ) - ), - userMenu, - logo - ), - c - ) - ), - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala deleted file mode 100644 index bf473c6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ /dev/null @@ -1,143 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden -import fiftyforms.ui.components.tailwind.list.IconText.ViewModel -import fiftyforms.ui.components.tailwind.Icons -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Action - -object Breadcrumbs: - - private def slash = - import svg.{*, given} - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-300", - xmlns := "http://www.w3.org/2000/svg", - fill := "currentColor", - viewBox := "0 0 20 20", - ariaHidden := true, - path( - d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" - ) - ) - - def backIcon = - Icons.solid - .`arrow-narrow-left`() - .amend( - svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" - ) - - object Link: - case class ViewModel( - page: Page, - icon: Option[SvgElement], - extraClasses: String, - text: String, - textClasses: Option[String] = None - ) - - val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" - - def shortHome(p: Page) = ViewModel( - p, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - "Zpět na úvodní stránku", - None - ) - - def fullHome(p: Page) = ViewModel( - p, - Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), - "text-gray-400 hover:text-gray-500", - "Domů", - Some("sr-only") - ) - - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - PageLink - .container($m.map(_.page), actionBus) - .amend( - cls <-- $m.map(_.extraClasses), - child.maybe <-- $m.map(_.icon), - span( - cls <-- $m.map(_.textClasses.getOrElse("")), - child.text <-- $m.map(_.text) - ) - ) - - object FullBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - ol( - role := "list", - cls := "flex items-center space-x-4", - children <-- $m.map(_.path).split(_.id)((_, _, $p) => - li( - div( - cls := "flex items-center", - child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), - Link( - $p.map(p => - if (p.isRoot) then Link.fullHome(p) - else - Link.ViewModel( - p, - None, - s"ml-4 max-w-xs truncate ${Link.baseClasses}", - p.title - ) - ), - actionBus - ) - ) - ) - ) - ) - - object ShortBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - Link( - $m.map { p => - val target = p.parent.getOrElse(p) - if target.isRoot then Link.shortHome(target) - else - Link.ViewModel( - target, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - s"Zpět na ${target.title}" - ) - }, - actionBus - ) - - def apply(actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - nav( - cls := "flex", - aria.label := "Breadcrumb", - div( - cls := "flex sm:hidden", - ShortBreadcrumbs(router.$currentPage, actionBus) - ), - div( - cls := "hidden sm:block", - FullBreadcrumbs(router.$currentPage, actionBus) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala deleted file mode 100644 index 0db2047..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ /dev/null @@ -1,242 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent -import fiftyforms.ui.components.tailwind.Icons -import fiftyforms.ui.components.tailwind.Avatar -import cz.e_bs.cmi.mdr.pdb.UserInfo -import io.laminext.syntax.core.* - -object NavigationBar: - - case class Logo(img: String, name: String) - case class Link(a: () => Anchor, active: Boolean) - case class MenuItem(title: String) - - case class ViewModel( - userInfo: UserInfo, - pages: List[Link], - userMenu: List[MenuItem], - logo: Logo - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - val $userInfo = $m.map(_.userInfo) - val mobileMenuOpen = Var(false) - - val desktopOnly = cls("hidden md:block") - val mobileOnly = cls("md:hidden") - - inline def avatarImage(size: Int = 8) = - Avatar($userInfo.map(_.img)).avatarImage(size) - - def notificationButton = button( - tpe := "button", - cls := List( - "bg-indigo-600", - "focus:outline-none", - "focus:ring-2", - "focus:ring-offset-2", - "focus:ring-offset-indigo-600", - "focus:ring-white", - "hover:text-white", - "p-1", - "rounded-full", - "text-indigo-200" - ), - span(cls := "sr-only", "View notifications"), - Icons.outline.bell() - ) - - def userProfile: HtmlElement = - val menuOpen = Var(false) - - def menuItem(item: MenuItem, idx: Int): HtmlElement = - a( - href := "#", - cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", - role := "menuitem", - tabIndex := -1, - idAttr := s"user-menu-item-$idx", - item.title - ) - - div( - cls := "ml-3 relative", - div( - button( - tpe := "button", - cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - idAttr := "user-menu-button", - aria.expanded <-- menuOpen.signal, - aria.hasPopup := true, - span(cls := "sr-only", "Open user menu"), - child <-- avatarImage(), - onClick.preventDefault.mapTo( - !menuOpen.now() - ) --> menuOpen.writer - ) - ), - /* - * */ - div( - cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", - cls <-- menuOpen.signal.map { o => - if (o) "md:block" else "md:hidden" - }, - role := "menu", - aria.orientation := "vertical", - aria.labelledBy := "user-menu-button", - tabIndex := -1, - // : keyboard navigation - children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) - ) - ) - - def mobileProfile = - def menuItem(item: MenuItem): HtmlElement = - a( - href := "#", - cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", - item.title - ) - - div( - cls := "pt-4 pb-3 border-t border-indigo-700", - div( - cls := "flex items-center px-5", - div( - cls := "flex-shrink-0", - child <-- avatarImage(10) - ), - div( - cls := "ml-3", - div( - cls := "text-base font-medium text-white", - child.text <-- $userInfo.map(_.name) - ), - child.maybe <-- $userInfo.map( - _.email.map(e => - div( - cls := "text-sm font-medium text-indigo-300", - e - ) - ) - ) - ), - notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) - ), - div( - cls := "mt-3 px-2 space-y-1", - children <-- $m.map(_.userMenu.map(menuItem)) - ) - ) - - def pageLink(page: Link): Anchor = - page - .a() - .amend( - cls := "text-white px-3 py-2 rounded-md text-sm font-medium", - cls := Seq( - "bg-indigo-700" -> page.active, - "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active - ), - ariaCurrent := (if page.active then "page" else "false") - ) - - def logoImg: Image = - img( - cls := "h-8 w-8", - src <-- $m.map(_.logo.img), - alt <-- $m.map(_.logo.name) - ) - - def mobileMenuButton = button( - tpe := "button", - cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - aria.controls := "mobile-menu", - aria.expanded <-- mobileMenuOpen.signal, - span(cls := "sr-only", "Open main menu"), - Icons.outline - .menu() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") - ), - Icons.outline - .x() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") - ), - onClick.preventDefault.mapTo( - !mobileMenuOpen.now() - ) --> mobileMenuOpen.writer - ) - - def navBarLeft = - div( - cls := "flex items-center", - div(cls := "flex-shrink-0", logoImg), - div( - desktopOnly, - div( - cls := "ml-10 flex items-baseline space-x-4", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ) - ) - ) - - def navBarRight = - div( - desktopOnly, - div( - cls := "ml-4 flex items-center md:ml-6", - notificationButton, - userProfile - ) - ) - - def navBarMobile = - div( - cls := "-mr-2 flex", - mobileOnly, - mobileMenuButton - ) - - def navBar = - div( - cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex items-center justify-between h-16", - navBarLeft, - navBarRight, - navBarMobile - ) - ) - - def mobileMenu = - div( - mobileOnly, - cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }, - idAttr := "mobile-menu", - div( - cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ), - mobileProfile - ) - - nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala deleted file mode 100644 index 75339df..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object PageHeader: - def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = - header( - cls := "bg-white shadow-sm", - div( - cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", - h1( - cls := "text-lg leading-6 font-semibold text-gray-900", - Breadcrumbs(actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala deleted file mode 100644 index 5096463..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ /dev/null @@ -1,30 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import fiftyforms.ui.components.tailwind.Loading -import io.laminext.syntax.core.* - -object PageLayout: - case class ViewModel( - navigation: NavigationBar.ViewModel, - content: Option[HtmlElement] - ) - def apply(actionBus: Observer[Action])( - $m: Signal[ViewModel], - mods: Modifier[HtmlElement]* - )(using router: Router[Page]): HtmlElement = - val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) - div( - cls := "h-full flex flex-col", - NavigationBar($m.map(_.navigation)), - child.maybe <-- router.$currentPage.map(_.isRoot) - .switch(None, Some(PageHeader(actionBus))), - main( - cls := "flex-grow-1 overflow-y-auto", - mods, - child <-- $maybeContent.map(_.getOrElse(Loading)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala deleted file mode 100644 index 62d820f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.LinkSupport.* -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo - -object PageLink: - type ViewModel = Page - def apply($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = - a(mods($m, actions), child.text <-- $m.map(_.title)) - - def apply(m: ViewModel, actions: Observer[Action])(using - router: Router[Page] - ): Anchor = apply(Val(m), actions) - - def container($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = a(mods($m, actions)) - - def container(m: ViewModel, actions: Observer[Action])(using - Router[Page] - ): Anchor = container(Val(m), actions) - - private def mods($m: Signal[Page], actions: Observer[Action])(using - router: Router[Page] - ): Modifier[Anchor] = - Seq( - href <-- $m.map(router.absoluteUrlForPage).recover { case _ => - Some("invalid url") - }, - composeEvents(onClick.noKeyMod.preventDefault)( - _.sample($m) - .map(NavigateTo.apply) - ) --> actions - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala deleted file mode 100644 index 87f825a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.pages.dashboard.DashboardPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -class DashboardPageConnector(actionBus: Observer[Action])(using - router: Router[Page] -): - def apply: HtmlElement = - AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala deleted file mode 100644 index e9b0f71..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala +++ /dev/null @@ -1,76 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailKriteriaPage -import pages.detail.components.DukazyKriteria -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -object DetailKriteriaPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailKriteriaPageConnector( - state: DetailKriteriaPageConnector.AppState -)( - $page: Signal[Page.DetailKriteria] -)(using Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => - (p.osobniCislo.value, p.parametr.value, p.kriterium.value) - )((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map( - FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) - ) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, s, $s) => - DetailKriteriaPage($s)(state.actionBus.contramap { - case DukazyKriteria.Add => - NavigateTo( - Page.UpravDukazKriteria( - Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), - Page.Titled(s.parametr.id, Some(s.parametr.nazev)), - Page.Titled(s.kriterium.id, Some(s.kriterium.id)) - ) - ) - }) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): DetailKriteriaPage.ViewModel = - DetailKriteriaPage.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala deleted file mode 100644 index 4d8024f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ /dev/null @@ -1,57 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import pages.detail.DetailPage -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import fiftyforms.ui.components.tailwind.Color - -object DetailPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailPageConnector(state: DetailPageConnector.AppState)( - $page: Signal[Page.Detail] -)(using router: Router[Page]): - val $oscChangeSignal = $page.splitOne(_.osobniCislo.value)((osc, _, _) => osc) - val $pageChangeSignal = - $oscChangeSignal.flatMap(osc => - EventStream.fromSeq(Seq(FetchUserDetails(osc), FetchParameters(osc))) - ) - // TODO: filter the value based on the current osc - // OSC change will fetch new data, but still - // - we need to be sure that what we got is really what we ought to display - // - we want to display stale data accordingly (at least with loading indicator) - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - def apply: HtmlElement = - AppPage(state.actionBus)( - $data.combineWithFn($params)(_ zip _) - .map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: List[Parameter] - ): DetailPage.ViewModel = - DetailPage.ViewModel( - o.toDetailOsoby, - p.map( - _.toParametr(param => - PageLink.container(Page.DetailParametru(o, param), state.actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala deleted file mode 100644 index fdaa7f8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailParametruPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink - -object DetailParametruPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailParametruPageConnector( - state: DetailParametruPageConnector.AppState -)( - $page: Signal[Page.DetailParametru] -)(using router: Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => (p.osobniCislo.value, p.parametr.value))((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map(FetchParameter(_, _)) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - } yield (da, pb) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailParametruPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter - ): DetailParametruPage.ViewModel = - DetailParametruPage.ViewModel( - o.toDetailOsoby, - p.toParametr(p => - PageLink.container(Page.DetailParametru(o, p), state.actionBus) - ), - p.criteria.map( - _.toKriterium { c => - PageLink.container( - Page.DetailKriteria(o, p, c), - state.actionBus - ) - } - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala deleted file mode 100644 index 049aa22..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -case class DirectoryPageConnector( - $input: EventStream[List[UserInfo]], - actionBus: Observer[Action] -)(using router: Router[Page]): - val $data = $input.startWithNone - val $actionSignal = EventStream.fromValue(FetchDirectory) - - def apply: HtmlElement = - AppPage(actionBus)( - $data.split(_ => ())((_, _, s) => - pages.directory.DirectoryPage( - s.map( - _.map( - _.toUserRow(u => - PageLink.container( - Page.Detail(Page.Titled(u.personalNumber)), - actionBus - ) - ) - ) - ) - ) - ), - $actionSignal --> actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala deleted file mode 100644 index 88e7450..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ /dev/null @@ -1,68 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii -import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria -import fiftyforms.ui.components.tailwind.Color - -extension (o: UserInfo) - def toDetailOsoby: DetailOsoby.ViewModel = - DetailOsoby.ViewModel( - o.personalNumber, - o.name, - o.email, - o.phone, - o.img, - o.mainFunction.map(f => - DetailOsoby.Funkce.ViewModel(f.name, f.dept, f.ou) - ), - o.userContracts.headOption.map(c => - DetailOsoby.PracovniPomer.ViewModel(c.rel, c.startDate, c.endDate) - ) - ) - -extension (param: Parameter) - def toParametr(container: Parameter => Anchor): DetailParametru.ViewModel = - DetailParametru.ViewModel( - id = param.id, - nazev = param.name, - popis = param.description, - status = "Nesplněno", - statusColor = Color.red, - a = container(param) - ) - -extension (crit: ParameterCriteria) - def toKriterium( - container: ParameterCriteria => Anchor - ): DetailKriteria.ViewModel = - DetailKriteria.ViewModel( - nazev = crit.criteriumText, - kapitola = crit.chapterId, - bod = crit.itemId, - status = "Nesplněno", - statusColor = Color.red, - splneno = false, - dukazy = Nil, - container = container(crit) - ) - -extension (user: UserInfo) - def toUserRow( - container: UserInfo => HtmlElement = (_: UserInfo) => div() - ): UserRow.ViewModel = - UserRow.ViewModel( - osobniCislo = user.personalNumber.toString, - celeJmeno = user.name, - prijmeni = user.surname, - hlavniFunkce = user.mainFunction.map(_.name), - img = user.img, - container = () => container(user) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala deleted file mode 100644 index 7d5b2c1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.dashboard - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.Page - -object DashboardPage: - - def render: HtmlElement = - div("Dashboard page") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala deleted file mode 100644 index 9ef0698..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala +++ /dev/null @@ -1,31 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailKriteriaPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply($m: Signal[ViewModel])( - action: Observer[DukazyKriteria.Action] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - DukazyKriteria($m.map(_.kriterium.dukazy))(action) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala deleted file mode 100644 index 3b316e1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object DetailPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametry: SeznamParametru.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby($m.map(_.osoba)), - SeznamParametru($m.map(_.parametry)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala deleted file mode 100644 index 17224cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala +++ /dev/null @@ -1,24 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailParametruPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriteria: SeznamKriterii.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby.header($m.map(_.osoba)), - DetailParametru($m.map(_.parametr)), - SeznamKriterii($m.map(_.kriteria)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala deleted file mode 100644 index dc912fa..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ /dev/null @@ -1,107 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb -package app -package pages.detail - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.UpravDukazForm -import fiftyforms.services.files.File - -object UpravDukaz: - - type ThisPage = Page.UpravDukazKriteria - type PageKey = (OsobniCislo, String, String) - - trait State { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def availableFiles: EventStream[List[File]] - def actionBus: Observer[Action] - } - - def keyOfPage(page: ThisPage): PageKey = - (page.osobniCislo.value, page.parametr.value, page.kriterium.value) - - def onChangeAction(key: PageKey): Action = - FetchParameterCriteria( - key._1, - key._2, - key._3, - Page.UpravDukazKriteria(_, _, _) - ) - - class Connector(state: State)($page: Signal[ThisPage])(using Router[Page]): - val $paramChangeSignal = $page.splitOne(keyOfPage)((x, _, _) => x) - val $pageChangeSignal = $paramChangeSignal.map(onChangeAction) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.split(_ => ())((_, s, $s) => - PageComponent( - $s.map(buildModel), - state.availableFiles, - state.actionBus.contramap { - case UpravDukazForm.Cancelled => - NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) - case UpravDukazForm.AvailableFilesRequested => - FetchAvailableFiles(s._1.personalNumber) - } - ) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): PageComponent.ViewModel = - import connectors.* - PageComponent.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) - - object PageComponent: - import components.* - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply( - $m: Signal[ViewModel], - availableFilesStream: EventStream[List[File]], - events: Observer[UpravDukazForm.Event] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - UpravDukazForm(availableFilesStream)(events) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala deleted file mode 100644 index ca311b0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.Icons -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Color - -object DetailKriteria: - case class ViewModel( - nazev: String, - kapitola: String, - bod: String, - status: String, - statusColor: Color, - splneno: Boolean, - dukazy: List[DukazKriteria.ViewModel], - container: HtmlElement = div() - ) { - val id = s"${kapitola}${bod}" - } - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h3( - cls := "text-xl leading-6 font-bold text-gray-900", - child.text <-- $m.map(_.nazev) - ), - h3( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - "Kapitola ", - child.text <-- $m.map(_.kapitola), - " bod ", - child <-- $m.map(_.bod) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala deleted file mode 100644 index ae90290..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala +++ /dev/null @@ -1,100 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Avatar - -object DetailOsoby: - - object Funkce: - case class ViewModel( - nazev: String, - stredisko: String, - voj: String - ) - - def render($m: Signal[ViewModel]): HtmlElement = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.nazev), - span( - cls := "hidden md:inline", - " @ ", - child.text <-- $m.map(_.stredisko), - ", ", - child.text <-- $m.map(_.voj) - ) - ) - - object PracovniPomer: - case class ViewModel( - druh: String, - pocatek: LocalDate, - konec: Option[LocalDate] - ) - - def render($m: Signal[ViewModel]): HtmlElement = - import fiftyforms.ui.components.tailwind.CustomAttrs.datetime - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.druh), - " od ", - time( - datetime <-- $m.map(_.pocatek.toString), - child.text <-- $m.map(_.pocatek.toString) - ) - ) - - case class ViewModel( - osobniCislo: OsobniCislo, - jmeno: String, - email: Option[String], - telefon: Option[String], - img: Option[String], - hlavniFunkce: Option[Funkce.ViewModel], - pracovniPomer: Option[PracovniPomer.ViewModel] - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-4", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(16) - ), - div( - h1( - cls := "text-2xl font-bold text-gray-900", - child.text <-- $m.map(_.jmeno) - ), - child.maybe <-- $m.map(_.hlavniFunkce).split(_ => ())((_, _, d) => - Funkce.render(d) - ), - child.maybe <-- $m.map(_.pracovniPomer).split(_ => ())((_, _, d) => - PracovniPomer.render(d) - ) - ) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-5", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(8) - ), - div( - h1( - cls := "text-lg font-medium text-gray-800", - child.text <-- $m.map(_.jmeno) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala deleted file mode 100644 index e11cb62..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color - -object DetailParametru: - case class ViewModel( - id: String, - nazev: String, - popis: String, - status: String, - statusColor: Color, - a: Anchor - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h2( - cls := "text-2xl leading-6 font-medium text-gray-900", - child.text <-- $m.map(_.nazev) - ), - p( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - child.text <-- $m.map(_.popis) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - h2( - cls := "text-lg leading-6 font-medium text-gray-600", - child.text <-- $m.map(_.nazev) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala deleted file mode 100644 index a00ede8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala +++ /dev/null @@ -1,177 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.CustomAttrs - -object DukazKriteria: - case class Osoba(osobniCislo: String, jmeno: String) - case class Udalost(osoba: Osoba, datum: LocalDate) - case class Dokument( - url: String, - nazev: String, - pridal: Udalost, - odebral: Option[Udalost] - ) - case class ViewModel( - dokumenty: List[Dokument], - autorizoval: Option[Udalost], - platiDo: Option[LocalDate], - poznámka: Option[String] - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-lg", - div( - cls := "px-4 py-5 sm:px-6", - h3( - cls := "text-lg leading-6 font-medium text-gray-900", - """Applicant Information""" - ), - p( - cls := "mt-1 max-w-2xl text-sm text-gray-500", - """Personal details and application.""" - ) - ), - div( - cls := "border-t border-gray-200 px-4 py-5 sm:px-6", - dl( - cls := "grid grid-cols-1 gap-x-4 gap-y-8 sm:grid-cols-2", - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Full name""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Margot Foster""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Application for""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Backend Developer""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Email address""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """margotfoster@example.com""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Salary expectation""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """$120,000""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """About""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Fugiat ipsum ipsum deserunt culpa aute sint do nostrud anim incididunt cillum culpa consequat. Excepteur qui ipsum aliquip consequat sint. Sit id mollit nulla mollit nostrud in ea officia proident. Irure nostrud pariatur mollit ad adipisicing reprehenderit deserunt qui eu.""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """Attachments""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - ul( - role := "list", - cls := "border border-gray-200 rounded-md divide-y divide-gray-200", - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """resume_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ), - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """coverletter_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala deleted file mode 100644 index 22f134f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala +++ /dev/null @@ -1,35 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object DukazyKriteria: - sealed trait Action - case object Add extends Action - - type ViewModel = List[DukazKriteria.ViewModel] - - def apply($m: Signal[ViewModel])(actions: Observer[Action]): HtmlElement = - div( - child.maybe <-- $m.map(m => - if (m.isEmpty) then Some(prazdnyDukaz(actions)) - else None - ), - children <-- $m.map(_.zipWithIndex).split(_._2)((_, _, $s) => - DukazKriteria($s.map(_._1)) - ) - ) - - private def prazdnyDukaz(actions: Observer[Action]): HtmlElement = - button( - tpe := "button", - onClick.preventDefault.mapTo(Add) --> actions, - cls := "relative block w-full border-2 border-gray-300 border-dashed rounded-lg p-12 text-center hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - Icons.outline - .`document-add`(12) - .amend(svg.cls := "mx-auto text-gray-400"), - span( - cls := "mt-2 block text-sm font-medium text-gray-900", - "Přidat důkaz" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala deleted file mode 100644 index f628697..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.list.StackedList -import fiftyforms.ui.components.tailwind.list.ListRow -import fiftyforms.ui.components.tailwind.list.RowTag -import fiftyforms.ui.components.tailwind.list.RowNext -import java.time.LocalDate - -object SeznamKriterii: - type ViewModel = List[DetailKriteria.ViewModel] - - private val kritList = new StackedList[DetailKriteria.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - kritList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = - p(cls := "text-sm text-gray-500", s"${i.kapitola}${i.bod}"), - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.container - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala deleted file mode 100644 index 2bfe4bc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.list.{ - StackedList, - ListRow, - RowTag, - PropList, - IconText, - RowNext -} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.LinkSupport.* - -object SeznamParametru: - type ViewModel = List[DetailParametru.ViewModel] - - private val parametrList = new StackedList[DetailParametru.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - parametrList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = emptyNode, - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.a - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala deleted file mode 100644 index f048e9d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ /dev/null @@ -1,101 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.form.* -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement -import fiftyforms.services.files.components.tailwind.FilePicker -import cz.e_bs.cmi.mdr.pdb.frontend.AutorizujDukaz -import cz.e_bs.cmi.mdr.pdb.frontend.DocumentRef -import fiftyforms.services.files.File - -object UpravDukazForm: - sealed trait Event - case object Cancelled extends Event - case object AvailableFilesRequested extends Event - def apply(availableFilesStream: EventStream[List[File]])( - updates: Observer[Event] - ): HtmlElement = - val (filesStream, filesObserver) = - EventStream.withObserver[FilePicker.Event] - val files = Var[List[File]](Nil) - def submitButtons: HtmlElement = - div( - cls := "pt-5", - div( - cls := "flex justify-end", - button( - tpe := "button", - cls := "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zrušit", - onClick.mapTo(Cancelled) --> updates - ), - button( - tpe := "submit", - cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - disabled <-- files.signal.map(_.isEmpty), - "Autorizovat" - ) - ) - ) - - div( - filesStream.collect { case FilePicker.SelectionUpdated(files) => - files.to(List) - } --> files.writer, - filesStream.collect { case FilePicker.AvailableFilesRequested => - AvailableFilesRequested - } --> updates, - cls := "bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6", - Form( - Form.Body( - Form.Section( - FormHeader( - FormHeader.ViewModel( - "Doložení kritéria", - "Sestavte doklady poskytující důkaz kritéria a potvrďte odesláním formuláře. Případné limitace či jiné relevantní údaje vepište do pole Komentář." - ) - ), - FormFields( - FormRow( - "dokumenty", - "Dokumenty", - FilePicker(files.signal, availableFilesStream)(filesObserver) - .amend(idAttr := "dokumenty", cls("max-w-lg")) - ).toHtml, - FormRow( - "platnost", - "Platnost", - input( - idAttr := "platnost", - name := "platnost", - tpe := "date", - autoComplete := "date", - cls := "block max-w-lg w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" - ) - ).toHtml, - FormRow( - "komentar", - "Komentář", - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - textArea( - idAttr := "komentar", - name := "about", - rows := 3, - cls := "max-w-lg shadow-sm block w-full focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border border-gray-300 rounded-md" - ), - p( - cls := "mt-2 text-sm text-gray-500", - "Doplňte prosím potřebné informace související s doložením kritéria, včetně případných limitací." - ) - ) - ).toHtml - ) - ) - ), - submitButtons - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala deleted file mode 100644 index 41033f6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -import components._ - -object DirectoryPage: - - type ViewModel = List[UserRow.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - val (actionsStream, actionObserver) = - EventStream.withObserver[SearchForm.Action] - val $filter = actionsStream - .collect { - case SearchForm.ClearFilter => None - case SearchForm.SetFilter(t) => Some(t) - } - .startWith(None) - val byLetter = for { - d <- $m - f <- $filter - } yield for { - (letter, users) <- d - .filter { user => - f match - case None => true - case Some(t) => user.search.contains(t) - } - .groupBy(_.prijmeni.head) - .to(List) - .sortBy(_._1) - } yield (letter.toString, users.sortBy(_.prijmeni)) - - div( - cls := "h-full max-w-7xl mx-auto order-first flex flex-col flex-shrink-0", - SearchForm(actionObserver), - Directory(byLetter) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala deleted file mode 100644 index 73f915d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -object Directory: - - object Header: - type ViewModel = String - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500", - h3(child.text <-- $m) - ) - - object UserList: - type ViewModel = List[UserRow.ViewModel] - def apply($m: Signal[ViewModel]): HtmlElement = - ul( - role := "list", - cls := "relative z-0 divide-y divide-gray-200", - children <-- $m.split(_.osobniCislo)((_, _, s) => UserRow(s)) - ) - - type ViewModel = List[(String, List[UserRow.ViewModel])] - def apply($m: Signal[ViewModel]): HtmlElement = - nav( - cls := "flex-1 min-h-0 overflow-y-auto", - aria.label := "Directory", - children <-- $m.split(_._1)((_, _, s) => - div( - cls := "relative", - Header(s.map(_._1)), - UserList(s.map(_._2)) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala deleted file mode 100644 index e182910..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala +++ /dev/null @@ -1,54 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object SearchForm: - sealed trait Action - case object SubmitFilter extends Action - sealed trait FilterAction extends Action - case object ClearFilter extends FilterAction - case class SetFilter(value: String) extends FilterAction - - def apply(actions: Observer[Action]): HtmlElement = - div( - cls := "px-6 pt-4 pb-4", - form( - cls := "flex space-x-4", - onSubmit.mapTo(SubmitFilter) --> actions, - div( - cls := "flex-1 min-w-0", - label( - forId := "search", - cls := "sr-only", - "Hledat" - ), - div( - cls := "relative rounded-md shadow-sm", - div( - cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", - Icons.solid.search().amend(svg.cls := "text-gray-400") - ), - input( - tpe := "search", - name := "search", - idAttr := "search", - cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", - placeholder := "Hledat", - composeEvents(onInput.mapToValue.setAsValue.map(SetFilter(_)))( - _.throttle(500) - ) --> actions - ) - ) - ), - button( - tpe := "submit", - cls := "inline-flex justify-center px-3.5 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500", - Icons.solid.filter().amend(svg.cls := "text-gray-400"), - span( - cls := "sr-only", - "Hledat" - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala deleted file mode 100644 index 82f7723..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala +++ /dev/null @@ -1,52 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.airstream.core.Signal -import fiftyforms.ui.components.tailwind.Avatar - -object UserRow: - case class ViewModel( - osobniCislo: String, - celeJmeno: String, - prijmeni: String, - hlavniFunkce: Option[String], - img: Option[String], - container: () => HtmlElement = () => div() - ) { - val search = prijmeni.toLowerCase - } - - def apply($m: Signal[ViewModel]): HtmlElement = - inline def avatarImage = - Avatar($m.map(_.img)).avatarImage(10) - - li( - div( - cls := "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500", - div( - cls := "flex-shrink-0", - child <-- avatarImage - ), - div( - cls := "flex-1 min-w-0", - child <-- $m.map { o => - o.container() - .amend( - cls := "focus:outline-none", - span( - cls := "absolute inset-0", - aria.hidden := true - ), - p( - cls := "text-sm font-medium text-gray-900", - o.celeJmeno - ), - p( - cls := "text-sm text-gray-500 truncate", - o.hlavniFunkce - ) - ) - } - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala deleted file mode 100644 index 467c5f5..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala +++ /dev/null @@ -1,99 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router - -object ErrorPage: - case class ViewModel( - homePage: Page, - errorName: String, - title: String, - subTitle: String - ) - - def apply(m: ViewModel, actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - val ViewModel(homePage, errorName, title, subTitle) = m - div( - cls := "min-h-full pt-16 pb-12 flex flex-col bg-white", - main( - cls := "flex-grow flex flex-col justify-center max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex-shrink-0 flex justify-center", - a( - href := "/", - cls := "inline-flex", - span( - cls := "sr-only", - """Workflow""" - ), - img( - cls := "h-12 w-auto", - src := "https://tailwindui.com/img/logos/workflow-mark.svg?color=indigo&shade=600", - alt := "" - ) - ) - ), - div( - cls := "py-16", - div( - cls := "text-center", - p( - cls := "text-sm font-semibold text-indigo-600 uppercase tracking-wide", - errorName - ), - h1( - cls := "mt-2 text-4xl font-extrabold text-gray-900 tracking-tight sm:text-5xl", - title - ), - p( - cls := "mt-2 text-base text-gray-500", - subTitle - ), - div( - cls := "mt-6", - PageLink - .container(homePage, actionBus) - .amend( - cls := "text-base font-medium text-indigo-600 hover:text-indigo-500", - """Go back home""" - ) - ) - ) - ) - ), - footer( - cls := "flex-shrink-0 max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", - nav( - cls := "flex justify-center space-x-4", - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Contact Support""" - ), - span( - cls := "inline-block border-l border-gray-300", - aria.hidden := true - ), - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Status""" - ), - span( - cls := "inline-block border-l border-gray-300", - aria.hidden := true - ), - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Twitter""" - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala deleted file mode 100644 index bd9baec..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action - -object NotFoundPage: - def apply(homePage: Page, url: String, actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - ErrorPage( - ErrorPage.ViewModel( - homePage, - "404 error", - "Page not found.", - s"Sorry, but page $url doesn't exist." - ), - actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala deleted file mode 100644 index 476fe3c..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action - -object UnhandledErrorPage: - - case class ViewModel( - homePage: Page, - errorName: Option[String], - errorMessage: Option[String] - ) - - def apply(m: ViewModel, actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - ErrorPage( - ErrorPage.ViewModel( - m.homePage, - "Unexpected error occurred", - m.errorName.getOrElse( - "Uh oh!" - ), // TODO: translations, better text than uh oh - m.errorMessage.getOrElse( - "This wasn't supposed to happen! Please try again." - ) - ), - actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala deleted file mode 100644 index 25f059f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala +++ /dev/null @@ -1,6 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.services - -import com.raquo.airstream.core.Observer - -trait DataFetcher[K, A]: - def fetch(id: K, sink: Observer[A]): Unit diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala deleted file mode 100644 index a469830..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala +++ /dev/null @@ -1,120 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package state - -import com.raquo.airstream.core.EventStream -import cz.e_bs.cmi.mdr.pdb.{UserInfo, OsobniCislo} -import com.raquo.airstream.core.Observer -import scala.scalajs.js -import scala.scalajs.js.JSON -import zio.json._ -import com.raquo.airstream.eventbus.EventBus -import com.raquo.airstream.ownership.Owner -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.UserFunction -import cz.e_bs.cmi.mdr.pdb.UserContract -import fiftyforms.services.files.File - -trait AppState - extends connectors.DetailPageConnector.AppState - with connectors.DetailParametruPageConnector.AppState - with connectors.DetailKriteriaPageConnector.AppState - with pages.detail.UpravDukaz.State: - def users: EventStream[List[UserInfo]] - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - -class MockAppState(implicit owner: Owner, router: Router[Page]) - extends AppState: - - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonDecoder[UserFunction] = DeriveJsonDecoder.gen - given JsonDecoder[UserContract] = DeriveJsonDecoder.gen - given JsonDecoder[UserInfo] = DeriveJsonDecoder.gen - - given JsonDecoder[ParameterCriteria] = DeriveJsonDecoder.gen - given JsonDecoder[Parameter] = DeriveJsonDecoder.gen - - private val actions = EventBus[Action]() - private val (parametersStream, pushParameters) = - EventStream.withCallback[List[Parameter]] - private val (usersStream, pushUsers) = - EventStream.withCallback[List[UserInfo]] - private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] - private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] - - private val mockData: List[UserInfo] = - mockUsers - .asInstanceOf[js.Dictionary[js.Object]] - .values - // TODO: is there a more efficient way to parse from JS object directly? - .map { o => - val parsed = JSON.stringify(o).fromJson[UserInfo] - parsed.left.foreach(org.scalajs.dom.console.log(_)) - parsed - } - .collect { case Right(u) => - u - } - .toList - - private val mockParameters: List[Parameter] = - pdbParams - .asInstanceOf[js.Dictionary[js.Object]] - .values - .map(o => JSON.stringify(o).fromJson[Parameter]) - .collect { case Right(p) => p } - .toList - - // TODO: Extract to separate event handler - actions.events.foreach { - case FetchDirectory => pushUsers(mockData) - case FetchUserDetails(osc) => - mockData.find(_.personalNumber == osc).foreach { o => - pushDetails(o) - router.replaceState(Page.Detail(o)) - } - case FetchParameters(osc) => - pushParameters(mockParameters) - case FetchParameter(osc, paramId) => - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(Page.DetailParametru(o, p)) - case FetchParameterCriteria(osc, paramId, critId, page) => - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - c <- p.criteria.find(_.id == critId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(page(o, p, c)) - case NavigateTo(page) => router.pushState(page) - case FetchAvailableFiles(osc) => - pushFiles( - List( - File("https://tc163.cmi.cz/here", "Example file") - ) - ) - } - - override def users: EventStream[List[UserInfo]] = - usersStream.debugWithName("users") - - override def details: EventStream[UserInfo] = - detailsStream.debugWithName("details") - - override def parameters: EventStream[List[Parameter]] = - parametersStream.debugWithName("parameters") - - override def availableFiles: EventStream[List[File]] = - filesStream.debugWithName("available files") - - override def actionBus: Observer[Action] = - actions.writer.debugWithName("actions writer") diff --git a/app/src/main/scala/mdr/pdb/app/Main.scala b/app/src/main/scala/mdr/pdb/app/Main.scala new file mode 100644 index 0000000..081b4ee --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/Main.scala @@ -0,0 +1,103 @@ +package mdr.pdb.app + +import scala.scalajs.js.annotation.JSExportTopLevel +import scala.scalajs.js.annotation.JSExport +import scala.scalajs.js.annotation.JSImport +import scala.scalajs.js +import org.scalajs.dom +import com.raquo.laminar.api.L.{*, given} +import scala.scalajs.js.Date +import com.raquo.waypoint.Router +import com.raquo.waypoint.SplitRender +import mdr.pdb.app.services.DataFetcher +import scala.scalajs.js.JSON +import zio.json._ +import mdr.pdb.UserInfo +import mdr.pdb.app.state.AppState + +@js.native +@JSImport("stylesheets/main.css", JSImport.Namespace) +object Css extends js.Any + +@js.native +@JSImport("data/users.json", JSImport.Default) +object mockUsers extends js.Object + +@js.native +@JSImport("params/pdb-params.json", JSImport.Default) +object pdbParams extends js.Object + +@JSExportTopLevel("app") +object Main: + + @JSExport + def main(args: Array[String]): Unit = + import Routes.given + onLoad { + setupAirstream() + val appContainer = dom.document.querySelector("#app") + val _ = + render( + appContainer, + renderPage(state.MockAppState(using unsafeWindowOwner, router)) + ) + } + + private def onLoad(f: => Unit): Unit = + documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) + + private def setupAirstream()(using router: Router[Page]): Unit = + AirstreamError.registerUnhandledErrorCallback(err => + router.forcePage( + Page.UnhandledError( + Some(err.getClass.getName), // TODO: Fill only in dev mode + Some(err.getMessage) + ) + ) + ) + + def renderPage(state: AppState)(using + router: Router[Page] + ): HtmlElement = + val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) + .collectSignal[Page.Detail]( + connectors + .DetailPageConnector(state)(_) + .apply + ) + .collectSignal[Page.DetailParametru]( + connectors + .DetailParametruPageConnector(state)(_) + .apply + ) + .collectSignal[Page.DetailKriteria]( + connectors + .DetailKriteriaPageConnector(state)(_) + .apply + ) + .collectSignal[Page.UpravDukazKriteria]( + pages.detail.UpravDukaz.Connector(state)(_).apply + ) + .collectStatic(Page.Dashboard)( + connectors.DashboardPageConnector(state.actionBus).apply + ) + .collect[Page.NotFound](pg => + pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) + ) + .collect[Page.UnhandledError](pg => + pages.errors + .UnhandledErrorPage( + pages.errors.UnhandledErrorPage + .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), + state.actionBus + ) + ) + .collectStatic(Page.Directory)( + connectors + .DirectoryPageConnector(state.users, state.actionBus) + .apply + ) + div(cls := "h-full", child <-- pageSplitter.$view) + + // Pull in the stylesheet + val css: Css.type = Css diff --git a/app/src/main/scala/mdr/pdb/app/Routes.scala b/app/src/main/scala/mdr/pdb/app/Routes.scala new file mode 100644 index 0000000..2cc860c --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/Routes.scala @@ -0,0 +1,198 @@ +package mdr.pdb.app + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.* +import org.scalajs.dom +import zio.json.{*, given} +import mdr.pdb.OsobniCislo + +import scala.scalajs.js +import mdr.pdb.UserInfo +import mdr.pdb.Parameter +import mdr.pdb.ParameterCriteria +import mdr.pdb.app.Page.Titled + +// enum is not working with Waypoints' SplitRender collectStatic +sealed abstract class Page( + val id: String, + val title: String, + val parent: Option[Page] +) { + val path: Vector[Page] = + parent match + case None => Vector(this) + case Some(p) => p.path :+ this + + val isRoot: Boolean = parent.isEmpty +} + +object Page: + + case class Titled[V](value: V, title: Option[String] = None): + val show: String = title.getOrElse(value.toString) + + case object Directory extends Page("directory", "Adresář", None) + + case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) + + case class Detail(osobniCislo: Titled[OsobniCislo]) + extends Page("user", osobniCislo.show, Some(Directory)) + + object Detail { + def apply(o: UserInfo): Detail = Detail( + Titled(o.personalNumber, Some(o.name)) + ) + } + + case class DetailParametru( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String] + ) extends Page( + "parameter", + parametr.show, + Some(Detail(osobniCislo)) + ) + + object DetailParametru { + def apply(o: UserInfo, p: Parameter): DetailParametru = + DetailParametru( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)) + ) + } + + case class DetailKriteria( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String], + kriterium: Titled[String] + ) extends Page( + "criteria", + kriterium.show, + Some(DetailParametru(osobniCislo, parametr)) + ) + + object DetailKriteria { + def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = + DetailKriteria( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)), + Titled(k.id, Some(k.id)) + ) + } + + case class UpravDukazKriteria( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String], + kriterium: Titled[String] + ) extends Page( + "addProof", + "Důkaz", + Some(DetailKriteria(osobniCislo, parametr, kriterium)) + ) + + object UpravDukazKriteria { + def apply( + o: UserInfo, + p: Parameter, + k: ParameterCriteria + ): UpravDukazKriteria = + UpravDukazKriteria( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)), + Titled(k.id, Some(k.id)) + ) + } + + case class NotFound(url: String) extends Page("404", "404", Some(Directory)) + + case class UnhandledError( + errorName: Option[String], + errorMessage: Option[String] + ) extends Page("500", "Unexpected error", Some(Directory)) + +object Routes: + given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) + given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) + given [V: JsonEncoder]: JsonEncoder[Titled[V]] = + DeriveJsonEncoder.gen[Titled[V]] + given [V: JsonDecoder]: JsonDecoder[Titled[V]] = + DeriveJsonDecoder.gen[Titled[V]] + given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] + given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] + + val base = + js.`import`.meta.env.BASE_URL + .asInstanceOf[String] + "app" + + val homePage: Page = Page.Directory + + given router: Router[Page] = Router[Page]( + routes = List( + Route.static(homePage, root / endOfSegments, basePath = base), + Route.static( + Page.Dashboard, + root / "dashboard" / endOfSegments, + basePath = base + ), + Route[Page.Detail, String]( + encode = _.osobniCislo.value.toString, + decode = osc => Page.Detail(Titled(OsobniCislo(osc))), + root / "osoba" / segment[String] / endOfSegments, + basePath = base + ), + Route[Page.DetailParametru, (String, String)]( + encode = p => (p.osobniCislo.value.toString, p.parametr.value), + decode = + p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / endOfSegments, + basePath = base + ), + Route[Page.DetailKriteria, (String, String, String)]( + encode = p => + ( + p.osobniCislo.value.toString, + p.parametr.value, + p.kriterium.value.replaceAll("\\.", "--") + ), + decode = p => + Page.DetailKriteria( + Titled(OsobniCislo(p._1)), + Titled(p._2), + Titled(p._3.replaceAll("--", ".")) + ), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / "kriterium" / segment[String] / endOfSegments, + basePath = base + ), + Route[Page.UpravDukazKriteria, (String, String, String)]( + encode = p => + ( + p.osobniCislo.value.toString, + p.parametr.value, + p.kriterium.value.replaceAll("\\.", "--") + ), + decode = p => + Page.UpravDukazKriteria( + Titled(OsobniCislo(p._1)), + Titled(p._2), + Titled(p._3.replaceAll("--", ".")) + ), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / "kriterium" / segment[String] / "edit" / endOfSegments, + basePath = base + ) + ), + serializePage = _.toJson, + deserializePage = _.fromJson[Page] + .fold(s => throw IllegalStateException(s), identity), + getPageTitle = _.title, + routeFallback = url => Page.NotFound(url), + deserializeFallback = _ => Page.Dashboard + )( + $popStateEvent = windowEvents.onPopState, + owner = unsafeWindowOwner + ) diff --git a/app/src/main/scala/mdr/pdb/app/actions.scala b/app/src/main/scala/mdr/pdb/app/actions.scala new file mode 100644 index 0000000..d078b3a --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/actions.scala @@ -0,0 +1,21 @@ +package mdr.pdb.app + +import mdr.pdb.OsobniCislo +import mdr.pdb.UserInfo +import mdr.pdb.Parameter +import mdr.pdb.ParameterCriteria + +sealed trait Action + +case object FetchDirectory extends Action +case class FetchUserDetails(osc: OsobniCislo) extends Action +case class FetchParameters(osc: OsobniCislo) extends Action +case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action +case class FetchParameterCriteria( + osc: OsobniCislo, + paramId: String, + critId: String, + page: (UserInfo, Parameter, ParameterCriteria) => Page +) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action +case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/mdr/pdb/app/components/AppPage.scala new file mode 100644 index 0000000..130c817 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/AppPage.scala @@ -0,0 +1,82 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.Page +import mdr.pdb.{UserProfile, UserInfo, OsobniCislo} +import com.raquo.waypoint.Router +import mdr.pdb.app.Action +import mdr.pdb.app.NavigateTo +import mdr.pdb.UserFunction + +object AppPage: + // TODO: pages by logged in user + val pages: List[Page] = List(Page.Directory, Page.Dashboard) + + import NavigationBar.{Logo, MenuItem} + + val logo = Logo( + "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", + "Workflow" + ) + + // TODO: menu items by user profile + val userMenu = + List( + MenuItem("Your Profile"), + MenuItem("Settings"), + MenuItem("Sign out") + ) + + // TODO: load user profile + val $userProfile = Var( + UserProfile( + "tom", + UserInfo( + OsobniCislo("1031"), + "tom", + "Tom", + "Cook", + None, + None, + Some("tom@example.com"), + Some("+420 222 866 180"), + Some( + UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") + ), + Nil, + None + ) + ) + ) + + val $userInfo = $userProfile.signal.map(_.userInfo) + + type ViewModel = Option[HtmlElement] + def apply( + actionBus: Observer[Action] + )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using + router: Router[Page] + ): HtmlElement = + PageLayout(actionBus)( + $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => + PageLayout.ViewModel( + NavigationBar.ViewModel( + u, + pages.map(p => + NavigationBar.Link( + () => + PageLink( + p, + actionBus + ), + p == cp + ) + ), + userMenu, + logo + ), + c + ) + ), + mods + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala new file mode 100644 index 0000000..9375398 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala @@ -0,0 +1,143 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.tailwind.list.IconText.ViewModel +import fiftyforms.ui.components.tailwind.Icons +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.Page + +import com.raquo.laminar.api.L.{*, given} +import io.laminext.syntax.core.{*, given} +import mdr.pdb.app.Action + +object Breadcrumbs: + + private def slash = + import svg.{*, given} + svg( + cls := "flex-shrink-0 h-5 w-5 text-gray-300", + xmlns := "http://www.w3.org/2000/svg", + fill := "currentColor", + viewBox := "0 0 20 20", + ariaHidden := true, + path( + d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" + ) + ) + + def backIcon = + Icons.solid + .`arrow-narrow-left`() + .amend( + svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" + ) + + object Link: + case class ViewModel( + page: Page, + icon: Option[SvgElement], + extraClasses: String, + text: String, + textClasses: Option[String] = None + ) + + val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" + + def shortHome(p: Page) = ViewModel( + p, + Some(backIcon), + "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", + "Zpět na úvodní stránku", + None + ) + + def fullHome(p: Page) = ViewModel( + p, + Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), + "text-gray-400 hover:text-gray-500", + "Domů", + Some("sr-only") + ) + + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + PageLink + .container($m.map(_.page), actionBus) + .amend( + cls <-- $m.map(_.extraClasses), + child.maybe <-- $m.map(_.icon), + span( + cls <-- $m.map(_.textClasses.getOrElse("")), + child.text <-- $m.map(_.text) + ) + ) + + object FullBreadcrumbs: + type ViewModel = Page + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + ol( + role := "list", + cls := "flex items-center space-x-4", + children <-- $m.map(_.path).split(_.id)((_, _, $p) => + li( + div( + cls := "flex items-center", + child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), + Link( + $p.map(p => + if (p.isRoot) then Link.fullHome(p) + else + Link.ViewModel( + p, + None, + s"ml-4 max-w-xs truncate ${Link.baseClasses}", + p.title + ) + ), + actionBus + ) + ) + ) + ) + ) + + object ShortBreadcrumbs: + type ViewModel = Page + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + Link( + $m.map { p => + val target = p.parent.getOrElse(p) + if target.isRoot then Link.shortHome(target) + else + Link.ViewModel( + target, + Some(backIcon), + "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", + s"Zpět na ${target.title}" + ) + }, + actionBus + ) + + def apply(actionBus: Observer[Action])(using + router: Router[Page] + ): HtmlElement = + nav( + cls := "flex", + aria.label := "Breadcrumb", + div( + cls := "flex sm:hidden", + ShortBreadcrumbs(router.$currentPage, actionBus) + ), + div( + cls := "hidden sm:block", + FullBreadcrumbs(router.$currentPage, actionBus) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala new file mode 100644 index 0000000..4bb8b8d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala @@ -0,0 +1,242 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent +import fiftyforms.ui.components.tailwind.Icons +import fiftyforms.ui.components.tailwind.Avatar +import mdr.pdb.UserInfo +import io.laminext.syntax.core.* + +object NavigationBar: + + case class Logo(img: String, name: String) + case class Link(a: () => Anchor, active: Boolean) + case class MenuItem(title: String) + + case class ViewModel( + userInfo: UserInfo, + pages: List[Link], + userMenu: List[MenuItem], + logo: Logo + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + val $userInfo = $m.map(_.userInfo) + val mobileMenuOpen = Var(false) + + val desktopOnly = cls("hidden md:block") + val mobileOnly = cls("md:hidden") + + inline def avatarImage(size: Int = 8) = + Avatar($userInfo.map(_.img)).avatarImage(size) + + def notificationButton = button( + tpe := "button", + cls := List( + "bg-indigo-600", + "focus:outline-none", + "focus:ring-2", + "focus:ring-offset-2", + "focus:ring-offset-indigo-600", + "focus:ring-white", + "hover:text-white", + "p-1", + "rounded-full", + "text-indigo-200" + ), + span(cls := "sr-only", "View notifications"), + Icons.outline.bell() + ) + + def userProfile: HtmlElement = + val menuOpen = Var(false) + + def menuItem(item: MenuItem, idx: Int): HtmlElement = + a( + href := "#", + cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", + role := "menuitem", + tabIndex := -1, + idAttr := s"user-menu-item-$idx", + item.title + ) + + div( + cls := "ml-3 relative", + div( + button( + tpe := "button", + cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", + idAttr := "user-menu-button", + aria.expanded <-- menuOpen.signal, + aria.hasPopup := true, + span(cls := "sr-only", "Open user menu"), + child <-- avatarImage(), + onClick.preventDefault.mapTo( + !menuOpen.now() + ) --> menuOpen.writer + ) + ), + /* + * */ + div( + cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", + cls <-- menuOpen.signal.map { o => + if (o) "md:block" else "md:hidden" + }, + role := "menu", + aria.orientation := "vertical", + aria.labelledBy := "user-menu-button", + tabIndex := -1, + // : keyboard navigation + children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) + ) + ) + + def mobileProfile = + def menuItem(item: MenuItem): HtmlElement = + a( + href := "#", + cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", + item.title + ) + + div( + cls := "pt-4 pb-3 border-t border-indigo-700", + div( + cls := "flex items-center px-5", + div( + cls := "flex-shrink-0", + child <-- avatarImage(10) + ), + div( + cls := "ml-3", + div( + cls := "text-base font-medium text-white", + child.text <-- $userInfo.map(_.name) + ), + child.maybe <-- $userInfo.map( + _.email.map(e => + div( + cls := "text-sm font-medium text-indigo-300", + e + ) + ) + ) + ), + notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) + ), + div( + cls := "mt-3 px-2 space-y-1", + children <-- $m.map(_.userMenu.map(menuItem)) + ) + ) + + def pageLink(page: Link): Anchor = + page + .a() + .amend( + cls := "text-white px-3 py-2 rounded-md text-sm font-medium", + cls := Seq( + "bg-indigo-700" -> page.active, + "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active + ), + ariaCurrent := (if page.active then "page" else "false") + ) + + def logoImg: Image = + img( + cls := "h-8 w-8", + src <-- $m.map(_.logo.img), + alt <-- $m.map(_.logo.name) + ) + + def mobileMenuButton = button( + tpe := "button", + cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", + aria.controls := "mobile-menu", + aria.expanded <-- mobileMenuOpen.signal, + span(cls := "sr-only", "Open main menu"), + Icons.outline + .menu() + .amend( + svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") + ), + Icons.outline + .x() + .amend( + svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") + ), + onClick.preventDefault.mapTo( + !mobileMenuOpen.now() + ) --> mobileMenuOpen.writer + ) + + def navBarLeft = + div( + cls := "flex items-center", + div(cls := "flex-shrink-0", logoImg), + div( + desktopOnly, + div( + cls := "ml-10 flex items-baseline space-x-4", + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) + ) + ) + ) + + def navBarRight = + div( + desktopOnly, + div( + cls := "ml-4 flex items-center md:ml-6", + notificationButton, + userProfile + ) + ) + + def navBarMobile = + div( + cls := "-mr-2 flex", + mobileOnly, + mobileMenuButton + ) + + def navBar = + div( + cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", + div( + cls := "flex items-center justify-between h-16", + navBarLeft, + navBarRight, + navBarMobile + ) + ) + + def mobileMenu = + div( + mobileOnly, + cls <-- mobileMenuOpen.signal.map { o => + if (o) "block" else "hidden" + }, + idAttr := "mobile-menu", + div( + cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) + ), + mobileProfile + ) + + nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala new file mode 100644 index 0000000..c4cb10d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala @@ -0,0 +1,19 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.Page +import com.raquo.waypoint.Router +import mdr.pdb.app.Action + +object PageHeader: + def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = + header( + cls := "bg-white shadow-sm", + div( + cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", + h1( + cls := "text-lg leading-6 font-semibold text-gray-900", + Breadcrumbs(actionBus) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala new file mode 100644 index 0000000..aae4539 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala @@ -0,0 +1,30 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.Page +import mdr.pdb.app.Action +import fiftyforms.ui.components.tailwind.Loading +import io.laminext.syntax.core.* + +object PageLayout: + case class ViewModel( + navigation: NavigationBar.ViewModel, + content: Option[HtmlElement] + ) + def apply(actionBus: Observer[Action])( + $m: Signal[ViewModel], + mods: Modifier[HtmlElement]* + )(using router: Router[Page]): HtmlElement = + val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) + div( + cls := "h-full flex flex-col", + NavigationBar($m.map(_.navigation)), + child.maybe <-- router.$currentPage.map(_.isRoot) + .switch(None, Some(PageHeader(actionBus))), + main( + cls := "flex-grow-1 overflow-y-auto", + mods, + child <-- $maybeContent.map(_.getOrElse(Loading)) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/mdr/pdb/app/components/PageLink.scala new file mode 100644 index 0000000..e6e7052 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageLink.scala @@ -0,0 +1,40 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.LinkSupport.* +import mdr.pdb.app.Page +import mdr.pdb.app.Action +import com.raquo.waypoint.Router +import mdr.pdb.app.NavigateTo + +object PageLink: + type ViewModel = Page + def apply($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = + a(mods($m, actions), child.text <-- $m.map(_.title)) + + def apply(m: ViewModel, actions: Observer[Action])(using + router: Router[Page] + ): Anchor = apply(Val(m), actions) + + def container($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = a(mods($m, actions)) + + def container(m: ViewModel, actions: Observer[Action])(using + Router[Page] + ): Anchor = container(Val(m), actions) + + private def mods($m: Signal[Page], actions: Observer[Action])(using + router: Router[Page] + ): Modifier[Anchor] = + Seq( + href <-- $m.map(router.absoluteUrlForPage).recover { case _ => + Some("invalid url") + }, + composeEvents(onClick.noKeyMod.preventDefault)( + _.sample($m) + .map(NavigateTo.apply) + ) --> actions + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala new file mode 100644 index 0000000..7682772 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala @@ -0,0 +1,13 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.pages.dashboard.DashboardPage +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage + +class DashboardPageConnector(actionBus: Observer[Action])(using + router: Router[Page] +): + def apply: HtmlElement = + AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala new file mode 100644 index 0000000..76b9133 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala @@ -0,0 +1,76 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.Parameter +import mdr.pdb.UserInfo +import mdr.pdb.app.pages.detail.DetailParametruPage +import pages.detail.DetailKriteriaPage +import pages.detail.components.DukazyKriteria +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.ParameterCriteria + +object DetailKriteriaPageConnector { + trait AppState { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + } +} + +case class DetailKriteriaPageConnector( + state: DetailKriteriaPageConnector.AppState +)( + $page: Signal[Page.DetailKriteria] +)(using Router[Page]): + val $paramChangeSignal = + $page.splitOne(p => + (p.osobniCislo.value, p.parametr.value, p.kriterium.value) + )((x, _, _) => x) + val $pageChangeSignal = + $paramChangeSignal.map( + FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) + ) + + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + val $merged = + $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => + for { + da <- d + pa <- p + pb <- pa.find(_.id == pc._2) + ka <- pb.criteria.find(_.id == pc._3) + } yield (da, pb, ka) + ) + + def apply: HtmlElement = + AppPage(state.actionBus)( + $merged.map(_.map(buildModel)) + .split(_ => ())((_, s, $s) => + DetailKriteriaPage($s)(state.actionBus.contramap { + case DukazyKriteria.Add => + NavigateTo( + Page.UpravDukazKriteria( + Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), + Page.Titled(s.parametr.id, Some(s.parametr.nazev)), + Page.Titled(s.kriterium.id, Some(s.kriterium.id)) + ) + ) + }) + ), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: Parameter, + k: ParameterCriteria + ): DetailKriteriaPage.ViewModel = + DetailKriteriaPage.ViewModel( + o.toDetailOsoby, + p.toParametr(_ => a()), + k.toKriterium(_ => a()) + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DetailPageConnector.scala new file mode 100644 index 0000000..df3a6b7 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DetailPageConnector.scala @@ -0,0 +1,57 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.UserInfo +import pages.detail.DetailPage +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.pages.detail.components.DetailOsoby +import mdr.pdb.Parameter +import mdr.pdb.app.pages.detail.components.SeznamParametru +import fiftyforms.ui.components.tailwind.Color + +object DetailPageConnector { + trait AppState { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + } +} + +case class DetailPageConnector(state: DetailPageConnector.AppState)( + $page: Signal[Page.Detail] +)(using router: Router[Page]): + val $oscChangeSignal = $page.splitOne(_.osobniCislo.value)((osc, _, _) => osc) + val $pageChangeSignal = + $oscChangeSignal.flatMap(osc => + EventStream.fromSeq(Seq(FetchUserDetails(osc), FetchParameters(osc))) + ) + // TODO: filter the value based on the current osc + // OSC change will fetch new data, but still + // - we need to be sure that what we got is really what we ought to display + // - we want to display stale data accordingly (at least with loading indicator) + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + def apply: HtmlElement = + AppPage(state.actionBus)( + $data.combineWithFn($params)(_ zip _) + .map(_.map(buildModel)) + .split(_ => ())((_, _, s) => DetailPage(s)), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: List[Parameter] + ): DetailPage.ViewModel = + DetailPage.ViewModel( + o.toDetailOsoby, + p.map( + _.toParametr(param => + PageLink.container(Page.DetailParametru(o, param), state.actionBus) + ) + ) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala deleted file mode 100644 index a3b00af..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala +++ /dev/null @@ -1,103 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import scala.scalajs.js.annotation.JSExportTopLevel -import scala.scalajs.js.annotation.JSExport -import scala.scalajs.js.annotation.JSImport -import scala.scalajs.js -import org.scalajs.dom -import com.raquo.laminar.api.L.{*, given} -import scala.scalajs.js.Date -import com.raquo.waypoint.Router -import com.raquo.waypoint.SplitRender -import cz.e_bs.cmi.mdr.pdb.app.services.DataFetcher -import scala.scalajs.js.JSON -import zio.json._ -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.state.AppState - -@js.native -@JSImport("stylesheets/main.css", JSImport.Namespace) -object Css extends js.Any - -@js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native -@JSImport("params/pdb-params.json", JSImport.Default) -object pdbParams extends js.Object - -@JSExportTopLevel("app") -object Main: - - @JSExport - def main(args: Array[String]): Unit = - import Routes.given - onLoad { - setupAirstream() - val appContainer = dom.document.querySelector("#app") - val _ = - render( - appContainer, - renderPage(state.MockAppState(using unsafeWindowOwner, router)) - ) - } - - private def onLoad(f: => Unit): Unit = - documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) - - private def setupAirstream()(using router: Router[Page]): Unit = - AirstreamError.registerUnhandledErrorCallback(err => - router.forcePage( - Page.UnhandledError( - Some(err.getClass.getName), // TODO: Fill only in dev mode - Some(err.getMessage) - ) - ) - ) - - def renderPage(state: AppState)(using - router: Router[Page] - ): HtmlElement = - val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) - .collectSignal[Page.Detail]( - connectors - .DetailPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailParametru]( - connectors - .DetailParametruPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailKriteria]( - connectors - .DetailKriteriaPageConnector(state)(_) - .apply - ) - .collectSignal[Page.UpravDukazKriteria]( - pages.detail.UpravDukaz.Connector(state)(_).apply - ) - .collectStatic(Page.Dashboard)( - connectors.DashboardPageConnector(state.actionBus).apply - ) - .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) - ) - .collect[Page.UnhandledError](pg => - pages.errors - .UnhandledErrorPage( - pages.errors.UnhandledErrorPage - .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), - state.actionBus - ) - ) - .collectStatic(Page.Directory)( - connectors - .DirectoryPageConnector(state.users, state.actionBus) - .apply - ) - div(cls := "h-full", child <-- pageSplitter.$view) - - // Pull in the stylesheet - val css: Css.type = Css diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala deleted file mode 100644 index 3bbc8c0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ /dev/null @@ -1,198 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.* -import org.scalajs.dom -import zio.json.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import scala.scalajs.js -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.Page.Titled - -// enum is not working with Waypoints' SplitRender collectStatic -sealed abstract class Page( - val id: String, - val title: String, - val parent: Option[Page] -) { - val path: Vector[Page] = - parent match - case None => Vector(this) - case Some(p) => p.path :+ this - - val isRoot: Boolean = parent.isEmpty -} - -object Page: - - case class Titled[V](value: V, title: Option[String] = None): - val show: String = title.getOrElse(value.toString) - - case object Directory extends Page("directory", "Adresář", None) - - case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) - - case class Detail(osobniCislo: Titled[OsobniCislo]) - extends Page("user", osobniCislo.show, Some(Directory)) - - object Detail { - def apply(o: UserInfo): Detail = Detail( - Titled(o.personalNumber, Some(o.name)) - ) - } - - case class DetailParametru( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String] - ) extends Page( - "parameter", - parametr.show, - Some(Detail(osobniCislo)) - ) - - object DetailParametru { - def apply(o: UserInfo, p: Parameter): DetailParametru = - DetailParametru( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)) - ) - } - - case class DetailKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "criteria", - kriterium.show, - Some(DetailParametru(osobniCislo, parametr)) - ) - - object DetailKriteria { - def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = - DetailKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class UpravDukazKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "addProof", - "Důkaz", - Some(DetailKriteria(osobniCislo, parametr, kriterium)) - ) - - object UpravDukazKriteria { - def apply( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): UpravDukazKriteria = - UpravDukazKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class NotFound(url: String) extends Page("404", "404", Some(Directory)) - - case class UnhandledError( - errorName: Option[String], - errorMessage: Option[String] - ) extends Page("500", "Unexpected error", Some(Directory)) - -object Routes: - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) - given [V: JsonEncoder]: JsonEncoder[Titled[V]] = - DeriveJsonEncoder.gen[Titled[V]] - given [V: JsonDecoder]: JsonDecoder[Titled[V]] = - DeriveJsonDecoder.gen[Titled[V]] - given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] - given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] - - val base = - js.`import`.meta.env.BASE_URL - .asInstanceOf[String] + "app" - - val homePage: Page = Page.Directory - - given router: Router[Page] = Router[Page]( - routes = List( - Route.static(homePage, root / endOfSegments, basePath = base), - Route.static( - Page.Dashboard, - root / "dashboard" / endOfSegments, - basePath = base - ), - Route[Page.Detail, String]( - encode = _.osobniCislo.value.toString, - decode = osc => Page.Detail(Titled(OsobniCislo(osc))), - root / "osoba" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.DetailParametru, (String, String)]( - encode = p => (p.osobniCislo.value.toString, p.parametr.value), - decode = - p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / endOfSegments, - basePath = base - ), - Route[Page.DetailKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.DetailKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.UpravDukazKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.UpravDukazKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / "edit" / endOfSegments, - basePath = base - ) - ), - serializePage = _.toJson, - deserializePage = _.fromJson[Page] - .fold(s => throw IllegalStateException(s), identity), - getPageTitle = _.title, - routeFallback = url => Page.NotFound(url), - deserializeFallback = _ => Page.Dashboard - )( - $popStateEvent = windowEvents.onPopState, - owner = unsafeWindowOwner - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala deleted file mode 100644 index b24b82b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -sealed trait Action - -case object FetchDirectory extends Action -case class FetchUserDetails(osc: OsobniCislo) extends Action -case class FetchParameters(osc: OsobniCislo) extends Action -case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action -case class FetchParameterCriteria( - osc: OsobniCislo, - paramId: String, - critId: String, - page: (UserInfo, Parameter, ParameterCriteria) => Page -) extends Action -case class FetchAvailableFiles(osc: OsobniCislo) extends Action -case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala deleted file mode 100644 index c637d35..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala +++ /dev/null @@ -1,82 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.{UserProfile, UserInfo, OsobniCislo} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo -import cz.e_bs.cmi.mdr.pdb.UserFunction - -object AppPage: - // TODO: pages by logged in user - val pages: List[Page] = List(Page.Directory, Page.Dashboard) - - import NavigationBar.{Logo, MenuItem} - - val logo = Logo( - "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", - "Workflow" - ) - - // TODO: menu items by user profile - val userMenu = - List( - MenuItem("Your Profile"), - MenuItem("Settings"), - MenuItem("Sign out") - ) - - // TODO: load user profile - val $userProfile = Var( - UserProfile( - "tom", - UserInfo( - OsobniCislo("1031"), - "tom", - "Tom", - "Cook", - None, - None, - Some("tom@example.com"), - Some("+420 222 866 180"), - Some( - UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") - ), - Nil, - None - ) - ) - ) - - val $userInfo = $userProfile.signal.map(_.userInfo) - - type ViewModel = Option[HtmlElement] - def apply( - actionBus: Observer[Action] - )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using - router: Router[Page] - ): HtmlElement = - PageLayout(actionBus)( - $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => - PageLayout.ViewModel( - NavigationBar.ViewModel( - u, - pages.map(p => - NavigationBar.Link( - () => - PageLink( - p, - actionBus - ), - p == cp - ) - ), - userMenu, - logo - ), - c - ) - ), - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala deleted file mode 100644 index bf473c6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ /dev/null @@ -1,143 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden -import fiftyforms.ui.components.tailwind.list.IconText.ViewModel -import fiftyforms.ui.components.tailwind.Icons -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Action - -object Breadcrumbs: - - private def slash = - import svg.{*, given} - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-300", - xmlns := "http://www.w3.org/2000/svg", - fill := "currentColor", - viewBox := "0 0 20 20", - ariaHidden := true, - path( - d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" - ) - ) - - def backIcon = - Icons.solid - .`arrow-narrow-left`() - .amend( - svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" - ) - - object Link: - case class ViewModel( - page: Page, - icon: Option[SvgElement], - extraClasses: String, - text: String, - textClasses: Option[String] = None - ) - - val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" - - def shortHome(p: Page) = ViewModel( - p, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - "Zpět na úvodní stránku", - None - ) - - def fullHome(p: Page) = ViewModel( - p, - Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), - "text-gray-400 hover:text-gray-500", - "Domů", - Some("sr-only") - ) - - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - PageLink - .container($m.map(_.page), actionBus) - .amend( - cls <-- $m.map(_.extraClasses), - child.maybe <-- $m.map(_.icon), - span( - cls <-- $m.map(_.textClasses.getOrElse("")), - child.text <-- $m.map(_.text) - ) - ) - - object FullBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - ol( - role := "list", - cls := "flex items-center space-x-4", - children <-- $m.map(_.path).split(_.id)((_, _, $p) => - li( - div( - cls := "flex items-center", - child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), - Link( - $p.map(p => - if (p.isRoot) then Link.fullHome(p) - else - Link.ViewModel( - p, - None, - s"ml-4 max-w-xs truncate ${Link.baseClasses}", - p.title - ) - ), - actionBus - ) - ) - ) - ) - ) - - object ShortBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - Link( - $m.map { p => - val target = p.parent.getOrElse(p) - if target.isRoot then Link.shortHome(target) - else - Link.ViewModel( - target, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - s"Zpět na ${target.title}" - ) - }, - actionBus - ) - - def apply(actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - nav( - cls := "flex", - aria.label := "Breadcrumb", - div( - cls := "flex sm:hidden", - ShortBreadcrumbs(router.$currentPage, actionBus) - ), - div( - cls := "hidden sm:block", - FullBreadcrumbs(router.$currentPage, actionBus) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala deleted file mode 100644 index 0db2047..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ /dev/null @@ -1,242 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent -import fiftyforms.ui.components.tailwind.Icons -import fiftyforms.ui.components.tailwind.Avatar -import cz.e_bs.cmi.mdr.pdb.UserInfo -import io.laminext.syntax.core.* - -object NavigationBar: - - case class Logo(img: String, name: String) - case class Link(a: () => Anchor, active: Boolean) - case class MenuItem(title: String) - - case class ViewModel( - userInfo: UserInfo, - pages: List[Link], - userMenu: List[MenuItem], - logo: Logo - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - val $userInfo = $m.map(_.userInfo) - val mobileMenuOpen = Var(false) - - val desktopOnly = cls("hidden md:block") - val mobileOnly = cls("md:hidden") - - inline def avatarImage(size: Int = 8) = - Avatar($userInfo.map(_.img)).avatarImage(size) - - def notificationButton = button( - tpe := "button", - cls := List( - "bg-indigo-600", - "focus:outline-none", - "focus:ring-2", - "focus:ring-offset-2", - "focus:ring-offset-indigo-600", - "focus:ring-white", - "hover:text-white", - "p-1", - "rounded-full", - "text-indigo-200" - ), - span(cls := "sr-only", "View notifications"), - Icons.outline.bell() - ) - - def userProfile: HtmlElement = - val menuOpen = Var(false) - - def menuItem(item: MenuItem, idx: Int): HtmlElement = - a( - href := "#", - cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", - role := "menuitem", - tabIndex := -1, - idAttr := s"user-menu-item-$idx", - item.title - ) - - div( - cls := "ml-3 relative", - div( - button( - tpe := "button", - cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - idAttr := "user-menu-button", - aria.expanded <-- menuOpen.signal, - aria.hasPopup := true, - span(cls := "sr-only", "Open user menu"), - child <-- avatarImage(), - onClick.preventDefault.mapTo( - !menuOpen.now() - ) --> menuOpen.writer - ) - ), - /* - * */ - div( - cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", - cls <-- menuOpen.signal.map { o => - if (o) "md:block" else "md:hidden" - }, - role := "menu", - aria.orientation := "vertical", - aria.labelledBy := "user-menu-button", - tabIndex := -1, - // : keyboard navigation - children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) - ) - ) - - def mobileProfile = - def menuItem(item: MenuItem): HtmlElement = - a( - href := "#", - cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", - item.title - ) - - div( - cls := "pt-4 pb-3 border-t border-indigo-700", - div( - cls := "flex items-center px-5", - div( - cls := "flex-shrink-0", - child <-- avatarImage(10) - ), - div( - cls := "ml-3", - div( - cls := "text-base font-medium text-white", - child.text <-- $userInfo.map(_.name) - ), - child.maybe <-- $userInfo.map( - _.email.map(e => - div( - cls := "text-sm font-medium text-indigo-300", - e - ) - ) - ) - ), - notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) - ), - div( - cls := "mt-3 px-2 space-y-1", - children <-- $m.map(_.userMenu.map(menuItem)) - ) - ) - - def pageLink(page: Link): Anchor = - page - .a() - .amend( - cls := "text-white px-3 py-2 rounded-md text-sm font-medium", - cls := Seq( - "bg-indigo-700" -> page.active, - "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active - ), - ariaCurrent := (if page.active then "page" else "false") - ) - - def logoImg: Image = - img( - cls := "h-8 w-8", - src <-- $m.map(_.logo.img), - alt <-- $m.map(_.logo.name) - ) - - def mobileMenuButton = button( - tpe := "button", - cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - aria.controls := "mobile-menu", - aria.expanded <-- mobileMenuOpen.signal, - span(cls := "sr-only", "Open main menu"), - Icons.outline - .menu() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") - ), - Icons.outline - .x() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") - ), - onClick.preventDefault.mapTo( - !mobileMenuOpen.now() - ) --> mobileMenuOpen.writer - ) - - def navBarLeft = - div( - cls := "flex items-center", - div(cls := "flex-shrink-0", logoImg), - div( - desktopOnly, - div( - cls := "ml-10 flex items-baseline space-x-4", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ) - ) - ) - - def navBarRight = - div( - desktopOnly, - div( - cls := "ml-4 flex items-center md:ml-6", - notificationButton, - userProfile - ) - ) - - def navBarMobile = - div( - cls := "-mr-2 flex", - mobileOnly, - mobileMenuButton - ) - - def navBar = - div( - cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex items-center justify-between h-16", - navBarLeft, - navBarRight, - navBarMobile - ) - ) - - def mobileMenu = - div( - mobileOnly, - cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }, - idAttr := "mobile-menu", - div( - cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ), - mobileProfile - ) - - nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala deleted file mode 100644 index 75339df..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object PageHeader: - def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = - header( - cls := "bg-white shadow-sm", - div( - cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", - h1( - cls := "text-lg leading-6 font-semibold text-gray-900", - Breadcrumbs(actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala deleted file mode 100644 index 5096463..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ /dev/null @@ -1,30 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import fiftyforms.ui.components.tailwind.Loading -import io.laminext.syntax.core.* - -object PageLayout: - case class ViewModel( - navigation: NavigationBar.ViewModel, - content: Option[HtmlElement] - ) - def apply(actionBus: Observer[Action])( - $m: Signal[ViewModel], - mods: Modifier[HtmlElement]* - )(using router: Router[Page]): HtmlElement = - val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) - div( - cls := "h-full flex flex-col", - NavigationBar($m.map(_.navigation)), - child.maybe <-- router.$currentPage.map(_.isRoot) - .switch(None, Some(PageHeader(actionBus))), - main( - cls := "flex-grow-1 overflow-y-auto", - mods, - child <-- $maybeContent.map(_.getOrElse(Loading)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala deleted file mode 100644 index 62d820f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.LinkSupport.* -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo - -object PageLink: - type ViewModel = Page - def apply($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = - a(mods($m, actions), child.text <-- $m.map(_.title)) - - def apply(m: ViewModel, actions: Observer[Action])(using - router: Router[Page] - ): Anchor = apply(Val(m), actions) - - def container($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = a(mods($m, actions)) - - def container(m: ViewModel, actions: Observer[Action])(using - Router[Page] - ): Anchor = container(Val(m), actions) - - private def mods($m: Signal[Page], actions: Observer[Action])(using - router: Router[Page] - ): Modifier[Anchor] = - Seq( - href <-- $m.map(router.absoluteUrlForPage).recover { case _ => - Some("invalid url") - }, - composeEvents(onClick.noKeyMod.preventDefault)( - _.sample($m) - .map(NavigateTo.apply) - ) --> actions - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala deleted file mode 100644 index 87f825a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.pages.dashboard.DashboardPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -class DashboardPageConnector(actionBus: Observer[Action])(using - router: Router[Page] -): - def apply: HtmlElement = - AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala deleted file mode 100644 index e9b0f71..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala +++ /dev/null @@ -1,76 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailKriteriaPage -import pages.detail.components.DukazyKriteria -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -object DetailKriteriaPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailKriteriaPageConnector( - state: DetailKriteriaPageConnector.AppState -)( - $page: Signal[Page.DetailKriteria] -)(using Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => - (p.osobniCislo.value, p.parametr.value, p.kriterium.value) - )((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map( - FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) - ) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, s, $s) => - DetailKriteriaPage($s)(state.actionBus.contramap { - case DukazyKriteria.Add => - NavigateTo( - Page.UpravDukazKriteria( - Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), - Page.Titled(s.parametr.id, Some(s.parametr.nazev)), - Page.Titled(s.kriterium.id, Some(s.kriterium.id)) - ) - ) - }) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): DetailKriteriaPage.ViewModel = - DetailKriteriaPage.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala deleted file mode 100644 index 4d8024f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ /dev/null @@ -1,57 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import pages.detail.DetailPage -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import fiftyforms.ui.components.tailwind.Color - -object DetailPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailPageConnector(state: DetailPageConnector.AppState)( - $page: Signal[Page.Detail] -)(using router: Router[Page]): - val $oscChangeSignal = $page.splitOne(_.osobniCislo.value)((osc, _, _) => osc) - val $pageChangeSignal = - $oscChangeSignal.flatMap(osc => - EventStream.fromSeq(Seq(FetchUserDetails(osc), FetchParameters(osc))) - ) - // TODO: filter the value based on the current osc - // OSC change will fetch new data, but still - // - we need to be sure that what we got is really what we ought to display - // - we want to display stale data accordingly (at least with loading indicator) - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - def apply: HtmlElement = - AppPage(state.actionBus)( - $data.combineWithFn($params)(_ zip _) - .map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: List[Parameter] - ): DetailPage.ViewModel = - DetailPage.ViewModel( - o.toDetailOsoby, - p.map( - _.toParametr(param => - PageLink.container(Page.DetailParametru(o, param), state.actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala deleted file mode 100644 index fdaa7f8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailParametruPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink - -object DetailParametruPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailParametruPageConnector( - state: DetailParametruPageConnector.AppState -)( - $page: Signal[Page.DetailParametru] -)(using router: Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => (p.osobniCislo.value, p.parametr.value))((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map(FetchParameter(_, _)) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - } yield (da, pb) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailParametruPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter - ): DetailParametruPage.ViewModel = - DetailParametruPage.ViewModel( - o.toDetailOsoby, - p.toParametr(p => - PageLink.container(Page.DetailParametru(o, p), state.actionBus) - ), - p.criteria.map( - _.toKriterium { c => - PageLink.container( - Page.DetailKriteria(o, p, c), - state.actionBus - ) - } - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala deleted file mode 100644 index 049aa22..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -case class DirectoryPageConnector( - $input: EventStream[List[UserInfo]], - actionBus: Observer[Action] -)(using router: Router[Page]): - val $data = $input.startWithNone - val $actionSignal = EventStream.fromValue(FetchDirectory) - - def apply: HtmlElement = - AppPage(actionBus)( - $data.split(_ => ())((_, _, s) => - pages.directory.DirectoryPage( - s.map( - _.map( - _.toUserRow(u => - PageLink.container( - Page.Detail(Page.Titled(u.personalNumber)), - actionBus - ) - ) - ) - ) - ) - ), - $actionSignal --> actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala deleted file mode 100644 index 88e7450..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ /dev/null @@ -1,68 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii -import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria -import fiftyforms.ui.components.tailwind.Color - -extension (o: UserInfo) - def toDetailOsoby: DetailOsoby.ViewModel = - DetailOsoby.ViewModel( - o.personalNumber, - o.name, - o.email, - o.phone, - o.img, - o.mainFunction.map(f => - DetailOsoby.Funkce.ViewModel(f.name, f.dept, f.ou) - ), - o.userContracts.headOption.map(c => - DetailOsoby.PracovniPomer.ViewModel(c.rel, c.startDate, c.endDate) - ) - ) - -extension (param: Parameter) - def toParametr(container: Parameter => Anchor): DetailParametru.ViewModel = - DetailParametru.ViewModel( - id = param.id, - nazev = param.name, - popis = param.description, - status = "Nesplněno", - statusColor = Color.red, - a = container(param) - ) - -extension (crit: ParameterCriteria) - def toKriterium( - container: ParameterCriteria => Anchor - ): DetailKriteria.ViewModel = - DetailKriteria.ViewModel( - nazev = crit.criteriumText, - kapitola = crit.chapterId, - bod = crit.itemId, - status = "Nesplněno", - statusColor = Color.red, - splneno = false, - dukazy = Nil, - container = container(crit) - ) - -extension (user: UserInfo) - def toUserRow( - container: UserInfo => HtmlElement = (_: UserInfo) => div() - ): UserRow.ViewModel = - UserRow.ViewModel( - osobniCislo = user.personalNumber.toString, - celeJmeno = user.name, - prijmeni = user.surname, - hlavniFunkce = user.mainFunction.map(_.name), - img = user.img, - container = () => container(user) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala deleted file mode 100644 index 7d5b2c1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.dashboard - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.Page - -object DashboardPage: - - def render: HtmlElement = - div("Dashboard page") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala deleted file mode 100644 index 9ef0698..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala +++ /dev/null @@ -1,31 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailKriteriaPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply($m: Signal[ViewModel])( - action: Observer[DukazyKriteria.Action] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - DukazyKriteria($m.map(_.kriterium.dukazy))(action) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala deleted file mode 100644 index 3b316e1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object DetailPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametry: SeznamParametru.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby($m.map(_.osoba)), - SeznamParametru($m.map(_.parametry)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala deleted file mode 100644 index 17224cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala +++ /dev/null @@ -1,24 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailParametruPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriteria: SeznamKriterii.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby.header($m.map(_.osoba)), - DetailParametru($m.map(_.parametr)), - SeznamKriterii($m.map(_.kriteria)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala deleted file mode 100644 index dc912fa..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ /dev/null @@ -1,107 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb -package app -package pages.detail - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.UpravDukazForm -import fiftyforms.services.files.File - -object UpravDukaz: - - type ThisPage = Page.UpravDukazKriteria - type PageKey = (OsobniCislo, String, String) - - trait State { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def availableFiles: EventStream[List[File]] - def actionBus: Observer[Action] - } - - def keyOfPage(page: ThisPage): PageKey = - (page.osobniCislo.value, page.parametr.value, page.kriterium.value) - - def onChangeAction(key: PageKey): Action = - FetchParameterCriteria( - key._1, - key._2, - key._3, - Page.UpravDukazKriteria(_, _, _) - ) - - class Connector(state: State)($page: Signal[ThisPage])(using Router[Page]): - val $paramChangeSignal = $page.splitOne(keyOfPage)((x, _, _) => x) - val $pageChangeSignal = $paramChangeSignal.map(onChangeAction) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.split(_ => ())((_, s, $s) => - PageComponent( - $s.map(buildModel), - state.availableFiles, - state.actionBus.contramap { - case UpravDukazForm.Cancelled => - NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) - case UpravDukazForm.AvailableFilesRequested => - FetchAvailableFiles(s._1.personalNumber) - } - ) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): PageComponent.ViewModel = - import connectors.* - PageComponent.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) - - object PageComponent: - import components.* - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply( - $m: Signal[ViewModel], - availableFilesStream: EventStream[List[File]], - events: Observer[UpravDukazForm.Event] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - UpravDukazForm(availableFilesStream)(events) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala deleted file mode 100644 index ca311b0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.Icons -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Color - -object DetailKriteria: - case class ViewModel( - nazev: String, - kapitola: String, - bod: String, - status: String, - statusColor: Color, - splneno: Boolean, - dukazy: List[DukazKriteria.ViewModel], - container: HtmlElement = div() - ) { - val id = s"${kapitola}${bod}" - } - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h3( - cls := "text-xl leading-6 font-bold text-gray-900", - child.text <-- $m.map(_.nazev) - ), - h3( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - "Kapitola ", - child.text <-- $m.map(_.kapitola), - " bod ", - child <-- $m.map(_.bod) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala deleted file mode 100644 index ae90290..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala +++ /dev/null @@ -1,100 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Avatar - -object DetailOsoby: - - object Funkce: - case class ViewModel( - nazev: String, - stredisko: String, - voj: String - ) - - def render($m: Signal[ViewModel]): HtmlElement = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.nazev), - span( - cls := "hidden md:inline", - " @ ", - child.text <-- $m.map(_.stredisko), - ", ", - child.text <-- $m.map(_.voj) - ) - ) - - object PracovniPomer: - case class ViewModel( - druh: String, - pocatek: LocalDate, - konec: Option[LocalDate] - ) - - def render($m: Signal[ViewModel]): HtmlElement = - import fiftyforms.ui.components.tailwind.CustomAttrs.datetime - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.druh), - " od ", - time( - datetime <-- $m.map(_.pocatek.toString), - child.text <-- $m.map(_.pocatek.toString) - ) - ) - - case class ViewModel( - osobniCislo: OsobniCislo, - jmeno: String, - email: Option[String], - telefon: Option[String], - img: Option[String], - hlavniFunkce: Option[Funkce.ViewModel], - pracovniPomer: Option[PracovniPomer.ViewModel] - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-4", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(16) - ), - div( - h1( - cls := "text-2xl font-bold text-gray-900", - child.text <-- $m.map(_.jmeno) - ), - child.maybe <-- $m.map(_.hlavniFunkce).split(_ => ())((_, _, d) => - Funkce.render(d) - ), - child.maybe <-- $m.map(_.pracovniPomer).split(_ => ())((_, _, d) => - PracovniPomer.render(d) - ) - ) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-5", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(8) - ), - div( - h1( - cls := "text-lg font-medium text-gray-800", - child.text <-- $m.map(_.jmeno) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala deleted file mode 100644 index e11cb62..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color - -object DetailParametru: - case class ViewModel( - id: String, - nazev: String, - popis: String, - status: String, - statusColor: Color, - a: Anchor - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h2( - cls := "text-2xl leading-6 font-medium text-gray-900", - child.text <-- $m.map(_.nazev) - ), - p( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - child.text <-- $m.map(_.popis) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - h2( - cls := "text-lg leading-6 font-medium text-gray-600", - child.text <-- $m.map(_.nazev) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala deleted file mode 100644 index a00ede8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala +++ /dev/null @@ -1,177 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.CustomAttrs - -object DukazKriteria: - case class Osoba(osobniCislo: String, jmeno: String) - case class Udalost(osoba: Osoba, datum: LocalDate) - case class Dokument( - url: String, - nazev: String, - pridal: Udalost, - odebral: Option[Udalost] - ) - case class ViewModel( - dokumenty: List[Dokument], - autorizoval: Option[Udalost], - platiDo: Option[LocalDate], - poznámka: Option[String] - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-lg", - div( - cls := "px-4 py-5 sm:px-6", - h3( - cls := "text-lg leading-6 font-medium text-gray-900", - """Applicant Information""" - ), - p( - cls := "mt-1 max-w-2xl text-sm text-gray-500", - """Personal details and application.""" - ) - ), - div( - cls := "border-t border-gray-200 px-4 py-5 sm:px-6", - dl( - cls := "grid grid-cols-1 gap-x-4 gap-y-8 sm:grid-cols-2", - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Full name""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Margot Foster""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Application for""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Backend Developer""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Email address""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """margotfoster@example.com""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Salary expectation""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """$120,000""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """About""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Fugiat ipsum ipsum deserunt culpa aute sint do nostrud anim incididunt cillum culpa consequat. Excepteur qui ipsum aliquip consequat sint. Sit id mollit nulla mollit nostrud in ea officia proident. Irure nostrud pariatur mollit ad adipisicing reprehenderit deserunt qui eu.""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """Attachments""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - ul( - role := "list", - cls := "border border-gray-200 rounded-md divide-y divide-gray-200", - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """resume_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ), - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """coverletter_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala deleted file mode 100644 index 22f134f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala +++ /dev/null @@ -1,35 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object DukazyKriteria: - sealed trait Action - case object Add extends Action - - type ViewModel = List[DukazKriteria.ViewModel] - - def apply($m: Signal[ViewModel])(actions: Observer[Action]): HtmlElement = - div( - child.maybe <-- $m.map(m => - if (m.isEmpty) then Some(prazdnyDukaz(actions)) - else None - ), - children <-- $m.map(_.zipWithIndex).split(_._2)((_, _, $s) => - DukazKriteria($s.map(_._1)) - ) - ) - - private def prazdnyDukaz(actions: Observer[Action]): HtmlElement = - button( - tpe := "button", - onClick.preventDefault.mapTo(Add) --> actions, - cls := "relative block w-full border-2 border-gray-300 border-dashed rounded-lg p-12 text-center hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - Icons.outline - .`document-add`(12) - .amend(svg.cls := "mx-auto text-gray-400"), - span( - cls := "mt-2 block text-sm font-medium text-gray-900", - "Přidat důkaz" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala deleted file mode 100644 index f628697..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.list.StackedList -import fiftyforms.ui.components.tailwind.list.ListRow -import fiftyforms.ui.components.tailwind.list.RowTag -import fiftyforms.ui.components.tailwind.list.RowNext -import java.time.LocalDate - -object SeznamKriterii: - type ViewModel = List[DetailKriteria.ViewModel] - - private val kritList = new StackedList[DetailKriteria.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - kritList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = - p(cls := "text-sm text-gray-500", s"${i.kapitola}${i.bod}"), - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.container - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala deleted file mode 100644 index 2bfe4bc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.list.{ - StackedList, - ListRow, - RowTag, - PropList, - IconText, - RowNext -} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.LinkSupport.* - -object SeznamParametru: - type ViewModel = List[DetailParametru.ViewModel] - - private val parametrList = new StackedList[DetailParametru.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - parametrList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = emptyNode, - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.a - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala deleted file mode 100644 index f048e9d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ /dev/null @@ -1,101 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.form.* -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement -import fiftyforms.services.files.components.tailwind.FilePicker -import cz.e_bs.cmi.mdr.pdb.frontend.AutorizujDukaz -import cz.e_bs.cmi.mdr.pdb.frontend.DocumentRef -import fiftyforms.services.files.File - -object UpravDukazForm: - sealed trait Event - case object Cancelled extends Event - case object AvailableFilesRequested extends Event - def apply(availableFilesStream: EventStream[List[File]])( - updates: Observer[Event] - ): HtmlElement = - val (filesStream, filesObserver) = - EventStream.withObserver[FilePicker.Event] - val files = Var[List[File]](Nil) - def submitButtons: HtmlElement = - div( - cls := "pt-5", - div( - cls := "flex justify-end", - button( - tpe := "button", - cls := "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zrušit", - onClick.mapTo(Cancelled) --> updates - ), - button( - tpe := "submit", - cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - disabled <-- files.signal.map(_.isEmpty), - "Autorizovat" - ) - ) - ) - - div( - filesStream.collect { case FilePicker.SelectionUpdated(files) => - files.to(List) - } --> files.writer, - filesStream.collect { case FilePicker.AvailableFilesRequested => - AvailableFilesRequested - } --> updates, - cls := "bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6", - Form( - Form.Body( - Form.Section( - FormHeader( - FormHeader.ViewModel( - "Doložení kritéria", - "Sestavte doklady poskytující důkaz kritéria a potvrďte odesláním formuláře. Případné limitace či jiné relevantní údaje vepište do pole Komentář." - ) - ), - FormFields( - FormRow( - "dokumenty", - "Dokumenty", - FilePicker(files.signal, availableFilesStream)(filesObserver) - .amend(idAttr := "dokumenty", cls("max-w-lg")) - ).toHtml, - FormRow( - "platnost", - "Platnost", - input( - idAttr := "platnost", - name := "platnost", - tpe := "date", - autoComplete := "date", - cls := "block max-w-lg w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" - ) - ).toHtml, - FormRow( - "komentar", - "Komentář", - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - textArea( - idAttr := "komentar", - name := "about", - rows := 3, - cls := "max-w-lg shadow-sm block w-full focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border border-gray-300 rounded-md" - ), - p( - cls := "mt-2 text-sm text-gray-500", - "Doplňte prosím potřebné informace související s doložením kritéria, včetně případných limitací." - ) - ) - ).toHtml - ) - ) - ), - submitButtons - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala deleted file mode 100644 index 41033f6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -import components._ - -object DirectoryPage: - - type ViewModel = List[UserRow.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - val (actionsStream, actionObserver) = - EventStream.withObserver[SearchForm.Action] - val $filter = actionsStream - .collect { - case SearchForm.ClearFilter => None - case SearchForm.SetFilter(t) => Some(t) - } - .startWith(None) - val byLetter = for { - d <- $m - f <- $filter - } yield for { - (letter, users) <- d - .filter { user => - f match - case None => true - case Some(t) => user.search.contains(t) - } - .groupBy(_.prijmeni.head) - .to(List) - .sortBy(_._1) - } yield (letter.toString, users.sortBy(_.prijmeni)) - - div( - cls := "h-full max-w-7xl mx-auto order-first flex flex-col flex-shrink-0", - SearchForm(actionObserver), - Directory(byLetter) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala deleted file mode 100644 index 73f915d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -object Directory: - - object Header: - type ViewModel = String - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500", - h3(child.text <-- $m) - ) - - object UserList: - type ViewModel = List[UserRow.ViewModel] - def apply($m: Signal[ViewModel]): HtmlElement = - ul( - role := "list", - cls := "relative z-0 divide-y divide-gray-200", - children <-- $m.split(_.osobniCislo)((_, _, s) => UserRow(s)) - ) - - type ViewModel = List[(String, List[UserRow.ViewModel])] - def apply($m: Signal[ViewModel]): HtmlElement = - nav( - cls := "flex-1 min-h-0 overflow-y-auto", - aria.label := "Directory", - children <-- $m.split(_._1)((_, _, s) => - div( - cls := "relative", - Header(s.map(_._1)), - UserList(s.map(_._2)) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala deleted file mode 100644 index e182910..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala +++ /dev/null @@ -1,54 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object SearchForm: - sealed trait Action - case object SubmitFilter extends Action - sealed trait FilterAction extends Action - case object ClearFilter extends FilterAction - case class SetFilter(value: String) extends FilterAction - - def apply(actions: Observer[Action]): HtmlElement = - div( - cls := "px-6 pt-4 pb-4", - form( - cls := "flex space-x-4", - onSubmit.mapTo(SubmitFilter) --> actions, - div( - cls := "flex-1 min-w-0", - label( - forId := "search", - cls := "sr-only", - "Hledat" - ), - div( - cls := "relative rounded-md shadow-sm", - div( - cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", - Icons.solid.search().amend(svg.cls := "text-gray-400") - ), - input( - tpe := "search", - name := "search", - idAttr := "search", - cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", - placeholder := "Hledat", - composeEvents(onInput.mapToValue.setAsValue.map(SetFilter(_)))( - _.throttle(500) - ) --> actions - ) - ) - ), - button( - tpe := "submit", - cls := "inline-flex justify-center px-3.5 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500", - Icons.solid.filter().amend(svg.cls := "text-gray-400"), - span( - cls := "sr-only", - "Hledat" - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala deleted file mode 100644 index 82f7723..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala +++ /dev/null @@ -1,52 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.airstream.core.Signal -import fiftyforms.ui.components.tailwind.Avatar - -object UserRow: - case class ViewModel( - osobniCislo: String, - celeJmeno: String, - prijmeni: String, - hlavniFunkce: Option[String], - img: Option[String], - container: () => HtmlElement = () => div() - ) { - val search = prijmeni.toLowerCase - } - - def apply($m: Signal[ViewModel]): HtmlElement = - inline def avatarImage = - Avatar($m.map(_.img)).avatarImage(10) - - li( - div( - cls := "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500", - div( - cls := "flex-shrink-0", - child <-- avatarImage - ), - div( - cls := "flex-1 min-w-0", - child <-- $m.map { o => - o.container() - .amend( - cls := "focus:outline-none", - span( - cls := "absolute inset-0", - aria.hidden := true - ), - p( - cls := "text-sm font-medium text-gray-900", - o.celeJmeno - ), - p( - cls := "text-sm text-gray-500 truncate", - o.hlavniFunkce - ) - ) - } - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala deleted file mode 100644 index 467c5f5..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala +++ /dev/null @@ -1,99 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router - -object ErrorPage: - case class ViewModel( - homePage: Page, - errorName: String, - title: String, - subTitle: String - ) - - def apply(m: ViewModel, actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - val ViewModel(homePage, errorName, title, subTitle) = m - div( - cls := "min-h-full pt-16 pb-12 flex flex-col bg-white", - main( - cls := "flex-grow flex flex-col justify-center max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex-shrink-0 flex justify-center", - a( - href := "/", - cls := "inline-flex", - span( - cls := "sr-only", - """Workflow""" - ), - img( - cls := "h-12 w-auto", - src := "https://tailwindui.com/img/logos/workflow-mark.svg?color=indigo&shade=600", - alt := "" - ) - ) - ), - div( - cls := "py-16", - div( - cls := "text-center", - p( - cls := "text-sm font-semibold text-indigo-600 uppercase tracking-wide", - errorName - ), - h1( - cls := "mt-2 text-4xl font-extrabold text-gray-900 tracking-tight sm:text-5xl", - title - ), - p( - cls := "mt-2 text-base text-gray-500", - subTitle - ), - div( - cls := "mt-6", - PageLink - .container(homePage, actionBus) - .amend( - cls := "text-base font-medium text-indigo-600 hover:text-indigo-500", - """Go back home""" - ) - ) - ) - ) - ), - footer( - cls := "flex-shrink-0 max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", - nav( - cls := "flex justify-center space-x-4", - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Contact Support""" - ), - span( - cls := "inline-block border-l border-gray-300", - aria.hidden := true - ), - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Status""" - ), - span( - cls := "inline-block border-l border-gray-300", - aria.hidden := true - ), - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Twitter""" - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala deleted file mode 100644 index bd9baec..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action - -object NotFoundPage: - def apply(homePage: Page, url: String, actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - ErrorPage( - ErrorPage.ViewModel( - homePage, - "404 error", - "Page not found.", - s"Sorry, but page $url doesn't exist." - ), - actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala deleted file mode 100644 index 476fe3c..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action - -object UnhandledErrorPage: - - case class ViewModel( - homePage: Page, - errorName: Option[String], - errorMessage: Option[String] - ) - - def apply(m: ViewModel, actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - ErrorPage( - ErrorPage.ViewModel( - m.homePage, - "Unexpected error occurred", - m.errorName.getOrElse( - "Uh oh!" - ), // TODO: translations, better text than uh oh - m.errorMessage.getOrElse( - "This wasn't supposed to happen! Please try again." - ) - ), - actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala deleted file mode 100644 index 25f059f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala +++ /dev/null @@ -1,6 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.services - -import com.raquo.airstream.core.Observer - -trait DataFetcher[K, A]: - def fetch(id: K, sink: Observer[A]): Unit diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala deleted file mode 100644 index a469830..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala +++ /dev/null @@ -1,120 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package state - -import com.raquo.airstream.core.EventStream -import cz.e_bs.cmi.mdr.pdb.{UserInfo, OsobniCislo} -import com.raquo.airstream.core.Observer -import scala.scalajs.js -import scala.scalajs.js.JSON -import zio.json._ -import com.raquo.airstream.eventbus.EventBus -import com.raquo.airstream.ownership.Owner -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.UserFunction -import cz.e_bs.cmi.mdr.pdb.UserContract -import fiftyforms.services.files.File - -trait AppState - extends connectors.DetailPageConnector.AppState - with connectors.DetailParametruPageConnector.AppState - with connectors.DetailKriteriaPageConnector.AppState - with pages.detail.UpravDukaz.State: - def users: EventStream[List[UserInfo]] - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - -class MockAppState(implicit owner: Owner, router: Router[Page]) - extends AppState: - - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonDecoder[UserFunction] = DeriveJsonDecoder.gen - given JsonDecoder[UserContract] = DeriveJsonDecoder.gen - given JsonDecoder[UserInfo] = DeriveJsonDecoder.gen - - given JsonDecoder[ParameterCriteria] = DeriveJsonDecoder.gen - given JsonDecoder[Parameter] = DeriveJsonDecoder.gen - - private val actions = EventBus[Action]() - private val (parametersStream, pushParameters) = - EventStream.withCallback[List[Parameter]] - private val (usersStream, pushUsers) = - EventStream.withCallback[List[UserInfo]] - private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] - private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] - - private val mockData: List[UserInfo] = - mockUsers - .asInstanceOf[js.Dictionary[js.Object]] - .values - // TODO: is there a more efficient way to parse from JS object directly? - .map { o => - val parsed = JSON.stringify(o).fromJson[UserInfo] - parsed.left.foreach(org.scalajs.dom.console.log(_)) - parsed - } - .collect { case Right(u) => - u - } - .toList - - private val mockParameters: List[Parameter] = - pdbParams - .asInstanceOf[js.Dictionary[js.Object]] - .values - .map(o => JSON.stringify(o).fromJson[Parameter]) - .collect { case Right(p) => p } - .toList - - // TODO: Extract to separate event handler - actions.events.foreach { - case FetchDirectory => pushUsers(mockData) - case FetchUserDetails(osc) => - mockData.find(_.personalNumber == osc).foreach { o => - pushDetails(o) - router.replaceState(Page.Detail(o)) - } - case FetchParameters(osc) => - pushParameters(mockParameters) - case FetchParameter(osc, paramId) => - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(Page.DetailParametru(o, p)) - case FetchParameterCriteria(osc, paramId, critId, page) => - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - c <- p.criteria.find(_.id == critId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(page(o, p, c)) - case NavigateTo(page) => router.pushState(page) - case FetchAvailableFiles(osc) => - pushFiles( - List( - File("https://tc163.cmi.cz/here", "Example file") - ) - ) - } - - override def users: EventStream[List[UserInfo]] = - usersStream.debugWithName("users") - - override def details: EventStream[UserInfo] = - detailsStream.debugWithName("details") - - override def parameters: EventStream[List[Parameter]] = - parametersStream.debugWithName("parameters") - - override def availableFiles: EventStream[List[File]] = - filesStream.debugWithName("available files") - - override def actionBus: Observer[Action] = - actions.writer.debugWithName("actions writer") diff --git a/app/src/main/scala/mdr/pdb/app/Main.scala b/app/src/main/scala/mdr/pdb/app/Main.scala new file mode 100644 index 0000000..081b4ee --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/Main.scala @@ -0,0 +1,103 @@ +package mdr.pdb.app + +import scala.scalajs.js.annotation.JSExportTopLevel +import scala.scalajs.js.annotation.JSExport +import scala.scalajs.js.annotation.JSImport +import scala.scalajs.js +import org.scalajs.dom +import com.raquo.laminar.api.L.{*, given} +import scala.scalajs.js.Date +import com.raquo.waypoint.Router +import com.raquo.waypoint.SplitRender +import mdr.pdb.app.services.DataFetcher +import scala.scalajs.js.JSON +import zio.json._ +import mdr.pdb.UserInfo +import mdr.pdb.app.state.AppState + +@js.native +@JSImport("stylesheets/main.css", JSImport.Namespace) +object Css extends js.Any + +@js.native +@JSImport("data/users.json", JSImport.Default) +object mockUsers extends js.Object + +@js.native +@JSImport("params/pdb-params.json", JSImport.Default) +object pdbParams extends js.Object + +@JSExportTopLevel("app") +object Main: + + @JSExport + def main(args: Array[String]): Unit = + import Routes.given + onLoad { + setupAirstream() + val appContainer = dom.document.querySelector("#app") + val _ = + render( + appContainer, + renderPage(state.MockAppState(using unsafeWindowOwner, router)) + ) + } + + private def onLoad(f: => Unit): Unit = + documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) + + private def setupAirstream()(using router: Router[Page]): Unit = + AirstreamError.registerUnhandledErrorCallback(err => + router.forcePage( + Page.UnhandledError( + Some(err.getClass.getName), // TODO: Fill only in dev mode + Some(err.getMessage) + ) + ) + ) + + def renderPage(state: AppState)(using + router: Router[Page] + ): HtmlElement = + val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) + .collectSignal[Page.Detail]( + connectors + .DetailPageConnector(state)(_) + .apply + ) + .collectSignal[Page.DetailParametru]( + connectors + .DetailParametruPageConnector(state)(_) + .apply + ) + .collectSignal[Page.DetailKriteria]( + connectors + .DetailKriteriaPageConnector(state)(_) + .apply + ) + .collectSignal[Page.UpravDukazKriteria]( + pages.detail.UpravDukaz.Connector(state)(_).apply + ) + .collectStatic(Page.Dashboard)( + connectors.DashboardPageConnector(state.actionBus).apply + ) + .collect[Page.NotFound](pg => + pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) + ) + .collect[Page.UnhandledError](pg => + pages.errors + .UnhandledErrorPage( + pages.errors.UnhandledErrorPage + .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), + state.actionBus + ) + ) + .collectStatic(Page.Directory)( + connectors + .DirectoryPageConnector(state.users, state.actionBus) + .apply + ) + div(cls := "h-full", child <-- pageSplitter.$view) + + // Pull in the stylesheet + val css: Css.type = Css diff --git a/app/src/main/scala/mdr/pdb/app/Routes.scala b/app/src/main/scala/mdr/pdb/app/Routes.scala new file mode 100644 index 0000000..2cc860c --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/Routes.scala @@ -0,0 +1,198 @@ +package mdr.pdb.app + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.* +import org.scalajs.dom +import zio.json.{*, given} +import mdr.pdb.OsobniCislo + +import scala.scalajs.js +import mdr.pdb.UserInfo +import mdr.pdb.Parameter +import mdr.pdb.ParameterCriteria +import mdr.pdb.app.Page.Titled + +// enum is not working with Waypoints' SplitRender collectStatic +sealed abstract class Page( + val id: String, + val title: String, + val parent: Option[Page] +) { + val path: Vector[Page] = + parent match + case None => Vector(this) + case Some(p) => p.path :+ this + + val isRoot: Boolean = parent.isEmpty +} + +object Page: + + case class Titled[V](value: V, title: Option[String] = None): + val show: String = title.getOrElse(value.toString) + + case object Directory extends Page("directory", "Adresář", None) + + case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) + + case class Detail(osobniCislo: Titled[OsobniCislo]) + extends Page("user", osobniCislo.show, Some(Directory)) + + object Detail { + def apply(o: UserInfo): Detail = Detail( + Titled(o.personalNumber, Some(o.name)) + ) + } + + case class DetailParametru( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String] + ) extends Page( + "parameter", + parametr.show, + Some(Detail(osobniCislo)) + ) + + object DetailParametru { + def apply(o: UserInfo, p: Parameter): DetailParametru = + DetailParametru( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)) + ) + } + + case class DetailKriteria( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String], + kriterium: Titled[String] + ) extends Page( + "criteria", + kriterium.show, + Some(DetailParametru(osobniCislo, parametr)) + ) + + object DetailKriteria { + def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = + DetailKriteria( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)), + Titled(k.id, Some(k.id)) + ) + } + + case class UpravDukazKriteria( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String], + kriterium: Titled[String] + ) extends Page( + "addProof", + "Důkaz", + Some(DetailKriteria(osobniCislo, parametr, kriterium)) + ) + + object UpravDukazKriteria { + def apply( + o: UserInfo, + p: Parameter, + k: ParameterCriteria + ): UpravDukazKriteria = + UpravDukazKriteria( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)), + Titled(k.id, Some(k.id)) + ) + } + + case class NotFound(url: String) extends Page("404", "404", Some(Directory)) + + case class UnhandledError( + errorName: Option[String], + errorMessage: Option[String] + ) extends Page("500", "Unexpected error", Some(Directory)) + +object Routes: + given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) + given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) + given [V: JsonEncoder]: JsonEncoder[Titled[V]] = + DeriveJsonEncoder.gen[Titled[V]] + given [V: JsonDecoder]: JsonDecoder[Titled[V]] = + DeriveJsonDecoder.gen[Titled[V]] + given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] + given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] + + val base = + js.`import`.meta.env.BASE_URL + .asInstanceOf[String] + "app" + + val homePage: Page = Page.Directory + + given router: Router[Page] = Router[Page]( + routes = List( + Route.static(homePage, root / endOfSegments, basePath = base), + Route.static( + Page.Dashboard, + root / "dashboard" / endOfSegments, + basePath = base + ), + Route[Page.Detail, String]( + encode = _.osobniCislo.value.toString, + decode = osc => Page.Detail(Titled(OsobniCislo(osc))), + root / "osoba" / segment[String] / endOfSegments, + basePath = base + ), + Route[Page.DetailParametru, (String, String)]( + encode = p => (p.osobniCislo.value.toString, p.parametr.value), + decode = + p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / endOfSegments, + basePath = base + ), + Route[Page.DetailKriteria, (String, String, String)]( + encode = p => + ( + p.osobniCislo.value.toString, + p.parametr.value, + p.kriterium.value.replaceAll("\\.", "--") + ), + decode = p => + Page.DetailKriteria( + Titled(OsobniCislo(p._1)), + Titled(p._2), + Titled(p._3.replaceAll("--", ".")) + ), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / "kriterium" / segment[String] / endOfSegments, + basePath = base + ), + Route[Page.UpravDukazKriteria, (String, String, String)]( + encode = p => + ( + p.osobniCislo.value.toString, + p.parametr.value, + p.kriterium.value.replaceAll("\\.", "--") + ), + decode = p => + Page.UpravDukazKriteria( + Titled(OsobniCislo(p._1)), + Titled(p._2), + Titled(p._3.replaceAll("--", ".")) + ), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / "kriterium" / segment[String] / "edit" / endOfSegments, + basePath = base + ) + ), + serializePage = _.toJson, + deserializePage = _.fromJson[Page] + .fold(s => throw IllegalStateException(s), identity), + getPageTitle = _.title, + routeFallback = url => Page.NotFound(url), + deserializeFallback = _ => Page.Dashboard + )( + $popStateEvent = windowEvents.onPopState, + owner = unsafeWindowOwner + ) diff --git a/app/src/main/scala/mdr/pdb/app/actions.scala b/app/src/main/scala/mdr/pdb/app/actions.scala new file mode 100644 index 0000000..d078b3a --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/actions.scala @@ -0,0 +1,21 @@ +package mdr.pdb.app + +import mdr.pdb.OsobniCislo +import mdr.pdb.UserInfo +import mdr.pdb.Parameter +import mdr.pdb.ParameterCriteria + +sealed trait Action + +case object FetchDirectory extends Action +case class FetchUserDetails(osc: OsobniCislo) extends Action +case class FetchParameters(osc: OsobniCislo) extends Action +case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action +case class FetchParameterCriteria( + osc: OsobniCislo, + paramId: String, + critId: String, + page: (UserInfo, Parameter, ParameterCriteria) => Page +) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action +case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/mdr/pdb/app/components/AppPage.scala new file mode 100644 index 0000000..130c817 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/AppPage.scala @@ -0,0 +1,82 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.Page +import mdr.pdb.{UserProfile, UserInfo, OsobniCislo} +import com.raquo.waypoint.Router +import mdr.pdb.app.Action +import mdr.pdb.app.NavigateTo +import mdr.pdb.UserFunction + +object AppPage: + // TODO: pages by logged in user + val pages: List[Page] = List(Page.Directory, Page.Dashboard) + + import NavigationBar.{Logo, MenuItem} + + val logo = Logo( + "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", + "Workflow" + ) + + // TODO: menu items by user profile + val userMenu = + List( + MenuItem("Your Profile"), + MenuItem("Settings"), + MenuItem("Sign out") + ) + + // TODO: load user profile + val $userProfile = Var( + UserProfile( + "tom", + UserInfo( + OsobniCislo("1031"), + "tom", + "Tom", + "Cook", + None, + None, + Some("tom@example.com"), + Some("+420 222 866 180"), + Some( + UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") + ), + Nil, + None + ) + ) + ) + + val $userInfo = $userProfile.signal.map(_.userInfo) + + type ViewModel = Option[HtmlElement] + def apply( + actionBus: Observer[Action] + )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using + router: Router[Page] + ): HtmlElement = + PageLayout(actionBus)( + $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => + PageLayout.ViewModel( + NavigationBar.ViewModel( + u, + pages.map(p => + NavigationBar.Link( + () => + PageLink( + p, + actionBus + ), + p == cp + ) + ), + userMenu, + logo + ), + c + ) + ), + mods + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala new file mode 100644 index 0000000..9375398 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala @@ -0,0 +1,143 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.tailwind.list.IconText.ViewModel +import fiftyforms.ui.components.tailwind.Icons +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.Page + +import com.raquo.laminar.api.L.{*, given} +import io.laminext.syntax.core.{*, given} +import mdr.pdb.app.Action + +object Breadcrumbs: + + private def slash = + import svg.{*, given} + svg( + cls := "flex-shrink-0 h-5 w-5 text-gray-300", + xmlns := "http://www.w3.org/2000/svg", + fill := "currentColor", + viewBox := "0 0 20 20", + ariaHidden := true, + path( + d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" + ) + ) + + def backIcon = + Icons.solid + .`arrow-narrow-left`() + .amend( + svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" + ) + + object Link: + case class ViewModel( + page: Page, + icon: Option[SvgElement], + extraClasses: String, + text: String, + textClasses: Option[String] = None + ) + + val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" + + def shortHome(p: Page) = ViewModel( + p, + Some(backIcon), + "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", + "Zpět na úvodní stránku", + None + ) + + def fullHome(p: Page) = ViewModel( + p, + Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), + "text-gray-400 hover:text-gray-500", + "Domů", + Some("sr-only") + ) + + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + PageLink + .container($m.map(_.page), actionBus) + .amend( + cls <-- $m.map(_.extraClasses), + child.maybe <-- $m.map(_.icon), + span( + cls <-- $m.map(_.textClasses.getOrElse("")), + child.text <-- $m.map(_.text) + ) + ) + + object FullBreadcrumbs: + type ViewModel = Page + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + ol( + role := "list", + cls := "flex items-center space-x-4", + children <-- $m.map(_.path).split(_.id)((_, _, $p) => + li( + div( + cls := "flex items-center", + child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), + Link( + $p.map(p => + if (p.isRoot) then Link.fullHome(p) + else + Link.ViewModel( + p, + None, + s"ml-4 max-w-xs truncate ${Link.baseClasses}", + p.title + ) + ), + actionBus + ) + ) + ) + ) + ) + + object ShortBreadcrumbs: + type ViewModel = Page + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + Link( + $m.map { p => + val target = p.parent.getOrElse(p) + if target.isRoot then Link.shortHome(target) + else + Link.ViewModel( + target, + Some(backIcon), + "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", + s"Zpět na ${target.title}" + ) + }, + actionBus + ) + + def apply(actionBus: Observer[Action])(using + router: Router[Page] + ): HtmlElement = + nav( + cls := "flex", + aria.label := "Breadcrumb", + div( + cls := "flex sm:hidden", + ShortBreadcrumbs(router.$currentPage, actionBus) + ), + div( + cls := "hidden sm:block", + FullBreadcrumbs(router.$currentPage, actionBus) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala new file mode 100644 index 0000000..4bb8b8d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala @@ -0,0 +1,242 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent +import fiftyforms.ui.components.tailwind.Icons +import fiftyforms.ui.components.tailwind.Avatar +import mdr.pdb.UserInfo +import io.laminext.syntax.core.* + +object NavigationBar: + + case class Logo(img: String, name: String) + case class Link(a: () => Anchor, active: Boolean) + case class MenuItem(title: String) + + case class ViewModel( + userInfo: UserInfo, + pages: List[Link], + userMenu: List[MenuItem], + logo: Logo + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + val $userInfo = $m.map(_.userInfo) + val mobileMenuOpen = Var(false) + + val desktopOnly = cls("hidden md:block") + val mobileOnly = cls("md:hidden") + + inline def avatarImage(size: Int = 8) = + Avatar($userInfo.map(_.img)).avatarImage(size) + + def notificationButton = button( + tpe := "button", + cls := List( + "bg-indigo-600", + "focus:outline-none", + "focus:ring-2", + "focus:ring-offset-2", + "focus:ring-offset-indigo-600", + "focus:ring-white", + "hover:text-white", + "p-1", + "rounded-full", + "text-indigo-200" + ), + span(cls := "sr-only", "View notifications"), + Icons.outline.bell() + ) + + def userProfile: HtmlElement = + val menuOpen = Var(false) + + def menuItem(item: MenuItem, idx: Int): HtmlElement = + a( + href := "#", + cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", + role := "menuitem", + tabIndex := -1, + idAttr := s"user-menu-item-$idx", + item.title + ) + + div( + cls := "ml-3 relative", + div( + button( + tpe := "button", + cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", + idAttr := "user-menu-button", + aria.expanded <-- menuOpen.signal, + aria.hasPopup := true, + span(cls := "sr-only", "Open user menu"), + child <-- avatarImage(), + onClick.preventDefault.mapTo( + !menuOpen.now() + ) --> menuOpen.writer + ) + ), + /* + * */ + div( + cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", + cls <-- menuOpen.signal.map { o => + if (o) "md:block" else "md:hidden" + }, + role := "menu", + aria.orientation := "vertical", + aria.labelledBy := "user-menu-button", + tabIndex := -1, + // : keyboard navigation + children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) + ) + ) + + def mobileProfile = + def menuItem(item: MenuItem): HtmlElement = + a( + href := "#", + cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", + item.title + ) + + div( + cls := "pt-4 pb-3 border-t border-indigo-700", + div( + cls := "flex items-center px-5", + div( + cls := "flex-shrink-0", + child <-- avatarImage(10) + ), + div( + cls := "ml-3", + div( + cls := "text-base font-medium text-white", + child.text <-- $userInfo.map(_.name) + ), + child.maybe <-- $userInfo.map( + _.email.map(e => + div( + cls := "text-sm font-medium text-indigo-300", + e + ) + ) + ) + ), + notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) + ), + div( + cls := "mt-3 px-2 space-y-1", + children <-- $m.map(_.userMenu.map(menuItem)) + ) + ) + + def pageLink(page: Link): Anchor = + page + .a() + .amend( + cls := "text-white px-3 py-2 rounded-md text-sm font-medium", + cls := Seq( + "bg-indigo-700" -> page.active, + "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active + ), + ariaCurrent := (if page.active then "page" else "false") + ) + + def logoImg: Image = + img( + cls := "h-8 w-8", + src <-- $m.map(_.logo.img), + alt <-- $m.map(_.logo.name) + ) + + def mobileMenuButton = button( + tpe := "button", + cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", + aria.controls := "mobile-menu", + aria.expanded <-- mobileMenuOpen.signal, + span(cls := "sr-only", "Open main menu"), + Icons.outline + .menu() + .amend( + svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") + ), + Icons.outline + .x() + .amend( + svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") + ), + onClick.preventDefault.mapTo( + !mobileMenuOpen.now() + ) --> mobileMenuOpen.writer + ) + + def navBarLeft = + div( + cls := "flex items-center", + div(cls := "flex-shrink-0", logoImg), + div( + desktopOnly, + div( + cls := "ml-10 flex items-baseline space-x-4", + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) + ) + ) + ) + + def navBarRight = + div( + desktopOnly, + div( + cls := "ml-4 flex items-center md:ml-6", + notificationButton, + userProfile + ) + ) + + def navBarMobile = + div( + cls := "-mr-2 flex", + mobileOnly, + mobileMenuButton + ) + + def navBar = + div( + cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", + div( + cls := "flex items-center justify-between h-16", + navBarLeft, + navBarRight, + navBarMobile + ) + ) + + def mobileMenu = + div( + mobileOnly, + cls <-- mobileMenuOpen.signal.map { o => + if (o) "block" else "hidden" + }, + idAttr := "mobile-menu", + div( + cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) + ), + mobileProfile + ) + + nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala new file mode 100644 index 0000000..c4cb10d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala @@ -0,0 +1,19 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.Page +import com.raquo.waypoint.Router +import mdr.pdb.app.Action + +object PageHeader: + def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = + header( + cls := "bg-white shadow-sm", + div( + cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", + h1( + cls := "text-lg leading-6 font-semibold text-gray-900", + Breadcrumbs(actionBus) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala new file mode 100644 index 0000000..aae4539 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala @@ -0,0 +1,30 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.Page +import mdr.pdb.app.Action +import fiftyforms.ui.components.tailwind.Loading +import io.laminext.syntax.core.* + +object PageLayout: + case class ViewModel( + navigation: NavigationBar.ViewModel, + content: Option[HtmlElement] + ) + def apply(actionBus: Observer[Action])( + $m: Signal[ViewModel], + mods: Modifier[HtmlElement]* + )(using router: Router[Page]): HtmlElement = + val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) + div( + cls := "h-full flex flex-col", + NavigationBar($m.map(_.navigation)), + child.maybe <-- router.$currentPage.map(_.isRoot) + .switch(None, Some(PageHeader(actionBus))), + main( + cls := "flex-grow-1 overflow-y-auto", + mods, + child <-- $maybeContent.map(_.getOrElse(Loading)) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/mdr/pdb/app/components/PageLink.scala new file mode 100644 index 0000000..e6e7052 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageLink.scala @@ -0,0 +1,40 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.LinkSupport.* +import mdr.pdb.app.Page +import mdr.pdb.app.Action +import com.raquo.waypoint.Router +import mdr.pdb.app.NavigateTo + +object PageLink: + type ViewModel = Page + def apply($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = + a(mods($m, actions), child.text <-- $m.map(_.title)) + + def apply(m: ViewModel, actions: Observer[Action])(using + router: Router[Page] + ): Anchor = apply(Val(m), actions) + + def container($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = a(mods($m, actions)) + + def container(m: ViewModel, actions: Observer[Action])(using + Router[Page] + ): Anchor = container(Val(m), actions) + + private def mods($m: Signal[Page], actions: Observer[Action])(using + router: Router[Page] + ): Modifier[Anchor] = + Seq( + href <-- $m.map(router.absoluteUrlForPage).recover { case _ => + Some("invalid url") + }, + composeEvents(onClick.noKeyMod.preventDefault)( + _.sample($m) + .map(NavigateTo.apply) + ) --> actions + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala new file mode 100644 index 0000000..7682772 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala @@ -0,0 +1,13 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.pages.dashboard.DashboardPage +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage + +class DashboardPageConnector(actionBus: Observer[Action])(using + router: Router[Page] +): + def apply: HtmlElement = + AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala new file mode 100644 index 0000000..76b9133 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala @@ -0,0 +1,76 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.Parameter +import mdr.pdb.UserInfo +import mdr.pdb.app.pages.detail.DetailParametruPage +import pages.detail.DetailKriteriaPage +import pages.detail.components.DukazyKriteria +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.ParameterCriteria + +object DetailKriteriaPageConnector { + trait AppState { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + } +} + +case class DetailKriteriaPageConnector( + state: DetailKriteriaPageConnector.AppState +)( + $page: Signal[Page.DetailKriteria] +)(using Router[Page]): + val $paramChangeSignal = + $page.splitOne(p => + (p.osobniCislo.value, p.parametr.value, p.kriterium.value) + )((x, _, _) => x) + val $pageChangeSignal = + $paramChangeSignal.map( + FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) + ) + + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + val $merged = + $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => + for { + da <- d + pa <- p + pb <- pa.find(_.id == pc._2) + ka <- pb.criteria.find(_.id == pc._3) + } yield (da, pb, ka) + ) + + def apply: HtmlElement = + AppPage(state.actionBus)( + $merged.map(_.map(buildModel)) + .split(_ => ())((_, s, $s) => + DetailKriteriaPage($s)(state.actionBus.contramap { + case DukazyKriteria.Add => + NavigateTo( + Page.UpravDukazKriteria( + Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), + Page.Titled(s.parametr.id, Some(s.parametr.nazev)), + Page.Titled(s.kriterium.id, Some(s.kriterium.id)) + ) + ) + }) + ), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: Parameter, + k: ParameterCriteria + ): DetailKriteriaPage.ViewModel = + DetailKriteriaPage.ViewModel( + o.toDetailOsoby, + p.toParametr(_ => a()), + k.toKriterium(_ => a()) + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DetailPageConnector.scala new file mode 100644 index 0000000..df3a6b7 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DetailPageConnector.scala @@ -0,0 +1,57 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.UserInfo +import pages.detail.DetailPage +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.pages.detail.components.DetailOsoby +import mdr.pdb.Parameter +import mdr.pdb.app.pages.detail.components.SeznamParametru +import fiftyforms.ui.components.tailwind.Color + +object DetailPageConnector { + trait AppState { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + } +} + +case class DetailPageConnector(state: DetailPageConnector.AppState)( + $page: Signal[Page.Detail] +)(using router: Router[Page]): + val $oscChangeSignal = $page.splitOne(_.osobniCislo.value)((osc, _, _) => osc) + val $pageChangeSignal = + $oscChangeSignal.flatMap(osc => + EventStream.fromSeq(Seq(FetchUserDetails(osc), FetchParameters(osc))) + ) + // TODO: filter the value based on the current osc + // OSC change will fetch new data, but still + // - we need to be sure that what we got is really what we ought to display + // - we want to display stale data accordingly (at least with loading indicator) + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + def apply: HtmlElement = + AppPage(state.actionBus)( + $data.combineWithFn($params)(_ zip _) + .map(_.map(buildModel)) + .split(_ => ())((_, _, s) => DetailPage(s)), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: List[Parameter] + ): DetailPage.ViewModel = + DetailPage.ViewModel( + o.toDetailOsoby, + p.map( + _.toParametr(param => + PageLink.container(Page.DetailParametru(o, param), state.actionBus) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DetailParametruPageConnector.scala new file mode 100644 index 0000000..fa7193d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DetailParametruPageConnector.scala @@ -0,0 +1,67 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.Parameter +import mdr.pdb.UserInfo +import mdr.pdb.app.pages.detail.DetailParametruPage +import pages.detail.DetailParametruPage +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.components.PageLink + +object DetailParametruPageConnector { + trait AppState { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + } +} + +case class DetailParametruPageConnector( + state: DetailParametruPageConnector.AppState +)( + $page: Signal[Page.DetailParametru] +)(using router: Router[Page]): + val $paramChangeSignal = + $page.splitOne(p => (p.osobniCislo.value, p.parametr.value))((x, _, _) => x) + val $pageChangeSignal = + $paramChangeSignal.map(FetchParameter(_, _)) + + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + val $merged = + $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => + for { + da <- d + pa <- p + pb <- pa.find(_.id == pc._2) + } yield (da, pb) + ) + + def apply: HtmlElement = + AppPage(state.actionBus)( + $merged.map(_.map(buildModel)) + .split(_ => ())((_, _, s) => DetailParametruPage(s)), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: Parameter + ): DetailParametruPage.ViewModel = + DetailParametruPage.ViewModel( + o.toDetailOsoby, + p.toParametr(p => + PageLink.container(Page.DetailParametru(o, p), state.actionBus) + ), + p.criteria.map( + _.toKriterium { c => + PageLink.container( + Page.DetailKriteria(o, p, c), + state.actionBus + ) + } + ) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala deleted file mode 100644 index a3b00af..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala +++ /dev/null @@ -1,103 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import scala.scalajs.js.annotation.JSExportTopLevel -import scala.scalajs.js.annotation.JSExport -import scala.scalajs.js.annotation.JSImport -import scala.scalajs.js -import org.scalajs.dom -import com.raquo.laminar.api.L.{*, given} -import scala.scalajs.js.Date -import com.raquo.waypoint.Router -import com.raquo.waypoint.SplitRender -import cz.e_bs.cmi.mdr.pdb.app.services.DataFetcher -import scala.scalajs.js.JSON -import zio.json._ -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.state.AppState - -@js.native -@JSImport("stylesheets/main.css", JSImport.Namespace) -object Css extends js.Any - -@js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native -@JSImport("params/pdb-params.json", JSImport.Default) -object pdbParams extends js.Object - -@JSExportTopLevel("app") -object Main: - - @JSExport - def main(args: Array[String]): Unit = - import Routes.given - onLoad { - setupAirstream() - val appContainer = dom.document.querySelector("#app") - val _ = - render( - appContainer, - renderPage(state.MockAppState(using unsafeWindowOwner, router)) - ) - } - - private def onLoad(f: => Unit): Unit = - documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) - - private def setupAirstream()(using router: Router[Page]): Unit = - AirstreamError.registerUnhandledErrorCallback(err => - router.forcePage( - Page.UnhandledError( - Some(err.getClass.getName), // TODO: Fill only in dev mode - Some(err.getMessage) - ) - ) - ) - - def renderPage(state: AppState)(using - router: Router[Page] - ): HtmlElement = - val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) - .collectSignal[Page.Detail]( - connectors - .DetailPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailParametru]( - connectors - .DetailParametruPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailKriteria]( - connectors - .DetailKriteriaPageConnector(state)(_) - .apply - ) - .collectSignal[Page.UpravDukazKriteria]( - pages.detail.UpravDukaz.Connector(state)(_).apply - ) - .collectStatic(Page.Dashboard)( - connectors.DashboardPageConnector(state.actionBus).apply - ) - .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) - ) - .collect[Page.UnhandledError](pg => - pages.errors - .UnhandledErrorPage( - pages.errors.UnhandledErrorPage - .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), - state.actionBus - ) - ) - .collectStatic(Page.Directory)( - connectors - .DirectoryPageConnector(state.users, state.actionBus) - .apply - ) - div(cls := "h-full", child <-- pageSplitter.$view) - - // Pull in the stylesheet - val css: Css.type = Css diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala deleted file mode 100644 index 3bbc8c0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ /dev/null @@ -1,198 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.* -import org.scalajs.dom -import zio.json.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import scala.scalajs.js -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.Page.Titled - -// enum is not working with Waypoints' SplitRender collectStatic -sealed abstract class Page( - val id: String, - val title: String, - val parent: Option[Page] -) { - val path: Vector[Page] = - parent match - case None => Vector(this) - case Some(p) => p.path :+ this - - val isRoot: Boolean = parent.isEmpty -} - -object Page: - - case class Titled[V](value: V, title: Option[String] = None): - val show: String = title.getOrElse(value.toString) - - case object Directory extends Page("directory", "Adresář", None) - - case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) - - case class Detail(osobniCislo: Titled[OsobniCislo]) - extends Page("user", osobniCislo.show, Some(Directory)) - - object Detail { - def apply(o: UserInfo): Detail = Detail( - Titled(o.personalNumber, Some(o.name)) - ) - } - - case class DetailParametru( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String] - ) extends Page( - "parameter", - parametr.show, - Some(Detail(osobniCislo)) - ) - - object DetailParametru { - def apply(o: UserInfo, p: Parameter): DetailParametru = - DetailParametru( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)) - ) - } - - case class DetailKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "criteria", - kriterium.show, - Some(DetailParametru(osobniCislo, parametr)) - ) - - object DetailKriteria { - def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = - DetailKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class UpravDukazKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "addProof", - "Důkaz", - Some(DetailKriteria(osobniCislo, parametr, kriterium)) - ) - - object UpravDukazKriteria { - def apply( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): UpravDukazKriteria = - UpravDukazKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class NotFound(url: String) extends Page("404", "404", Some(Directory)) - - case class UnhandledError( - errorName: Option[String], - errorMessage: Option[String] - ) extends Page("500", "Unexpected error", Some(Directory)) - -object Routes: - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) - given [V: JsonEncoder]: JsonEncoder[Titled[V]] = - DeriveJsonEncoder.gen[Titled[V]] - given [V: JsonDecoder]: JsonDecoder[Titled[V]] = - DeriveJsonDecoder.gen[Titled[V]] - given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] - given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] - - val base = - js.`import`.meta.env.BASE_URL - .asInstanceOf[String] + "app" - - val homePage: Page = Page.Directory - - given router: Router[Page] = Router[Page]( - routes = List( - Route.static(homePage, root / endOfSegments, basePath = base), - Route.static( - Page.Dashboard, - root / "dashboard" / endOfSegments, - basePath = base - ), - Route[Page.Detail, String]( - encode = _.osobniCislo.value.toString, - decode = osc => Page.Detail(Titled(OsobniCislo(osc))), - root / "osoba" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.DetailParametru, (String, String)]( - encode = p => (p.osobniCislo.value.toString, p.parametr.value), - decode = - p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / endOfSegments, - basePath = base - ), - Route[Page.DetailKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.DetailKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.UpravDukazKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.UpravDukazKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / "edit" / endOfSegments, - basePath = base - ) - ), - serializePage = _.toJson, - deserializePage = _.fromJson[Page] - .fold(s => throw IllegalStateException(s), identity), - getPageTitle = _.title, - routeFallback = url => Page.NotFound(url), - deserializeFallback = _ => Page.Dashboard - )( - $popStateEvent = windowEvents.onPopState, - owner = unsafeWindowOwner - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala deleted file mode 100644 index b24b82b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -sealed trait Action - -case object FetchDirectory extends Action -case class FetchUserDetails(osc: OsobniCislo) extends Action -case class FetchParameters(osc: OsobniCislo) extends Action -case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action -case class FetchParameterCriteria( - osc: OsobniCislo, - paramId: String, - critId: String, - page: (UserInfo, Parameter, ParameterCriteria) => Page -) extends Action -case class FetchAvailableFiles(osc: OsobniCislo) extends Action -case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala deleted file mode 100644 index c637d35..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala +++ /dev/null @@ -1,82 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.{UserProfile, UserInfo, OsobniCislo} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo -import cz.e_bs.cmi.mdr.pdb.UserFunction - -object AppPage: - // TODO: pages by logged in user - val pages: List[Page] = List(Page.Directory, Page.Dashboard) - - import NavigationBar.{Logo, MenuItem} - - val logo = Logo( - "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", - "Workflow" - ) - - // TODO: menu items by user profile - val userMenu = - List( - MenuItem("Your Profile"), - MenuItem("Settings"), - MenuItem("Sign out") - ) - - // TODO: load user profile - val $userProfile = Var( - UserProfile( - "tom", - UserInfo( - OsobniCislo("1031"), - "tom", - "Tom", - "Cook", - None, - None, - Some("tom@example.com"), - Some("+420 222 866 180"), - Some( - UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") - ), - Nil, - None - ) - ) - ) - - val $userInfo = $userProfile.signal.map(_.userInfo) - - type ViewModel = Option[HtmlElement] - def apply( - actionBus: Observer[Action] - )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using - router: Router[Page] - ): HtmlElement = - PageLayout(actionBus)( - $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => - PageLayout.ViewModel( - NavigationBar.ViewModel( - u, - pages.map(p => - NavigationBar.Link( - () => - PageLink( - p, - actionBus - ), - p == cp - ) - ), - userMenu, - logo - ), - c - ) - ), - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala deleted file mode 100644 index bf473c6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ /dev/null @@ -1,143 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden -import fiftyforms.ui.components.tailwind.list.IconText.ViewModel -import fiftyforms.ui.components.tailwind.Icons -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Action - -object Breadcrumbs: - - private def slash = - import svg.{*, given} - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-300", - xmlns := "http://www.w3.org/2000/svg", - fill := "currentColor", - viewBox := "0 0 20 20", - ariaHidden := true, - path( - d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" - ) - ) - - def backIcon = - Icons.solid - .`arrow-narrow-left`() - .amend( - svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" - ) - - object Link: - case class ViewModel( - page: Page, - icon: Option[SvgElement], - extraClasses: String, - text: String, - textClasses: Option[String] = None - ) - - val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" - - def shortHome(p: Page) = ViewModel( - p, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - "Zpět na úvodní stránku", - None - ) - - def fullHome(p: Page) = ViewModel( - p, - Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), - "text-gray-400 hover:text-gray-500", - "Domů", - Some("sr-only") - ) - - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - PageLink - .container($m.map(_.page), actionBus) - .amend( - cls <-- $m.map(_.extraClasses), - child.maybe <-- $m.map(_.icon), - span( - cls <-- $m.map(_.textClasses.getOrElse("")), - child.text <-- $m.map(_.text) - ) - ) - - object FullBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - ol( - role := "list", - cls := "flex items-center space-x-4", - children <-- $m.map(_.path).split(_.id)((_, _, $p) => - li( - div( - cls := "flex items-center", - child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), - Link( - $p.map(p => - if (p.isRoot) then Link.fullHome(p) - else - Link.ViewModel( - p, - None, - s"ml-4 max-w-xs truncate ${Link.baseClasses}", - p.title - ) - ), - actionBus - ) - ) - ) - ) - ) - - object ShortBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - Link( - $m.map { p => - val target = p.parent.getOrElse(p) - if target.isRoot then Link.shortHome(target) - else - Link.ViewModel( - target, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - s"Zpět na ${target.title}" - ) - }, - actionBus - ) - - def apply(actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - nav( - cls := "flex", - aria.label := "Breadcrumb", - div( - cls := "flex sm:hidden", - ShortBreadcrumbs(router.$currentPage, actionBus) - ), - div( - cls := "hidden sm:block", - FullBreadcrumbs(router.$currentPage, actionBus) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala deleted file mode 100644 index 0db2047..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ /dev/null @@ -1,242 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent -import fiftyforms.ui.components.tailwind.Icons -import fiftyforms.ui.components.tailwind.Avatar -import cz.e_bs.cmi.mdr.pdb.UserInfo -import io.laminext.syntax.core.* - -object NavigationBar: - - case class Logo(img: String, name: String) - case class Link(a: () => Anchor, active: Boolean) - case class MenuItem(title: String) - - case class ViewModel( - userInfo: UserInfo, - pages: List[Link], - userMenu: List[MenuItem], - logo: Logo - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - val $userInfo = $m.map(_.userInfo) - val mobileMenuOpen = Var(false) - - val desktopOnly = cls("hidden md:block") - val mobileOnly = cls("md:hidden") - - inline def avatarImage(size: Int = 8) = - Avatar($userInfo.map(_.img)).avatarImage(size) - - def notificationButton = button( - tpe := "button", - cls := List( - "bg-indigo-600", - "focus:outline-none", - "focus:ring-2", - "focus:ring-offset-2", - "focus:ring-offset-indigo-600", - "focus:ring-white", - "hover:text-white", - "p-1", - "rounded-full", - "text-indigo-200" - ), - span(cls := "sr-only", "View notifications"), - Icons.outline.bell() - ) - - def userProfile: HtmlElement = - val menuOpen = Var(false) - - def menuItem(item: MenuItem, idx: Int): HtmlElement = - a( - href := "#", - cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", - role := "menuitem", - tabIndex := -1, - idAttr := s"user-menu-item-$idx", - item.title - ) - - div( - cls := "ml-3 relative", - div( - button( - tpe := "button", - cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - idAttr := "user-menu-button", - aria.expanded <-- menuOpen.signal, - aria.hasPopup := true, - span(cls := "sr-only", "Open user menu"), - child <-- avatarImage(), - onClick.preventDefault.mapTo( - !menuOpen.now() - ) --> menuOpen.writer - ) - ), - /* - * */ - div( - cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", - cls <-- menuOpen.signal.map { o => - if (o) "md:block" else "md:hidden" - }, - role := "menu", - aria.orientation := "vertical", - aria.labelledBy := "user-menu-button", - tabIndex := -1, - // : keyboard navigation - children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) - ) - ) - - def mobileProfile = - def menuItem(item: MenuItem): HtmlElement = - a( - href := "#", - cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", - item.title - ) - - div( - cls := "pt-4 pb-3 border-t border-indigo-700", - div( - cls := "flex items-center px-5", - div( - cls := "flex-shrink-0", - child <-- avatarImage(10) - ), - div( - cls := "ml-3", - div( - cls := "text-base font-medium text-white", - child.text <-- $userInfo.map(_.name) - ), - child.maybe <-- $userInfo.map( - _.email.map(e => - div( - cls := "text-sm font-medium text-indigo-300", - e - ) - ) - ) - ), - notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) - ), - div( - cls := "mt-3 px-2 space-y-1", - children <-- $m.map(_.userMenu.map(menuItem)) - ) - ) - - def pageLink(page: Link): Anchor = - page - .a() - .amend( - cls := "text-white px-3 py-2 rounded-md text-sm font-medium", - cls := Seq( - "bg-indigo-700" -> page.active, - "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active - ), - ariaCurrent := (if page.active then "page" else "false") - ) - - def logoImg: Image = - img( - cls := "h-8 w-8", - src <-- $m.map(_.logo.img), - alt <-- $m.map(_.logo.name) - ) - - def mobileMenuButton = button( - tpe := "button", - cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - aria.controls := "mobile-menu", - aria.expanded <-- mobileMenuOpen.signal, - span(cls := "sr-only", "Open main menu"), - Icons.outline - .menu() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") - ), - Icons.outline - .x() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") - ), - onClick.preventDefault.mapTo( - !mobileMenuOpen.now() - ) --> mobileMenuOpen.writer - ) - - def navBarLeft = - div( - cls := "flex items-center", - div(cls := "flex-shrink-0", logoImg), - div( - desktopOnly, - div( - cls := "ml-10 flex items-baseline space-x-4", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ) - ) - ) - - def navBarRight = - div( - desktopOnly, - div( - cls := "ml-4 flex items-center md:ml-6", - notificationButton, - userProfile - ) - ) - - def navBarMobile = - div( - cls := "-mr-2 flex", - mobileOnly, - mobileMenuButton - ) - - def navBar = - div( - cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex items-center justify-between h-16", - navBarLeft, - navBarRight, - navBarMobile - ) - ) - - def mobileMenu = - div( - mobileOnly, - cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }, - idAttr := "mobile-menu", - div( - cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ), - mobileProfile - ) - - nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala deleted file mode 100644 index 75339df..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object PageHeader: - def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = - header( - cls := "bg-white shadow-sm", - div( - cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", - h1( - cls := "text-lg leading-6 font-semibold text-gray-900", - Breadcrumbs(actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala deleted file mode 100644 index 5096463..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ /dev/null @@ -1,30 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import fiftyforms.ui.components.tailwind.Loading -import io.laminext.syntax.core.* - -object PageLayout: - case class ViewModel( - navigation: NavigationBar.ViewModel, - content: Option[HtmlElement] - ) - def apply(actionBus: Observer[Action])( - $m: Signal[ViewModel], - mods: Modifier[HtmlElement]* - )(using router: Router[Page]): HtmlElement = - val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) - div( - cls := "h-full flex flex-col", - NavigationBar($m.map(_.navigation)), - child.maybe <-- router.$currentPage.map(_.isRoot) - .switch(None, Some(PageHeader(actionBus))), - main( - cls := "flex-grow-1 overflow-y-auto", - mods, - child <-- $maybeContent.map(_.getOrElse(Loading)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala deleted file mode 100644 index 62d820f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.LinkSupport.* -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo - -object PageLink: - type ViewModel = Page - def apply($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = - a(mods($m, actions), child.text <-- $m.map(_.title)) - - def apply(m: ViewModel, actions: Observer[Action])(using - router: Router[Page] - ): Anchor = apply(Val(m), actions) - - def container($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = a(mods($m, actions)) - - def container(m: ViewModel, actions: Observer[Action])(using - Router[Page] - ): Anchor = container(Val(m), actions) - - private def mods($m: Signal[Page], actions: Observer[Action])(using - router: Router[Page] - ): Modifier[Anchor] = - Seq( - href <-- $m.map(router.absoluteUrlForPage).recover { case _ => - Some("invalid url") - }, - composeEvents(onClick.noKeyMod.preventDefault)( - _.sample($m) - .map(NavigateTo.apply) - ) --> actions - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala deleted file mode 100644 index 87f825a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.pages.dashboard.DashboardPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -class DashboardPageConnector(actionBus: Observer[Action])(using - router: Router[Page] -): - def apply: HtmlElement = - AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala deleted file mode 100644 index e9b0f71..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala +++ /dev/null @@ -1,76 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailKriteriaPage -import pages.detail.components.DukazyKriteria -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -object DetailKriteriaPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailKriteriaPageConnector( - state: DetailKriteriaPageConnector.AppState -)( - $page: Signal[Page.DetailKriteria] -)(using Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => - (p.osobniCislo.value, p.parametr.value, p.kriterium.value) - )((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map( - FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) - ) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, s, $s) => - DetailKriteriaPage($s)(state.actionBus.contramap { - case DukazyKriteria.Add => - NavigateTo( - Page.UpravDukazKriteria( - Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), - Page.Titled(s.parametr.id, Some(s.parametr.nazev)), - Page.Titled(s.kriterium.id, Some(s.kriterium.id)) - ) - ) - }) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): DetailKriteriaPage.ViewModel = - DetailKriteriaPage.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala deleted file mode 100644 index 4d8024f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ /dev/null @@ -1,57 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import pages.detail.DetailPage -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import fiftyforms.ui.components.tailwind.Color - -object DetailPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailPageConnector(state: DetailPageConnector.AppState)( - $page: Signal[Page.Detail] -)(using router: Router[Page]): - val $oscChangeSignal = $page.splitOne(_.osobniCislo.value)((osc, _, _) => osc) - val $pageChangeSignal = - $oscChangeSignal.flatMap(osc => - EventStream.fromSeq(Seq(FetchUserDetails(osc), FetchParameters(osc))) - ) - // TODO: filter the value based on the current osc - // OSC change will fetch new data, but still - // - we need to be sure that what we got is really what we ought to display - // - we want to display stale data accordingly (at least with loading indicator) - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - def apply: HtmlElement = - AppPage(state.actionBus)( - $data.combineWithFn($params)(_ zip _) - .map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: List[Parameter] - ): DetailPage.ViewModel = - DetailPage.ViewModel( - o.toDetailOsoby, - p.map( - _.toParametr(param => - PageLink.container(Page.DetailParametru(o, param), state.actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala deleted file mode 100644 index fdaa7f8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailParametruPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink - -object DetailParametruPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailParametruPageConnector( - state: DetailParametruPageConnector.AppState -)( - $page: Signal[Page.DetailParametru] -)(using router: Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => (p.osobniCislo.value, p.parametr.value))((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map(FetchParameter(_, _)) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - } yield (da, pb) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailParametruPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter - ): DetailParametruPage.ViewModel = - DetailParametruPage.ViewModel( - o.toDetailOsoby, - p.toParametr(p => - PageLink.container(Page.DetailParametru(o, p), state.actionBus) - ), - p.criteria.map( - _.toKriterium { c => - PageLink.container( - Page.DetailKriteria(o, p, c), - state.actionBus - ) - } - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala deleted file mode 100644 index 049aa22..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -case class DirectoryPageConnector( - $input: EventStream[List[UserInfo]], - actionBus: Observer[Action] -)(using router: Router[Page]): - val $data = $input.startWithNone - val $actionSignal = EventStream.fromValue(FetchDirectory) - - def apply: HtmlElement = - AppPage(actionBus)( - $data.split(_ => ())((_, _, s) => - pages.directory.DirectoryPage( - s.map( - _.map( - _.toUserRow(u => - PageLink.container( - Page.Detail(Page.Titled(u.personalNumber)), - actionBus - ) - ) - ) - ) - ) - ), - $actionSignal --> actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala deleted file mode 100644 index 88e7450..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ /dev/null @@ -1,68 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii -import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria -import fiftyforms.ui.components.tailwind.Color - -extension (o: UserInfo) - def toDetailOsoby: DetailOsoby.ViewModel = - DetailOsoby.ViewModel( - o.personalNumber, - o.name, - o.email, - o.phone, - o.img, - o.mainFunction.map(f => - DetailOsoby.Funkce.ViewModel(f.name, f.dept, f.ou) - ), - o.userContracts.headOption.map(c => - DetailOsoby.PracovniPomer.ViewModel(c.rel, c.startDate, c.endDate) - ) - ) - -extension (param: Parameter) - def toParametr(container: Parameter => Anchor): DetailParametru.ViewModel = - DetailParametru.ViewModel( - id = param.id, - nazev = param.name, - popis = param.description, - status = "Nesplněno", - statusColor = Color.red, - a = container(param) - ) - -extension (crit: ParameterCriteria) - def toKriterium( - container: ParameterCriteria => Anchor - ): DetailKriteria.ViewModel = - DetailKriteria.ViewModel( - nazev = crit.criteriumText, - kapitola = crit.chapterId, - bod = crit.itemId, - status = "Nesplněno", - statusColor = Color.red, - splneno = false, - dukazy = Nil, - container = container(crit) - ) - -extension (user: UserInfo) - def toUserRow( - container: UserInfo => HtmlElement = (_: UserInfo) => div() - ): UserRow.ViewModel = - UserRow.ViewModel( - osobniCislo = user.personalNumber.toString, - celeJmeno = user.name, - prijmeni = user.surname, - hlavniFunkce = user.mainFunction.map(_.name), - img = user.img, - container = () => container(user) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala deleted file mode 100644 index 7d5b2c1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.dashboard - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.Page - -object DashboardPage: - - def render: HtmlElement = - div("Dashboard page") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala deleted file mode 100644 index 9ef0698..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala +++ /dev/null @@ -1,31 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailKriteriaPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply($m: Signal[ViewModel])( - action: Observer[DukazyKriteria.Action] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - DukazyKriteria($m.map(_.kriterium.dukazy))(action) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala deleted file mode 100644 index 3b316e1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object DetailPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametry: SeznamParametru.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby($m.map(_.osoba)), - SeznamParametru($m.map(_.parametry)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala deleted file mode 100644 index 17224cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala +++ /dev/null @@ -1,24 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailParametruPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriteria: SeznamKriterii.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby.header($m.map(_.osoba)), - DetailParametru($m.map(_.parametr)), - SeznamKriterii($m.map(_.kriteria)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala deleted file mode 100644 index dc912fa..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ /dev/null @@ -1,107 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb -package app -package pages.detail - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.UpravDukazForm -import fiftyforms.services.files.File - -object UpravDukaz: - - type ThisPage = Page.UpravDukazKriteria - type PageKey = (OsobniCislo, String, String) - - trait State { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def availableFiles: EventStream[List[File]] - def actionBus: Observer[Action] - } - - def keyOfPage(page: ThisPage): PageKey = - (page.osobniCislo.value, page.parametr.value, page.kriterium.value) - - def onChangeAction(key: PageKey): Action = - FetchParameterCriteria( - key._1, - key._2, - key._3, - Page.UpravDukazKriteria(_, _, _) - ) - - class Connector(state: State)($page: Signal[ThisPage])(using Router[Page]): - val $paramChangeSignal = $page.splitOne(keyOfPage)((x, _, _) => x) - val $pageChangeSignal = $paramChangeSignal.map(onChangeAction) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.split(_ => ())((_, s, $s) => - PageComponent( - $s.map(buildModel), - state.availableFiles, - state.actionBus.contramap { - case UpravDukazForm.Cancelled => - NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) - case UpravDukazForm.AvailableFilesRequested => - FetchAvailableFiles(s._1.personalNumber) - } - ) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): PageComponent.ViewModel = - import connectors.* - PageComponent.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) - - object PageComponent: - import components.* - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply( - $m: Signal[ViewModel], - availableFilesStream: EventStream[List[File]], - events: Observer[UpravDukazForm.Event] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - UpravDukazForm(availableFilesStream)(events) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala deleted file mode 100644 index ca311b0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.Icons -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Color - -object DetailKriteria: - case class ViewModel( - nazev: String, - kapitola: String, - bod: String, - status: String, - statusColor: Color, - splneno: Boolean, - dukazy: List[DukazKriteria.ViewModel], - container: HtmlElement = div() - ) { - val id = s"${kapitola}${bod}" - } - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h3( - cls := "text-xl leading-6 font-bold text-gray-900", - child.text <-- $m.map(_.nazev) - ), - h3( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - "Kapitola ", - child.text <-- $m.map(_.kapitola), - " bod ", - child <-- $m.map(_.bod) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala deleted file mode 100644 index ae90290..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala +++ /dev/null @@ -1,100 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Avatar - -object DetailOsoby: - - object Funkce: - case class ViewModel( - nazev: String, - stredisko: String, - voj: String - ) - - def render($m: Signal[ViewModel]): HtmlElement = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.nazev), - span( - cls := "hidden md:inline", - " @ ", - child.text <-- $m.map(_.stredisko), - ", ", - child.text <-- $m.map(_.voj) - ) - ) - - object PracovniPomer: - case class ViewModel( - druh: String, - pocatek: LocalDate, - konec: Option[LocalDate] - ) - - def render($m: Signal[ViewModel]): HtmlElement = - import fiftyforms.ui.components.tailwind.CustomAttrs.datetime - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.druh), - " od ", - time( - datetime <-- $m.map(_.pocatek.toString), - child.text <-- $m.map(_.pocatek.toString) - ) - ) - - case class ViewModel( - osobniCislo: OsobniCislo, - jmeno: String, - email: Option[String], - telefon: Option[String], - img: Option[String], - hlavniFunkce: Option[Funkce.ViewModel], - pracovniPomer: Option[PracovniPomer.ViewModel] - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-4", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(16) - ), - div( - h1( - cls := "text-2xl font-bold text-gray-900", - child.text <-- $m.map(_.jmeno) - ), - child.maybe <-- $m.map(_.hlavniFunkce).split(_ => ())((_, _, d) => - Funkce.render(d) - ), - child.maybe <-- $m.map(_.pracovniPomer).split(_ => ())((_, _, d) => - PracovniPomer.render(d) - ) - ) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-5", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(8) - ), - div( - h1( - cls := "text-lg font-medium text-gray-800", - child.text <-- $m.map(_.jmeno) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala deleted file mode 100644 index e11cb62..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color - -object DetailParametru: - case class ViewModel( - id: String, - nazev: String, - popis: String, - status: String, - statusColor: Color, - a: Anchor - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h2( - cls := "text-2xl leading-6 font-medium text-gray-900", - child.text <-- $m.map(_.nazev) - ), - p( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - child.text <-- $m.map(_.popis) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - h2( - cls := "text-lg leading-6 font-medium text-gray-600", - child.text <-- $m.map(_.nazev) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala deleted file mode 100644 index a00ede8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala +++ /dev/null @@ -1,177 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.CustomAttrs - -object DukazKriteria: - case class Osoba(osobniCislo: String, jmeno: String) - case class Udalost(osoba: Osoba, datum: LocalDate) - case class Dokument( - url: String, - nazev: String, - pridal: Udalost, - odebral: Option[Udalost] - ) - case class ViewModel( - dokumenty: List[Dokument], - autorizoval: Option[Udalost], - platiDo: Option[LocalDate], - poznámka: Option[String] - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-lg", - div( - cls := "px-4 py-5 sm:px-6", - h3( - cls := "text-lg leading-6 font-medium text-gray-900", - """Applicant Information""" - ), - p( - cls := "mt-1 max-w-2xl text-sm text-gray-500", - """Personal details and application.""" - ) - ), - div( - cls := "border-t border-gray-200 px-4 py-5 sm:px-6", - dl( - cls := "grid grid-cols-1 gap-x-4 gap-y-8 sm:grid-cols-2", - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Full name""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Margot Foster""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Application for""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Backend Developer""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Email address""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """margotfoster@example.com""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Salary expectation""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """$120,000""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """About""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Fugiat ipsum ipsum deserunt culpa aute sint do nostrud anim incididunt cillum culpa consequat. Excepteur qui ipsum aliquip consequat sint. Sit id mollit nulla mollit nostrud in ea officia proident. Irure nostrud pariatur mollit ad adipisicing reprehenderit deserunt qui eu.""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """Attachments""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - ul( - role := "list", - cls := "border border-gray-200 rounded-md divide-y divide-gray-200", - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """resume_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ), - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """coverletter_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala deleted file mode 100644 index 22f134f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala +++ /dev/null @@ -1,35 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object DukazyKriteria: - sealed trait Action - case object Add extends Action - - type ViewModel = List[DukazKriteria.ViewModel] - - def apply($m: Signal[ViewModel])(actions: Observer[Action]): HtmlElement = - div( - child.maybe <-- $m.map(m => - if (m.isEmpty) then Some(prazdnyDukaz(actions)) - else None - ), - children <-- $m.map(_.zipWithIndex).split(_._2)((_, _, $s) => - DukazKriteria($s.map(_._1)) - ) - ) - - private def prazdnyDukaz(actions: Observer[Action]): HtmlElement = - button( - tpe := "button", - onClick.preventDefault.mapTo(Add) --> actions, - cls := "relative block w-full border-2 border-gray-300 border-dashed rounded-lg p-12 text-center hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - Icons.outline - .`document-add`(12) - .amend(svg.cls := "mx-auto text-gray-400"), - span( - cls := "mt-2 block text-sm font-medium text-gray-900", - "Přidat důkaz" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala deleted file mode 100644 index f628697..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.list.StackedList -import fiftyforms.ui.components.tailwind.list.ListRow -import fiftyforms.ui.components.tailwind.list.RowTag -import fiftyforms.ui.components.tailwind.list.RowNext -import java.time.LocalDate - -object SeznamKriterii: - type ViewModel = List[DetailKriteria.ViewModel] - - private val kritList = new StackedList[DetailKriteria.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - kritList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = - p(cls := "text-sm text-gray-500", s"${i.kapitola}${i.bod}"), - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.container - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala deleted file mode 100644 index 2bfe4bc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.list.{ - StackedList, - ListRow, - RowTag, - PropList, - IconText, - RowNext -} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.LinkSupport.* - -object SeznamParametru: - type ViewModel = List[DetailParametru.ViewModel] - - private val parametrList = new StackedList[DetailParametru.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - parametrList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = emptyNode, - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.a - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala deleted file mode 100644 index f048e9d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ /dev/null @@ -1,101 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.form.* -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement -import fiftyforms.services.files.components.tailwind.FilePicker -import cz.e_bs.cmi.mdr.pdb.frontend.AutorizujDukaz -import cz.e_bs.cmi.mdr.pdb.frontend.DocumentRef -import fiftyforms.services.files.File - -object UpravDukazForm: - sealed trait Event - case object Cancelled extends Event - case object AvailableFilesRequested extends Event - def apply(availableFilesStream: EventStream[List[File]])( - updates: Observer[Event] - ): HtmlElement = - val (filesStream, filesObserver) = - EventStream.withObserver[FilePicker.Event] - val files = Var[List[File]](Nil) - def submitButtons: HtmlElement = - div( - cls := "pt-5", - div( - cls := "flex justify-end", - button( - tpe := "button", - cls := "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zrušit", - onClick.mapTo(Cancelled) --> updates - ), - button( - tpe := "submit", - cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - disabled <-- files.signal.map(_.isEmpty), - "Autorizovat" - ) - ) - ) - - div( - filesStream.collect { case FilePicker.SelectionUpdated(files) => - files.to(List) - } --> files.writer, - filesStream.collect { case FilePicker.AvailableFilesRequested => - AvailableFilesRequested - } --> updates, - cls := "bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6", - Form( - Form.Body( - Form.Section( - FormHeader( - FormHeader.ViewModel( - "Doložení kritéria", - "Sestavte doklady poskytující důkaz kritéria a potvrďte odesláním formuláře. Případné limitace či jiné relevantní údaje vepište do pole Komentář." - ) - ), - FormFields( - FormRow( - "dokumenty", - "Dokumenty", - FilePicker(files.signal, availableFilesStream)(filesObserver) - .amend(idAttr := "dokumenty", cls("max-w-lg")) - ).toHtml, - FormRow( - "platnost", - "Platnost", - input( - idAttr := "platnost", - name := "platnost", - tpe := "date", - autoComplete := "date", - cls := "block max-w-lg w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" - ) - ).toHtml, - FormRow( - "komentar", - "Komentář", - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - textArea( - idAttr := "komentar", - name := "about", - rows := 3, - cls := "max-w-lg shadow-sm block w-full focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border border-gray-300 rounded-md" - ), - p( - cls := "mt-2 text-sm text-gray-500", - "Doplňte prosím potřebné informace související s doložením kritéria, včetně případných limitací." - ) - ) - ).toHtml - ) - ) - ), - submitButtons - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala deleted file mode 100644 index 41033f6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -import components._ - -object DirectoryPage: - - type ViewModel = List[UserRow.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - val (actionsStream, actionObserver) = - EventStream.withObserver[SearchForm.Action] - val $filter = actionsStream - .collect { - case SearchForm.ClearFilter => None - case SearchForm.SetFilter(t) => Some(t) - } - .startWith(None) - val byLetter = for { - d <- $m - f <- $filter - } yield for { - (letter, users) <- d - .filter { user => - f match - case None => true - case Some(t) => user.search.contains(t) - } - .groupBy(_.prijmeni.head) - .to(List) - .sortBy(_._1) - } yield (letter.toString, users.sortBy(_.prijmeni)) - - div( - cls := "h-full max-w-7xl mx-auto order-first flex flex-col flex-shrink-0", - SearchForm(actionObserver), - Directory(byLetter) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala deleted file mode 100644 index 73f915d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -object Directory: - - object Header: - type ViewModel = String - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500", - h3(child.text <-- $m) - ) - - object UserList: - type ViewModel = List[UserRow.ViewModel] - def apply($m: Signal[ViewModel]): HtmlElement = - ul( - role := "list", - cls := "relative z-0 divide-y divide-gray-200", - children <-- $m.split(_.osobniCislo)((_, _, s) => UserRow(s)) - ) - - type ViewModel = List[(String, List[UserRow.ViewModel])] - def apply($m: Signal[ViewModel]): HtmlElement = - nav( - cls := "flex-1 min-h-0 overflow-y-auto", - aria.label := "Directory", - children <-- $m.split(_._1)((_, _, s) => - div( - cls := "relative", - Header(s.map(_._1)), - UserList(s.map(_._2)) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala deleted file mode 100644 index e182910..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala +++ /dev/null @@ -1,54 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object SearchForm: - sealed trait Action - case object SubmitFilter extends Action - sealed trait FilterAction extends Action - case object ClearFilter extends FilterAction - case class SetFilter(value: String) extends FilterAction - - def apply(actions: Observer[Action]): HtmlElement = - div( - cls := "px-6 pt-4 pb-4", - form( - cls := "flex space-x-4", - onSubmit.mapTo(SubmitFilter) --> actions, - div( - cls := "flex-1 min-w-0", - label( - forId := "search", - cls := "sr-only", - "Hledat" - ), - div( - cls := "relative rounded-md shadow-sm", - div( - cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", - Icons.solid.search().amend(svg.cls := "text-gray-400") - ), - input( - tpe := "search", - name := "search", - idAttr := "search", - cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", - placeholder := "Hledat", - composeEvents(onInput.mapToValue.setAsValue.map(SetFilter(_)))( - _.throttle(500) - ) --> actions - ) - ) - ), - button( - tpe := "submit", - cls := "inline-flex justify-center px-3.5 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500", - Icons.solid.filter().amend(svg.cls := "text-gray-400"), - span( - cls := "sr-only", - "Hledat" - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala deleted file mode 100644 index 82f7723..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala +++ /dev/null @@ -1,52 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.airstream.core.Signal -import fiftyforms.ui.components.tailwind.Avatar - -object UserRow: - case class ViewModel( - osobniCislo: String, - celeJmeno: String, - prijmeni: String, - hlavniFunkce: Option[String], - img: Option[String], - container: () => HtmlElement = () => div() - ) { - val search = prijmeni.toLowerCase - } - - def apply($m: Signal[ViewModel]): HtmlElement = - inline def avatarImage = - Avatar($m.map(_.img)).avatarImage(10) - - li( - div( - cls := "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500", - div( - cls := "flex-shrink-0", - child <-- avatarImage - ), - div( - cls := "flex-1 min-w-0", - child <-- $m.map { o => - o.container() - .amend( - cls := "focus:outline-none", - span( - cls := "absolute inset-0", - aria.hidden := true - ), - p( - cls := "text-sm font-medium text-gray-900", - o.celeJmeno - ), - p( - cls := "text-sm text-gray-500 truncate", - o.hlavniFunkce - ) - ) - } - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala deleted file mode 100644 index 467c5f5..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala +++ /dev/null @@ -1,99 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router - -object ErrorPage: - case class ViewModel( - homePage: Page, - errorName: String, - title: String, - subTitle: String - ) - - def apply(m: ViewModel, actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - val ViewModel(homePage, errorName, title, subTitle) = m - div( - cls := "min-h-full pt-16 pb-12 flex flex-col bg-white", - main( - cls := "flex-grow flex flex-col justify-center max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex-shrink-0 flex justify-center", - a( - href := "/", - cls := "inline-flex", - span( - cls := "sr-only", - """Workflow""" - ), - img( - cls := "h-12 w-auto", - src := "https://tailwindui.com/img/logos/workflow-mark.svg?color=indigo&shade=600", - alt := "" - ) - ) - ), - div( - cls := "py-16", - div( - cls := "text-center", - p( - cls := "text-sm font-semibold text-indigo-600 uppercase tracking-wide", - errorName - ), - h1( - cls := "mt-2 text-4xl font-extrabold text-gray-900 tracking-tight sm:text-5xl", - title - ), - p( - cls := "mt-2 text-base text-gray-500", - subTitle - ), - div( - cls := "mt-6", - PageLink - .container(homePage, actionBus) - .amend( - cls := "text-base font-medium text-indigo-600 hover:text-indigo-500", - """Go back home""" - ) - ) - ) - ) - ), - footer( - cls := "flex-shrink-0 max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", - nav( - cls := "flex justify-center space-x-4", - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Contact Support""" - ), - span( - cls := "inline-block border-l border-gray-300", - aria.hidden := true - ), - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Status""" - ), - span( - cls := "inline-block border-l border-gray-300", - aria.hidden := true - ), - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Twitter""" - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala deleted file mode 100644 index bd9baec..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action - -object NotFoundPage: - def apply(homePage: Page, url: String, actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - ErrorPage( - ErrorPage.ViewModel( - homePage, - "404 error", - "Page not found.", - s"Sorry, but page $url doesn't exist." - ), - actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala deleted file mode 100644 index 476fe3c..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action - -object UnhandledErrorPage: - - case class ViewModel( - homePage: Page, - errorName: Option[String], - errorMessage: Option[String] - ) - - def apply(m: ViewModel, actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - ErrorPage( - ErrorPage.ViewModel( - m.homePage, - "Unexpected error occurred", - m.errorName.getOrElse( - "Uh oh!" - ), // TODO: translations, better text than uh oh - m.errorMessage.getOrElse( - "This wasn't supposed to happen! Please try again." - ) - ), - actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala deleted file mode 100644 index 25f059f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala +++ /dev/null @@ -1,6 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.services - -import com.raquo.airstream.core.Observer - -trait DataFetcher[K, A]: - def fetch(id: K, sink: Observer[A]): Unit diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala deleted file mode 100644 index a469830..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala +++ /dev/null @@ -1,120 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package state - -import com.raquo.airstream.core.EventStream -import cz.e_bs.cmi.mdr.pdb.{UserInfo, OsobniCislo} -import com.raquo.airstream.core.Observer -import scala.scalajs.js -import scala.scalajs.js.JSON -import zio.json._ -import com.raquo.airstream.eventbus.EventBus -import com.raquo.airstream.ownership.Owner -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.UserFunction -import cz.e_bs.cmi.mdr.pdb.UserContract -import fiftyforms.services.files.File - -trait AppState - extends connectors.DetailPageConnector.AppState - with connectors.DetailParametruPageConnector.AppState - with connectors.DetailKriteriaPageConnector.AppState - with pages.detail.UpravDukaz.State: - def users: EventStream[List[UserInfo]] - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - -class MockAppState(implicit owner: Owner, router: Router[Page]) - extends AppState: - - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonDecoder[UserFunction] = DeriveJsonDecoder.gen - given JsonDecoder[UserContract] = DeriveJsonDecoder.gen - given JsonDecoder[UserInfo] = DeriveJsonDecoder.gen - - given JsonDecoder[ParameterCriteria] = DeriveJsonDecoder.gen - given JsonDecoder[Parameter] = DeriveJsonDecoder.gen - - private val actions = EventBus[Action]() - private val (parametersStream, pushParameters) = - EventStream.withCallback[List[Parameter]] - private val (usersStream, pushUsers) = - EventStream.withCallback[List[UserInfo]] - private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] - private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] - - private val mockData: List[UserInfo] = - mockUsers - .asInstanceOf[js.Dictionary[js.Object]] - .values - // TODO: is there a more efficient way to parse from JS object directly? - .map { o => - val parsed = JSON.stringify(o).fromJson[UserInfo] - parsed.left.foreach(org.scalajs.dom.console.log(_)) - parsed - } - .collect { case Right(u) => - u - } - .toList - - private val mockParameters: List[Parameter] = - pdbParams - .asInstanceOf[js.Dictionary[js.Object]] - .values - .map(o => JSON.stringify(o).fromJson[Parameter]) - .collect { case Right(p) => p } - .toList - - // TODO: Extract to separate event handler - actions.events.foreach { - case FetchDirectory => pushUsers(mockData) - case FetchUserDetails(osc) => - mockData.find(_.personalNumber == osc).foreach { o => - pushDetails(o) - router.replaceState(Page.Detail(o)) - } - case FetchParameters(osc) => - pushParameters(mockParameters) - case FetchParameter(osc, paramId) => - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(Page.DetailParametru(o, p)) - case FetchParameterCriteria(osc, paramId, critId, page) => - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - c <- p.criteria.find(_.id == critId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(page(o, p, c)) - case NavigateTo(page) => router.pushState(page) - case FetchAvailableFiles(osc) => - pushFiles( - List( - File("https://tc163.cmi.cz/here", "Example file") - ) - ) - } - - override def users: EventStream[List[UserInfo]] = - usersStream.debugWithName("users") - - override def details: EventStream[UserInfo] = - detailsStream.debugWithName("details") - - override def parameters: EventStream[List[Parameter]] = - parametersStream.debugWithName("parameters") - - override def availableFiles: EventStream[List[File]] = - filesStream.debugWithName("available files") - - override def actionBus: Observer[Action] = - actions.writer.debugWithName("actions writer") diff --git a/app/src/main/scala/mdr/pdb/app/Main.scala b/app/src/main/scala/mdr/pdb/app/Main.scala new file mode 100644 index 0000000..081b4ee --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/Main.scala @@ -0,0 +1,103 @@ +package mdr.pdb.app + +import scala.scalajs.js.annotation.JSExportTopLevel +import scala.scalajs.js.annotation.JSExport +import scala.scalajs.js.annotation.JSImport +import scala.scalajs.js +import org.scalajs.dom +import com.raquo.laminar.api.L.{*, given} +import scala.scalajs.js.Date +import com.raquo.waypoint.Router +import com.raquo.waypoint.SplitRender +import mdr.pdb.app.services.DataFetcher +import scala.scalajs.js.JSON +import zio.json._ +import mdr.pdb.UserInfo +import mdr.pdb.app.state.AppState + +@js.native +@JSImport("stylesheets/main.css", JSImport.Namespace) +object Css extends js.Any + +@js.native +@JSImport("data/users.json", JSImport.Default) +object mockUsers extends js.Object + +@js.native +@JSImport("params/pdb-params.json", JSImport.Default) +object pdbParams extends js.Object + +@JSExportTopLevel("app") +object Main: + + @JSExport + def main(args: Array[String]): Unit = + import Routes.given + onLoad { + setupAirstream() + val appContainer = dom.document.querySelector("#app") + val _ = + render( + appContainer, + renderPage(state.MockAppState(using unsafeWindowOwner, router)) + ) + } + + private def onLoad(f: => Unit): Unit = + documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) + + private def setupAirstream()(using router: Router[Page]): Unit = + AirstreamError.registerUnhandledErrorCallback(err => + router.forcePage( + Page.UnhandledError( + Some(err.getClass.getName), // TODO: Fill only in dev mode + Some(err.getMessage) + ) + ) + ) + + def renderPage(state: AppState)(using + router: Router[Page] + ): HtmlElement = + val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) + .collectSignal[Page.Detail]( + connectors + .DetailPageConnector(state)(_) + .apply + ) + .collectSignal[Page.DetailParametru]( + connectors + .DetailParametruPageConnector(state)(_) + .apply + ) + .collectSignal[Page.DetailKriteria]( + connectors + .DetailKriteriaPageConnector(state)(_) + .apply + ) + .collectSignal[Page.UpravDukazKriteria]( + pages.detail.UpravDukaz.Connector(state)(_).apply + ) + .collectStatic(Page.Dashboard)( + connectors.DashboardPageConnector(state.actionBus).apply + ) + .collect[Page.NotFound](pg => + pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) + ) + .collect[Page.UnhandledError](pg => + pages.errors + .UnhandledErrorPage( + pages.errors.UnhandledErrorPage + .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), + state.actionBus + ) + ) + .collectStatic(Page.Directory)( + connectors + .DirectoryPageConnector(state.users, state.actionBus) + .apply + ) + div(cls := "h-full", child <-- pageSplitter.$view) + + // Pull in the stylesheet + val css: Css.type = Css diff --git a/app/src/main/scala/mdr/pdb/app/Routes.scala b/app/src/main/scala/mdr/pdb/app/Routes.scala new file mode 100644 index 0000000..2cc860c --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/Routes.scala @@ -0,0 +1,198 @@ +package mdr.pdb.app + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.* +import org.scalajs.dom +import zio.json.{*, given} +import mdr.pdb.OsobniCislo + +import scala.scalajs.js +import mdr.pdb.UserInfo +import mdr.pdb.Parameter +import mdr.pdb.ParameterCriteria +import mdr.pdb.app.Page.Titled + +// enum is not working with Waypoints' SplitRender collectStatic +sealed abstract class Page( + val id: String, + val title: String, + val parent: Option[Page] +) { + val path: Vector[Page] = + parent match + case None => Vector(this) + case Some(p) => p.path :+ this + + val isRoot: Boolean = parent.isEmpty +} + +object Page: + + case class Titled[V](value: V, title: Option[String] = None): + val show: String = title.getOrElse(value.toString) + + case object Directory extends Page("directory", "Adresář", None) + + case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) + + case class Detail(osobniCislo: Titled[OsobniCislo]) + extends Page("user", osobniCislo.show, Some(Directory)) + + object Detail { + def apply(o: UserInfo): Detail = Detail( + Titled(o.personalNumber, Some(o.name)) + ) + } + + case class DetailParametru( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String] + ) extends Page( + "parameter", + parametr.show, + Some(Detail(osobniCislo)) + ) + + object DetailParametru { + def apply(o: UserInfo, p: Parameter): DetailParametru = + DetailParametru( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)) + ) + } + + case class DetailKriteria( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String], + kriterium: Titled[String] + ) extends Page( + "criteria", + kriterium.show, + Some(DetailParametru(osobniCislo, parametr)) + ) + + object DetailKriteria { + def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = + DetailKriteria( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)), + Titled(k.id, Some(k.id)) + ) + } + + case class UpravDukazKriteria( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String], + kriterium: Titled[String] + ) extends Page( + "addProof", + "Důkaz", + Some(DetailKriteria(osobniCislo, parametr, kriterium)) + ) + + object UpravDukazKriteria { + def apply( + o: UserInfo, + p: Parameter, + k: ParameterCriteria + ): UpravDukazKriteria = + UpravDukazKriteria( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)), + Titled(k.id, Some(k.id)) + ) + } + + case class NotFound(url: String) extends Page("404", "404", Some(Directory)) + + case class UnhandledError( + errorName: Option[String], + errorMessage: Option[String] + ) extends Page("500", "Unexpected error", Some(Directory)) + +object Routes: + given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) + given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) + given [V: JsonEncoder]: JsonEncoder[Titled[V]] = + DeriveJsonEncoder.gen[Titled[V]] + given [V: JsonDecoder]: JsonDecoder[Titled[V]] = + DeriveJsonDecoder.gen[Titled[V]] + given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] + given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] + + val base = + js.`import`.meta.env.BASE_URL + .asInstanceOf[String] + "app" + + val homePage: Page = Page.Directory + + given router: Router[Page] = Router[Page]( + routes = List( + Route.static(homePage, root / endOfSegments, basePath = base), + Route.static( + Page.Dashboard, + root / "dashboard" / endOfSegments, + basePath = base + ), + Route[Page.Detail, String]( + encode = _.osobniCislo.value.toString, + decode = osc => Page.Detail(Titled(OsobniCislo(osc))), + root / "osoba" / segment[String] / endOfSegments, + basePath = base + ), + Route[Page.DetailParametru, (String, String)]( + encode = p => (p.osobniCislo.value.toString, p.parametr.value), + decode = + p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / endOfSegments, + basePath = base + ), + Route[Page.DetailKriteria, (String, String, String)]( + encode = p => + ( + p.osobniCislo.value.toString, + p.parametr.value, + p.kriterium.value.replaceAll("\\.", "--") + ), + decode = p => + Page.DetailKriteria( + Titled(OsobniCislo(p._1)), + Titled(p._2), + Titled(p._3.replaceAll("--", ".")) + ), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / "kriterium" / segment[String] / endOfSegments, + basePath = base + ), + Route[Page.UpravDukazKriteria, (String, String, String)]( + encode = p => + ( + p.osobniCislo.value.toString, + p.parametr.value, + p.kriterium.value.replaceAll("\\.", "--") + ), + decode = p => + Page.UpravDukazKriteria( + Titled(OsobniCislo(p._1)), + Titled(p._2), + Titled(p._3.replaceAll("--", ".")) + ), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / "kriterium" / segment[String] / "edit" / endOfSegments, + basePath = base + ) + ), + serializePage = _.toJson, + deserializePage = _.fromJson[Page] + .fold(s => throw IllegalStateException(s), identity), + getPageTitle = _.title, + routeFallback = url => Page.NotFound(url), + deserializeFallback = _ => Page.Dashboard + )( + $popStateEvent = windowEvents.onPopState, + owner = unsafeWindowOwner + ) diff --git a/app/src/main/scala/mdr/pdb/app/actions.scala b/app/src/main/scala/mdr/pdb/app/actions.scala new file mode 100644 index 0000000..d078b3a --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/actions.scala @@ -0,0 +1,21 @@ +package mdr.pdb.app + +import mdr.pdb.OsobniCislo +import mdr.pdb.UserInfo +import mdr.pdb.Parameter +import mdr.pdb.ParameterCriteria + +sealed trait Action + +case object FetchDirectory extends Action +case class FetchUserDetails(osc: OsobniCislo) extends Action +case class FetchParameters(osc: OsobniCislo) extends Action +case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action +case class FetchParameterCriteria( + osc: OsobniCislo, + paramId: String, + critId: String, + page: (UserInfo, Parameter, ParameterCriteria) => Page +) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action +case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/mdr/pdb/app/components/AppPage.scala new file mode 100644 index 0000000..130c817 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/AppPage.scala @@ -0,0 +1,82 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.Page +import mdr.pdb.{UserProfile, UserInfo, OsobniCislo} +import com.raquo.waypoint.Router +import mdr.pdb.app.Action +import mdr.pdb.app.NavigateTo +import mdr.pdb.UserFunction + +object AppPage: + // TODO: pages by logged in user + val pages: List[Page] = List(Page.Directory, Page.Dashboard) + + import NavigationBar.{Logo, MenuItem} + + val logo = Logo( + "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", + "Workflow" + ) + + // TODO: menu items by user profile + val userMenu = + List( + MenuItem("Your Profile"), + MenuItem("Settings"), + MenuItem("Sign out") + ) + + // TODO: load user profile + val $userProfile = Var( + UserProfile( + "tom", + UserInfo( + OsobniCislo("1031"), + "tom", + "Tom", + "Cook", + None, + None, + Some("tom@example.com"), + Some("+420 222 866 180"), + Some( + UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") + ), + Nil, + None + ) + ) + ) + + val $userInfo = $userProfile.signal.map(_.userInfo) + + type ViewModel = Option[HtmlElement] + def apply( + actionBus: Observer[Action] + )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using + router: Router[Page] + ): HtmlElement = + PageLayout(actionBus)( + $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => + PageLayout.ViewModel( + NavigationBar.ViewModel( + u, + pages.map(p => + NavigationBar.Link( + () => + PageLink( + p, + actionBus + ), + p == cp + ) + ), + userMenu, + logo + ), + c + ) + ), + mods + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala new file mode 100644 index 0000000..9375398 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala @@ -0,0 +1,143 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.tailwind.list.IconText.ViewModel +import fiftyforms.ui.components.tailwind.Icons +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.Page + +import com.raquo.laminar.api.L.{*, given} +import io.laminext.syntax.core.{*, given} +import mdr.pdb.app.Action + +object Breadcrumbs: + + private def slash = + import svg.{*, given} + svg( + cls := "flex-shrink-0 h-5 w-5 text-gray-300", + xmlns := "http://www.w3.org/2000/svg", + fill := "currentColor", + viewBox := "0 0 20 20", + ariaHidden := true, + path( + d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" + ) + ) + + def backIcon = + Icons.solid + .`arrow-narrow-left`() + .amend( + svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" + ) + + object Link: + case class ViewModel( + page: Page, + icon: Option[SvgElement], + extraClasses: String, + text: String, + textClasses: Option[String] = None + ) + + val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" + + def shortHome(p: Page) = ViewModel( + p, + Some(backIcon), + "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", + "Zpět na úvodní stránku", + None + ) + + def fullHome(p: Page) = ViewModel( + p, + Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), + "text-gray-400 hover:text-gray-500", + "Domů", + Some("sr-only") + ) + + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + PageLink + .container($m.map(_.page), actionBus) + .amend( + cls <-- $m.map(_.extraClasses), + child.maybe <-- $m.map(_.icon), + span( + cls <-- $m.map(_.textClasses.getOrElse("")), + child.text <-- $m.map(_.text) + ) + ) + + object FullBreadcrumbs: + type ViewModel = Page + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + ol( + role := "list", + cls := "flex items-center space-x-4", + children <-- $m.map(_.path).split(_.id)((_, _, $p) => + li( + div( + cls := "flex items-center", + child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), + Link( + $p.map(p => + if (p.isRoot) then Link.fullHome(p) + else + Link.ViewModel( + p, + None, + s"ml-4 max-w-xs truncate ${Link.baseClasses}", + p.title + ) + ), + actionBus + ) + ) + ) + ) + ) + + object ShortBreadcrumbs: + type ViewModel = Page + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + Link( + $m.map { p => + val target = p.parent.getOrElse(p) + if target.isRoot then Link.shortHome(target) + else + Link.ViewModel( + target, + Some(backIcon), + "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", + s"Zpět na ${target.title}" + ) + }, + actionBus + ) + + def apply(actionBus: Observer[Action])(using + router: Router[Page] + ): HtmlElement = + nav( + cls := "flex", + aria.label := "Breadcrumb", + div( + cls := "flex sm:hidden", + ShortBreadcrumbs(router.$currentPage, actionBus) + ), + div( + cls := "hidden sm:block", + FullBreadcrumbs(router.$currentPage, actionBus) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala new file mode 100644 index 0000000..4bb8b8d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala @@ -0,0 +1,242 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent +import fiftyforms.ui.components.tailwind.Icons +import fiftyforms.ui.components.tailwind.Avatar +import mdr.pdb.UserInfo +import io.laminext.syntax.core.* + +object NavigationBar: + + case class Logo(img: String, name: String) + case class Link(a: () => Anchor, active: Boolean) + case class MenuItem(title: String) + + case class ViewModel( + userInfo: UserInfo, + pages: List[Link], + userMenu: List[MenuItem], + logo: Logo + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + val $userInfo = $m.map(_.userInfo) + val mobileMenuOpen = Var(false) + + val desktopOnly = cls("hidden md:block") + val mobileOnly = cls("md:hidden") + + inline def avatarImage(size: Int = 8) = + Avatar($userInfo.map(_.img)).avatarImage(size) + + def notificationButton = button( + tpe := "button", + cls := List( + "bg-indigo-600", + "focus:outline-none", + "focus:ring-2", + "focus:ring-offset-2", + "focus:ring-offset-indigo-600", + "focus:ring-white", + "hover:text-white", + "p-1", + "rounded-full", + "text-indigo-200" + ), + span(cls := "sr-only", "View notifications"), + Icons.outline.bell() + ) + + def userProfile: HtmlElement = + val menuOpen = Var(false) + + def menuItem(item: MenuItem, idx: Int): HtmlElement = + a( + href := "#", + cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", + role := "menuitem", + tabIndex := -1, + idAttr := s"user-menu-item-$idx", + item.title + ) + + div( + cls := "ml-3 relative", + div( + button( + tpe := "button", + cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", + idAttr := "user-menu-button", + aria.expanded <-- menuOpen.signal, + aria.hasPopup := true, + span(cls := "sr-only", "Open user menu"), + child <-- avatarImage(), + onClick.preventDefault.mapTo( + !menuOpen.now() + ) --> menuOpen.writer + ) + ), + /* + * */ + div( + cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", + cls <-- menuOpen.signal.map { o => + if (o) "md:block" else "md:hidden" + }, + role := "menu", + aria.orientation := "vertical", + aria.labelledBy := "user-menu-button", + tabIndex := -1, + // : keyboard navigation + children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) + ) + ) + + def mobileProfile = + def menuItem(item: MenuItem): HtmlElement = + a( + href := "#", + cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", + item.title + ) + + div( + cls := "pt-4 pb-3 border-t border-indigo-700", + div( + cls := "flex items-center px-5", + div( + cls := "flex-shrink-0", + child <-- avatarImage(10) + ), + div( + cls := "ml-3", + div( + cls := "text-base font-medium text-white", + child.text <-- $userInfo.map(_.name) + ), + child.maybe <-- $userInfo.map( + _.email.map(e => + div( + cls := "text-sm font-medium text-indigo-300", + e + ) + ) + ) + ), + notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) + ), + div( + cls := "mt-3 px-2 space-y-1", + children <-- $m.map(_.userMenu.map(menuItem)) + ) + ) + + def pageLink(page: Link): Anchor = + page + .a() + .amend( + cls := "text-white px-3 py-2 rounded-md text-sm font-medium", + cls := Seq( + "bg-indigo-700" -> page.active, + "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active + ), + ariaCurrent := (if page.active then "page" else "false") + ) + + def logoImg: Image = + img( + cls := "h-8 w-8", + src <-- $m.map(_.logo.img), + alt <-- $m.map(_.logo.name) + ) + + def mobileMenuButton = button( + tpe := "button", + cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", + aria.controls := "mobile-menu", + aria.expanded <-- mobileMenuOpen.signal, + span(cls := "sr-only", "Open main menu"), + Icons.outline + .menu() + .amend( + svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") + ), + Icons.outline + .x() + .amend( + svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") + ), + onClick.preventDefault.mapTo( + !mobileMenuOpen.now() + ) --> mobileMenuOpen.writer + ) + + def navBarLeft = + div( + cls := "flex items-center", + div(cls := "flex-shrink-0", logoImg), + div( + desktopOnly, + div( + cls := "ml-10 flex items-baseline space-x-4", + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) + ) + ) + ) + + def navBarRight = + div( + desktopOnly, + div( + cls := "ml-4 flex items-center md:ml-6", + notificationButton, + userProfile + ) + ) + + def navBarMobile = + div( + cls := "-mr-2 flex", + mobileOnly, + mobileMenuButton + ) + + def navBar = + div( + cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", + div( + cls := "flex items-center justify-between h-16", + navBarLeft, + navBarRight, + navBarMobile + ) + ) + + def mobileMenu = + div( + mobileOnly, + cls <-- mobileMenuOpen.signal.map { o => + if (o) "block" else "hidden" + }, + idAttr := "mobile-menu", + div( + cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) + ), + mobileProfile + ) + + nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala new file mode 100644 index 0000000..c4cb10d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala @@ -0,0 +1,19 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.Page +import com.raquo.waypoint.Router +import mdr.pdb.app.Action + +object PageHeader: + def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = + header( + cls := "bg-white shadow-sm", + div( + cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", + h1( + cls := "text-lg leading-6 font-semibold text-gray-900", + Breadcrumbs(actionBus) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala new file mode 100644 index 0000000..aae4539 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala @@ -0,0 +1,30 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.Page +import mdr.pdb.app.Action +import fiftyforms.ui.components.tailwind.Loading +import io.laminext.syntax.core.* + +object PageLayout: + case class ViewModel( + navigation: NavigationBar.ViewModel, + content: Option[HtmlElement] + ) + def apply(actionBus: Observer[Action])( + $m: Signal[ViewModel], + mods: Modifier[HtmlElement]* + )(using router: Router[Page]): HtmlElement = + val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) + div( + cls := "h-full flex flex-col", + NavigationBar($m.map(_.navigation)), + child.maybe <-- router.$currentPage.map(_.isRoot) + .switch(None, Some(PageHeader(actionBus))), + main( + cls := "flex-grow-1 overflow-y-auto", + mods, + child <-- $maybeContent.map(_.getOrElse(Loading)) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/mdr/pdb/app/components/PageLink.scala new file mode 100644 index 0000000..e6e7052 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageLink.scala @@ -0,0 +1,40 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.LinkSupport.* +import mdr.pdb.app.Page +import mdr.pdb.app.Action +import com.raquo.waypoint.Router +import mdr.pdb.app.NavigateTo + +object PageLink: + type ViewModel = Page + def apply($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = + a(mods($m, actions), child.text <-- $m.map(_.title)) + + def apply(m: ViewModel, actions: Observer[Action])(using + router: Router[Page] + ): Anchor = apply(Val(m), actions) + + def container($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = a(mods($m, actions)) + + def container(m: ViewModel, actions: Observer[Action])(using + Router[Page] + ): Anchor = container(Val(m), actions) + + private def mods($m: Signal[Page], actions: Observer[Action])(using + router: Router[Page] + ): Modifier[Anchor] = + Seq( + href <-- $m.map(router.absoluteUrlForPage).recover { case _ => + Some("invalid url") + }, + composeEvents(onClick.noKeyMod.preventDefault)( + _.sample($m) + .map(NavigateTo.apply) + ) --> actions + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala new file mode 100644 index 0000000..7682772 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala @@ -0,0 +1,13 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.pages.dashboard.DashboardPage +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage + +class DashboardPageConnector(actionBus: Observer[Action])(using + router: Router[Page] +): + def apply: HtmlElement = + AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala new file mode 100644 index 0000000..76b9133 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala @@ -0,0 +1,76 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.Parameter +import mdr.pdb.UserInfo +import mdr.pdb.app.pages.detail.DetailParametruPage +import pages.detail.DetailKriteriaPage +import pages.detail.components.DukazyKriteria +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.ParameterCriteria + +object DetailKriteriaPageConnector { + trait AppState { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + } +} + +case class DetailKriteriaPageConnector( + state: DetailKriteriaPageConnector.AppState +)( + $page: Signal[Page.DetailKriteria] +)(using Router[Page]): + val $paramChangeSignal = + $page.splitOne(p => + (p.osobniCislo.value, p.parametr.value, p.kriterium.value) + )((x, _, _) => x) + val $pageChangeSignal = + $paramChangeSignal.map( + FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) + ) + + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + val $merged = + $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => + for { + da <- d + pa <- p + pb <- pa.find(_.id == pc._2) + ka <- pb.criteria.find(_.id == pc._3) + } yield (da, pb, ka) + ) + + def apply: HtmlElement = + AppPage(state.actionBus)( + $merged.map(_.map(buildModel)) + .split(_ => ())((_, s, $s) => + DetailKriteriaPage($s)(state.actionBus.contramap { + case DukazyKriteria.Add => + NavigateTo( + Page.UpravDukazKriteria( + Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), + Page.Titled(s.parametr.id, Some(s.parametr.nazev)), + Page.Titled(s.kriterium.id, Some(s.kriterium.id)) + ) + ) + }) + ), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: Parameter, + k: ParameterCriteria + ): DetailKriteriaPage.ViewModel = + DetailKriteriaPage.ViewModel( + o.toDetailOsoby, + p.toParametr(_ => a()), + k.toKriterium(_ => a()) + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DetailPageConnector.scala new file mode 100644 index 0000000..df3a6b7 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DetailPageConnector.scala @@ -0,0 +1,57 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.UserInfo +import pages.detail.DetailPage +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.pages.detail.components.DetailOsoby +import mdr.pdb.Parameter +import mdr.pdb.app.pages.detail.components.SeznamParametru +import fiftyforms.ui.components.tailwind.Color + +object DetailPageConnector { + trait AppState { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + } +} + +case class DetailPageConnector(state: DetailPageConnector.AppState)( + $page: Signal[Page.Detail] +)(using router: Router[Page]): + val $oscChangeSignal = $page.splitOne(_.osobniCislo.value)((osc, _, _) => osc) + val $pageChangeSignal = + $oscChangeSignal.flatMap(osc => + EventStream.fromSeq(Seq(FetchUserDetails(osc), FetchParameters(osc))) + ) + // TODO: filter the value based on the current osc + // OSC change will fetch new data, but still + // - we need to be sure that what we got is really what we ought to display + // - we want to display stale data accordingly (at least with loading indicator) + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + def apply: HtmlElement = + AppPage(state.actionBus)( + $data.combineWithFn($params)(_ zip _) + .map(_.map(buildModel)) + .split(_ => ())((_, _, s) => DetailPage(s)), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: List[Parameter] + ): DetailPage.ViewModel = + DetailPage.ViewModel( + o.toDetailOsoby, + p.map( + _.toParametr(param => + PageLink.container(Page.DetailParametru(o, param), state.actionBus) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DetailParametruPageConnector.scala new file mode 100644 index 0000000..fa7193d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DetailParametruPageConnector.scala @@ -0,0 +1,67 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.Parameter +import mdr.pdb.UserInfo +import mdr.pdb.app.pages.detail.DetailParametruPage +import pages.detail.DetailParametruPage +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.components.PageLink + +object DetailParametruPageConnector { + trait AppState { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + } +} + +case class DetailParametruPageConnector( + state: DetailParametruPageConnector.AppState +)( + $page: Signal[Page.DetailParametru] +)(using router: Router[Page]): + val $paramChangeSignal = + $page.splitOne(p => (p.osobniCislo.value, p.parametr.value))((x, _, _) => x) + val $pageChangeSignal = + $paramChangeSignal.map(FetchParameter(_, _)) + + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + val $merged = + $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => + for { + da <- d + pa <- p + pb <- pa.find(_.id == pc._2) + } yield (da, pb) + ) + + def apply: HtmlElement = + AppPage(state.actionBus)( + $merged.map(_.map(buildModel)) + .split(_ => ())((_, _, s) => DetailParametruPage(s)), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: Parameter + ): DetailParametruPage.ViewModel = + DetailParametruPage.ViewModel( + o.toDetailOsoby, + p.toParametr(p => + PageLink.container(Page.DetailParametru(o, p), state.actionBus) + ), + p.criteria.map( + _.toKriterium { c => + PageLink.container( + Page.DetailKriteria(o, p, c), + state.actionBus + ) + } + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DirectoryPageConnector.scala new file mode 100644 index 0000000..ab2c045 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DirectoryPageConnector.scala @@ -0,0 +1,34 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.UserInfo +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.components.AppPage + +case class DirectoryPageConnector( + $input: EventStream[List[UserInfo]], + actionBus: Observer[Action] +)(using router: Router[Page]): + val $data = $input.startWithNone + val $actionSignal = EventStream.fromValue(FetchDirectory) + + def apply: HtmlElement = + AppPage(actionBus)( + $data.split(_ => ())((_, _, s) => + pages.directory.DirectoryPage( + s.map( + _.map( + _.toUserRow(u => + PageLink.container( + Page.Detail(Page.Titled(u.personalNumber)), + actionBus + ) + ) + ) + ) + ) + ), + $actionSignal --> actionBus + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala deleted file mode 100644 index a3b00af..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala +++ /dev/null @@ -1,103 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import scala.scalajs.js.annotation.JSExportTopLevel -import scala.scalajs.js.annotation.JSExport -import scala.scalajs.js.annotation.JSImport -import scala.scalajs.js -import org.scalajs.dom -import com.raquo.laminar.api.L.{*, given} -import scala.scalajs.js.Date -import com.raquo.waypoint.Router -import com.raquo.waypoint.SplitRender -import cz.e_bs.cmi.mdr.pdb.app.services.DataFetcher -import scala.scalajs.js.JSON -import zio.json._ -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.state.AppState - -@js.native -@JSImport("stylesheets/main.css", JSImport.Namespace) -object Css extends js.Any - -@js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native -@JSImport("params/pdb-params.json", JSImport.Default) -object pdbParams extends js.Object - -@JSExportTopLevel("app") -object Main: - - @JSExport - def main(args: Array[String]): Unit = - import Routes.given - onLoad { - setupAirstream() - val appContainer = dom.document.querySelector("#app") - val _ = - render( - appContainer, - renderPage(state.MockAppState(using unsafeWindowOwner, router)) - ) - } - - private def onLoad(f: => Unit): Unit = - documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) - - private def setupAirstream()(using router: Router[Page]): Unit = - AirstreamError.registerUnhandledErrorCallback(err => - router.forcePage( - Page.UnhandledError( - Some(err.getClass.getName), // TODO: Fill only in dev mode - Some(err.getMessage) - ) - ) - ) - - def renderPage(state: AppState)(using - router: Router[Page] - ): HtmlElement = - val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) - .collectSignal[Page.Detail]( - connectors - .DetailPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailParametru]( - connectors - .DetailParametruPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailKriteria]( - connectors - .DetailKriteriaPageConnector(state)(_) - .apply - ) - .collectSignal[Page.UpravDukazKriteria]( - pages.detail.UpravDukaz.Connector(state)(_).apply - ) - .collectStatic(Page.Dashboard)( - connectors.DashboardPageConnector(state.actionBus).apply - ) - .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) - ) - .collect[Page.UnhandledError](pg => - pages.errors - .UnhandledErrorPage( - pages.errors.UnhandledErrorPage - .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), - state.actionBus - ) - ) - .collectStatic(Page.Directory)( - connectors - .DirectoryPageConnector(state.users, state.actionBus) - .apply - ) - div(cls := "h-full", child <-- pageSplitter.$view) - - // Pull in the stylesheet - val css: Css.type = Css diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala deleted file mode 100644 index 3bbc8c0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ /dev/null @@ -1,198 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.* -import org.scalajs.dom -import zio.json.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import scala.scalajs.js -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.Page.Titled - -// enum is not working with Waypoints' SplitRender collectStatic -sealed abstract class Page( - val id: String, - val title: String, - val parent: Option[Page] -) { - val path: Vector[Page] = - parent match - case None => Vector(this) - case Some(p) => p.path :+ this - - val isRoot: Boolean = parent.isEmpty -} - -object Page: - - case class Titled[V](value: V, title: Option[String] = None): - val show: String = title.getOrElse(value.toString) - - case object Directory extends Page("directory", "Adresář", None) - - case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) - - case class Detail(osobniCislo: Titled[OsobniCislo]) - extends Page("user", osobniCislo.show, Some(Directory)) - - object Detail { - def apply(o: UserInfo): Detail = Detail( - Titled(o.personalNumber, Some(o.name)) - ) - } - - case class DetailParametru( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String] - ) extends Page( - "parameter", - parametr.show, - Some(Detail(osobniCislo)) - ) - - object DetailParametru { - def apply(o: UserInfo, p: Parameter): DetailParametru = - DetailParametru( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)) - ) - } - - case class DetailKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "criteria", - kriterium.show, - Some(DetailParametru(osobniCislo, parametr)) - ) - - object DetailKriteria { - def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = - DetailKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class UpravDukazKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "addProof", - "Důkaz", - Some(DetailKriteria(osobniCislo, parametr, kriterium)) - ) - - object UpravDukazKriteria { - def apply( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): UpravDukazKriteria = - UpravDukazKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class NotFound(url: String) extends Page("404", "404", Some(Directory)) - - case class UnhandledError( - errorName: Option[String], - errorMessage: Option[String] - ) extends Page("500", "Unexpected error", Some(Directory)) - -object Routes: - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) - given [V: JsonEncoder]: JsonEncoder[Titled[V]] = - DeriveJsonEncoder.gen[Titled[V]] - given [V: JsonDecoder]: JsonDecoder[Titled[V]] = - DeriveJsonDecoder.gen[Titled[V]] - given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] - given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] - - val base = - js.`import`.meta.env.BASE_URL - .asInstanceOf[String] + "app" - - val homePage: Page = Page.Directory - - given router: Router[Page] = Router[Page]( - routes = List( - Route.static(homePage, root / endOfSegments, basePath = base), - Route.static( - Page.Dashboard, - root / "dashboard" / endOfSegments, - basePath = base - ), - Route[Page.Detail, String]( - encode = _.osobniCislo.value.toString, - decode = osc => Page.Detail(Titled(OsobniCislo(osc))), - root / "osoba" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.DetailParametru, (String, String)]( - encode = p => (p.osobniCislo.value.toString, p.parametr.value), - decode = - p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / endOfSegments, - basePath = base - ), - Route[Page.DetailKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.DetailKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.UpravDukazKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.UpravDukazKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / "edit" / endOfSegments, - basePath = base - ) - ), - serializePage = _.toJson, - deserializePage = _.fromJson[Page] - .fold(s => throw IllegalStateException(s), identity), - getPageTitle = _.title, - routeFallback = url => Page.NotFound(url), - deserializeFallback = _ => Page.Dashboard - )( - $popStateEvent = windowEvents.onPopState, - owner = unsafeWindowOwner - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala deleted file mode 100644 index b24b82b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -sealed trait Action - -case object FetchDirectory extends Action -case class FetchUserDetails(osc: OsobniCislo) extends Action -case class FetchParameters(osc: OsobniCislo) extends Action -case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action -case class FetchParameterCriteria( - osc: OsobniCislo, - paramId: String, - critId: String, - page: (UserInfo, Parameter, ParameterCriteria) => Page -) extends Action -case class FetchAvailableFiles(osc: OsobniCislo) extends Action -case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala deleted file mode 100644 index c637d35..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala +++ /dev/null @@ -1,82 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.{UserProfile, UserInfo, OsobniCislo} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo -import cz.e_bs.cmi.mdr.pdb.UserFunction - -object AppPage: - // TODO: pages by logged in user - val pages: List[Page] = List(Page.Directory, Page.Dashboard) - - import NavigationBar.{Logo, MenuItem} - - val logo = Logo( - "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", - "Workflow" - ) - - // TODO: menu items by user profile - val userMenu = - List( - MenuItem("Your Profile"), - MenuItem("Settings"), - MenuItem("Sign out") - ) - - // TODO: load user profile - val $userProfile = Var( - UserProfile( - "tom", - UserInfo( - OsobniCislo("1031"), - "tom", - "Tom", - "Cook", - None, - None, - Some("tom@example.com"), - Some("+420 222 866 180"), - Some( - UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") - ), - Nil, - None - ) - ) - ) - - val $userInfo = $userProfile.signal.map(_.userInfo) - - type ViewModel = Option[HtmlElement] - def apply( - actionBus: Observer[Action] - )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using - router: Router[Page] - ): HtmlElement = - PageLayout(actionBus)( - $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => - PageLayout.ViewModel( - NavigationBar.ViewModel( - u, - pages.map(p => - NavigationBar.Link( - () => - PageLink( - p, - actionBus - ), - p == cp - ) - ), - userMenu, - logo - ), - c - ) - ), - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala deleted file mode 100644 index bf473c6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ /dev/null @@ -1,143 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden -import fiftyforms.ui.components.tailwind.list.IconText.ViewModel -import fiftyforms.ui.components.tailwind.Icons -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Action - -object Breadcrumbs: - - private def slash = - import svg.{*, given} - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-300", - xmlns := "http://www.w3.org/2000/svg", - fill := "currentColor", - viewBox := "0 0 20 20", - ariaHidden := true, - path( - d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" - ) - ) - - def backIcon = - Icons.solid - .`arrow-narrow-left`() - .amend( - svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" - ) - - object Link: - case class ViewModel( - page: Page, - icon: Option[SvgElement], - extraClasses: String, - text: String, - textClasses: Option[String] = None - ) - - val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" - - def shortHome(p: Page) = ViewModel( - p, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - "Zpět na úvodní stránku", - None - ) - - def fullHome(p: Page) = ViewModel( - p, - Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), - "text-gray-400 hover:text-gray-500", - "Domů", - Some("sr-only") - ) - - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - PageLink - .container($m.map(_.page), actionBus) - .amend( - cls <-- $m.map(_.extraClasses), - child.maybe <-- $m.map(_.icon), - span( - cls <-- $m.map(_.textClasses.getOrElse("")), - child.text <-- $m.map(_.text) - ) - ) - - object FullBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - ol( - role := "list", - cls := "flex items-center space-x-4", - children <-- $m.map(_.path).split(_.id)((_, _, $p) => - li( - div( - cls := "flex items-center", - child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), - Link( - $p.map(p => - if (p.isRoot) then Link.fullHome(p) - else - Link.ViewModel( - p, - None, - s"ml-4 max-w-xs truncate ${Link.baseClasses}", - p.title - ) - ), - actionBus - ) - ) - ) - ) - ) - - object ShortBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - Link( - $m.map { p => - val target = p.parent.getOrElse(p) - if target.isRoot then Link.shortHome(target) - else - Link.ViewModel( - target, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - s"Zpět na ${target.title}" - ) - }, - actionBus - ) - - def apply(actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - nav( - cls := "flex", - aria.label := "Breadcrumb", - div( - cls := "flex sm:hidden", - ShortBreadcrumbs(router.$currentPage, actionBus) - ), - div( - cls := "hidden sm:block", - FullBreadcrumbs(router.$currentPage, actionBus) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala deleted file mode 100644 index 0db2047..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ /dev/null @@ -1,242 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent -import fiftyforms.ui.components.tailwind.Icons -import fiftyforms.ui.components.tailwind.Avatar -import cz.e_bs.cmi.mdr.pdb.UserInfo -import io.laminext.syntax.core.* - -object NavigationBar: - - case class Logo(img: String, name: String) - case class Link(a: () => Anchor, active: Boolean) - case class MenuItem(title: String) - - case class ViewModel( - userInfo: UserInfo, - pages: List[Link], - userMenu: List[MenuItem], - logo: Logo - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - val $userInfo = $m.map(_.userInfo) - val mobileMenuOpen = Var(false) - - val desktopOnly = cls("hidden md:block") - val mobileOnly = cls("md:hidden") - - inline def avatarImage(size: Int = 8) = - Avatar($userInfo.map(_.img)).avatarImage(size) - - def notificationButton = button( - tpe := "button", - cls := List( - "bg-indigo-600", - "focus:outline-none", - "focus:ring-2", - "focus:ring-offset-2", - "focus:ring-offset-indigo-600", - "focus:ring-white", - "hover:text-white", - "p-1", - "rounded-full", - "text-indigo-200" - ), - span(cls := "sr-only", "View notifications"), - Icons.outline.bell() - ) - - def userProfile: HtmlElement = - val menuOpen = Var(false) - - def menuItem(item: MenuItem, idx: Int): HtmlElement = - a( - href := "#", - cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", - role := "menuitem", - tabIndex := -1, - idAttr := s"user-menu-item-$idx", - item.title - ) - - div( - cls := "ml-3 relative", - div( - button( - tpe := "button", - cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - idAttr := "user-menu-button", - aria.expanded <-- menuOpen.signal, - aria.hasPopup := true, - span(cls := "sr-only", "Open user menu"), - child <-- avatarImage(), - onClick.preventDefault.mapTo( - !menuOpen.now() - ) --> menuOpen.writer - ) - ), - /* - * */ - div( - cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", - cls <-- menuOpen.signal.map { o => - if (o) "md:block" else "md:hidden" - }, - role := "menu", - aria.orientation := "vertical", - aria.labelledBy := "user-menu-button", - tabIndex := -1, - // : keyboard navigation - children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) - ) - ) - - def mobileProfile = - def menuItem(item: MenuItem): HtmlElement = - a( - href := "#", - cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", - item.title - ) - - div( - cls := "pt-4 pb-3 border-t border-indigo-700", - div( - cls := "flex items-center px-5", - div( - cls := "flex-shrink-0", - child <-- avatarImage(10) - ), - div( - cls := "ml-3", - div( - cls := "text-base font-medium text-white", - child.text <-- $userInfo.map(_.name) - ), - child.maybe <-- $userInfo.map( - _.email.map(e => - div( - cls := "text-sm font-medium text-indigo-300", - e - ) - ) - ) - ), - notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) - ), - div( - cls := "mt-3 px-2 space-y-1", - children <-- $m.map(_.userMenu.map(menuItem)) - ) - ) - - def pageLink(page: Link): Anchor = - page - .a() - .amend( - cls := "text-white px-3 py-2 rounded-md text-sm font-medium", - cls := Seq( - "bg-indigo-700" -> page.active, - "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active - ), - ariaCurrent := (if page.active then "page" else "false") - ) - - def logoImg: Image = - img( - cls := "h-8 w-8", - src <-- $m.map(_.logo.img), - alt <-- $m.map(_.logo.name) - ) - - def mobileMenuButton = button( - tpe := "button", - cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - aria.controls := "mobile-menu", - aria.expanded <-- mobileMenuOpen.signal, - span(cls := "sr-only", "Open main menu"), - Icons.outline - .menu() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") - ), - Icons.outline - .x() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") - ), - onClick.preventDefault.mapTo( - !mobileMenuOpen.now() - ) --> mobileMenuOpen.writer - ) - - def navBarLeft = - div( - cls := "flex items-center", - div(cls := "flex-shrink-0", logoImg), - div( - desktopOnly, - div( - cls := "ml-10 flex items-baseline space-x-4", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ) - ) - ) - - def navBarRight = - div( - desktopOnly, - div( - cls := "ml-4 flex items-center md:ml-6", - notificationButton, - userProfile - ) - ) - - def navBarMobile = - div( - cls := "-mr-2 flex", - mobileOnly, - mobileMenuButton - ) - - def navBar = - div( - cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex items-center justify-between h-16", - navBarLeft, - navBarRight, - navBarMobile - ) - ) - - def mobileMenu = - div( - mobileOnly, - cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }, - idAttr := "mobile-menu", - div( - cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ), - mobileProfile - ) - - nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala deleted file mode 100644 index 75339df..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object PageHeader: - def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = - header( - cls := "bg-white shadow-sm", - div( - cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", - h1( - cls := "text-lg leading-6 font-semibold text-gray-900", - Breadcrumbs(actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala deleted file mode 100644 index 5096463..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ /dev/null @@ -1,30 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import fiftyforms.ui.components.tailwind.Loading -import io.laminext.syntax.core.* - -object PageLayout: - case class ViewModel( - navigation: NavigationBar.ViewModel, - content: Option[HtmlElement] - ) - def apply(actionBus: Observer[Action])( - $m: Signal[ViewModel], - mods: Modifier[HtmlElement]* - )(using router: Router[Page]): HtmlElement = - val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) - div( - cls := "h-full flex flex-col", - NavigationBar($m.map(_.navigation)), - child.maybe <-- router.$currentPage.map(_.isRoot) - .switch(None, Some(PageHeader(actionBus))), - main( - cls := "flex-grow-1 overflow-y-auto", - mods, - child <-- $maybeContent.map(_.getOrElse(Loading)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala deleted file mode 100644 index 62d820f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.LinkSupport.* -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo - -object PageLink: - type ViewModel = Page - def apply($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = - a(mods($m, actions), child.text <-- $m.map(_.title)) - - def apply(m: ViewModel, actions: Observer[Action])(using - router: Router[Page] - ): Anchor = apply(Val(m), actions) - - def container($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = a(mods($m, actions)) - - def container(m: ViewModel, actions: Observer[Action])(using - Router[Page] - ): Anchor = container(Val(m), actions) - - private def mods($m: Signal[Page], actions: Observer[Action])(using - router: Router[Page] - ): Modifier[Anchor] = - Seq( - href <-- $m.map(router.absoluteUrlForPage).recover { case _ => - Some("invalid url") - }, - composeEvents(onClick.noKeyMod.preventDefault)( - _.sample($m) - .map(NavigateTo.apply) - ) --> actions - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala deleted file mode 100644 index 87f825a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.pages.dashboard.DashboardPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -class DashboardPageConnector(actionBus: Observer[Action])(using - router: Router[Page] -): - def apply: HtmlElement = - AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala deleted file mode 100644 index e9b0f71..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala +++ /dev/null @@ -1,76 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailKriteriaPage -import pages.detail.components.DukazyKriteria -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -object DetailKriteriaPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailKriteriaPageConnector( - state: DetailKriteriaPageConnector.AppState -)( - $page: Signal[Page.DetailKriteria] -)(using Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => - (p.osobniCislo.value, p.parametr.value, p.kriterium.value) - )((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map( - FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) - ) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, s, $s) => - DetailKriteriaPage($s)(state.actionBus.contramap { - case DukazyKriteria.Add => - NavigateTo( - Page.UpravDukazKriteria( - Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), - Page.Titled(s.parametr.id, Some(s.parametr.nazev)), - Page.Titled(s.kriterium.id, Some(s.kriterium.id)) - ) - ) - }) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): DetailKriteriaPage.ViewModel = - DetailKriteriaPage.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala deleted file mode 100644 index 4d8024f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ /dev/null @@ -1,57 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import pages.detail.DetailPage -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import fiftyforms.ui.components.tailwind.Color - -object DetailPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailPageConnector(state: DetailPageConnector.AppState)( - $page: Signal[Page.Detail] -)(using router: Router[Page]): - val $oscChangeSignal = $page.splitOne(_.osobniCislo.value)((osc, _, _) => osc) - val $pageChangeSignal = - $oscChangeSignal.flatMap(osc => - EventStream.fromSeq(Seq(FetchUserDetails(osc), FetchParameters(osc))) - ) - // TODO: filter the value based on the current osc - // OSC change will fetch new data, but still - // - we need to be sure that what we got is really what we ought to display - // - we want to display stale data accordingly (at least with loading indicator) - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - def apply: HtmlElement = - AppPage(state.actionBus)( - $data.combineWithFn($params)(_ zip _) - .map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: List[Parameter] - ): DetailPage.ViewModel = - DetailPage.ViewModel( - o.toDetailOsoby, - p.map( - _.toParametr(param => - PageLink.container(Page.DetailParametru(o, param), state.actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala deleted file mode 100644 index fdaa7f8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailParametruPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink - -object DetailParametruPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailParametruPageConnector( - state: DetailParametruPageConnector.AppState -)( - $page: Signal[Page.DetailParametru] -)(using router: Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => (p.osobniCislo.value, p.parametr.value))((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map(FetchParameter(_, _)) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - } yield (da, pb) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailParametruPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter - ): DetailParametruPage.ViewModel = - DetailParametruPage.ViewModel( - o.toDetailOsoby, - p.toParametr(p => - PageLink.container(Page.DetailParametru(o, p), state.actionBus) - ), - p.criteria.map( - _.toKriterium { c => - PageLink.container( - Page.DetailKriteria(o, p, c), - state.actionBus - ) - } - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala deleted file mode 100644 index 049aa22..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -case class DirectoryPageConnector( - $input: EventStream[List[UserInfo]], - actionBus: Observer[Action] -)(using router: Router[Page]): - val $data = $input.startWithNone - val $actionSignal = EventStream.fromValue(FetchDirectory) - - def apply: HtmlElement = - AppPage(actionBus)( - $data.split(_ => ())((_, _, s) => - pages.directory.DirectoryPage( - s.map( - _.map( - _.toUserRow(u => - PageLink.container( - Page.Detail(Page.Titled(u.personalNumber)), - actionBus - ) - ) - ) - ) - ) - ), - $actionSignal --> actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala deleted file mode 100644 index 88e7450..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ /dev/null @@ -1,68 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii -import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria -import fiftyforms.ui.components.tailwind.Color - -extension (o: UserInfo) - def toDetailOsoby: DetailOsoby.ViewModel = - DetailOsoby.ViewModel( - o.personalNumber, - o.name, - o.email, - o.phone, - o.img, - o.mainFunction.map(f => - DetailOsoby.Funkce.ViewModel(f.name, f.dept, f.ou) - ), - o.userContracts.headOption.map(c => - DetailOsoby.PracovniPomer.ViewModel(c.rel, c.startDate, c.endDate) - ) - ) - -extension (param: Parameter) - def toParametr(container: Parameter => Anchor): DetailParametru.ViewModel = - DetailParametru.ViewModel( - id = param.id, - nazev = param.name, - popis = param.description, - status = "Nesplněno", - statusColor = Color.red, - a = container(param) - ) - -extension (crit: ParameterCriteria) - def toKriterium( - container: ParameterCriteria => Anchor - ): DetailKriteria.ViewModel = - DetailKriteria.ViewModel( - nazev = crit.criteriumText, - kapitola = crit.chapterId, - bod = crit.itemId, - status = "Nesplněno", - statusColor = Color.red, - splneno = false, - dukazy = Nil, - container = container(crit) - ) - -extension (user: UserInfo) - def toUserRow( - container: UserInfo => HtmlElement = (_: UserInfo) => div() - ): UserRow.ViewModel = - UserRow.ViewModel( - osobniCislo = user.personalNumber.toString, - celeJmeno = user.name, - prijmeni = user.surname, - hlavniFunkce = user.mainFunction.map(_.name), - img = user.img, - container = () => container(user) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala deleted file mode 100644 index 7d5b2c1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.dashboard - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.Page - -object DashboardPage: - - def render: HtmlElement = - div("Dashboard page") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala deleted file mode 100644 index 9ef0698..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala +++ /dev/null @@ -1,31 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailKriteriaPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply($m: Signal[ViewModel])( - action: Observer[DukazyKriteria.Action] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - DukazyKriteria($m.map(_.kriterium.dukazy))(action) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala deleted file mode 100644 index 3b316e1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object DetailPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametry: SeznamParametru.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby($m.map(_.osoba)), - SeznamParametru($m.map(_.parametry)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala deleted file mode 100644 index 17224cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala +++ /dev/null @@ -1,24 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailParametruPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriteria: SeznamKriterii.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby.header($m.map(_.osoba)), - DetailParametru($m.map(_.parametr)), - SeznamKriterii($m.map(_.kriteria)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala deleted file mode 100644 index dc912fa..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ /dev/null @@ -1,107 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb -package app -package pages.detail - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.UpravDukazForm -import fiftyforms.services.files.File - -object UpravDukaz: - - type ThisPage = Page.UpravDukazKriteria - type PageKey = (OsobniCislo, String, String) - - trait State { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def availableFiles: EventStream[List[File]] - def actionBus: Observer[Action] - } - - def keyOfPage(page: ThisPage): PageKey = - (page.osobniCislo.value, page.parametr.value, page.kriterium.value) - - def onChangeAction(key: PageKey): Action = - FetchParameterCriteria( - key._1, - key._2, - key._3, - Page.UpravDukazKriteria(_, _, _) - ) - - class Connector(state: State)($page: Signal[ThisPage])(using Router[Page]): - val $paramChangeSignal = $page.splitOne(keyOfPage)((x, _, _) => x) - val $pageChangeSignal = $paramChangeSignal.map(onChangeAction) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.split(_ => ())((_, s, $s) => - PageComponent( - $s.map(buildModel), - state.availableFiles, - state.actionBus.contramap { - case UpravDukazForm.Cancelled => - NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) - case UpravDukazForm.AvailableFilesRequested => - FetchAvailableFiles(s._1.personalNumber) - } - ) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): PageComponent.ViewModel = - import connectors.* - PageComponent.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) - - object PageComponent: - import components.* - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply( - $m: Signal[ViewModel], - availableFilesStream: EventStream[List[File]], - events: Observer[UpravDukazForm.Event] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - UpravDukazForm(availableFilesStream)(events) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala deleted file mode 100644 index ca311b0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.Icons -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Color - -object DetailKriteria: - case class ViewModel( - nazev: String, - kapitola: String, - bod: String, - status: String, - statusColor: Color, - splneno: Boolean, - dukazy: List[DukazKriteria.ViewModel], - container: HtmlElement = div() - ) { - val id = s"${kapitola}${bod}" - } - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h3( - cls := "text-xl leading-6 font-bold text-gray-900", - child.text <-- $m.map(_.nazev) - ), - h3( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - "Kapitola ", - child.text <-- $m.map(_.kapitola), - " bod ", - child <-- $m.map(_.bod) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala deleted file mode 100644 index ae90290..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala +++ /dev/null @@ -1,100 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Avatar - -object DetailOsoby: - - object Funkce: - case class ViewModel( - nazev: String, - stredisko: String, - voj: String - ) - - def render($m: Signal[ViewModel]): HtmlElement = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.nazev), - span( - cls := "hidden md:inline", - " @ ", - child.text <-- $m.map(_.stredisko), - ", ", - child.text <-- $m.map(_.voj) - ) - ) - - object PracovniPomer: - case class ViewModel( - druh: String, - pocatek: LocalDate, - konec: Option[LocalDate] - ) - - def render($m: Signal[ViewModel]): HtmlElement = - import fiftyforms.ui.components.tailwind.CustomAttrs.datetime - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.druh), - " od ", - time( - datetime <-- $m.map(_.pocatek.toString), - child.text <-- $m.map(_.pocatek.toString) - ) - ) - - case class ViewModel( - osobniCislo: OsobniCislo, - jmeno: String, - email: Option[String], - telefon: Option[String], - img: Option[String], - hlavniFunkce: Option[Funkce.ViewModel], - pracovniPomer: Option[PracovniPomer.ViewModel] - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-4", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(16) - ), - div( - h1( - cls := "text-2xl font-bold text-gray-900", - child.text <-- $m.map(_.jmeno) - ), - child.maybe <-- $m.map(_.hlavniFunkce).split(_ => ())((_, _, d) => - Funkce.render(d) - ), - child.maybe <-- $m.map(_.pracovniPomer).split(_ => ())((_, _, d) => - PracovniPomer.render(d) - ) - ) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-5", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(8) - ), - div( - h1( - cls := "text-lg font-medium text-gray-800", - child.text <-- $m.map(_.jmeno) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala deleted file mode 100644 index e11cb62..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color - -object DetailParametru: - case class ViewModel( - id: String, - nazev: String, - popis: String, - status: String, - statusColor: Color, - a: Anchor - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h2( - cls := "text-2xl leading-6 font-medium text-gray-900", - child.text <-- $m.map(_.nazev) - ), - p( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - child.text <-- $m.map(_.popis) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - h2( - cls := "text-lg leading-6 font-medium text-gray-600", - child.text <-- $m.map(_.nazev) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala deleted file mode 100644 index a00ede8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala +++ /dev/null @@ -1,177 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.CustomAttrs - -object DukazKriteria: - case class Osoba(osobniCislo: String, jmeno: String) - case class Udalost(osoba: Osoba, datum: LocalDate) - case class Dokument( - url: String, - nazev: String, - pridal: Udalost, - odebral: Option[Udalost] - ) - case class ViewModel( - dokumenty: List[Dokument], - autorizoval: Option[Udalost], - platiDo: Option[LocalDate], - poznámka: Option[String] - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-lg", - div( - cls := "px-4 py-5 sm:px-6", - h3( - cls := "text-lg leading-6 font-medium text-gray-900", - """Applicant Information""" - ), - p( - cls := "mt-1 max-w-2xl text-sm text-gray-500", - """Personal details and application.""" - ) - ), - div( - cls := "border-t border-gray-200 px-4 py-5 sm:px-6", - dl( - cls := "grid grid-cols-1 gap-x-4 gap-y-8 sm:grid-cols-2", - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Full name""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Margot Foster""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Application for""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Backend Developer""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Email address""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """margotfoster@example.com""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Salary expectation""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """$120,000""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """About""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Fugiat ipsum ipsum deserunt culpa aute sint do nostrud anim incididunt cillum culpa consequat. Excepteur qui ipsum aliquip consequat sint. Sit id mollit nulla mollit nostrud in ea officia proident. Irure nostrud pariatur mollit ad adipisicing reprehenderit deserunt qui eu.""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """Attachments""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - ul( - role := "list", - cls := "border border-gray-200 rounded-md divide-y divide-gray-200", - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """resume_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ), - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """coverletter_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala deleted file mode 100644 index 22f134f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala +++ /dev/null @@ -1,35 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object DukazyKriteria: - sealed trait Action - case object Add extends Action - - type ViewModel = List[DukazKriteria.ViewModel] - - def apply($m: Signal[ViewModel])(actions: Observer[Action]): HtmlElement = - div( - child.maybe <-- $m.map(m => - if (m.isEmpty) then Some(prazdnyDukaz(actions)) - else None - ), - children <-- $m.map(_.zipWithIndex).split(_._2)((_, _, $s) => - DukazKriteria($s.map(_._1)) - ) - ) - - private def prazdnyDukaz(actions: Observer[Action]): HtmlElement = - button( - tpe := "button", - onClick.preventDefault.mapTo(Add) --> actions, - cls := "relative block w-full border-2 border-gray-300 border-dashed rounded-lg p-12 text-center hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - Icons.outline - .`document-add`(12) - .amend(svg.cls := "mx-auto text-gray-400"), - span( - cls := "mt-2 block text-sm font-medium text-gray-900", - "Přidat důkaz" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala deleted file mode 100644 index f628697..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.list.StackedList -import fiftyforms.ui.components.tailwind.list.ListRow -import fiftyforms.ui.components.tailwind.list.RowTag -import fiftyforms.ui.components.tailwind.list.RowNext -import java.time.LocalDate - -object SeznamKriterii: - type ViewModel = List[DetailKriteria.ViewModel] - - private val kritList = new StackedList[DetailKriteria.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - kritList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = - p(cls := "text-sm text-gray-500", s"${i.kapitola}${i.bod}"), - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.container - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala deleted file mode 100644 index 2bfe4bc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.list.{ - StackedList, - ListRow, - RowTag, - PropList, - IconText, - RowNext -} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.LinkSupport.* - -object SeznamParametru: - type ViewModel = List[DetailParametru.ViewModel] - - private val parametrList = new StackedList[DetailParametru.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - parametrList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = emptyNode, - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.a - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala deleted file mode 100644 index f048e9d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ /dev/null @@ -1,101 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.form.* -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement -import fiftyforms.services.files.components.tailwind.FilePicker -import cz.e_bs.cmi.mdr.pdb.frontend.AutorizujDukaz -import cz.e_bs.cmi.mdr.pdb.frontend.DocumentRef -import fiftyforms.services.files.File - -object UpravDukazForm: - sealed trait Event - case object Cancelled extends Event - case object AvailableFilesRequested extends Event - def apply(availableFilesStream: EventStream[List[File]])( - updates: Observer[Event] - ): HtmlElement = - val (filesStream, filesObserver) = - EventStream.withObserver[FilePicker.Event] - val files = Var[List[File]](Nil) - def submitButtons: HtmlElement = - div( - cls := "pt-5", - div( - cls := "flex justify-end", - button( - tpe := "button", - cls := "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zrušit", - onClick.mapTo(Cancelled) --> updates - ), - button( - tpe := "submit", - cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - disabled <-- files.signal.map(_.isEmpty), - "Autorizovat" - ) - ) - ) - - div( - filesStream.collect { case FilePicker.SelectionUpdated(files) => - files.to(List) - } --> files.writer, - filesStream.collect { case FilePicker.AvailableFilesRequested => - AvailableFilesRequested - } --> updates, - cls := "bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6", - Form( - Form.Body( - Form.Section( - FormHeader( - FormHeader.ViewModel( - "Doložení kritéria", - "Sestavte doklady poskytující důkaz kritéria a potvrďte odesláním formuláře. Případné limitace či jiné relevantní údaje vepište do pole Komentář." - ) - ), - FormFields( - FormRow( - "dokumenty", - "Dokumenty", - FilePicker(files.signal, availableFilesStream)(filesObserver) - .amend(idAttr := "dokumenty", cls("max-w-lg")) - ).toHtml, - FormRow( - "platnost", - "Platnost", - input( - idAttr := "platnost", - name := "platnost", - tpe := "date", - autoComplete := "date", - cls := "block max-w-lg w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" - ) - ).toHtml, - FormRow( - "komentar", - "Komentář", - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - textArea( - idAttr := "komentar", - name := "about", - rows := 3, - cls := "max-w-lg shadow-sm block w-full focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border border-gray-300 rounded-md" - ), - p( - cls := "mt-2 text-sm text-gray-500", - "Doplňte prosím potřebné informace související s doložením kritéria, včetně případných limitací." - ) - ) - ).toHtml - ) - ) - ), - submitButtons - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala deleted file mode 100644 index 41033f6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -import components._ - -object DirectoryPage: - - type ViewModel = List[UserRow.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - val (actionsStream, actionObserver) = - EventStream.withObserver[SearchForm.Action] - val $filter = actionsStream - .collect { - case SearchForm.ClearFilter => None - case SearchForm.SetFilter(t) => Some(t) - } - .startWith(None) - val byLetter = for { - d <- $m - f <- $filter - } yield for { - (letter, users) <- d - .filter { user => - f match - case None => true - case Some(t) => user.search.contains(t) - } - .groupBy(_.prijmeni.head) - .to(List) - .sortBy(_._1) - } yield (letter.toString, users.sortBy(_.prijmeni)) - - div( - cls := "h-full max-w-7xl mx-auto order-first flex flex-col flex-shrink-0", - SearchForm(actionObserver), - Directory(byLetter) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala deleted file mode 100644 index 73f915d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -object Directory: - - object Header: - type ViewModel = String - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500", - h3(child.text <-- $m) - ) - - object UserList: - type ViewModel = List[UserRow.ViewModel] - def apply($m: Signal[ViewModel]): HtmlElement = - ul( - role := "list", - cls := "relative z-0 divide-y divide-gray-200", - children <-- $m.split(_.osobniCislo)((_, _, s) => UserRow(s)) - ) - - type ViewModel = List[(String, List[UserRow.ViewModel])] - def apply($m: Signal[ViewModel]): HtmlElement = - nav( - cls := "flex-1 min-h-0 overflow-y-auto", - aria.label := "Directory", - children <-- $m.split(_._1)((_, _, s) => - div( - cls := "relative", - Header(s.map(_._1)), - UserList(s.map(_._2)) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala deleted file mode 100644 index e182910..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala +++ /dev/null @@ -1,54 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object SearchForm: - sealed trait Action - case object SubmitFilter extends Action - sealed trait FilterAction extends Action - case object ClearFilter extends FilterAction - case class SetFilter(value: String) extends FilterAction - - def apply(actions: Observer[Action]): HtmlElement = - div( - cls := "px-6 pt-4 pb-4", - form( - cls := "flex space-x-4", - onSubmit.mapTo(SubmitFilter) --> actions, - div( - cls := "flex-1 min-w-0", - label( - forId := "search", - cls := "sr-only", - "Hledat" - ), - div( - cls := "relative rounded-md shadow-sm", - div( - cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", - Icons.solid.search().amend(svg.cls := "text-gray-400") - ), - input( - tpe := "search", - name := "search", - idAttr := "search", - cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", - placeholder := "Hledat", - composeEvents(onInput.mapToValue.setAsValue.map(SetFilter(_)))( - _.throttle(500) - ) --> actions - ) - ) - ), - button( - tpe := "submit", - cls := "inline-flex justify-center px-3.5 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500", - Icons.solid.filter().amend(svg.cls := "text-gray-400"), - span( - cls := "sr-only", - "Hledat" - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala deleted file mode 100644 index 82f7723..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala +++ /dev/null @@ -1,52 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.airstream.core.Signal -import fiftyforms.ui.components.tailwind.Avatar - -object UserRow: - case class ViewModel( - osobniCislo: String, - celeJmeno: String, - prijmeni: String, - hlavniFunkce: Option[String], - img: Option[String], - container: () => HtmlElement = () => div() - ) { - val search = prijmeni.toLowerCase - } - - def apply($m: Signal[ViewModel]): HtmlElement = - inline def avatarImage = - Avatar($m.map(_.img)).avatarImage(10) - - li( - div( - cls := "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500", - div( - cls := "flex-shrink-0", - child <-- avatarImage - ), - div( - cls := "flex-1 min-w-0", - child <-- $m.map { o => - o.container() - .amend( - cls := "focus:outline-none", - span( - cls := "absolute inset-0", - aria.hidden := true - ), - p( - cls := "text-sm font-medium text-gray-900", - o.celeJmeno - ), - p( - cls := "text-sm text-gray-500 truncate", - o.hlavniFunkce - ) - ) - } - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala deleted file mode 100644 index 467c5f5..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala +++ /dev/null @@ -1,99 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router - -object ErrorPage: - case class ViewModel( - homePage: Page, - errorName: String, - title: String, - subTitle: String - ) - - def apply(m: ViewModel, actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - val ViewModel(homePage, errorName, title, subTitle) = m - div( - cls := "min-h-full pt-16 pb-12 flex flex-col bg-white", - main( - cls := "flex-grow flex flex-col justify-center max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex-shrink-0 flex justify-center", - a( - href := "/", - cls := "inline-flex", - span( - cls := "sr-only", - """Workflow""" - ), - img( - cls := "h-12 w-auto", - src := "https://tailwindui.com/img/logos/workflow-mark.svg?color=indigo&shade=600", - alt := "" - ) - ) - ), - div( - cls := "py-16", - div( - cls := "text-center", - p( - cls := "text-sm font-semibold text-indigo-600 uppercase tracking-wide", - errorName - ), - h1( - cls := "mt-2 text-4xl font-extrabold text-gray-900 tracking-tight sm:text-5xl", - title - ), - p( - cls := "mt-2 text-base text-gray-500", - subTitle - ), - div( - cls := "mt-6", - PageLink - .container(homePage, actionBus) - .amend( - cls := "text-base font-medium text-indigo-600 hover:text-indigo-500", - """Go back home""" - ) - ) - ) - ) - ), - footer( - cls := "flex-shrink-0 max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", - nav( - cls := "flex justify-center space-x-4", - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Contact Support""" - ), - span( - cls := "inline-block border-l border-gray-300", - aria.hidden := true - ), - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Status""" - ), - span( - cls := "inline-block border-l border-gray-300", - aria.hidden := true - ), - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Twitter""" - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala deleted file mode 100644 index bd9baec..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action - -object NotFoundPage: - def apply(homePage: Page, url: String, actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - ErrorPage( - ErrorPage.ViewModel( - homePage, - "404 error", - "Page not found.", - s"Sorry, but page $url doesn't exist." - ), - actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala deleted file mode 100644 index 476fe3c..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action - -object UnhandledErrorPage: - - case class ViewModel( - homePage: Page, - errorName: Option[String], - errorMessage: Option[String] - ) - - def apply(m: ViewModel, actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - ErrorPage( - ErrorPage.ViewModel( - m.homePage, - "Unexpected error occurred", - m.errorName.getOrElse( - "Uh oh!" - ), // TODO: translations, better text than uh oh - m.errorMessage.getOrElse( - "This wasn't supposed to happen! Please try again." - ) - ), - actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala deleted file mode 100644 index 25f059f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala +++ /dev/null @@ -1,6 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.services - -import com.raquo.airstream.core.Observer - -trait DataFetcher[K, A]: - def fetch(id: K, sink: Observer[A]): Unit diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala deleted file mode 100644 index a469830..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala +++ /dev/null @@ -1,120 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package state - -import com.raquo.airstream.core.EventStream -import cz.e_bs.cmi.mdr.pdb.{UserInfo, OsobniCislo} -import com.raquo.airstream.core.Observer -import scala.scalajs.js -import scala.scalajs.js.JSON -import zio.json._ -import com.raquo.airstream.eventbus.EventBus -import com.raquo.airstream.ownership.Owner -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.UserFunction -import cz.e_bs.cmi.mdr.pdb.UserContract -import fiftyforms.services.files.File - -trait AppState - extends connectors.DetailPageConnector.AppState - with connectors.DetailParametruPageConnector.AppState - with connectors.DetailKriteriaPageConnector.AppState - with pages.detail.UpravDukaz.State: - def users: EventStream[List[UserInfo]] - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - -class MockAppState(implicit owner: Owner, router: Router[Page]) - extends AppState: - - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonDecoder[UserFunction] = DeriveJsonDecoder.gen - given JsonDecoder[UserContract] = DeriveJsonDecoder.gen - given JsonDecoder[UserInfo] = DeriveJsonDecoder.gen - - given JsonDecoder[ParameterCriteria] = DeriveJsonDecoder.gen - given JsonDecoder[Parameter] = DeriveJsonDecoder.gen - - private val actions = EventBus[Action]() - private val (parametersStream, pushParameters) = - EventStream.withCallback[List[Parameter]] - private val (usersStream, pushUsers) = - EventStream.withCallback[List[UserInfo]] - private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] - private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] - - private val mockData: List[UserInfo] = - mockUsers - .asInstanceOf[js.Dictionary[js.Object]] - .values - // TODO: is there a more efficient way to parse from JS object directly? - .map { o => - val parsed = JSON.stringify(o).fromJson[UserInfo] - parsed.left.foreach(org.scalajs.dom.console.log(_)) - parsed - } - .collect { case Right(u) => - u - } - .toList - - private val mockParameters: List[Parameter] = - pdbParams - .asInstanceOf[js.Dictionary[js.Object]] - .values - .map(o => JSON.stringify(o).fromJson[Parameter]) - .collect { case Right(p) => p } - .toList - - // TODO: Extract to separate event handler - actions.events.foreach { - case FetchDirectory => pushUsers(mockData) - case FetchUserDetails(osc) => - mockData.find(_.personalNumber == osc).foreach { o => - pushDetails(o) - router.replaceState(Page.Detail(o)) - } - case FetchParameters(osc) => - pushParameters(mockParameters) - case FetchParameter(osc, paramId) => - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(Page.DetailParametru(o, p)) - case FetchParameterCriteria(osc, paramId, critId, page) => - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - c <- p.criteria.find(_.id == critId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(page(o, p, c)) - case NavigateTo(page) => router.pushState(page) - case FetchAvailableFiles(osc) => - pushFiles( - List( - File("https://tc163.cmi.cz/here", "Example file") - ) - ) - } - - override def users: EventStream[List[UserInfo]] = - usersStream.debugWithName("users") - - override def details: EventStream[UserInfo] = - detailsStream.debugWithName("details") - - override def parameters: EventStream[List[Parameter]] = - parametersStream.debugWithName("parameters") - - override def availableFiles: EventStream[List[File]] = - filesStream.debugWithName("available files") - - override def actionBus: Observer[Action] = - actions.writer.debugWithName("actions writer") diff --git a/app/src/main/scala/mdr/pdb/app/Main.scala b/app/src/main/scala/mdr/pdb/app/Main.scala new file mode 100644 index 0000000..081b4ee --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/Main.scala @@ -0,0 +1,103 @@ +package mdr.pdb.app + +import scala.scalajs.js.annotation.JSExportTopLevel +import scala.scalajs.js.annotation.JSExport +import scala.scalajs.js.annotation.JSImport +import scala.scalajs.js +import org.scalajs.dom +import com.raquo.laminar.api.L.{*, given} +import scala.scalajs.js.Date +import com.raquo.waypoint.Router +import com.raquo.waypoint.SplitRender +import mdr.pdb.app.services.DataFetcher +import scala.scalajs.js.JSON +import zio.json._ +import mdr.pdb.UserInfo +import mdr.pdb.app.state.AppState + +@js.native +@JSImport("stylesheets/main.css", JSImport.Namespace) +object Css extends js.Any + +@js.native +@JSImport("data/users.json", JSImport.Default) +object mockUsers extends js.Object + +@js.native +@JSImport("params/pdb-params.json", JSImport.Default) +object pdbParams extends js.Object + +@JSExportTopLevel("app") +object Main: + + @JSExport + def main(args: Array[String]): Unit = + import Routes.given + onLoad { + setupAirstream() + val appContainer = dom.document.querySelector("#app") + val _ = + render( + appContainer, + renderPage(state.MockAppState(using unsafeWindowOwner, router)) + ) + } + + private def onLoad(f: => Unit): Unit = + documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) + + private def setupAirstream()(using router: Router[Page]): Unit = + AirstreamError.registerUnhandledErrorCallback(err => + router.forcePage( + Page.UnhandledError( + Some(err.getClass.getName), // TODO: Fill only in dev mode + Some(err.getMessage) + ) + ) + ) + + def renderPage(state: AppState)(using + router: Router[Page] + ): HtmlElement = + val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) + .collectSignal[Page.Detail]( + connectors + .DetailPageConnector(state)(_) + .apply + ) + .collectSignal[Page.DetailParametru]( + connectors + .DetailParametruPageConnector(state)(_) + .apply + ) + .collectSignal[Page.DetailKriteria]( + connectors + .DetailKriteriaPageConnector(state)(_) + .apply + ) + .collectSignal[Page.UpravDukazKriteria]( + pages.detail.UpravDukaz.Connector(state)(_).apply + ) + .collectStatic(Page.Dashboard)( + connectors.DashboardPageConnector(state.actionBus).apply + ) + .collect[Page.NotFound](pg => + pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) + ) + .collect[Page.UnhandledError](pg => + pages.errors + .UnhandledErrorPage( + pages.errors.UnhandledErrorPage + .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), + state.actionBus + ) + ) + .collectStatic(Page.Directory)( + connectors + .DirectoryPageConnector(state.users, state.actionBus) + .apply + ) + div(cls := "h-full", child <-- pageSplitter.$view) + + // Pull in the stylesheet + val css: Css.type = Css diff --git a/app/src/main/scala/mdr/pdb/app/Routes.scala b/app/src/main/scala/mdr/pdb/app/Routes.scala new file mode 100644 index 0000000..2cc860c --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/Routes.scala @@ -0,0 +1,198 @@ +package mdr.pdb.app + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.* +import org.scalajs.dom +import zio.json.{*, given} +import mdr.pdb.OsobniCislo + +import scala.scalajs.js +import mdr.pdb.UserInfo +import mdr.pdb.Parameter +import mdr.pdb.ParameterCriteria +import mdr.pdb.app.Page.Titled + +// enum is not working with Waypoints' SplitRender collectStatic +sealed abstract class Page( + val id: String, + val title: String, + val parent: Option[Page] +) { + val path: Vector[Page] = + parent match + case None => Vector(this) + case Some(p) => p.path :+ this + + val isRoot: Boolean = parent.isEmpty +} + +object Page: + + case class Titled[V](value: V, title: Option[String] = None): + val show: String = title.getOrElse(value.toString) + + case object Directory extends Page("directory", "Adresář", None) + + case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) + + case class Detail(osobniCislo: Titled[OsobniCislo]) + extends Page("user", osobniCislo.show, Some(Directory)) + + object Detail { + def apply(o: UserInfo): Detail = Detail( + Titled(o.personalNumber, Some(o.name)) + ) + } + + case class DetailParametru( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String] + ) extends Page( + "parameter", + parametr.show, + Some(Detail(osobniCislo)) + ) + + object DetailParametru { + def apply(o: UserInfo, p: Parameter): DetailParametru = + DetailParametru( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)) + ) + } + + case class DetailKriteria( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String], + kriterium: Titled[String] + ) extends Page( + "criteria", + kriterium.show, + Some(DetailParametru(osobniCislo, parametr)) + ) + + object DetailKriteria { + def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = + DetailKriteria( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)), + Titled(k.id, Some(k.id)) + ) + } + + case class UpravDukazKriteria( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String], + kriterium: Titled[String] + ) extends Page( + "addProof", + "Důkaz", + Some(DetailKriteria(osobniCislo, parametr, kriterium)) + ) + + object UpravDukazKriteria { + def apply( + o: UserInfo, + p: Parameter, + k: ParameterCriteria + ): UpravDukazKriteria = + UpravDukazKriteria( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)), + Titled(k.id, Some(k.id)) + ) + } + + case class NotFound(url: String) extends Page("404", "404", Some(Directory)) + + case class UnhandledError( + errorName: Option[String], + errorMessage: Option[String] + ) extends Page("500", "Unexpected error", Some(Directory)) + +object Routes: + given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) + given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) + given [V: JsonEncoder]: JsonEncoder[Titled[V]] = + DeriveJsonEncoder.gen[Titled[V]] + given [V: JsonDecoder]: JsonDecoder[Titled[V]] = + DeriveJsonDecoder.gen[Titled[V]] + given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] + given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] + + val base = + js.`import`.meta.env.BASE_URL + .asInstanceOf[String] + "app" + + val homePage: Page = Page.Directory + + given router: Router[Page] = Router[Page]( + routes = List( + Route.static(homePage, root / endOfSegments, basePath = base), + Route.static( + Page.Dashboard, + root / "dashboard" / endOfSegments, + basePath = base + ), + Route[Page.Detail, String]( + encode = _.osobniCislo.value.toString, + decode = osc => Page.Detail(Titled(OsobniCislo(osc))), + root / "osoba" / segment[String] / endOfSegments, + basePath = base + ), + Route[Page.DetailParametru, (String, String)]( + encode = p => (p.osobniCislo.value.toString, p.parametr.value), + decode = + p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / endOfSegments, + basePath = base + ), + Route[Page.DetailKriteria, (String, String, String)]( + encode = p => + ( + p.osobniCislo.value.toString, + p.parametr.value, + p.kriterium.value.replaceAll("\\.", "--") + ), + decode = p => + Page.DetailKriteria( + Titled(OsobniCislo(p._1)), + Titled(p._2), + Titled(p._3.replaceAll("--", ".")) + ), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / "kriterium" / segment[String] / endOfSegments, + basePath = base + ), + Route[Page.UpravDukazKriteria, (String, String, String)]( + encode = p => + ( + p.osobniCislo.value.toString, + p.parametr.value, + p.kriterium.value.replaceAll("\\.", "--") + ), + decode = p => + Page.UpravDukazKriteria( + Titled(OsobniCislo(p._1)), + Titled(p._2), + Titled(p._3.replaceAll("--", ".")) + ), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / "kriterium" / segment[String] / "edit" / endOfSegments, + basePath = base + ) + ), + serializePage = _.toJson, + deserializePage = _.fromJson[Page] + .fold(s => throw IllegalStateException(s), identity), + getPageTitle = _.title, + routeFallback = url => Page.NotFound(url), + deserializeFallback = _ => Page.Dashboard + )( + $popStateEvent = windowEvents.onPopState, + owner = unsafeWindowOwner + ) diff --git a/app/src/main/scala/mdr/pdb/app/actions.scala b/app/src/main/scala/mdr/pdb/app/actions.scala new file mode 100644 index 0000000..d078b3a --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/actions.scala @@ -0,0 +1,21 @@ +package mdr.pdb.app + +import mdr.pdb.OsobniCislo +import mdr.pdb.UserInfo +import mdr.pdb.Parameter +import mdr.pdb.ParameterCriteria + +sealed trait Action + +case object FetchDirectory extends Action +case class FetchUserDetails(osc: OsobniCislo) extends Action +case class FetchParameters(osc: OsobniCislo) extends Action +case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action +case class FetchParameterCriteria( + osc: OsobniCislo, + paramId: String, + critId: String, + page: (UserInfo, Parameter, ParameterCriteria) => Page +) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action +case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/mdr/pdb/app/components/AppPage.scala new file mode 100644 index 0000000..130c817 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/AppPage.scala @@ -0,0 +1,82 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.Page +import mdr.pdb.{UserProfile, UserInfo, OsobniCislo} +import com.raquo.waypoint.Router +import mdr.pdb.app.Action +import mdr.pdb.app.NavigateTo +import mdr.pdb.UserFunction + +object AppPage: + // TODO: pages by logged in user + val pages: List[Page] = List(Page.Directory, Page.Dashboard) + + import NavigationBar.{Logo, MenuItem} + + val logo = Logo( + "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", + "Workflow" + ) + + // TODO: menu items by user profile + val userMenu = + List( + MenuItem("Your Profile"), + MenuItem("Settings"), + MenuItem("Sign out") + ) + + // TODO: load user profile + val $userProfile = Var( + UserProfile( + "tom", + UserInfo( + OsobniCislo("1031"), + "tom", + "Tom", + "Cook", + None, + None, + Some("tom@example.com"), + Some("+420 222 866 180"), + Some( + UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") + ), + Nil, + None + ) + ) + ) + + val $userInfo = $userProfile.signal.map(_.userInfo) + + type ViewModel = Option[HtmlElement] + def apply( + actionBus: Observer[Action] + )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using + router: Router[Page] + ): HtmlElement = + PageLayout(actionBus)( + $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => + PageLayout.ViewModel( + NavigationBar.ViewModel( + u, + pages.map(p => + NavigationBar.Link( + () => + PageLink( + p, + actionBus + ), + p == cp + ) + ), + userMenu, + logo + ), + c + ) + ), + mods + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala new file mode 100644 index 0000000..9375398 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala @@ -0,0 +1,143 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.tailwind.list.IconText.ViewModel +import fiftyforms.ui.components.tailwind.Icons +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.Page + +import com.raquo.laminar.api.L.{*, given} +import io.laminext.syntax.core.{*, given} +import mdr.pdb.app.Action + +object Breadcrumbs: + + private def slash = + import svg.{*, given} + svg( + cls := "flex-shrink-0 h-5 w-5 text-gray-300", + xmlns := "http://www.w3.org/2000/svg", + fill := "currentColor", + viewBox := "0 0 20 20", + ariaHidden := true, + path( + d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" + ) + ) + + def backIcon = + Icons.solid + .`arrow-narrow-left`() + .amend( + svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" + ) + + object Link: + case class ViewModel( + page: Page, + icon: Option[SvgElement], + extraClasses: String, + text: String, + textClasses: Option[String] = None + ) + + val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" + + def shortHome(p: Page) = ViewModel( + p, + Some(backIcon), + "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", + "Zpět na úvodní stránku", + None + ) + + def fullHome(p: Page) = ViewModel( + p, + Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), + "text-gray-400 hover:text-gray-500", + "Domů", + Some("sr-only") + ) + + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + PageLink + .container($m.map(_.page), actionBus) + .amend( + cls <-- $m.map(_.extraClasses), + child.maybe <-- $m.map(_.icon), + span( + cls <-- $m.map(_.textClasses.getOrElse("")), + child.text <-- $m.map(_.text) + ) + ) + + object FullBreadcrumbs: + type ViewModel = Page + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + ol( + role := "list", + cls := "flex items-center space-x-4", + children <-- $m.map(_.path).split(_.id)((_, _, $p) => + li( + div( + cls := "flex items-center", + child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), + Link( + $p.map(p => + if (p.isRoot) then Link.fullHome(p) + else + Link.ViewModel( + p, + None, + s"ml-4 max-w-xs truncate ${Link.baseClasses}", + p.title + ) + ), + actionBus + ) + ) + ) + ) + ) + + object ShortBreadcrumbs: + type ViewModel = Page + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + Link( + $m.map { p => + val target = p.parent.getOrElse(p) + if target.isRoot then Link.shortHome(target) + else + Link.ViewModel( + target, + Some(backIcon), + "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", + s"Zpět na ${target.title}" + ) + }, + actionBus + ) + + def apply(actionBus: Observer[Action])(using + router: Router[Page] + ): HtmlElement = + nav( + cls := "flex", + aria.label := "Breadcrumb", + div( + cls := "flex sm:hidden", + ShortBreadcrumbs(router.$currentPage, actionBus) + ), + div( + cls := "hidden sm:block", + FullBreadcrumbs(router.$currentPage, actionBus) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala new file mode 100644 index 0000000..4bb8b8d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala @@ -0,0 +1,242 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent +import fiftyforms.ui.components.tailwind.Icons +import fiftyforms.ui.components.tailwind.Avatar +import mdr.pdb.UserInfo +import io.laminext.syntax.core.* + +object NavigationBar: + + case class Logo(img: String, name: String) + case class Link(a: () => Anchor, active: Boolean) + case class MenuItem(title: String) + + case class ViewModel( + userInfo: UserInfo, + pages: List[Link], + userMenu: List[MenuItem], + logo: Logo + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + val $userInfo = $m.map(_.userInfo) + val mobileMenuOpen = Var(false) + + val desktopOnly = cls("hidden md:block") + val mobileOnly = cls("md:hidden") + + inline def avatarImage(size: Int = 8) = + Avatar($userInfo.map(_.img)).avatarImage(size) + + def notificationButton = button( + tpe := "button", + cls := List( + "bg-indigo-600", + "focus:outline-none", + "focus:ring-2", + "focus:ring-offset-2", + "focus:ring-offset-indigo-600", + "focus:ring-white", + "hover:text-white", + "p-1", + "rounded-full", + "text-indigo-200" + ), + span(cls := "sr-only", "View notifications"), + Icons.outline.bell() + ) + + def userProfile: HtmlElement = + val menuOpen = Var(false) + + def menuItem(item: MenuItem, idx: Int): HtmlElement = + a( + href := "#", + cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", + role := "menuitem", + tabIndex := -1, + idAttr := s"user-menu-item-$idx", + item.title + ) + + div( + cls := "ml-3 relative", + div( + button( + tpe := "button", + cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", + idAttr := "user-menu-button", + aria.expanded <-- menuOpen.signal, + aria.hasPopup := true, + span(cls := "sr-only", "Open user menu"), + child <-- avatarImage(), + onClick.preventDefault.mapTo( + !menuOpen.now() + ) --> menuOpen.writer + ) + ), + /* + * */ + div( + cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", + cls <-- menuOpen.signal.map { o => + if (o) "md:block" else "md:hidden" + }, + role := "menu", + aria.orientation := "vertical", + aria.labelledBy := "user-menu-button", + tabIndex := -1, + // : keyboard navigation + children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) + ) + ) + + def mobileProfile = + def menuItem(item: MenuItem): HtmlElement = + a( + href := "#", + cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", + item.title + ) + + div( + cls := "pt-4 pb-3 border-t border-indigo-700", + div( + cls := "flex items-center px-5", + div( + cls := "flex-shrink-0", + child <-- avatarImage(10) + ), + div( + cls := "ml-3", + div( + cls := "text-base font-medium text-white", + child.text <-- $userInfo.map(_.name) + ), + child.maybe <-- $userInfo.map( + _.email.map(e => + div( + cls := "text-sm font-medium text-indigo-300", + e + ) + ) + ) + ), + notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) + ), + div( + cls := "mt-3 px-2 space-y-1", + children <-- $m.map(_.userMenu.map(menuItem)) + ) + ) + + def pageLink(page: Link): Anchor = + page + .a() + .amend( + cls := "text-white px-3 py-2 rounded-md text-sm font-medium", + cls := Seq( + "bg-indigo-700" -> page.active, + "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active + ), + ariaCurrent := (if page.active then "page" else "false") + ) + + def logoImg: Image = + img( + cls := "h-8 w-8", + src <-- $m.map(_.logo.img), + alt <-- $m.map(_.logo.name) + ) + + def mobileMenuButton = button( + tpe := "button", + cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", + aria.controls := "mobile-menu", + aria.expanded <-- mobileMenuOpen.signal, + span(cls := "sr-only", "Open main menu"), + Icons.outline + .menu() + .amend( + svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") + ), + Icons.outline + .x() + .amend( + svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") + ), + onClick.preventDefault.mapTo( + !mobileMenuOpen.now() + ) --> mobileMenuOpen.writer + ) + + def navBarLeft = + div( + cls := "flex items-center", + div(cls := "flex-shrink-0", logoImg), + div( + desktopOnly, + div( + cls := "ml-10 flex items-baseline space-x-4", + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) + ) + ) + ) + + def navBarRight = + div( + desktopOnly, + div( + cls := "ml-4 flex items-center md:ml-6", + notificationButton, + userProfile + ) + ) + + def navBarMobile = + div( + cls := "-mr-2 flex", + mobileOnly, + mobileMenuButton + ) + + def navBar = + div( + cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", + div( + cls := "flex items-center justify-between h-16", + navBarLeft, + navBarRight, + navBarMobile + ) + ) + + def mobileMenu = + div( + mobileOnly, + cls <-- mobileMenuOpen.signal.map { o => + if (o) "block" else "hidden" + }, + idAttr := "mobile-menu", + div( + cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) + ), + mobileProfile + ) + + nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala new file mode 100644 index 0000000..c4cb10d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala @@ -0,0 +1,19 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.Page +import com.raquo.waypoint.Router +import mdr.pdb.app.Action + +object PageHeader: + def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = + header( + cls := "bg-white shadow-sm", + div( + cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", + h1( + cls := "text-lg leading-6 font-semibold text-gray-900", + Breadcrumbs(actionBus) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala new file mode 100644 index 0000000..aae4539 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala @@ -0,0 +1,30 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.Page +import mdr.pdb.app.Action +import fiftyforms.ui.components.tailwind.Loading +import io.laminext.syntax.core.* + +object PageLayout: + case class ViewModel( + navigation: NavigationBar.ViewModel, + content: Option[HtmlElement] + ) + def apply(actionBus: Observer[Action])( + $m: Signal[ViewModel], + mods: Modifier[HtmlElement]* + )(using router: Router[Page]): HtmlElement = + val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) + div( + cls := "h-full flex flex-col", + NavigationBar($m.map(_.navigation)), + child.maybe <-- router.$currentPage.map(_.isRoot) + .switch(None, Some(PageHeader(actionBus))), + main( + cls := "flex-grow-1 overflow-y-auto", + mods, + child <-- $maybeContent.map(_.getOrElse(Loading)) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/mdr/pdb/app/components/PageLink.scala new file mode 100644 index 0000000..e6e7052 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageLink.scala @@ -0,0 +1,40 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.LinkSupport.* +import mdr.pdb.app.Page +import mdr.pdb.app.Action +import com.raquo.waypoint.Router +import mdr.pdb.app.NavigateTo + +object PageLink: + type ViewModel = Page + def apply($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = + a(mods($m, actions), child.text <-- $m.map(_.title)) + + def apply(m: ViewModel, actions: Observer[Action])(using + router: Router[Page] + ): Anchor = apply(Val(m), actions) + + def container($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = a(mods($m, actions)) + + def container(m: ViewModel, actions: Observer[Action])(using + Router[Page] + ): Anchor = container(Val(m), actions) + + private def mods($m: Signal[Page], actions: Observer[Action])(using + router: Router[Page] + ): Modifier[Anchor] = + Seq( + href <-- $m.map(router.absoluteUrlForPage).recover { case _ => + Some("invalid url") + }, + composeEvents(onClick.noKeyMod.preventDefault)( + _.sample($m) + .map(NavigateTo.apply) + ) --> actions + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala new file mode 100644 index 0000000..7682772 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala @@ -0,0 +1,13 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.pages.dashboard.DashboardPage +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage + +class DashboardPageConnector(actionBus: Observer[Action])(using + router: Router[Page] +): + def apply: HtmlElement = + AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala new file mode 100644 index 0000000..76b9133 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala @@ -0,0 +1,76 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.Parameter +import mdr.pdb.UserInfo +import mdr.pdb.app.pages.detail.DetailParametruPage +import pages.detail.DetailKriteriaPage +import pages.detail.components.DukazyKriteria +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.ParameterCriteria + +object DetailKriteriaPageConnector { + trait AppState { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + } +} + +case class DetailKriteriaPageConnector( + state: DetailKriteriaPageConnector.AppState +)( + $page: Signal[Page.DetailKriteria] +)(using Router[Page]): + val $paramChangeSignal = + $page.splitOne(p => + (p.osobniCislo.value, p.parametr.value, p.kriterium.value) + )((x, _, _) => x) + val $pageChangeSignal = + $paramChangeSignal.map( + FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) + ) + + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + val $merged = + $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => + for { + da <- d + pa <- p + pb <- pa.find(_.id == pc._2) + ka <- pb.criteria.find(_.id == pc._3) + } yield (da, pb, ka) + ) + + def apply: HtmlElement = + AppPage(state.actionBus)( + $merged.map(_.map(buildModel)) + .split(_ => ())((_, s, $s) => + DetailKriteriaPage($s)(state.actionBus.contramap { + case DukazyKriteria.Add => + NavigateTo( + Page.UpravDukazKriteria( + Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), + Page.Titled(s.parametr.id, Some(s.parametr.nazev)), + Page.Titled(s.kriterium.id, Some(s.kriterium.id)) + ) + ) + }) + ), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: Parameter, + k: ParameterCriteria + ): DetailKriteriaPage.ViewModel = + DetailKriteriaPage.ViewModel( + o.toDetailOsoby, + p.toParametr(_ => a()), + k.toKriterium(_ => a()) + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DetailPageConnector.scala new file mode 100644 index 0000000..df3a6b7 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DetailPageConnector.scala @@ -0,0 +1,57 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.UserInfo +import pages.detail.DetailPage +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.pages.detail.components.DetailOsoby +import mdr.pdb.Parameter +import mdr.pdb.app.pages.detail.components.SeznamParametru +import fiftyforms.ui.components.tailwind.Color + +object DetailPageConnector { + trait AppState { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + } +} + +case class DetailPageConnector(state: DetailPageConnector.AppState)( + $page: Signal[Page.Detail] +)(using router: Router[Page]): + val $oscChangeSignal = $page.splitOne(_.osobniCislo.value)((osc, _, _) => osc) + val $pageChangeSignal = + $oscChangeSignal.flatMap(osc => + EventStream.fromSeq(Seq(FetchUserDetails(osc), FetchParameters(osc))) + ) + // TODO: filter the value based on the current osc + // OSC change will fetch new data, but still + // - we need to be sure that what we got is really what we ought to display + // - we want to display stale data accordingly (at least with loading indicator) + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + def apply: HtmlElement = + AppPage(state.actionBus)( + $data.combineWithFn($params)(_ zip _) + .map(_.map(buildModel)) + .split(_ => ())((_, _, s) => DetailPage(s)), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: List[Parameter] + ): DetailPage.ViewModel = + DetailPage.ViewModel( + o.toDetailOsoby, + p.map( + _.toParametr(param => + PageLink.container(Page.DetailParametru(o, param), state.actionBus) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DetailParametruPageConnector.scala new file mode 100644 index 0000000..fa7193d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DetailParametruPageConnector.scala @@ -0,0 +1,67 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.Parameter +import mdr.pdb.UserInfo +import mdr.pdb.app.pages.detail.DetailParametruPage +import pages.detail.DetailParametruPage +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.components.PageLink + +object DetailParametruPageConnector { + trait AppState { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + } +} + +case class DetailParametruPageConnector( + state: DetailParametruPageConnector.AppState +)( + $page: Signal[Page.DetailParametru] +)(using router: Router[Page]): + val $paramChangeSignal = + $page.splitOne(p => (p.osobniCislo.value, p.parametr.value))((x, _, _) => x) + val $pageChangeSignal = + $paramChangeSignal.map(FetchParameter(_, _)) + + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + val $merged = + $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => + for { + da <- d + pa <- p + pb <- pa.find(_.id == pc._2) + } yield (da, pb) + ) + + def apply: HtmlElement = + AppPage(state.actionBus)( + $merged.map(_.map(buildModel)) + .split(_ => ())((_, _, s) => DetailParametruPage(s)), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: Parameter + ): DetailParametruPage.ViewModel = + DetailParametruPage.ViewModel( + o.toDetailOsoby, + p.toParametr(p => + PageLink.container(Page.DetailParametru(o, p), state.actionBus) + ), + p.criteria.map( + _.toKriterium { c => + PageLink.container( + Page.DetailKriteria(o, p, c), + state.actionBus + ) + } + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DirectoryPageConnector.scala new file mode 100644 index 0000000..ab2c045 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DirectoryPageConnector.scala @@ -0,0 +1,34 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.UserInfo +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.components.AppPage + +case class DirectoryPageConnector( + $input: EventStream[List[UserInfo]], + actionBus: Observer[Action] +)(using router: Router[Page]): + val $data = $input.startWithNone + val $actionSignal = EventStream.fromValue(FetchDirectory) + + def apply: HtmlElement = + AppPage(actionBus)( + $data.split(_ => ())((_, _, s) => + pages.directory.DirectoryPage( + s.map( + _.map( + _.toUserRow(u => + PageLink.container( + Page.Detail(Page.Titled(u.personalNumber)), + actionBus + ) + ) + ) + ) + ) + ), + $actionSignal --> actionBus + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/mdr/pdb/app/connectors/conversions.scala new file mode 100644 index 0000000..f581796 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/conversions.scala @@ -0,0 +1,68 @@ +package mdr.pdb.app.connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.UserInfo +import mdr.pdb.app.pages.detail.components.DetailOsoby +import mdr.pdb.Parameter +import mdr.pdb.app.pages.detail.components.SeznamParametru +import mdr.pdb.ParameterCriteria +import mdr.pdb.app.pages.detail.components.SeznamKriterii +import mdr.pdb.app.pages.directory.components.UserRow +import mdr.pdb.app.pages.detail.components.DetailParametru +import mdr.pdb.app.pages.detail.components.DetailKriteria +import fiftyforms.ui.components.tailwind.Color + +extension (o: UserInfo) + def toDetailOsoby: DetailOsoby.ViewModel = + DetailOsoby.ViewModel( + o.personalNumber, + o.name, + o.email, + o.phone, + o.img, + o.mainFunction.map(f => + DetailOsoby.Funkce.ViewModel(f.name, f.dept, f.ou) + ), + o.userContracts.headOption.map(c => + DetailOsoby.PracovniPomer.ViewModel(c.rel, c.startDate, c.endDate) + ) + ) + +extension (param: Parameter) + def toParametr(container: Parameter => Anchor): DetailParametru.ViewModel = + DetailParametru.ViewModel( + id = param.id, + nazev = param.name, + popis = param.description, + status = "Nesplněno", + statusColor = Color.red, + a = container(param) + ) + +extension (crit: ParameterCriteria) + def toKriterium( + container: ParameterCriteria => Anchor + ): DetailKriteria.ViewModel = + DetailKriteria.ViewModel( + nazev = crit.criteriumText, + kapitola = crit.chapterId, + bod = crit.itemId, + status = "Nesplněno", + statusColor = Color.red, + splneno = false, + dukazy = Nil, + container = container(crit) + ) + +extension (user: UserInfo) + def toUserRow( + container: UserInfo => HtmlElement = (_: UserInfo) => div() + ): UserRow.ViewModel = + UserRow.ViewModel( + osobniCislo = user.personalNumber.toString, + celeJmeno = user.name, + prijmeni = user.surname, + hlavniFunkce = user.mainFunction.map(_.name), + img = user.img, + container = () => container(user) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala deleted file mode 100644 index a3b00af..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala +++ /dev/null @@ -1,103 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import scala.scalajs.js.annotation.JSExportTopLevel -import scala.scalajs.js.annotation.JSExport -import scala.scalajs.js.annotation.JSImport -import scala.scalajs.js -import org.scalajs.dom -import com.raquo.laminar.api.L.{*, given} -import scala.scalajs.js.Date -import com.raquo.waypoint.Router -import com.raquo.waypoint.SplitRender -import cz.e_bs.cmi.mdr.pdb.app.services.DataFetcher -import scala.scalajs.js.JSON -import zio.json._ -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.state.AppState - -@js.native -@JSImport("stylesheets/main.css", JSImport.Namespace) -object Css extends js.Any - -@js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native -@JSImport("params/pdb-params.json", JSImport.Default) -object pdbParams extends js.Object - -@JSExportTopLevel("app") -object Main: - - @JSExport - def main(args: Array[String]): Unit = - import Routes.given - onLoad { - setupAirstream() - val appContainer = dom.document.querySelector("#app") - val _ = - render( - appContainer, - renderPage(state.MockAppState(using unsafeWindowOwner, router)) - ) - } - - private def onLoad(f: => Unit): Unit = - documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) - - private def setupAirstream()(using router: Router[Page]): Unit = - AirstreamError.registerUnhandledErrorCallback(err => - router.forcePage( - Page.UnhandledError( - Some(err.getClass.getName), // TODO: Fill only in dev mode - Some(err.getMessage) - ) - ) - ) - - def renderPage(state: AppState)(using - router: Router[Page] - ): HtmlElement = - val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) - .collectSignal[Page.Detail]( - connectors - .DetailPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailParametru]( - connectors - .DetailParametruPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailKriteria]( - connectors - .DetailKriteriaPageConnector(state)(_) - .apply - ) - .collectSignal[Page.UpravDukazKriteria]( - pages.detail.UpravDukaz.Connector(state)(_).apply - ) - .collectStatic(Page.Dashboard)( - connectors.DashboardPageConnector(state.actionBus).apply - ) - .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) - ) - .collect[Page.UnhandledError](pg => - pages.errors - .UnhandledErrorPage( - pages.errors.UnhandledErrorPage - .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), - state.actionBus - ) - ) - .collectStatic(Page.Directory)( - connectors - .DirectoryPageConnector(state.users, state.actionBus) - .apply - ) - div(cls := "h-full", child <-- pageSplitter.$view) - - // Pull in the stylesheet - val css: Css.type = Css diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala deleted file mode 100644 index 3bbc8c0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ /dev/null @@ -1,198 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.* -import org.scalajs.dom -import zio.json.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import scala.scalajs.js -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.Page.Titled - -// enum is not working with Waypoints' SplitRender collectStatic -sealed abstract class Page( - val id: String, - val title: String, - val parent: Option[Page] -) { - val path: Vector[Page] = - parent match - case None => Vector(this) - case Some(p) => p.path :+ this - - val isRoot: Boolean = parent.isEmpty -} - -object Page: - - case class Titled[V](value: V, title: Option[String] = None): - val show: String = title.getOrElse(value.toString) - - case object Directory extends Page("directory", "Adresář", None) - - case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) - - case class Detail(osobniCislo: Titled[OsobniCislo]) - extends Page("user", osobniCislo.show, Some(Directory)) - - object Detail { - def apply(o: UserInfo): Detail = Detail( - Titled(o.personalNumber, Some(o.name)) - ) - } - - case class DetailParametru( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String] - ) extends Page( - "parameter", - parametr.show, - Some(Detail(osobniCislo)) - ) - - object DetailParametru { - def apply(o: UserInfo, p: Parameter): DetailParametru = - DetailParametru( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)) - ) - } - - case class DetailKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "criteria", - kriterium.show, - Some(DetailParametru(osobniCislo, parametr)) - ) - - object DetailKriteria { - def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = - DetailKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class UpravDukazKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "addProof", - "Důkaz", - Some(DetailKriteria(osobniCislo, parametr, kriterium)) - ) - - object UpravDukazKriteria { - def apply( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): UpravDukazKriteria = - UpravDukazKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class NotFound(url: String) extends Page("404", "404", Some(Directory)) - - case class UnhandledError( - errorName: Option[String], - errorMessage: Option[String] - ) extends Page("500", "Unexpected error", Some(Directory)) - -object Routes: - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) - given [V: JsonEncoder]: JsonEncoder[Titled[V]] = - DeriveJsonEncoder.gen[Titled[V]] - given [V: JsonDecoder]: JsonDecoder[Titled[V]] = - DeriveJsonDecoder.gen[Titled[V]] - given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] - given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] - - val base = - js.`import`.meta.env.BASE_URL - .asInstanceOf[String] + "app" - - val homePage: Page = Page.Directory - - given router: Router[Page] = Router[Page]( - routes = List( - Route.static(homePage, root / endOfSegments, basePath = base), - Route.static( - Page.Dashboard, - root / "dashboard" / endOfSegments, - basePath = base - ), - Route[Page.Detail, String]( - encode = _.osobniCislo.value.toString, - decode = osc => Page.Detail(Titled(OsobniCislo(osc))), - root / "osoba" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.DetailParametru, (String, String)]( - encode = p => (p.osobniCislo.value.toString, p.parametr.value), - decode = - p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / endOfSegments, - basePath = base - ), - Route[Page.DetailKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.DetailKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.UpravDukazKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.UpravDukazKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / "edit" / endOfSegments, - basePath = base - ) - ), - serializePage = _.toJson, - deserializePage = _.fromJson[Page] - .fold(s => throw IllegalStateException(s), identity), - getPageTitle = _.title, - routeFallback = url => Page.NotFound(url), - deserializeFallback = _ => Page.Dashboard - )( - $popStateEvent = windowEvents.onPopState, - owner = unsafeWindowOwner - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala deleted file mode 100644 index b24b82b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -sealed trait Action - -case object FetchDirectory extends Action -case class FetchUserDetails(osc: OsobniCislo) extends Action -case class FetchParameters(osc: OsobniCislo) extends Action -case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action -case class FetchParameterCriteria( - osc: OsobniCislo, - paramId: String, - critId: String, - page: (UserInfo, Parameter, ParameterCriteria) => Page -) extends Action -case class FetchAvailableFiles(osc: OsobniCislo) extends Action -case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala deleted file mode 100644 index c637d35..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala +++ /dev/null @@ -1,82 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.{UserProfile, UserInfo, OsobniCislo} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo -import cz.e_bs.cmi.mdr.pdb.UserFunction - -object AppPage: - // TODO: pages by logged in user - val pages: List[Page] = List(Page.Directory, Page.Dashboard) - - import NavigationBar.{Logo, MenuItem} - - val logo = Logo( - "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", - "Workflow" - ) - - // TODO: menu items by user profile - val userMenu = - List( - MenuItem("Your Profile"), - MenuItem("Settings"), - MenuItem("Sign out") - ) - - // TODO: load user profile - val $userProfile = Var( - UserProfile( - "tom", - UserInfo( - OsobniCislo("1031"), - "tom", - "Tom", - "Cook", - None, - None, - Some("tom@example.com"), - Some("+420 222 866 180"), - Some( - UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") - ), - Nil, - None - ) - ) - ) - - val $userInfo = $userProfile.signal.map(_.userInfo) - - type ViewModel = Option[HtmlElement] - def apply( - actionBus: Observer[Action] - )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using - router: Router[Page] - ): HtmlElement = - PageLayout(actionBus)( - $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => - PageLayout.ViewModel( - NavigationBar.ViewModel( - u, - pages.map(p => - NavigationBar.Link( - () => - PageLink( - p, - actionBus - ), - p == cp - ) - ), - userMenu, - logo - ), - c - ) - ), - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala deleted file mode 100644 index bf473c6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ /dev/null @@ -1,143 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden -import fiftyforms.ui.components.tailwind.list.IconText.ViewModel -import fiftyforms.ui.components.tailwind.Icons -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Action - -object Breadcrumbs: - - private def slash = - import svg.{*, given} - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-300", - xmlns := "http://www.w3.org/2000/svg", - fill := "currentColor", - viewBox := "0 0 20 20", - ariaHidden := true, - path( - d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" - ) - ) - - def backIcon = - Icons.solid - .`arrow-narrow-left`() - .amend( - svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" - ) - - object Link: - case class ViewModel( - page: Page, - icon: Option[SvgElement], - extraClasses: String, - text: String, - textClasses: Option[String] = None - ) - - val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" - - def shortHome(p: Page) = ViewModel( - p, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - "Zpět na úvodní stránku", - None - ) - - def fullHome(p: Page) = ViewModel( - p, - Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), - "text-gray-400 hover:text-gray-500", - "Domů", - Some("sr-only") - ) - - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - PageLink - .container($m.map(_.page), actionBus) - .amend( - cls <-- $m.map(_.extraClasses), - child.maybe <-- $m.map(_.icon), - span( - cls <-- $m.map(_.textClasses.getOrElse("")), - child.text <-- $m.map(_.text) - ) - ) - - object FullBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - ol( - role := "list", - cls := "flex items-center space-x-4", - children <-- $m.map(_.path).split(_.id)((_, _, $p) => - li( - div( - cls := "flex items-center", - child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), - Link( - $p.map(p => - if (p.isRoot) then Link.fullHome(p) - else - Link.ViewModel( - p, - None, - s"ml-4 max-w-xs truncate ${Link.baseClasses}", - p.title - ) - ), - actionBus - ) - ) - ) - ) - ) - - object ShortBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - Link( - $m.map { p => - val target = p.parent.getOrElse(p) - if target.isRoot then Link.shortHome(target) - else - Link.ViewModel( - target, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - s"Zpět na ${target.title}" - ) - }, - actionBus - ) - - def apply(actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - nav( - cls := "flex", - aria.label := "Breadcrumb", - div( - cls := "flex sm:hidden", - ShortBreadcrumbs(router.$currentPage, actionBus) - ), - div( - cls := "hidden sm:block", - FullBreadcrumbs(router.$currentPage, actionBus) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala deleted file mode 100644 index 0db2047..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ /dev/null @@ -1,242 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent -import fiftyforms.ui.components.tailwind.Icons -import fiftyforms.ui.components.tailwind.Avatar -import cz.e_bs.cmi.mdr.pdb.UserInfo -import io.laminext.syntax.core.* - -object NavigationBar: - - case class Logo(img: String, name: String) - case class Link(a: () => Anchor, active: Boolean) - case class MenuItem(title: String) - - case class ViewModel( - userInfo: UserInfo, - pages: List[Link], - userMenu: List[MenuItem], - logo: Logo - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - val $userInfo = $m.map(_.userInfo) - val mobileMenuOpen = Var(false) - - val desktopOnly = cls("hidden md:block") - val mobileOnly = cls("md:hidden") - - inline def avatarImage(size: Int = 8) = - Avatar($userInfo.map(_.img)).avatarImage(size) - - def notificationButton = button( - tpe := "button", - cls := List( - "bg-indigo-600", - "focus:outline-none", - "focus:ring-2", - "focus:ring-offset-2", - "focus:ring-offset-indigo-600", - "focus:ring-white", - "hover:text-white", - "p-1", - "rounded-full", - "text-indigo-200" - ), - span(cls := "sr-only", "View notifications"), - Icons.outline.bell() - ) - - def userProfile: HtmlElement = - val menuOpen = Var(false) - - def menuItem(item: MenuItem, idx: Int): HtmlElement = - a( - href := "#", - cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", - role := "menuitem", - tabIndex := -1, - idAttr := s"user-menu-item-$idx", - item.title - ) - - div( - cls := "ml-3 relative", - div( - button( - tpe := "button", - cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - idAttr := "user-menu-button", - aria.expanded <-- menuOpen.signal, - aria.hasPopup := true, - span(cls := "sr-only", "Open user menu"), - child <-- avatarImage(), - onClick.preventDefault.mapTo( - !menuOpen.now() - ) --> menuOpen.writer - ) - ), - /* - * */ - div( - cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", - cls <-- menuOpen.signal.map { o => - if (o) "md:block" else "md:hidden" - }, - role := "menu", - aria.orientation := "vertical", - aria.labelledBy := "user-menu-button", - tabIndex := -1, - // : keyboard navigation - children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) - ) - ) - - def mobileProfile = - def menuItem(item: MenuItem): HtmlElement = - a( - href := "#", - cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", - item.title - ) - - div( - cls := "pt-4 pb-3 border-t border-indigo-700", - div( - cls := "flex items-center px-5", - div( - cls := "flex-shrink-0", - child <-- avatarImage(10) - ), - div( - cls := "ml-3", - div( - cls := "text-base font-medium text-white", - child.text <-- $userInfo.map(_.name) - ), - child.maybe <-- $userInfo.map( - _.email.map(e => - div( - cls := "text-sm font-medium text-indigo-300", - e - ) - ) - ) - ), - notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) - ), - div( - cls := "mt-3 px-2 space-y-1", - children <-- $m.map(_.userMenu.map(menuItem)) - ) - ) - - def pageLink(page: Link): Anchor = - page - .a() - .amend( - cls := "text-white px-3 py-2 rounded-md text-sm font-medium", - cls := Seq( - "bg-indigo-700" -> page.active, - "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active - ), - ariaCurrent := (if page.active then "page" else "false") - ) - - def logoImg: Image = - img( - cls := "h-8 w-8", - src <-- $m.map(_.logo.img), - alt <-- $m.map(_.logo.name) - ) - - def mobileMenuButton = button( - tpe := "button", - cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - aria.controls := "mobile-menu", - aria.expanded <-- mobileMenuOpen.signal, - span(cls := "sr-only", "Open main menu"), - Icons.outline - .menu() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") - ), - Icons.outline - .x() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") - ), - onClick.preventDefault.mapTo( - !mobileMenuOpen.now() - ) --> mobileMenuOpen.writer - ) - - def navBarLeft = - div( - cls := "flex items-center", - div(cls := "flex-shrink-0", logoImg), - div( - desktopOnly, - div( - cls := "ml-10 flex items-baseline space-x-4", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ) - ) - ) - - def navBarRight = - div( - desktopOnly, - div( - cls := "ml-4 flex items-center md:ml-6", - notificationButton, - userProfile - ) - ) - - def navBarMobile = - div( - cls := "-mr-2 flex", - mobileOnly, - mobileMenuButton - ) - - def navBar = - div( - cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex items-center justify-between h-16", - navBarLeft, - navBarRight, - navBarMobile - ) - ) - - def mobileMenu = - div( - mobileOnly, - cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }, - idAttr := "mobile-menu", - div( - cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ), - mobileProfile - ) - - nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala deleted file mode 100644 index 75339df..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object PageHeader: - def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = - header( - cls := "bg-white shadow-sm", - div( - cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", - h1( - cls := "text-lg leading-6 font-semibold text-gray-900", - Breadcrumbs(actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala deleted file mode 100644 index 5096463..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ /dev/null @@ -1,30 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import fiftyforms.ui.components.tailwind.Loading -import io.laminext.syntax.core.* - -object PageLayout: - case class ViewModel( - navigation: NavigationBar.ViewModel, - content: Option[HtmlElement] - ) - def apply(actionBus: Observer[Action])( - $m: Signal[ViewModel], - mods: Modifier[HtmlElement]* - )(using router: Router[Page]): HtmlElement = - val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) - div( - cls := "h-full flex flex-col", - NavigationBar($m.map(_.navigation)), - child.maybe <-- router.$currentPage.map(_.isRoot) - .switch(None, Some(PageHeader(actionBus))), - main( - cls := "flex-grow-1 overflow-y-auto", - mods, - child <-- $maybeContent.map(_.getOrElse(Loading)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala deleted file mode 100644 index 62d820f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.LinkSupport.* -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo - -object PageLink: - type ViewModel = Page - def apply($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = - a(mods($m, actions), child.text <-- $m.map(_.title)) - - def apply(m: ViewModel, actions: Observer[Action])(using - router: Router[Page] - ): Anchor = apply(Val(m), actions) - - def container($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = a(mods($m, actions)) - - def container(m: ViewModel, actions: Observer[Action])(using - Router[Page] - ): Anchor = container(Val(m), actions) - - private def mods($m: Signal[Page], actions: Observer[Action])(using - router: Router[Page] - ): Modifier[Anchor] = - Seq( - href <-- $m.map(router.absoluteUrlForPage).recover { case _ => - Some("invalid url") - }, - composeEvents(onClick.noKeyMod.preventDefault)( - _.sample($m) - .map(NavigateTo.apply) - ) --> actions - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala deleted file mode 100644 index 87f825a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.pages.dashboard.DashboardPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -class DashboardPageConnector(actionBus: Observer[Action])(using - router: Router[Page] -): - def apply: HtmlElement = - AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala deleted file mode 100644 index e9b0f71..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala +++ /dev/null @@ -1,76 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailKriteriaPage -import pages.detail.components.DukazyKriteria -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -object DetailKriteriaPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailKriteriaPageConnector( - state: DetailKriteriaPageConnector.AppState -)( - $page: Signal[Page.DetailKriteria] -)(using Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => - (p.osobniCislo.value, p.parametr.value, p.kriterium.value) - )((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map( - FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) - ) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, s, $s) => - DetailKriteriaPage($s)(state.actionBus.contramap { - case DukazyKriteria.Add => - NavigateTo( - Page.UpravDukazKriteria( - Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), - Page.Titled(s.parametr.id, Some(s.parametr.nazev)), - Page.Titled(s.kriterium.id, Some(s.kriterium.id)) - ) - ) - }) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): DetailKriteriaPage.ViewModel = - DetailKriteriaPage.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala deleted file mode 100644 index 4d8024f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ /dev/null @@ -1,57 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import pages.detail.DetailPage -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import fiftyforms.ui.components.tailwind.Color - -object DetailPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailPageConnector(state: DetailPageConnector.AppState)( - $page: Signal[Page.Detail] -)(using router: Router[Page]): - val $oscChangeSignal = $page.splitOne(_.osobniCislo.value)((osc, _, _) => osc) - val $pageChangeSignal = - $oscChangeSignal.flatMap(osc => - EventStream.fromSeq(Seq(FetchUserDetails(osc), FetchParameters(osc))) - ) - // TODO: filter the value based on the current osc - // OSC change will fetch new data, but still - // - we need to be sure that what we got is really what we ought to display - // - we want to display stale data accordingly (at least with loading indicator) - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - def apply: HtmlElement = - AppPage(state.actionBus)( - $data.combineWithFn($params)(_ zip _) - .map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: List[Parameter] - ): DetailPage.ViewModel = - DetailPage.ViewModel( - o.toDetailOsoby, - p.map( - _.toParametr(param => - PageLink.container(Page.DetailParametru(o, param), state.actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala deleted file mode 100644 index fdaa7f8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailParametruPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink - -object DetailParametruPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailParametruPageConnector( - state: DetailParametruPageConnector.AppState -)( - $page: Signal[Page.DetailParametru] -)(using router: Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => (p.osobniCislo.value, p.parametr.value))((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map(FetchParameter(_, _)) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - } yield (da, pb) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailParametruPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter - ): DetailParametruPage.ViewModel = - DetailParametruPage.ViewModel( - o.toDetailOsoby, - p.toParametr(p => - PageLink.container(Page.DetailParametru(o, p), state.actionBus) - ), - p.criteria.map( - _.toKriterium { c => - PageLink.container( - Page.DetailKriteria(o, p, c), - state.actionBus - ) - } - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala deleted file mode 100644 index 049aa22..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -case class DirectoryPageConnector( - $input: EventStream[List[UserInfo]], - actionBus: Observer[Action] -)(using router: Router[Page]): - val $data = $input.startWithNone - val $actionSignal = EventStream.fromValue(FetchDirectory) - - def apply: HtmlElement = - AppPage(actionBus)( - $data.split(_ => ())((_, _, s) => - pages.directory.DirectoryPage( - s.map( - _.map( - _.toUserRow(u => - PageLink.container( - Page.Detail(Page.Titled(u.personalNumber)), - actionBus - ) - ) - ) - ) - ) - ), - $actionSignal --> actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala deleted file mode 100644 index 88e7450..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ /dev/null @@ -1,68 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii -import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria -import fiftyforms.ui.components.tailwind.Color - -extension (o: UserInfo) - def toDetailOsoby: DetailOsoby.ViewModel = - DetailOsoby.ViewModel( - o.personalNumber, - o.name, - o.email, - o.phone, - o.img, - o.mainFunction.map(f => - DetailOsoby.Funkce.ViewModel(f.name, f.dept, f.ou) - ), - o.userContracts.headOption.map(c => - DetailOsoby.PracovniPomer.ViewModel(c.rel, c.startDate, c.endDate) - ) - ) - -extension (param: Parameter) - def toParametr(container: Parameter => Anchor): DetailParametru.ViewModel = - DetailParametru.ViewModel( - id = param.id, - nazev = param.name, - popis = param.description, - status = "Nesplněno", - statusColor = Color.red, - a = container(param) - ) - -extension (crit: ParameterCriteria) - def toKriterium( - container: ParameterCriteria => Anchor - ): DetailKriteria.ViewModel = - DetailKriteria.ViewModel( - nazev = crit.criteriumText, - kapitola = crit.chapterId, - bod = crit.itemId, - status = "Nesplněno", - statusColor = Color.red, - splneno = false, - dukazy = Nil, - container = container(crit) - ) - -extension (user: UserInfo) - def toUserRow( - container: UserInfo => HtmlElement = (_: UserInfo) => div() - ): UserRow.ViewModel = - UserRow.ViewModel( - osobniCislo = user.personalNumber.toString, - celeJmeno = user.name, - prijmeni = user.surname, - hlavniFunkce = user.mainFunction.map(_.name), - img = user.img, - container = () => container(user) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala deleted file mode 100644 index 7d5b2c1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.dashboard - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.Page - -object DashboardPage: - - def render: HtmlElement = - div("Dashboard page") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala deleted file mode 100644 index 9ef0698..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala +++ /dev/null @@ -1,31 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailKriteriaPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply($m: Signal[ViewModel])( - action: Observer[DukazyKriteria.Action] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - DukazyKriteria($m.map(_.kriterium.dukazy))(action) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala deleted file mode 100644 index 3b316e1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object DetailPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametry: SeznamParametru.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby($m.map(_.osoba)), - SeznamParametru($m.map(_.parametry)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala deleted file mode 100644 index 17224cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala +++ /dev/null @@ -1,24 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailParametruPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriteria: SeznamKriterii.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby.header($m.map(_.osoba)), - DetailParametru($m.map(_.parametr)), - SeznamKriterii($m.map(_.kriteria)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala deleted file mode 100644 index dc912fa..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ /dev/null @@ -1,107 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb -package app -package pages.detail - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.UpravDukazForm -import fiftyforms.services.files.File - -object UpravDukaz: - - type ThisPage = Page.UpravDukazKriteria - type PageKey = (OsobniCislo, String, String) - - trait State { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def availableFiles: EventStream[List[File]] - def actionBus: Observer[Action] - } - - def keyOfPage(page: ThisPage): PageKey = - (page.osobniCislo.value, page.parametr.value, page.kriterium.value) - - def onChangeAction(key: PageKey): Action = - FetchParameterCriteria( - key._1, - key._2, - key._3, - Page.UpravDukazKriteria(_, _, _) - ) - - class Connector(state: State)($page: Signal[ThisPage])(using Router[Page]): - val $paramChangeSignal = $page.splitOne(keyOfPage)((x, _, _) => x) - val $pageChangeSignal = $paramChangeSignal.map(onChangeAction) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.split(_ => ())((_, s, $s) => - PageComponent( - $s.map(buildModel), - state.availableFiles, - state.actionBus.contramap { - case UpravDukazForm.Cancelled => - NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) - case UpravDukazForm.AvailableFilesRequested => - FetchAvailableFiles(s._1.personalNumber) - } - ) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): PageComponent.ViewModel = - import connectors.* - PageComponent.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) - - object PageComponent: - import components.* - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply( - $m: Signal[ViewModel], - availableFilesStream: EventStream[List[File]], - events: Observer[UpravDukazForm.Event] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - UpravDukazForm(availableFilesStream)(events) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala deleted file mode 100644 index ca311b0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.Icons -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Color - -object DetailKriteria: - case class ViewModel( - nazev: String, - kapitola: String, - bod: String, - status: String, - statusColor: Color, - splneno: Boolean, - dukazy: List[DukazKriteria.ViewModel], - container: HtmlElement = div() - ) { - val id = s"${kapitola}${bod}" - } - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h3( - cls := "text-xl leading-6 font-bold text-gray-900", - child.text <-- $m.map(_.nazev) - ), - h3( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - "Kapitola ", - child.text <-- $m.map(_.kapitola), - " bod ", - child <-- $m.map(_.bod) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala deleted file mode 100644 index ae90290..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala +++ /dev/null @@ -1,100 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Avatar - -object DetailOsoby: - - object Funkce: - case class ViewModel( - nazev: String, - stredisko: String, - voj: String - ) - - def render($m: Signal[ViewModel]): HtmlElement = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.nazev), - span( - cls := "hidden md:inline", - " @ ", - child.text <-- $m.map(_.stredisko), - ", ", - child.text <-- $m.map(_.voj) - ) - ) - - object PracovniPomer: - case class ViewModel( - druh: String, - pocatek: LocalDate, - konec: Option[LocalDate] - ) - - def render($m: Signal[ViewModel]): HtmlElement = - import fiftyforms.ui.components.tailwind.CustomAttrs.datetime - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.druh), - " od ", - time( - datetime <-- $m.map(_.pocatek.toString), - child.text <-- $m.map(_.pocatek.toString) - ) - ) - - case class ViewModel( - osobniCislo: OsobniCislo, - jmeno: String, - email: Option[String], - telefon: Option[String], - img: Option[String], - hlavniFunkce: Option[Funkce.ViewModel], - pracovniPomer: Option[PracovniPomer.ViewModel] - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-4", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(16) - ), - div( - h1( - cls := "text-2xl font-bold text-gray-900", - child.text <-- $m.map(_.jmeno) - ), - child.maybe <-- $m.map(_.hlavniFunkce).split(_ => ())((_, _, d) => - Funkce.render(d) - ), - child.maybe <-- $m.map(_.pracovniPomer).split(_ => ())((_, _, d) => - PracovniPomer.render(d) - ) - ) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-5", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(8) - ), - div( - h1( - cls := "text-lg font-medium text-gray-800", - child.text <-- $m.map(_.jmeno) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala deleted file mode 100644 index e11cb62..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color - -object DetailParametru: - case class ViewModel( - id: String, - nazev: String, - popis: String, - status: String, - statusColor: Color, - a: Anchor - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h2( - cls := "text-2xl leading-6 font-medium text-gray-900", - child.text <-- $m.map(_.nazev) - ), - p( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - child.text <-- $m.map(_.popis) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - h2( - cls := "text-lg leading-6 font-medium text-gray-600", - child.text <-- $m.map(_.nazev) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala deleted file mode 100644 index a00ede8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala +++ /dev/null @@ -1,177 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.CustomAttrs - -object DukazKriteria: - case class Osoba(osobniCislo: String, jmeno: String) - case class Udalost(osoba: Osoba, datum: LocalDate) - case class Dokument( - url: String, - nazev: String, - pridal: Udalost, - odebral: Option[Udalost] - ) - case class ViewModel( - dokumenty: List[Dokument], - autorizoval: Option[Udalost], - platiDo: Option[LocalDate], - poznámka: Option[String] - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-lg", - div( - cls := "px-4 py-5 sm:px-6", - h3( - cls := "text-lg leading-6 font-medium text-gray-900", - """Applicant Information""" - ), - p( - cls := "mt-1 max-w-2xl text-sm text-gray-500", - """Personal details and application.""" - ) - ), - div( - cls := "border-t border-gray-200 px-4 py-5 sm:px-6", - dl( - cls := "grid grid-cols-1 gap-x-4 gap-y-8 sm:grid-cols-2", - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Full name""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Margot Foster""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Application for""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Backend Developer""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Email address""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """margotfoster@example.com""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Salary expectation""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """$120,000""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """About""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Fugiat ipsum ipsum deserunt culpa aute sint do nostrud anim incididunt cillum culpa consequat. Excepteur qui ipsum aliquip consequat sint. Sit id mollit nulla mollit nostrud in ea officia proident. Irure nostrud pariatur mollit ad adipisicing reprehenderit deserunt qui eu.""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """Attachments""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - ul( - role := "list", - cls := "border border-gray-200 rounded-md divide-y divide-gray-200", - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """resume_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ), - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """coverletter_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala deleted file mode 100644 index 22f134f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala +++ /dev/null @@ -1,35 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object DukazyKriteria: - sealed trait Action - case object Add extends Action - - type ViewModel = List[DukazKriteria.ViewModel] - - def apply($m: Signal[ViewModel])(actions: Observer[Action]): HtmlElement = - div( - child.maybe <-- $m.map(m => - if (m.isEmpty) then Some(prazdnyDukaz(actions)) - else None - ), - children <-- $m.map(_.zipWithIndex).split(_._2)((_, _, $s) => - DukazKriteria($s.map(_._1)) - ) - ) - - private def prazdnyDukaz(actions: Observer[Action]): HtmlElement = - button( - tpe := "button", - onClick.preventDefault.mapTo(Add) --> actions, - cls := "relative block w-full border-2 border-gray-300 border-dashed rounded-lg p-12 text-center hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - Icons.outline - .`document-add`(12) - .amend(svg.cls := "mx-auto text-gray-400"), - span( - cls := "mt-2 block text-sm font-medium text-gray-900", - "Přidat důkaz" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala deleted file mode 100644 index f628697..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.list.StackedList -import fiftyforms.ui.components.tailwind.list.ListRow -import fiftyforms.ui.components.tailwind.list.RowTag -import fiftyforms.ui.components.tailwind.list.RowNext -import java.time.LocalDate - -object SeznamKriterii: - type ViewModel = List[DetailKriteria.ViewModel] - - private val kritList = new StackedList[DetailKriteria.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - kritList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = - p(cls := "text-sm text-gray-500", s"${i.kapitola}${i.bod}"), - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.container - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala deleted file mode 100644 index 2bfe4bc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.list.{ - StackedList, - ListRow, - RowTag, - PropList, - IconText, - RowNext -} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.LinkSupport.* - -object SeznamParametru: - type ViewModel = List[DetailParametru.ViewModel] - - private val parametrList = new StackedList[DetailParametru.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - parametrList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = emptyNode, - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.a - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala deleted file mode 100644 index f048e9d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ /dev/null @@ -1,101 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.form.* -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement -import fiftyforms.services.files.components.tailwind.FilePicker -import cz.e_bs.cmi.mdr.pdb.frontend.AutorizujDukaz -import cz.e_bs.cmi.mdr.pdb.frontend.DocumentRef -import fiftyforms.services.files.File - -object UpravDukazForm: - sealed trait Event - case object Cancelled extends Event - case object AvailableFilesRequested extends Event - def apply(availableFilesStream: EventStream[List[File]])( - updates: Observer[Event] - ): HtmlElement = - val (filesStream, filesObserver) = - EventStream.withObserver[FilePicker.Event] - val files = Var[List[File]](Nil) - def submitButtons: HtmlElement = - div( - cls := "pt-5", - div( - cls := "flex justify-end", - button( - tpe := "button", - cls := "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zrušit", - onClick.mapTo(Cancelled) --> updates - ), - button( - tpe := "submit", - cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - disabled <-- files.signal.map(_.isEmpty), - "Autorizovat" - ) - ) - ) - - div( - filesStream.collect { case FilePicker.SelectionUpdated(files) => - files.to(List) - } --> files.writer, - filesStream.collect { case FilePicker.AvailableFilesRequested => - AvailableFilesRequested - } --> updates, - cls := "bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6", - Form( - Form.Body( - Form.Section( - FormHeader( - FormHeader.ViewModel( - "Doložení kritéria", - "Sestavte doklady poskytující důkaz kritéria a potvrďte odesláním formuláře. Případné limitace či jiné relevantní údaje vepište do pole Komentář." - ) - ), - FormFields( - FormRow( - "dokumenty", - "Dokumenty", - FilePicker(files.signal, availableFilesStream)(filesObserver) - .amend(idAttr := "dokumenty", cls("max-w-lg")) - ).toHtml, - FormRow( - "platnost", - "Platnost", - input( - idAttr := "platnost", - name := "platnost", - tpe := "date", - autoComplete := "date", - cls := "block max-w-lg w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" - ) - ).toHtml, - FormRow( - "komentar", - "Komentář", - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - textArea( - idAttr := "komentar", - name := "about", - rows := 3, - cls := "max-w-lg shadow-sm block w-full focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border border-gray-300 rounded-md" - ), - p( - cls := "mt-2 text-sm text-gray-500", - "Doplňte prosím potřebné informace související s doložením kritéria, včetně případných limitací." - ) - ) - ).toHtml - ) - ) - ), - submitButtons - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala deleted file mode 100644 index 41033f6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -import components._ - -object DirectoryPage: - - type ViewModel = List[UserRow.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - val (actionsStream, actionObserver) = - EventStream.withObserver[SearchForm.Action] - val $filter = actionsStream - .collect { - case SearchForm.ClearFilter => None - case SearchForm.SetFilter(t) => Some(t) - } - .startWith(None) - val byLetter = for { - d <- $m - f <- $filter - } yield for { - (letter, users) <- d - .filter { user => - f match - case None => true - case Some(t) => user.search.contains(t) - } - .groupBy(_.prijmeni.head) - .to(List) - .sortBy(_._1) - } yield (letter.toString, users.sortBy(_.prijmeni)) - - div( - cls := "h-full max-w-7xl mx-auto order-first flex flex-col flex-shrink-0", - SearchForm(actionObserver), - Directory(byLetter) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala deleted file mode 100644 index 73f915d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -object Directory: - - object Header: - type ViewModel = String - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500", - h3(child.text <-- $m) - ) - - object UserList: - type ViewModel = List[UserRow.ViewModel] - def apply($m: Signal[ViewModel]): HtmlElement = - ul( - role := "list", - cls := "relative z-0 divide-y divide-gray-200", - children <-- $m.split(_.osobniCislo)((_, _, s) => UserRow(s)) - ) - - type ViewModel = List[(String, List[UserRow.ViewModel])] - def apply($m: Signal[ViewModel]): HtmlElement = - nav( - cls := "flex-1 min-h-0 overflow-y-auto", - aria.label := "Directory", - children <-- $m.split(_._1)((_, _, s) => - div( - cls := "relative", - Header(s.map(_._1)), - UserList(s.map(_._2)) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala deleted file mode 100644 index e182910..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala +++ /dev/null @@ -1,54 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object SearchForm: - sealed trait Action - case object SubmitFilter extends Action - sealed trait FilterAction extends Action - case object ClearFilter extends FilterAction - case class SetFilter(value: String) extends FilterAction - - def apply(actions: Observer[Action]): HtmlElement = - div( - cls := "px-6 pt-4 pb-4", - form( - cls := "flex space-x-4", - onSubmit.mapTo(SubmitFilter) --> actions, - div( - cls := "flex-1 min-w-0", - label( - forId := "search", - cls := "sr-only", - "Hledat" - ), - div( - cls := "relative rounded-md shadow-sm", - div( - cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", - Icons.solid.search().amend(svg.cls := "text-gray-400") - ), - input( - tpe := "search", - name := "search", - idAttr := "search", - cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", - placeholder := "Hledat", - composeEvents(onInput.mapToValue.setAsValue.map(SetFilter(_)))( - _.throttle(500) - ) --> actions - ) - ) - ), - button( - tpe := "submit", - cls := "inline-flex justify-center px-3.5 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500", - Icons.solid.filter().amend(svg.cls := "text-gray-400"), - span( - cls := "sr-only", - "Hledat" - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala deleted file mode 100644 index 82f7723..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala +++ /dev/null @@ -1,52 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.airstream.core.Signal -import fiftyforms.ui.components.tailwind.Avatar - -object UserRow: - case class ViewModel( - osobniCislo: String, - celeJmeno: String, - prijmeni: String, - hlavniFunkce: Option[String], - img: Option[String], - container: () => HtmlElement = () => div() - ) { - val search = prijmeni.toLowerCase - } - - def apply($m: Signal[ViewModel]): HtmlElement = - inline def avatarImage = - Avatar($m.map(_.img)).avatarImage(10) - - li( - div( - cls := "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500", - div( - cls := "flex-shrink-0", - child <-- avatarImage - ), - div( - cls := "flex-1 min-w-0", - child <-- $m.map { o => - o.container() - .amend( - cls := "focus:outline-none", - span( - cls := "absolute inset-0", - aria.hidden := true - ), - p( - cls := "text-sm font-medium text-gray-900", - o.celeJmeno - ), - p( - cls := "text-sm text-gray-500 truncate", - o.hlavniFunkce - ) - ) - } - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala deleted file mode 100644 index 467c5f5..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala +++ /dev/null @@ -1,99 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router - -object ErrorPage: - case class ViewModel( - homePage: Page, - errorName: String, - title: String, - subTitle: String - ) - - def apply(m: ViewModel, actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - val ViewModel(homePage, errorName, title, subTitle) = m - div( - cls := "min-h-full pt-16 pb-12 flex flex-col bg-white", - main( - cls := "flex-grow flex flex-col justify-center max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex-shrink-0 flex justify-center", - a( - href := "/", - cls := "inline-flex", - span( - cls := "sr-only", - """Workflow""" - ), - img( - cls := "h-12 w-auto", - src := "https://tailwindui.com/img/logos/workflow-mark.svg?color=indigo&shade=600", - alt := "" - ) - ) - ), - div( - cls := "py-16", - div( - cls := "text-center", - p( - cls := "text-sm font-semibold text-indigo-600 uppercase tracking-wide", - errorName - ), - h1( - cls := "mt-2 text-4xl font-extrabold text-gray-900 tracking-tight sm:text-5xl", - title - ), - p( - cls := "mt-2 text-base text-gray-500", - subTitle - ), - div( - cls := "mt-6", - PageLink - .container(homePage, actionBus) - .amend( - cls := "text-base font-medium text-indigo-600 hover:text-indigo-500", - """Go back home""" - ) - ) - ) - ) - ), - footer( - cls := "flex-shrink-0 max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", - nav( - cls := "flex justify-center space-x-4", - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Contact Support""" - ), - span( - cls := "inline-block border-l border-gray-300", - aria.hidden := true - ), - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Status""" - ), - span( - cls := "inline-block border-l border-gray-300", - aria.hidden := true - ), - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Twitter""" - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala deleted file mode 100644 index bd9baec..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action - -object NotFoundPage: - def apply(homePage: Page, url: String, actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - ErrorPage( - ErrorPage.ViewModel( - homePage, - "404 error", - "Page not found.", - s"Sorry, but page $url doesn't exist." - ), - actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala deleted file mode 100644 index 476fe3c..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action - -object UnhandledErrorPage: - - case class ViewModel( - homePage: Page, - errorName: Option[String], - errorMessage: Option[String] - ) - - def apply(m: ViewModel, actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - ErrorPage( - ErrorPage.ViewModel( - m.homePage, - "Unexpected error occurred", - m.errorName.getOrElse( - "Uh oh!" - ), // TODO: translations, better text than uh oh - m.errorMessage.getOrElse( - "This wasn't supposed to happen! Please try again." - ) - ), - actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala deleted file mode 100644 index 25f059f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala +++ /dev/null @@ -1,6 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.services - -import com.raquo.airstream.core.Observer - -trait DataFetcher[K, A]: - def fetch(id: K, sink: Observer[A]): Unit diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala deleted file mode 100644 index a469830..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala +++ /dev/null @@ -1,120 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package state - -import com.raquo.airstream.core.EventStream -import cz.e_bs.cmi.mdr.pdb.{UserInfo, OsobniCislo} -import com.raquo.airstream.core.Observer -import scala.scalajs.js -import scala.scalajs.js.JSON -import zio.json._ -import com.raquo.airstream.eventbus.EventBus -import com.raquo.airstream.ownership.Owner -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.UserFunction -import cz.e_bs.cmi.mdr.pdb.UserContract -import fiftyforms.services.files.File - -trait AppState - extends connectors.DetailPageConnector.AppState - with connectors.DetailParametruPageConnector.AppState - with connectors.DetailKriteriaPageConnector.AppState - with pages.detail.UpravDukaz.State: - def users: EventStream[List[UserInfo]] - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - -class MockAppState(implicit owner: Owner, router: Router[Page]) - extends AppState: - - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonDecoder[UserFunction] = DeriveJsonDecoder.gen - given JsonDecoder[UserContract] = DeriveJsonDecoder.gen - given JsonDecoder[UserInfo] = DeriveJsonDecoder.gen - - given JsonDecoder[ParameterCriteria] = DeriveJsonDecoder.gen - given JsonDecoder[Parameter] = DeriveJsonDecoder.gen - - private val actions = EventBus[Action]() - private val (parametersStream, pushParameters) = - EventStream.withCallback[List[Parameter]] - private val (usersStream, pushUsers) = - EventStream.withCallback[List[UserInfo]] - private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] - private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] - - private val mockData: List[UserInfo] = - mockUsers - .asInstanceOf[js.Dictionary[js.Object]] - .values - // TODO: is there a more efficient way to parse from JS object directly? - .map { o => - val parsed = JSON.stringify(o).fromJson[UserInfo] - parsed.left.foreach(org.scalajs.dom.console.log(_)) - parsed - } - .collect { case Right(u) => - u - } - .toList - - private val mockParameters: List[Parameter] = - pdbParams - .asInstanceOf[js.Dictionary[js.Object]] - .values - .map(o => JSON.stringify(o).fromJson[Parameter]) - .collect { case Right(p) => p } - .toList - - // TODO: Extract to separate event handler - actions.events.foreach { - case FetchDirectory => pushUsers(mockData) - case FetchUserDetails(osc) => - mockData.find(_.personalNumber == osc).foreach { o => - pushDetails(o) - router.replaceState(Page.Detail(o)) - } - case FetchParameters(osc) => - pushParameters(mockParameters) - case FetchParameter(osc, paramId) => - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(Page.DetailParametru(o, p)) - case FetchParameterCriteria(osc, paramId, critId, page) => - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - c <- p.criteria.find(_.id == critId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(page(o, p, c)) - case NavigateTo(page) => router.pushState(page) - case FetchAvailableFiles(osc) => - pushFiles( - List( - File("https://tc163.cmi.cz/here", "Example file") - ) - ) - } - - override def users: EventStream[List[UserInfo]] = - usersStream.debugWithName("users") - - override def details: EventStream[UserInfo] = - detailsStream.debugWithName("details") - - override def parameters: EventStream[List[Parameter]] = - parametersStream.debugWithName("parameters") - - override def availableFiles: EventStream[List[File]] = - filesStream.debugWithName("available files") - - override def actionBus: Observer[Action] = - actions.writer.debugWithName("actions writer") diff --git a/app/src/main/scala/mdr/pdb/app/Main.scala b/app/src/main/scala/mdr/pdb/app/Main.scala new file mode 100644 index 0000000..081b4ee --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/Main.scala @@ -0,0 +1,103 @@ +package mdr.pdb.app + +import scala.scalajs.js.annotation.JSExportTopLevel +import scala.scalajs.js.annotation.JSExport +import scala.scalajs.js.annotation.JSImport +import scala.scalajs.js +import org.scalajs.dom +import com.raquo.laminar.api.L.{*, given} +import scala.scalajs.js.Date +import com.raquo.waypoint.Router +import com.raquo.waypoint.SplitRender +import mdr.pdb.app.services.DataFetcher +import scala.scalajs.js.JSON +import zio.json._ +import mdr.pdb.UserInfo +import mdr.pdb.app.state.AppState + +@js.native +@JSImport("stylesheets/main.css", JSImport.Namespace) +object Css extends js.Any + +@js.native +@JSImport("data/users.json", JSImport.Default) +object mockUsers extends js.Object + +@js.native +@JSImport("params/pdb-params.json", JSImport.Default) +object pdbParams extends js.Object + +@JSExportTopLevel("app") +object Main: + + @JSExport + def main(args: Array[String]): Unit = + import Routes.given + onLoad { + setupAirstream() + val appContainer = dom.document.querySelector("#app") + val _ = + render( + appContainer, + renderPage(state.MockAppState(using unsafeWindowOwner, router)) + ) + } + + private def onLoad(f: => Unit): Unit = + documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) + + private def setupAirstream()(using router: Router[Page]): Unit = + AirstreamError.registerUnhandledErrorCallback(err => + router.forcePage( + Page.UnhandledError( + Some(err.getClass.getName), // TODO: Fill only in dev mode + Some(err.getMessage) + ) + ) + ) + + def renderPage(state: AppState)(using + router: Router[Page] + ): HtmlElement = + val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) + .collectSignal[Page.Detail]( + connectors + .DetailPageConnector(state)(_) + .apply + ) + .collectSignal[Page.DetailParametru]( + connectors + .DetailParametruPageConnector(state)(_) + .apply + ) + .collectSignal[Page.DetailKriteria]( + connectors + .DetailKriteriaPageConnector(state)(_) + .apply + ) + .collectSignal[Page.UpravDukazKriteria]( + pages.detail.UpravDukaz.Connector(state)(_).apply + ) + .collectStatic(Page.Dashboard)( + connectors.DashboardPageConnector(state.actionBus).apply + ) + .collect[Page.NotFound](pg => + pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) + ) + .collect[Page.UnhandledError](pg => + pages.errors + .UnhandledErrorPage( + pages.errors.UnhandledErrorPage + .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), + state.actionBus + ) + ) + .collectStatic(Page.Directory)( + connectors + .DirectoryPageConnector(state.users, state.actionBus) + .apply + ) + div(cls := "h-full", child <-- pageSplitter.$view) + + // Pull in the stylesheet + val css: Css.type = Css diff --git a/app/src/main/scala/mdr/pdb/app/Routes.scala b/app/src/main/scala/mdr/pdb/app/Routes.scala new file mode 100644 index 0000000..2cc860c --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/Routes.scala @@ -0,0 +1,198 @@ +package mdr.pdb.app + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.* +import org.scalajs.dom +import zio.json.{*, given} +import mdr.pdb.OsobniCislo + +import scala.scalajs.js +import mdr.pdb.UserInfo +import mdr.pdb.Parameter +import mdr.pdb.ParameterCriteria +import mdr.pdb.app.Page.Titled + +// enum is not working with Waypoints' SplitRender collectStatic +sealed abstract class Page( + val id: String, + val title: String, + val parent: Option[Page] +) { + val path: Vector[Page] = + parent match + case None => Vector(this) + case Some(p) => p.path :+ this + + val isRoot: Boolean = parent.isEmpty +} + +object Page: + + case class Titled[V](value: V, title: Option[String] = None): + val show: String = title.getOrElse(value.toString) + + case object Directory extends Page("directory", "Adresář", None) + + case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) + + case class Detail(osobniCislo: Titled[OsobniCislo]) + extends Page("user", osobniCislo.show, Some(Directory)) + + object Detail { + def apply(o: UserInfo): Detail = Detail( + Titled(o.personalNumber, Some(o.name)) + ) + } + + case class DetailParametru( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String] + ) extends Page( + "parameter", + parametr.show, + Some(Detail(osobniCislo)) + ) + + object DetailParametru { + def apply(o: UserInfo, p: Parameter): DetailParametru = + DetailParametru( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)) + ) + } + + case class DetailKriteria( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String], + kriterium: Titled[String] + ) extends Page( + "criteria", + kriterium.show, + Some(DetailParametru(osobniCislo, parametr)) + ) + + object DetailKriteria { + def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = + DetailKriteria( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)), + Titled(k.id, Some(k.id)) + ) + } + + case class UpravDukazKriteria( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String], + kriterium: Titled[String] + ) extends Page( + "addProof", + "Důkaz", + Some(DetailKriteria(osobniCislo, parametr, kriterium)) + ) + + object UpravDukazKriteria { + def apply( + o: UserInfo, + p: Parameter, + k: ParameterCriteria + ): UpravDukazKriteria = + UpravDukazKriteria( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)), + Titled(k.id, Some(k.id)) + ) + } + + case class NotFound(url: String) extends Page("404", "404", Some(Directory)) + + case class UnhandledError( + errorName: Option[String], + errorMessage: Option[String] + ) extends Page("500", "Unexpected error", Some(Directory)) + +object Routes: + given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) + given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) + given [V: JsonEncoder]: JsonEncoder[Titled[V]] = + DeriveJsonEncoder.gen[Titled[V]] + given [V: JsonDecoder]: JsonDecoder[Titled[V]] = + DeriveJsonDecoder.gen[Titled[V]] + given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] + given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] + + val base = + js.`import`.meta.env.BASE_URL + .asInstanceOf[String] + "app" + + val homePage: Page = Page.Directory + + given router: Router[Page] = Router[Page]( + routes = List( + Route.static(homePage, root / endOfSegments, basePath = base), + Route.static( + Page.Dashboard, + root / "dashboard" / endOfSegments, + basePath = base + ), + Route[Page.Detail, String]( + encode = _.osobniCislo.value.toString, + decode = osc => Page.Detail(Titled(OsobniCislo(osc))), + root / "osoba" / segment[String] / endOfSegments, + basePath = base + ), + Route[Page.DetailParametru, (String, String)]( + encode = p => (p.osobniCislo.value.toString, p.parametr.value), + decode = + p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / endOfSegments, + basePath = base + ), + Route[Page.DetailKriteria, (String, String, String)]( + encode = p => + ( + p.osobniCislo.value.toString, + p.parametr.value, + p.kriterium.value.replaceAll("\\.", "--") + ), + decode = p => + Page.DetailKriteria( + Titled(OsobniCislo(p._1)), + Titled(p._2), + Titled(p._3.replaceAll("--", ".")) + ), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / "kriterium" / segment[String] / endOfSegments, + basePath = base + ), + Route[Page.UpravDukazKriteria, (String, String, String)]( + encode = p => + ( + p.osobniCislo.value.toString, + p.parametr.value, + p.kriterium.value.replaceAll("\\.", "--") + ), + decode = p => + Page.UpravDukazKriteria( + Titled(OsobniCislo(p._1)), + Titled(p._2), + Titled(p._3.replaceAll("--", ".")) + ), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / "kriterium" / segment[String] / "edit" / endOfSegments, + basePath = base + ) + ), + serializePage = _.toJson, + deserializePage = _.fromJson[Page] + .fold(s => throw IllegalStateException(s), identity), + getPageTitle = _.title, + routeFallback = url => Page.NotFound(url), + deserializeFallback = _ => Page.Dashboard + )( + $popStateEvent = windowEvents.onPopState, + owner = unsafeWindowOwner + ) diff --git a/app/src/main/scala/mdr/pdb/app/actions.scala b/app/src/main/scala/mdr/pdb/app/actions.scala new file mode 100644 index 0000000..d078b3a --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/actions.scala @@ -0,0 +1,21 @@ +package mdr.pdb.app + +import mdr.pdb.OsobniCislo +import mdr.pdb.UserInfo +import mdr.pdb.Parameter +import mdr.pdb.ParameterCriteria + +sealed trait Action + +case object FetchDirectory extends Action +case class FetchUserDetails(osc: OsobniCislo) extends Action +case class FetchParameters(osc: OsobniCislo) extends Action +case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action +case class FetchParameterCriteria( + osc: OsobniCislo, + paramId: String, + critId: String, + page: (UserInfo, Parameter, ParameterCriteria) => Page +) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action +case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/mdr/pdb/app/components/AppPage.scala new file mode 100644 index 0000000..130c817 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/AppPage.scala @@ -0,0 +1,82 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.Page +import mdr.pdb.{UserProfile, UserInfo, OsobniCislo} +import com.raquo.waypoint.Router +import mdr.pdb.app.Action +import mdr.pdb.app.NavigateTo +import mdr.pdb.UserFunction + +object AppPage: + // TODO: pages by logged in user + val pages: List[Page] = List(Page.Directory, Page.Dashboard) + + import NavigationBar.{Logo, MenuItem} + + val logo = Logo( + "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", + "Workflow" + ) + + // TODO: menu items by user profile + val userMenu = + List( + MenuItem("Your Profile"), + MenuItem("Settings"), + MenuItem("Sign out") + ) + + // TODO: load user profile + val $userProfile = Var( + UserProfile( + "tom", + UserInfo( + OsobniCislo("1031"), + "tom", + "Tom", + "Cook", + None, + None, + Some("tom@example.com"), + Some("+420 222 866 180"), + Some( + UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") + ), + Nil, + None + ) + ) + ) + + val $userInfo = $userProfile.signal.map(_.userInfo) + + type ViewModel = Option[HtmlElement] + def apply( + actionBus: Observer[Action] + )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using + router: Router[Page] + ): HtmlElement = + PageLayout(actionBus)( + $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => + PageLayout.ViewModel( + NavigationBar.ViewModel( + u, + pages.map(p => + NavigationBar.Link( + () => + PageLink( + p, + actionBus + ), + p == cp + ) + ), + userMenu, + logo + ), + c + ) + ), + mods + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala new file mode 100644 index 0000000..9375398 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala @@ -0,0 +1,143 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.tailwind.list.IconText.ViewModel +import fiftyforms.ui.components.tailwind.Icons +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.Page + +import com.raquo.laminar.api.L.{*, given} +import io.laminext.syntax.core.{*, given} +import mdr.pdb.app.Action + +object Breadcrumbs: + + private def slash = + import svg.{*, given} + svg( + cls := "flex-shrink-0 h-5 w-5 text-gray-300", + xmlns := "http://www.w3.org/2000/svg", + fill := "currentColor", + viewBox := "0 0 20 20", + ariaHidden := true, + path( + d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" + ) + ) + + def backIcon = + Icons.solid + .`arrow-narrow-left`() + .amend( + svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" + ) + + object Link: + case class ViewModel( + page: Page, + icon: Option[SvgElement], + extraClasses: String, + text: String, + textClasses: Option[String] = None + ) + + val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" + + def shortHome(p: Page) = ViewModel( + p, + Some(backIcon), + "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", + "Zpět na úvodní stránku", + None + ) + + def fullHome(p: Page) = ViewModel( + p, + Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), + "text-gray-400 hover:text-gray-500", + "Domů", + Some("sr-only") + ) + + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + PageLink + .container($m.map(_.page), actionBus) + .amend( + cls <-- $m.map(_.extraClasses), + child.maybe <-- $m.map(_.icon), + span( + cls <-- $m.map(_.textClasses.getOrElse("")), + child.text <-- $m.map(_.text) + ) + ) + + object FullBreadcrumbs: + type ViewModel = Page + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + ol( + role := "list", + cls := "flex items-center space-x-4", + children <-- $m.map(_.path).split(_.id)((_, _, $p) => + li( + div( + cls := "flex items-center", + child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), + Link( + $p.map(p => + if (p.isRoot) then Link.fullHome(p) + else + Link.ViewModel( + p, + None, + s"ml-4 max-w-xs truncate ${Link.baseClasses}", + p.title + ) + ), + actionBus + ) + ) + ) + ) + ) + + object ShortBreadcrumbs: + type ViewModel = Page + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + Link( + $m.map { p => + val target = p.parent.getOrElse(p) + if target.isRoot then Link.shortHome(target) + else + Link.ViewModel( + target, + Some(backIcon), + "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", + s"Zpět na ${target.title}" + ) + }, + actionBus + ) + + def apply(actionBus: Observer[Action])(using + router: Router[Page] + ): HtmlElement = + nav( + cls := "flex", + aria.label := "Breadcrumb", + div( + cls := "flex sm:hidden", + ShortBreadcrumbs(router.$currentPage, actionBus) + ), + div( + cls := "hidden sm:block", + FullBreadcrumbs(router.$currentPage, actionBus) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala new file mode 100644 index 0000000..4bb8b8d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala @@ -0,0 +1,242 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent +import fiftyforms.ui.components.tailwind.Icons +import fiftyforms.ui.components.tailwind.Avatar +import mdr.pdb.UserInfo +import io.laminext.syntax.core.* + +object NavigationBar: + + case class Logo(img: String, name: String) + case class Link(a: () => Anchor, active: Boolean) + case class MenuItem(title: String) + + case class ViewModel( + userInfo: UserInfo, + pages: List[Link], + userMenu: List[MenuItem], + logo: Logo + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + val $userInfo = $m.map(_.userInfo) + val mobileMenuOpen = Var(false) + + val desktopOnly = cls("hidden md:block") + val mobileOnly = cls("md:hidden") + + inline def avatarImage(size: Int = 8) = + Avatar($userInfo.map(_.img)).avatarImage(size) + + def notificationButton = button( + tpe := "button", + cls := List( + "bg-indigo-600", + "focus:outline-none", + "focus:ring-2", + "focus:ring-offset-2", + "focus:ring-offset-indigo-600", + "focus:ring-white", + "hover:text-white", + "p-1", + "rounded-full", + "text-indigo-200" + ), + span(cls := "sr-only", "View notifications"), + Icons.outline.bell() + ) + + def userProfile: HtmlElement = + val menuOpen = Var(false) + + def menuItem(item: MenuItem, idx: Int): HtmlElement = + a( + href := "#", + cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", + role := "menuitem", + tabIndex := -1, + idAttr := s"user-menu-item-$idx", + item.title + ) + + div( + cls := "ml-3 relative", + div( + button( + tpe := "button", + cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", + idAttr := "user-menu-button", + aria.expanded <-- menuOpen.signal, + aria.hasPopup := true, + span(cls := "sr-only", "Open user menu"), + child <-- avatarImage(), + onClick.preventDefault.mapTo( + !menuOpen.now() + ) --> menuOpen.writer + ) + ), + /* + * */ + div( + cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", + cls <-- menuOpen.signal.map { o => + if (o) "md:block" else "md:hidden" + }, + role := "menu", + aria.orientation := "vertical", + aria.labelledBy := "user-menu-button", + tabIndex := -1, + // : keyboard navigation + children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) + ) + ) + + def mobileProfile = + def menuItem(item: MenuItem): HtmlElement = + a( + href := "#", + cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", + item.title + ) + + div( + cls := "pt-4 pb-3 border-t border-indigo-700", + div( + cls := "flex items-center px-5", + div( + cls := "flex-shrink-0", + child <-- avatarImage(10) + ), + div( + cls := "ml-3", + div( + cls := "text-base font-medium text-white", + child.text <-- $userInfo.map(_.name) + ), + child.maybe <-- $userInfo.map( + _.email.map(e => + div( + cls := "text-sm font-medium text-indigo-300", + e + ) + ) + ) + ), + notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) + ), + div( + cls := "mt-3 px-2 space-y-1", + children <-- $m.map(_.userMenu.map(menuItem)) + ) + ) + + def pageLink(page: Link): Anchor = + page + .a() + .amend( + cls := "text-white px-3 py-2 rounded-md text-sm font-medium", + cls := Seq( + "bg-indigo-700" -> page.active, + "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active + ), + ariaCurrent := (if page.active then "page" else "false") + ) + + def logoImg: Image = + img( + cls := "h-8 w-8", + src <-- $m.map(_.logo.img), + alt <-- $m.map(_.logo.name) + ) + + def mobileMenuButton = button( + tpe := "button", + cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", + aria.controls := "mobile-menu", + aria.expanded <-- mobileMenuOpen.signal, + span(cls := "sr-only", "Open main menu"), + Icons.outline + .menu() + .amend( + svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") + ), + Icons.outline + .x() + .amend( + svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") + ), + onClick.preventDefault.mapTo( + !mobileMenuOpen.now() + ) --> mobileMenuOpen.writer + ) + + def navBarLeft = + div( + cls := "flex items-center", + div(cls := "flex-shrink-0", logoImg), + div( + desktopOnly, + div( + cls := "ml-10 flex items-baseline space-x-4", + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) + ) + ) + ) + + def navBarRight = + div( + desktopOnly, + div( + cls := "ml-4 flex items-center md:ml-6", + notificationButton, + userProfile + ) + ) + + def navBarMobile = + div( + cls := "-mr-2 flex", + mobileOnly, + mobileMenuButton + ) + + def navBar = + div( + cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", + div( + cls := "flex items-center justify-between h-16", + navBarLeft, + navBarRight, + navBarMobile + ) + ) + + def mobileMenu = + div( + mobileOnly, + cls <-- mobileMenuOpen.signal.map { o => + if (o) "block" else "hidden" + }, + idAttr := "mobile-menu", + div( + cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) + ), + mobileProfile + ) + + nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala new file mode 100644 index 0000000..c4cb10d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala @@ -0,0 +1,19 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.Page +import com.raquo.waypoint.Router +import mdr.pdb.app.Action + +object PageHeader: + def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = + header( + cls := "bg-white shadow-sm", + div( + cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", + h1( + cls := "text-lg leading-6 font-semibold text-gray-900", + Breadcrumbs(actionBus) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala new file mode 100644 index 0000000..aae4539 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala @@ -0,0 +1,30 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.Page +import mdr.pdb.app.Action +import fiftyforms.ui.components.tailwind.Loading +import io.laminext.syntax.core.* + +object PageLayout: + case class ViewModel( + navigation: NavigationBar.ViewModel, + content: Option[HtmlElement] + ) + def apply(actionBus: Observer[Action])( + $m: Signal[ViewModel], + mods: Modifier[HtmlElement]* + )(using router: Router[Page]): HtmlElement = + val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) + div( + cls := "h-full flex flex-col", + NavigationBar($m.map(_.navigation)), + child.maybe <-- router.$currentPage.map(_.isRoot) + .switch(None, Some(PageHeader(actionBus))), + main( + cls := "flex-grow-1 overflow-y-auto", + mods, + child <-- $maybeContent.map(_.getOrElse(Loading)) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/mdr/pdb/app/components/PageLink.scala new file mode 100644 index 0000000..e6e7052 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageLink.scala @@ -0,0 +1,40 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.LinkSupport.* +import mdr.pdb.app.Page +import mdr.pdb.app.Action +import com.raquo.waypoint.Router +import mdr.pdb.app.NavigateTo + +object PageLink: + type ViewModel = Page + def apply($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = + a(mods($m, actions), child.text <-- $m.map(_.title)) + + def apply(m: ViewModel, actions: Observer[Action])(using + router: Router[Page] + ): Anchor = apply(Val(m), actions) + + def container($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = a(mods($m, actions)) + + def container(m: ViewModel, actions: Observer[Action])(using + Router[Page] + ): Anchor = container(Val(m), actions) + + private def mods($m: Signal[Page], actions: Observer[Action])(using + router: Router[Page] + ): Modifier[Anchor] = + Seq( + href <-- $m.map(router.absoluteUrlForPage).recover { case _ => + Some("invalid url") + }, + composeEvents(onClick.noKeyMod.preventDefault)( + _.sample($m) + .map(NavigateTo.apply) + ) --> actions + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala new file mode 100644 index 0000000..7682772 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala @@ -0,0 +1,13 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.pages.dashboard.DashboardPage +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage + +class DashboardPageConnector(actionBus: Observer[Action])(using + router: Router[Page] +): + def apply: HtmlElement = + AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala new file mode 100644 index 0000000..76b9133 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala @@ -0,0 +1,76 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.Parameter +import mdr.pdb.UserInfo +import mdr.pdb.app.pages.detail.DetailParametruPage +import pages.detail.DetailKriteriaPage +import pages.detail.components.DukazyKriteria +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.ParameterCriteria + +object DetailKriteriaPageConnector { + trait AppState { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + } +} + +case class DetailKriteriaPageConnector( + state: DetailKriteriaPageConnector.AppState +)( + $page: Signal[Page.DetailKriteria] +)(using Router[Page]): + val $paramChangeSignal = + $page.splitOne(p => + (p.osobniCislo.value, p.parametr.value, p.kriterium.value) + )((x, _, _) => x) + val $pageChangeSignal = + $paramChangeSignal.map( + FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) + ) + + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + val $merged = + $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => + for { + da <- d + pa <- p + pb <- pa.find(_.id == pc._2) + ka <- pb.criteria.find(_.id == pc._3) + } yield (da, pb, ka) + ) + + def apply: HtmlElement = + AppPage(state.actionBus)( + $merged.map(_.map(buildModel)) + .split(_ => ())((_, s, $s) => + DetailKriteriaPage($s)(state.actionBus.contramap { + case DukazyKriteria.Add => + NavigateTo( + Page.UpravDukazKriteria( + Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), + Page.Titled(s.parametr.id, Some(s.parametr.nazev)), + Page.Titled(s.kriterium.id, Some(s.kriterium.id)) + ) + ) + }) + ), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: Parameter, + k: ParameterCriteria + ): DetailKriteriaPage.ViewModel = + DetailKriteriaPage.ViewModel( + o.toDetailOsoby, + p.toParametr(_ => a()), + k.toKriterium(_ => a()) + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DetailPageConnector.scala new file mode 100644 index 0000000..df3a6b7 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DetailPageConnector.scala @@ -0,0 +1,57 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.UserInfo +import pages.detail.DetailPage +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.pages.detail.components.DetailOsoby +import mdr.pdb.Parameter +import mdr.pdb.app.pages.detail.components.SeznamParametru +import fiftyforms.ui.components.tailwind.Color + +object DetailPageConnector { + trait AppState { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + } +} + +case class DetailPageConnector(state: DetailPageConnector.AppState)( + $page: Signal[Page.Detail] +)(using router: Router[Page]): + val $oscChangeSignal = $page.splitOne(_.osobniCislo.value)((osc, _, _) => osc) + val $pageChangeSignal = + $oscChangeSignal.flatMap(osc => + EventStream.fromSeq(Seq(FetchUserDetails(osc), FetchParameters(osc))) + ) + // TODO: filter the value based on the current osc + // OSC change will fetch new data, but still + // - we need to be sure that what we got is really what we ought to display + // - we want to display stale data accordingly (at least with loading indicator) + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + def apply: HtmlElement = + AppPage(state.actionBus)( + $data.combineWithFn($params)(_ zip _) + .map(_.map(buildModel)) + .split(_ => ())((_, _, s) => DetailPage(s)), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: List[Parameter] + ): DetailPage.ViewModel = + DetailPage.ViewModel( + o.toDetailOsoby, + p.map( + _.toParametr(param => + PageLink.container(Page.DetailParametru(o, param), state.actionBus) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DetailParametruPageConnector.scala new file mode 100644 index 0000000..fa7193d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DetailParametruPageConnector.scala @@ -0,0 +1,67 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.Parameter +import mdr.pdb.UserInfo +import mdr.pdb.app.pages.detail.DetailParametruPage +import pages.detail.DetailParametruPage +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.components.PageLink + +object DetailParametruPageConnector { + trait AppState { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + } +} + +case class DetailParametruPageConnector( + state: DetailParametruPageConnector.AppState +)( + $page: Signal[Page.DetailParametru] +)(using router: Router[Page]): + val $paramChangeSignal = + $page.splitOne(p => (p.osobniCislo.value, p.parametr.value))((x, _, _) => x) + val $pageChangeSignal = + $paramChangeSignal.map(FetchParameter(_, _)) + + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + val $merged = + $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => + for { + da <- d + pa <- p + pb <- pa.find(_.id == pc._2) + } yield (da, pb) + ) + + def apply: HtmlElement = + AppPage(state.actionBus)( + $merged.map(_.map(buildModel)) + .split(_ => ())((_, _, s) => DetailParametruPage(s)), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: Parameter + ): DetailParametruPage.ViewModel = + DetailParametruPage.ViewModel( + o.toDetailOsoby, + p.toParametr(p => + PageLink.container(Page.DetailParametru(o, p), state.actionBus) + ), + p.criteria.map( + _.toKriterium { c => + PageLink.container( + Page.DetailKriteria(o, p, c), + state.actionBus + ) + } + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DirectoryPageConnector.scala new file mode 100644 index 0000000..ab2c045 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DirectoryPageConnector.scala @@ -0,0 +1,34 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.UserInfo +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.components.AppPage + +case class DirectoryPageConnector( + $input: EventStream[List[UserInfo]], + actionBus: Observer[Action] +)(using router: Router[Page]): + val $data = $input.startWithNone + val $actionSignal = EventStream.fromValue(FetchDirectory) + + def apply: HtmlElement = + AppPage(actionBus)( + $data.split(_ => ())((_, _, s) => + pages.directory.DirectoryPage( + s.map( + _.map( + _.toUserRow(u => + PageLink.container( + Page.Detail(Page.Titled(u.personalNumber)), + actionBus + ) + ) + ) + ) + ) + ), + $actionSignal --> actionBus + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/mdr/pdb/app/connectors/conversions.scala new file mode 100644 index 0000000..f581796 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/conversions.scala @@ -0,0 +1,68 @@ +package mdr.pdb.app.connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.UserInfo +import mdr.pdb.app.pages.detail.components.DetailOsoby +import mdr.pdb.Parameter +import mdr.pdb.app.pages.detail.components.SeznamParametru +import mdr.pdb.ParameterCriteria +import mdr.pdb.app.pages.detail.components.SeznamKriterii +import mdr.pdb.app.pages.directory.components.UserRow +import mdr.pdb.app.pages.detail.components.DetailParametru +import mdr.pdb.app.pages.detail.components.DetailKriteria +import fiftyforms.ui.components.tailwind.Color + +extension (o: UserInfo) + def toDetailOsoby: DetailOsoby.ViewModel = + DetailOsoby.ViewModel( + o.personalNumber, + o.name, + o.email, + o.phone, + o.img, + o.mainFunction.map(f => + DetailOsoby.Funkce.ViewModel(f.name, f.dept, f.ou) + ), + o.userContracts.headOption.map(c => + DetailOsoby.PracovniPomer.ViewModel(c.rel, c.startDate, c.endDate) + ) + ) + +extension (param: Parameter) + def toParametr(container: Parameter => Anchor): DetailParametru.ViewModel = + DetailParametru.ViewModel( + id = param.id, + nazev = param.name, + popis = param.description, + status = "Nesplněno", + statusColor = Color.red, + a = container(param) + ) + +extension (crit: ParameterCriteria) + def toKriterium( + container: ParameterCriteria => Anchor + ): DetailKriteria.ViewModel = + DetailKriteria.ViewModel( + nazev = crit.criteriumText, + kapitola = crit.chapterId, + bod = crit.itemId, + status = "Nesplněno", + statusColor = Color.red, + splneno = false, + dukazy = Nil, + container = container(crit) + ) + +extension (user: UserInfo) + def toUserRow( + container: UserInfo => HtmlElement = (_: UserInfo) => div() + ): UserRow.ViewModel = + UserRow.ViewModel( + osobniCislo = user.personalNumber.toString, + celeJmeno = user.name, + prijmeni = user.surname, + hlavniFunkce = user.mainFunction.map(_.name), + img = user.img, + container = () => container(user) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/dashboard/DashboardPage.scala b/app/src/main/scala/mdr/pdb/app/pages/dashboard/DashboardPage.scala new file mode 100644 index 0000000..0eb8f7a --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/dashboard/DashboardPage.scala @@ -0,0 +1,11 @@ +package mdr.pdb.app.pages.dashboard + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.Page + +object DashboardPage: + + def render: HtmlElement = + div("Dashboard page") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala deleted file mode 100644 index a3b00af..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala +++ /dev/null @@ -1,103 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import scala.scalajs.js.annotation.JSExportTopLevel -import scala.scalajs.js.annotation.JSExport -import scala.scalajs.js.annotation.JSImport -import scala.scalajs.js -import org.scalajs.dom -import com.raquo.laminar.api.L.{*, given} -import scala.scalajs.js.Date -import com.raquo.waypoint.Router -import com.raquo.waypoint.SplitRender -import cz.e_bs.cmi.mdr.pdb.app.services.DataFetcher -import scala.scalajs.js.JSON -import zio.json._ -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.state.AppState - -@js.native -@JSImport("stylesheets/main.css", JSImport.Namespace) -object Css extends js.Any - -@js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native -@JSImport("params/pdb-params.json", JSImport.Default) -object pdbParams extends js.Object - -@JSExportTopLevel("app") -object Main: - - @JSExport - def main(args: Array[String]): Unit = - import Routes.given - onLoad { - setupAirstream() - val appContainer = dom.document.querySelector("#app") - val _ = - render( - appContainer, - renderPage(state.MockAppState(using unsafeWindowOwner, router)) - ) - } - - private def onLoad(f: => Unit): Unit = - documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) - - private def setupAirstream()(using router: Router[Page]): Unit = - AirstreamError.registerUnhandledErrorCallback(err => - router.forcePage( - Page.UnhandledError( - Some(err.getClass.getName), // TODO: Fill only in dev mode - Some(err.getMessage) - ) - ) - ) - - def renderPage(state: AppState)(using - router: Router[Page] - ): HtmlElement = - val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) - .collectSignal[Page.Detail]( - connectors - .DetailPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailParametru]( - connectors - .DetailParametruPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailKriteria]( - connectors - .DetailKriteriaPageConnector(state)(_) - .apply - ) - .collectSignal[Page.UpravDukazKriteria]( - pages.detail.UpravDukaz.Connector(state)(_).apply - ) - .collectStatic(Page.Dashboard)( - connectors.DashboardPageConnector(state.actionBus).apply - ) - .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) - ) - .collect[Page.UnhandledError](pg => - pages.errors - .UnhandledErrorPage( - pages.errors.UnhandledErrorPage - .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), - state.actionBus - ) - ) - .collectStatic(Page.Directory)( - connectors - .DirectoryPageConnector(state.users, state.actionBus) - .apply - ) - div(cls := "h-full", child <-- pageSplitter.$view) - - // Pull in the stylesheet - val css: Css.type = Css diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala deleted file mode 100644 index 3bbc8c0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ /dev/null @@ -1,198 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.* -import org.scalajs.dom -import zio.json.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import scala.scalajs.js -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.Page.Titled - -// enum is not working with Waypoints' SplitRender collectStatic -sealed abstract class Page( - val id: String, - val title: String, - val parent: Option[Page] -) { - val path: Vector[Page] = - parent match - case None => Vector(this) - case Some(p) => p.path :+ this - - val isRoot: Boolean = parent.isEmpty -} - -object Page: - - case class Titled[V](value: V, title: Option[String] = None): - val show: String = title.getOrElse(value.toString) - - case object Directory extends Page("directory", "Adresář", None) - - case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) - - case class Detail(osobniCislo: Titled[OsobniCislo]) - extends Page("user", osobniCislo.show, Some(Directory)) - - object Detail { - def apply(o: UserInfo): Detail = Detail( - Titled(o.personalNumber, Some(o.name)) - ) - } - - case class DetailParametru( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String] - ) extends Page( - "parameter", - parametr.show, - Some(Detail(osobniCislo)) - ) - - object DetailParametru { - def apply(o: UserInfo, p: Parameter): DetailParametru = - DetailParametru( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)) - ) - } - - case class DetailKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "criteria", - kriterium.show, - Some(DetailParametru(osobniCislo, parametr)) - ) - - object DetailKriteria { - def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = - DetailKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class UpravDukazKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "addProof", - "Důkaz", - Some(DetailKriteria(osobniCislo, parametr, kriterium)) - ) - - object UpravDukazKriteria { - def apply( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): UpravDukazKriteria = - UpravDukazKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class NotFound(url: String) extends Page("404", "404", Some(Directory)) - - case class UnhandledError( - errorName: Option[String], - errorMessage: Option[String] - ) extends Page("500", "Unexpected error", Some(Directory)) - -object Routes: - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) - given [V: JsonEncoder]: JsonEncoder[Titled[V]] = - DeriveJsonEncoder.gen[Titled[V]] - given [V: JsonDecoder]: JsonDecoder[Titled[V]] = - DeriveJsonDecoder.gen[Titled[V]] - given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] - given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] - - val base = - js.`import`.meta.env.BASE_URL - .asInstanceOf[String] + "app" - - val homePage: Page = Page.Directory - - given router: Router[Page] = Router[Page]( - routes = List( - Route.static(homePage, root / endOfSegments, basePath = base), - Route.static( - Page.Dashboard, - root / "dashboard" / endOfSegments, - basePath = base - ), - Route[Page.Detail, String]( - encode = _.osobniCislo.value.toString, - decode = osc => Page.Detail(Titled(OsobniCislo(osc))), - root / "osoba" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.DetailParametru, (String, String)]( - encode = p => (p.osobniCislo.value.toString, p.parametr.value), - decode = - p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / endOfSegments, - basePath = base - ), - Route[Page.DetailKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.DetailKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.UpravDukazKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.UpravDukazKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / "edit" / endOfSegments, - basePath = base - ) - ), - serializePage = _.toJson, - deserializePage = _.fromJson[Page] - .fold(s => throw IllegalStateException(s), identity), - getPageTitle = _.title, - routeFallback = url => Page.NotFound(url), - deserializeFallback = _ => Page.Dashboard - )( - $popStateEvent = windowEvents.onPopState, - owner = unsafeWindowOwner - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala deleted file mode 100644 index b24b82b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -sealed trait Action - -case object FetchDirectory extends Action -case class FetchUserDetails(osc: OsobniCislo) extends Action -case class FetchParameters(osc: OsobniCislo) extends Action -case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action -case class FetchParameterCriteria( - osc: OsobniCislo, - paramId: String, - critId: String, - page: (UserInfo, Parameter, ParameterCriteria) => Page -) extends Action -case class FetchAvailableFiles(osc: OsobniCislo) extends Action -case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala deleted file mode 100644 index c637d35..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala +++ /dev/null @@ -1,82 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.{UserProfile, UserInfo, OsobniCislo} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo -import cz.e_bs.cmi.mdr.pdb.UserFunction - -object AppPage: - // TODO: pages by logged in user - val pages: List[Page] = List(Page.Directory, Page.Dashboard) - - import NavigationBar.{Logo, MenuItem} - - val logo = Logo( - "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", - "Workflow" - ) - - // TODO: menu items by user profile - val userMenu = - List( - MenuItem("Your Profile"), - MenuItem("Settings"), - MenuItem("Sign out") - ) - - // TODO: load user profile - val $userProfile = Var( - UserProfile( - "tom", - UserInfo( - OsobniCislo("1031"), - "tom", - "Tom", - "Cook", - None, - None, - Some("tom@example.com"), - Some("+420 222 866 180"), - Some( - UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") - ), - Nil, - None - ) - ) - ) - - val $userInfo = $userProfile.signal.map(_.userInfo) - - type ViewModel = Option[HtmlElement] - def apply( - actionBus: Observer[Action] - )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using - router: Router[Page] - ): HtmlElement = - PageLayout(actionBus)( - $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => - PageLayout.ViewModel( - NavigationBar.ViewModel( - u, - pages.map(p => - NavigationBar.Link( - () => - PageLink( - p, - actionBus - ), - p == cp - ) - ), - userMenu, - logo - ), - c - ) - ), - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala deleted file mode 100644 index bf473c6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ /dev/null @@ -1,143 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden -import fiftyforms.ui.components.tailwind.list.IconText.ViewModel -import fiftyforms.ui.components.tailwind.Icons -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Action - -object Breadcrumbs: - - private def slash = - import svg.{*, given} - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-300", - xmlns := "http://www.w3.org/2000/svg", - fill := "currentColor", - viewBox := "0 0 20 20", - ariaHidden := true, - path( - d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" - ) - ) - - def backIcon = - Icons.solid - .`arrow-narrow-left`() - .amend( - svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" - ) - - object Link: - case class ViewModel( - page: Page, - icon: Option[SvgElement], - extraClasses: String, - text: String, - textClasses: Option[String] = None - ) - - val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" - - def shortHome(p: Page) = ViewModel( - p, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - "Zpět na úvodní stránku", - None - ) - - def fullHome(p: Page) = ViewModel( - p, - Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), - "text-gray-400 hover:text-gray-500", - "Domů", - Some("sr-only") - ) - - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - PageLink - .container($m.map(_.page), actionBus) - .amend( - cls <-- $m.map(_.extraClasses), - child.maybe <-- $m.map(_.icon), - span( - cls <-- $m.map(_.textClasses.getOrElse("")), - child.text <-- $m.map(_.text) - ) - ) - - object FullBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - ol( - role := "list", - cls := "flex items-center space-x-4", - children <-- $m.map(_.path).split(_.id)((_, _, $p) => - li( - div( - cls := "flex items-center", - child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), - Link( - $p.map(p => - if (p.isRoot) then Link.fullHome(p) - else - Link.ViewModel( - p, - None, - s"ml-4 max-w-xs truncate ${Link.baseClasses}", - p.title - ) - ), - actionBus - ) - ) - ) - ) - ) - - object ShortBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - Link( - $m.map { p => - val target = p.parent.getOrElse(p) - if target.isRoot then Link.shortHome(target) - else - Link.ViewModel( - target, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - s"Zpět na ${target.title}" - ) - }, - actionBus - ) - - def apply(actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - nav( - cls := "flex", - aria.label := "Breadcrumb", - div( - cls := "flex sm:hidden", - ShortBreadcrumbs(router.$currentPage, actionBus) - ), - div( - cls := "hidden sm:block", - FullBreadcrumbs(router.$currentPage, actionBus) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala deleted file mode 100644 index 0db2047..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ /dev/null @@ -1,242 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent -import fiftyforms.ui.components.tailwind.Icons -import fiftyforms.ui.components.tailwind.Avatar -import cz.e_bs.cmi.mdr.pdb.UserInfo -import io.laminext.syntax.core.* - -object NavigationBar: - - case class Logo(img: String, name: String) - case class Link(a: () => Anchor, active: Boolean) - case class MenuItem(title: String) - - case class ViewModel( - userInfo: UserInfo, - pages: List[Link], - userMenu: List[MenuItem], - logo: Logo - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - val $userInfo = $m.map(_.userInfo) - val mobileMenuOpen = Var(false) - - val desktopOnly = cls("hidden md:block") - val mobileOnly = cls("md:hidden") - - inline def avatarImage(size: Int = 8) = - Avatar($userInfo.map(_.img)).avatarImage(size) - - def notificationButton = button( - tpe := "button", - cls := List( - "bg-indigo-600", - "focus:outline-none", - "focus:ring-2", - "focus:ring-offset-2", - "focus:ring-offset-indigo-600", - "focus:ring-white", - "hover:text-white", - "p-1", - "rounded-full", - "text-indigo-200" - ), - span(cls := "sr-only", "View notifications"), - Icons.outline.bell() - ) - - def userProfile: HtmlElement = - val menuOpen = Var(false) - - def menuItem(item: MenuItem, idx: Int): HtmlElement = - a( - href := "#", - cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", - role := "menuitem", - tabIndex := -1, - idAttr := s"user-menu-item-$idx", - item.title - ) - - div( - cls := "ml-3 relative", - div( - button( - tpe := "button", - cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - idAttr := "user-menu-button", - aria.expanded <-- menuOpen.signal, - aria.hasPopup := true, - span(cls := "sr-only", "Open user menu"), - child <-- avatarImage(), - onClick.preventDefault.mapTo( - !menuOpen.now() - ) --> menuOpen.writer - ) - ), - /* - * */ - div( - cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", - cls <-- menuOpen.signal.map { o => - if (o) "md:block" else "md:hidden" - }, - role := "menu", - aria.orientation := "vertical", - aria.labelledBy := "user-menu-button", - tabIndex := -1, - // : keyboard navigation - children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) - ) - ) - - def mobileProfile = - def menuItem(item: MenuItem): HtmlElement = - a( - href := "#", - cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", - item.title - ) - - div( - cls := "pt-4 pb-3 border-t border-indigo-700", - div( - cls := "flex items-center px-5", - div( - cls := "flex-shrink-0", - child <-- avatarImage(10) - ), - div( - cls := "ml-3", - div( - cls := "text-base font-medium text-white", - child.text <-- $userInfo.map(_.name) - ), - child.maybe <-- $userInfo.map( - _.email.map(e => - div( - cls := "text-sm font-medium text-indigo-300", - e - ) - ) - ) - ), - notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) - ), - div( - cls := "mt-3 px-2 space-y-1", - children <-- $m.map(_.userMenu.map(menuItem)) - ) - ) - - def pageLink(page: Link): Anchor = - page - .a() - .amend( - cls := "text-white px-3 py-2 rounded-md text-sm font-medium", - cls := Seq( - "bg-indigo-700" -> page.active, - "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active - ), - ariaCurrent := (if page.active then "page" else "false") - ) - - def logoImg: Image = - img( - cls := "h-8 w-8", - src <-- $m.map(_.logo.img), - alt <-- $m.map(_.logo.name) - ) - - def mobileMenuButton = button( - tpe := "button", - cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - aria.controls := "mobile-menu", - aria.expanded <-- mobileMenuOpen.signal, - span(cls := "sr-only", "Open main menu"), - Icons.outline - .menu() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") - ), - Icons.outline - .x() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") - ), - onClick.preventDefault.mapTo( - !mobileMenuOpen.now() - ) --> mobileMenuOpen.writer - ) - - def navBarLeft = - div( - cls := "flex items-center", - div(cls := "flex-shrink-0", logoImg), - div( - desktopOnly, - div( - cls := "ml-10 flex items-baseline space-x-4", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ) - ) - ) - - def navBarRight = - div( - desktopOnly, - div( - cls := "ml-4 flex items-center md:ml-6", - notificationButton, - userProfile - ) - ) - - def navBarMobile = - div( - cls := "-mr-2 flex", - mobileOnly, - mobileMenuButton - ) - - def navBar = - div( - cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex items-center justify-between h-16", - navBarLeft, - navBarRight, - navBarMobile - ) - ) - - def mobileMenu = - div( - mobileOnly, - cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }, - idAttr := "mobile-menu", - div( - cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ), - mobileProfile - ) - - nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala deleted file mode 100644 index 75339df..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object PageHeader: - def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = - header( - cls := "bg-white shadow-sm", - div( - cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", - h1( - cls := "text-lg leading-6 font-semibold text-gray-900", - Breadcrumbs(actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala deleted file mode 100644 index 5096463..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ /dev/null @@ -1,30 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import fiftyforms.ui.components.tailwind.Loading -import io.laminext.syntax.core.* - -object PageLayout: - case class ViewModel( - navigation: NavigationBar.ViewModel, - content: Option[HtmlElement] - ) - def apply(actionBus: Observer[Action])( - $m: Signal[ViewModel], - mods: Modifier[HtmlElement]* - )(using router: Router[Page]): HtmlElement = - val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) - div( - cls := "h-full flex flex-col", - NavigationBar($m.map(_.navigation)), - child.maybe <-- router.$currentPage.map(_.isRoot) - .switch(None, Some(PageHeader(actionBus))), - main( - cls := "flex-grow-1 overflow-y-auto", - mods, - child <-- $maybeContent.map(_.getOrElse(Loading)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala deleted file mode 100644 index 62d820f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.LinkSupport.* -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo - -object PageLink: - type ViewModel = Page - def apply($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = - a(mods($m, actions), child.text <-- $m.map(_.title)) - - def apply(m: ViewModel, actions: Observer[Action])(using - router: Router[Page] - ): Anchor = apply(Val(m), actions) - - def container($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = a(mods($m, actions)) - - def container(m: ViewModel, actions: Observer[Action])(using - Router[Page] - ): Anchor = container(Val(m), actions) - - private def mods($m: Signal[Page], actions: Observer[Action])(using - router: Router[Page] - ): Modifier[Anchor] = - Seq( - href <-- $m.map(router.absoluteUrlForPage).recover { case _ => - Some("invalid url") - }, - composeEvents(onClick.noKeyMod.preventDefault)( - _.sample($m) - .map(NavigateTo.apply) - ) --> actions - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala deleted file mode 100644 index 87f825a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.pages.dashboard.DashboardPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -class DashboardPageConnector(actionBus: Observer[Action])(using - router: Router[Page] -): - def apply: HtmlElement = - AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala deleted file mode 100644 index e9b0f71..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala +++ /dev/null @@ -1,76 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailKriteriaPage -import pages.detail.components.DukazyKriteria -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -object DetailKriteriaPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailKriteriaPageConnector( - state: DetailKriteriaPageConnector.AppState -)( - $page: Signal[Page.DetailKriteria] -)(using Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => - (p.osobniCislo.value, p.parametr.value, p.kriterium.value) - )((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map( - FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) - ) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, s, $s) => - DetailKriteriaPage($s)(state.actionBus.contramap { - case DukazyKriteria.Add => - NavigateTo( - Page.UpravDukazKriteria( - Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), - Page.Titled(s.parametr.id, Some(s.parametr.nazev)), - Page.Titled(s.kriterium.id, Some(s.kriterium.id)) - ) - ) - }) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): DetailKriteriaPage.ViewModel = - DetailKriteriaPage.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala deleted file mode 100644 index 4d8024f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ /dev/null @@ -1,57 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import pages.detail.DetailPage -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import fiftyforms.ui.components.tailwind.Color - -object DetailPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailPageConnector(state: DetailPageConnector.AppState)( - $page: Signal[Page.Detail] -)(using router: Router[Page]): - val $oscChangeSignal = $page.splitOne(_.osobniCislo.value)((osc, _, _) => osc) - val $pageChangeSignal = - $oscChangeSignal.flatMap(osc => - EventStream.fromSeq(Seq(FetchUserDetails(osc), FetchParameters(osc))) - ) - // TODO: filter the value based on the current osc - // OSC change will fetch new data, but still - // - we need to be sure that what we got is really what we ought to display - // - we want to display stale data accordingly (at least with loading indicator) - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - def apply: HtmlElement = - AppPage(state.actionBus)( - $data.combineWithFn($params)(_ zip _) - .map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: List[Parameter] - ): DetailPage.ViewModel = - DetailPage.ViewModel( - o.toDetailOsoby, - p.map( - _.toParametr(param => - PageLink.container(Page.DetailParametru(o, param), state.actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala deleted file mode 100644 index fdaa7f8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailParametruPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink - -object DetailParametruPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailParametruPageConnector( - state: DetailParametruPageConnector.AppState -)( - $page: Signal[Page.DetailParametru] -)(using router: Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => (p.osobniCislo.value, p.parametr.value))((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map(FetchParameter(_, _)) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - } yield (da, pb) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailParametruPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter - ): DetailParametruPage.ViewModel = - DetailParametruPage.ViewModel( - o.toDetailOsoby, - p.toParametr(p => - PageLink.container(Page.DetailParametru(o, p), state.actionBus) - ), - p.criteria.map( - _.toKriterium { c => - PageLink.container( - Page.DetailKriteria(o, p, c), - state.actionBus - ) - } - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala deleted file mode 100644 index 049aa22..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -case class DirectoryPageConnector( - $input: EventStream[List[UserInfo]], - actionBus: Observer[Action] -)(using router: Router[Page]): - val $data = $input.startWithNone - val $actionSignal = EventStream.fromValue(FetchDirectory) - - def apply: HtmlElement = - AppPage(actionBus)( - $data.split(_ => ())((_, _, s) => - pages.directory.DirectoryPage( - s.map( - _.map( - _.toUserRow(u => - PageLink.container( - Page.Detail(Page.Titled(u.personalNumber)), - actionBus - ) - ) - ) - ) - ) - ), - $actionSignal --> actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala deleted file mode 100644 index 88e7450..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ /dev/null @@ -1,68 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii -import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria -import fiftyforms.ui.components.tailwind.Color - -extension (o: UserInfo) - def toDetailOsoby: DetailOsoby.ViewModel = - DetailOsoby.ViewModel( - o.personalNumber, - o.name, - o.email, - o.phone, - o.img, - o.mainFunction.map(f => - DetailOsoby.Funkce.ViewModel(f.name, f.dept, f.ou) - ), - o.userContracts.headOption.map(c => - DetailOsoby.PracovniPomer.ViewModel(c.rel, c.startDate, c.endDate) - ) - ) - -extension (param: Parameter) - def toParametr(container: Parameter => Anchor): DetailParametru.ViewModel = - DetailParametru.ViewModel( - id = param.id, - nazev = param.name, - popis = param.description, - status = "Nesplněno", - statusColor = Color.red, - a = container(param) - ) - -extension (crit: ParameterCriteria) - def toKriterium( - container: ParameterCriteria => Anchor - ): DetailKriteria.ViewModel = - DetailKriteria.ViewModel( - nazev = crit.criteriumText, - kapitola = crit.chapterId, - bod = crit.itemId, - status = "Nesplněno", - statusColor = Color.red, - splneno = false, - dukazy = Nil, - container = container(crit) - ) - -extension (user: UserInfo) - def toUserRow( - container: UserInfo => HtmlElement = (_: UserInfo) => div() - ): UserRow.ViewModel = - UserRow.ViewModel( - osobniCislo = user.personalNumber.toString, - celeJmeno = user.name, - prijmeni = user.surname, - hlavniFunkce = user.mainFunction.map(_.name), - img = user.img, - container = () => container(user) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala deleted file mode 100644 index 7d5b2c1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.dashboard - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.Page - -object DashboardPage: - - def render: HtmlElement = - div("Dashboard page") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala deleted file mode 100644 index 9ef0698..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala +++ /dev/null @@ -1,31 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailKriteriaPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply($m: Signal[ViewModel])( - action: Observer[DukazyKriteria.Action] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - DukazyKriteria($m.map(_.kriterium.dukazy))(action) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala deleted file mode 100644 index 3b316e1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object DetailPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametry: SeznamParametru.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby($m.map(_.osoba)), - SeznamParametru($m.map(_.parametry)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala deleted file mode 100644 index 17224cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala +++ /dev/null @@ -1,24 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailParametruPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriteria: SeznamKriterii.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby.header($m.map(_.osoba)), - DetailParametru($m.map(_.parametr)), - SeznamKriterii($m.map(_.kriteria)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala deleted file mode 100644 index dc912fa..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ /dev/null @@ -1,107 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb -package app -package pages.detail - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.UpravDukazForm -import fiftyforms.services.files.File - -object UpravDukaz: - - type ThisPage = Page.UpravDukazKriteria - type PageKey = (OsobniCislo, String, String) - - trait State { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def availableFiles: EventStream[List[File]] - def actionBus: Observer[Action] - } - - def keyOfPage(page: ThisPage): PageKey = - (page.osobniCislo.value, page.parametr.value, page.kriterium.value) - - def onChangeAction(key: PageKey): Action = - FetchParameterCriteria( - key._1, - key._2, - key._3, - Page.UpravDukazKriteria(_, _, _) - ) - - class Connector(state: State)($page: Signal[ThisPage])(using Router[Page]): - val $paramChangeSignal = $page.splitOne(keyOfPage)((x, _, _) => x) - val $pageChangeSignal = $paramChangeSignal.map(onChangeAction) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.split(_ => ())((_, s, $s) => - PageComponent( - $s.map(buildModel), - state.availableFiles, - state.actionBus.contramap { - case UpravDukazForm.Cancelled => - NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) - case UpravDukazForm.AvailableFilesRequested => - FetchAvailableFiles(s._1.personalNumber) - } - ) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): PageComponent.ViewModel = - import connectors.* - PageComponent.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) - - object PageComponent: - import components.* - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply( - $m: Signal[ViewModel], - availableFilesStream: EventStream[List[File]], - events: Observer[UpravDukazForm.Event] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - UpravDukazForm(availableFilesStream)(events) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala deleted file mode 100644 index ca311b0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.Icons -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Color - -object DetailKriteria: - case class ViewModel( - nazev: String, - kapitola: String, - bod: String, - status: String, - statusColor: Color, - splneno: Boolean, - dukazy: List[DukazKriteria.ViewModel], - container: HtmlElement = div() - ) { - val id = s"${kapitola}${bod}" - } - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h3( - cls := "text-xl leading-6 font-bold text-gray-900", - child.text <-- $m.map(_.nazev) - ), - h3( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - "Kapitola ", - child.text <-- $m.map(_.kapitola), - " bod ", - child <-- $m.map(_.bod) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala deleted file mode 100644 index ae90290..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala +++ /dev/null @@ -1,100 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Avatar - -object DetailOsoby: - - object Funkce: - case class ViewModel( - nazev: String, - stredisko: String, - voj: String - ) - - def render($m: Signal[ViewModel]): HtmlElement = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.nazev), - span( - cls := "hidden md:inline", - " @ ", - child.text <-- $m.map(_.stredisko), - ", ", - child.text <-- $m.map(_.voj) - ) - ) - - object PracovniPomer: - case class ViewModel( - druh: String, - pocatek: LocalDate, - konec: Option[LocalDate] - ) - - def render($m: Signal[ViewModel]): HtmlElement = - import fiftyforms.ui.components.tailwind.CustomAttrs.datetime - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.druh), - " od ", - time( - datetime <-- $m.map(_.pocatek.toString), - child.text <-- $m.map(_.pocatek.toString) - ) - ) - - case class ViewModel( - osobniCislo: OsobniCislo, - jmeno: String, - email: Option[String], - telefon: Option[String], - img: Option[String], - hlavniFunkce: Option[Funkce.ViewModel], - pracovniPomer: Option[PracovniPomer.ViewModel] - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-4", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(16) - ), - div( - h1( - cls := "text-2xl font-bold text-gray-900", - child.text <-- $m.map(_.jmeno) - ), - child.maybe <-- $m.map(_.hlavniFunkce).split(_ => ())((_, _, d) => - Funkce.render(d) - ), - child.maybe <-- $m.map(_.pracovniPomer).split(_ => ())((_, _, d) => - PracovniPomer.render(d) - ) - ) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-5", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(8) - ), - div( - h1( - cls := "text-lg font-medium text-gray-800", - child.text <-- $m.map(_.jmeno) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala deleted file mode 100644 index e11cb62..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color - -object DetailParametru: - case class ViewModel( - id: String, - nazev: String, - popis: String, - status: String, - statusColor: Color, - a: Anchor - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h2( - cls := "text-2xl leading-6 font-medium text-gray-900", - child.text <-- $m.map(_.nazev) - ), - p( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - child.text <-- $m.map(_.popis) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - h2( - cls := "text-lg leading-6 font-medium text-gray-600", - child.text <-- $m.map(_.nazev) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala deleted file mode 100644 index a00ede8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala +++ /dev/null @@ -1,177 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.CustomAttrs - -object DukazKriteria: - case class Osoba(osobniCislo: String, jmeno: String) - case class Udalost(osoba: Osoba, datum: LocalDate) - case class Dokument( - url: String, - nazev: String, - pridal: Udalost, - odebral: Option[Udalost] - ) - case class ViewModel( - dokumenty: List[Dokument], - autorizoval: Option[Udalost], - platiDo: Option[LocalDate], - poznámka: Option[String] - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-lg", - div( - cls := "px-4 py-5 sm:px-6", - h3( - cls := "text-lg leading-6 font-medium text-gray-900", - """Applicant Information""" - ), - p( - cls := "mt-1 max-w-2xl text-sm text-gray-500", - """Personal details and application.""" - ) - ), - div( - cls := "border-t border-gray-200 px-4 py-5 sm:px-6", - dl( - cls := "grid grid-cols-1 gap-x-4 gap-y-8 sm:grid-cols-2", - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Full name""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Margot Foster""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Application for""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Backend Developer""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Email address""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """margotfoster@example.com""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Salary expectation""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """$120,000""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """About""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Fugiat ipsum ipsum deserunt culpa aute sint do nostrud anim incididunt cillum culpa consequat. Excepteur qui ipsum aliquip consequat sint. Sit id mollit nulla mollit nostrud in ea officia proident. Irure nostrud pariatur mollit ad adipisicing reprehenderit deserunt qui eu.""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """Attachments""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - ul( - role := "list", - cls := "border border-gray-200 rounded-md divide-y divide-gray-200", - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """resume_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ), - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """coverletter_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala deleted file mode 100644 index 22f134f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala +++ /dev/null @@ -1,35 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object DukazyKriteria: - sealed trait Action - case object Add extends Action - - type ViewModel = List[DukazKriteria.ViewModel] - - def apply($m: Signal[ViewModel])(actions: Observer[Action]): HtmlElement = - div( - child.maybe <-- $m.map(m => - if (m.isEmpty) then Some(prazdnyDukaz(actions)) - else None - ), - children <-- $m.map(_.zipWithIndex).split(_._2)((_, _, $s) => - DukazKriteria($s.map(_._1)) - ) - ) - - private def prazdnyDukaz(actions: Observer[Action]): HtmlElement = - button( - tpe := "button", - onClick.preventDefault.mapTo(Add) --> actions, - cls := "relative block w-full border-2 border-gray-300 border-dashed rounded-lg p-12 text-center hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - Icons.outline - .`document-add`(12) - .amend(svg.cls := "mx-auto text-gray-400"), - span( - cls := "mt-2 block text-sm font-medium text-gray-900", - "Přidat důkaz" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala deleted file mode 100644 index f628697..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.list.StackedList -import fiftyforms.ui.components.tailwind.list.ListRow -import fiftyforms.ui.components.tailwind.list.RowTag -import fiftyforms.ui.components.tailwind.list.RowNext -import java.time.LocalDate - -object SeznamKriterii: - type ViewModel = List[DetailKriteria.ViewModel] - - private val kritList = new StackedList[DetailKriteria.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - kritList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = - p(cls := "text-sm text-gray-500", s"${i.kapitola}${i.bod}"), - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.container - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala deleted file mode 100644 index 2bfe4bc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.list.{ - StackedList, - ListRow, - RowTag, - PropList, - IconText, - RowNext -} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.LinkSupport.* - -object SeznamParametru: - type ViewModel = List[DetailParametru.ViewModel] - - private val parametrList = new StackedList[DetailParametru.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - parametrList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = emptyNode, - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.a - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala deleted file mode 100644 index f048e9d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ /dev/null @@ -1,101 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.form.* -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement -import fiftyforms.services.files.components.tailwind.FilePicker -import cz.e_bs.cmi.mdr.pdb.frontend.AutorizujDukaz -import cz.e_bs.cmi.mdr.pdb.frontend.DocumentRef -import fiftyforms.services.files.File - -object UpravDukazForm: - sealed trait Event - case object Cancelled extends Event - case object AvailableFilesRequested extends Event - def apply(availableFilesStream: EventStream[List[File]])( - updates: Observer[Event] - ): HtmlElement = - val (filesStream, filesObserver) = - EventStream.withObserver[FilePicker.Event] - val files = Var[List[File]](Nil) - def submitButtons: HtmlElement = - div( - cls := "pt-5", - div( - cls := "flex justify-end", - button( - tpe := "button", - cls := "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zrušit", - onClick.mapTo(Cancelled) --> updates - ), - button( - tpe := "submit", - cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - disabled <-- files.signal.map(_.isEmpty), - "Autorizovat" - ) - ) - ) - - div( - filesStream.collect { case FilePicker.SelectionUpdated(files) => - files.to(List) - } --> files.writer, - filesStream.collect { case FilePicker.AvailableFilesRequested => - AvailableFilesRequested - } --> updates, - cls := "bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6", - Form( - Form.Body( - Form.Section( - FormHeader( - FormHeader.ViewModel( - "Doložení kritéria", - "Sestavte doklady poskytující důkaz kritéria a potvrďte odesláním formuláře. Případné limitace či jiné relevantní údaje vepište do pole Komentář." - ) - ), - FormFields( - FormRow( - "dokumenty", - "Dokumenty", - FilePicker(files.signal, availableFilesStream)(filesObserver) - .amend(idAttr := "dokumenty", cls("max-w-lg")) - ).toHtml, - FormRow( - "platnost", - "Platnost", - input( - idAttr := "platnost", - name := "platnost", - tpe := "date", - autoComplete := "date", - cls := "block max-w-lg w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" - ) - ).toHtml, - FormRow( - "komentar", - "Komentář", - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - textArea( - idAttr := "komentar", - name := "about", - rows := 3, - cls := "max-w-lg shadow-sm block w-full focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border border-gray-300 rounded-md" - ), - p( - cls := "mt-2 text-sm text-gray-500", - "Doplňte prosím potřebné informace související s doložením kritéria, včetně případných limitací." - ) - ) - ).toHtml - ) - ) - ), - submitButtons - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala deleted file mode 100644 index 41033f6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -import components._ - -object DirectoryPage: - - type ViewModel = List[UserRow.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - val (actionsStream, actionObserver) = - EventStream.withObserver[SearchForm.Action] - val $filter = actionsStream - .collect { - case SearchForm.ClearFilter => None - case SearchForm.SetFilter(t) => Some(t) - } - .startWith(None) - val byLetter = for { - d <- $m - f <- $filter - } yield for { - (letter, users) <- d - .filter { user => - f match - case None => true - case Some(t) => user.search.contains(t) - } - .groupBy(_.prijmeni.head) - .to(List) - .sortBy(_._1) - } yield (letter.toString, users.sortBy(_.prijmeni)) - - div( - cls := "h-full max-w-7xl mx-auto order-first flex flex-col flex-shrink-0", - SearchForm(actionObserver), - Directory(byLetter) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala deleted file mode 100644 index 73f915d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -object Directory: - - object Header: - type ViewModel = String - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500", - h3(child.text <-- $m) - ) - - object UserList: - type ViewModel = List[UserRow.ViewModel] - def apply($m: Signal[ViewModel]): HtmlElement = - ul( - role := "list", - cls := "relative z-0 divide-y divide-gray-200", - children <-- $m.split(_.osobniCislo)((_, _, s) => UserRow(s)) - ) - - type ViewModel = List[(String, List[UserRow.ViewModel])] - def apply($m: Signal[ViewModel]): HtmlElement = - nav( - cls := "flex-1 min-h-0 overflow-y-auto", - aria.label := "Directory", - children <-- $m.split(_._1)((_, _, s) => - div( - cls := "relative", - Header(s.map(_._1)), - UserList(s.map(_._2)) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala deleted file mode 100644 index e182910..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala +++ /dev/null @@ -1,54 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object SearchForm: - sealed trait Action - case object SubmitFilter extends Action - sealed trait FilterAction extends Action - case object ClearFilter extends FilterAction - case class SetFilter(value: String) extends FilterAction - - def apply(actions: Observer[Action]): HtmlElement = - div( - cls := "px-6 pt-4 pb-4", - form( - cls := "flex space-x-4", - onSubmit.mapTo(SubmitFilter) --> actions, - div( - cls := "flex-1 min-w-0", - label( - forId := "search", - cls := "sr-only", - "Hledat" - ), - div( - cls := "relative rounded-md shadow-sm", - div( - cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", - Icons.solid.search().amend(svg.cls := "text-gray-400") - ), - input( - tpe := "search", - name := "search", - idAttr := "search", - cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", - placeholder := "Hledat", - composeEvents(onInput.mapToValue.setAsValue.map(SetFilter(_)))( - _.throttle(500) - ) --> actions - ) - ) - ), - button( - tpe := "submit", - cls := "inline-flex justify-center px-3.5 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500", - Icons.solid.filter().amend(svg.cls := "text-gray-400"), - span( - cls := "sr-only", - "Hledat" - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala deleted file mode 100644 index 82f7723..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala +++ /dev/null @@ -1,52 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.airstream.core.Signal -import fiftyforms.ui.components.tailwind.Avatar - -object UserRow: - case class ViewModel( - osobniCislo: String, - celeJmeno: String, - prijmeni: String, - hlavniFunkce: Option[String], - img: Option[String], - container: () => HtmlElement = () => div() - ) { - val search = prijmeni.toLowerCase - } - - def apply($m: Signal[ViewModel]): HtmlElement = - inline def avatarImage = - Avatar($m.map(_.img)).avatarImage(10) - - li( - div( - cls := "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500", - div( - cls := "flex-shrink-0", - child <-- avatarImage - ), - div( - cls := "flex-1 min-w-0", - child <-- $m.map { o => - o.container() - .amend( - cls := "focus:outline-none", - span( - cls := "absolute inset-0", - aria.hidden := true - ), - p( - cls := "text-sm font-medium text-gray-900", - o.celeJmeno - ), - p( - cls := "text-sm text-gray-500 truncate", - o.hlavniFunkce - ) - ) - } - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala deleted file mode 100644 index 467c5f5..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala +++ /dev/null @@ -1,99 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router - -object ErrorPage: - case class ViewModel( - homePage: Page, - errorName: String, - title: String, - subTitle: String - ) - - def apply(m: ViewModel, actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - val ViewModel(homePage, errorName, title, subTitle) = m - div( - cls := "min-h-full pt-16 pb-12 flex flex-col bg-white", - main( - cls := "flex-grow flex flex-col justify-center max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex-shrink-0 flex justify-center", - a( - href := "/", - cls := "inline-flex", - span( - cls := "sr-only", - """Workflow""" - ), - img( - cls := "h-12 w-auto", - src := "https://tailwindui.com/img/logos/workflow-mark.svg?color=indigo&shade=600", - alt := "" - ) - ) - ), - div( - cls := "py-16", - div( - cls := "text-center", - p( - cls := "text-sm font-semibold text-indigo-600 uppercase tracking-wide", - errorName - ), - h1( - cls := "mt-2 text-4xl font-extrabold text-gray-900 tracking-tight sm:text-5xl", - title - ), - p( - cls := "mt-2 text-base text-gray-500", - subTitle - ), - div( - cls := "mt-6", - PageLink - .container(homePage, actionBus) - .amend( - cls := "text-base font-medium text-indigo-600 hover:text-indigo-500", - """Go back home""" - ) - ) - ) - ) - ), - footer( - cls := "flex-shrink-0 max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", - nav( - cls := "flex justify-center space-x-4", - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Contact Support""" - ), - span( - cls := "inline-block border-l border-gray-300", - aria.hidden := true - ), - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Status""" - ), - span( - cls := "inline-block border-l border-gray-300", - aria.hidden := true - ), - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Twitter""" - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala deleted file mode 100644 index bd9baec..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action - -object NotFoundPage: - def apply(homePage: Page, url: String, actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - ErrorPage( - ErrorPage.ViewModel( - homePage, - "404 error", - "Page not found.", - s"Sorry, but page $url doesn't exist." - ), - actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala deleted file mode 100644 index 476fe3c..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action - -object UnhandledErrorPage: - - case class ViewModel( - homePage: Page, - errorName: Option[String], - errorMessage: Option[String] - ) - - def apply(m: ViewModel, actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - ErrorPage( - ErrorPage.ViewModel( - m.homePage, - "Unexpected error occurred", - m.errorName.getOrElse( - "Uh oh!" - ), // TODO: translations, better text than uh oh - m.errorMessage.getOrElse( - "This wasn't supposed to happen! Please try again." - ) - ), - actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala deleted file mode 100644 index 25f059f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala +++ /dev/null @@ -1,6 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.services - -import com.raquo.airstream.core.Observer - -trait DataFetcher[K, A]: - def fetch(id: K, sink: Observer[A]): Unit diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala deleted file mode 100644 index a469830..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala +++ /dev/null @@ -1,120 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package state - -import com.raquo.airstream.core.EventStream -import cz.e_bs.cmi.mdr.pdb.{UserInfo, OsobniCislo} -import com.raquo.airstream.core.Observer -import scala.scalajs.js -import scala.scalajs.js.JSON -import zio.json._ -import com.raquo.airstream.eventbus.EventBus -import com.raquo.airstream.ownership.Owner -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.UserFunction -import cz.e_bs.cmi.mdr.pdb.UserContract -import fiftyforms.services.files.File - -trait AppState - extends connectors.DetailPageConnector.AppState - with connectors.DetailParametruPageConnector.AppState - with connectors.DetailKriteriaPageConnector.AppState - with pages.detail.UpravDukaz.State: - def users: EventStream[List[UserInfo]] - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - -class MockAppState(implicit owner: Owner, router: Router[Page]) - extends AppState: - - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonDecoder[UserFunction] = DeriveJsonDecoder.gen - given JsonDecoder[UserContract] = DeriveJsonDecoder.gen - given JsonDecoder[UserInfo] = DeriveJsonDecoder.gen - - given JsonDecoder[ParameterCriteria] = DeriveJsonDecoder.gen - given JsonDecoder[Parameter] = DeriveJsonDecoder.gen - - private val actions = EventBus[Action]() - private val (parametersStream, pushParameters) = - EventStream.withCallback[List[Parameter]] - private val (usersStream, pushUsers) = - EventStream.withCallback[List[UserInfo]] - private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] - private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] - - private val mockData: List[UserInfo] = - mockUsers - .asInstanceOf[js.Dictionary[js.Object]] - .values - // TODO: is there a more efficient way to parse from JS object directly? - .map { o => - val parsed = JSON.stringify(o).fromJson[UserInfo] - parsed.left.foreach(org.scalajs.dom.console.log(_)) - parsed - } - .collect { case Right(u) => - u - } - .toList - - private val mockParameters: List[Parameter] = - pdbParams - .asInstanceOf[js.Dictionary[js.Object]] - .values - .map(o => JSON.stringify(o).fromJson[Parameter]) - .collect { case Right(p) => p } - .toList - - // TODO: Extract to separate event handler - actions.events.foreach { - case FetchDirectory => pushUsers(mockData) - case FetchUserDetails(osc) => - mockData.find(_.personalNumber == osc).foreach { o => - pushDetails(o) - router.replaceState(Page.Detail(o)) - } - case FetchParameters(osc) => - pushParameters(mockParameters) - case FetchParameter(osc, paramId) => - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(Page.DetailParametru(o, p)) - case FetchParameterCriteria(osc, paramId, critId, page) => - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - c <- p.criteria.find(_.id == critId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(page(o, p, c)) - case NavigateTo(page) => router.pushState(page) - case FetchAvailableFiles(osc) => - pushFiles( - List( - File("https://tc163.cmi.cz/here", "Example file") - ) - ) - } - - override def users: EventStream[List[UserInfo]] = - usersStream.debugWithName("users") - - override def details: EventStream[UserInfo] = - detailsStream.debugWithName("details") - - override def parameters: EventStream[List[Parameter]] = - parametersStream.debugWithName("parameters") - - override def availableFiles: EventStream[List[File]] = - filesStream.debugWithName("available files") - - override def actionBus: Observer[Action] = - actions.writer.debugWithName("actions writer") diff --git a/app/src/main/scala/mdr/pdb/app/Main.scala b/app/src/main/scala/mdr/pdb/app/Main.scala new file mode 100644 index 0000000..081b4ee --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/Main.scala @@ -0,0 +1,103 @@ +package mdr.pdb.app + +import scala.scalajs.js.annotation.JSExportTopLevel +import scala.scalajs.js.annotation.JSExport +import scala.scalajs.js.annotation.JSImport +import scala.scalajs.js +import org.scalajs.dom +import com.raquo.laminar.api.L.{*, given} +import scala.scalajs.js.Date +import com.raquo.waypoint.Router +import com.raquo.waypoint.SplitRender +import mdr.pdb.app.services.DataFetcher +import scala.scalajs.js.JSON +import zio.json._ +import mdr.pdb.UserInfo +import mdr.pdb.app.state.AppState + +@js.native +@JSImport("stylesheets/main.css", JSImport.Namespace) +object Css extends js.Any + +@js.native +@JSImport("data/users.json", JSImport.Default) +object mockUsers extends js.Object + +@js.native +@JSImport("params/pdb-params.json", JSImport.Default) +object pdbParams extends js.Object + +@JSExportTopLevel("app") +object Main: + + @JSExport + def main(args: Array[String]): Unit = + import Routes.given + onLoad { + setupAirstream() + val appContainer = dom.document.querySelector("#app") + val _ = + render( + appContainer, + renderPage(state.MockAppState(using unsafeWindowOwner, router)) + ) + } + + private def onLoad(f: => Unit): Unit = + documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) + + private def setupAirstream()(using router: Router[Page]): Unit = + AirstreamError.registerUnhandledErrorCallback(err => + router.forcePage( + Page.UnhandledError( + Some(err.getClass.getName), // TODO: Fill only in dev mode + Some(err.getMessage) + ) + ) + ) + + def renderPage(state: AppState)(using + router: Router[Page] + ): HtmlElement = + val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) + .collectSignal[Page.Detail]( + connectors + .DetailPageConnector(state)(_) + .apply + ) + .collectSignal[Page.DetailParametru]( + connectors + .DetailParametruPageConnector(state)(_) + .apply + ) + .collectSignal[Page.DetailKriteria]( + connectors + .DetailKriteriaPageConnector(state)(_) + .apply + ) + .collectSignal[Page.UpravDukazKriteria]( + pages.detail.UpravDukaz.Connector(state)(_).apply + ) + .collectStatic(Page.Dashboard)( + connectors.DashboardPageConnector(state.actionBus).apply + ) + .collect[Page.NotFound](pg => + pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) + ) + .collect[Page.UnhandledError](pg => + pages.errors + .UnhandledErrorPage( + pages.errors.UnhandledErrorPage + .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), + state.actionBus + ) + ) + .collectStatic(Page.Directory)( + connectors + .DirectoryPageConnector(state.users, state.actionBus) + .apply + ) + div(cls := "h-full", child <-- pageSplitter.$view) + + // Pull in the stylesheet + val css: Css.type = Css diff --git a/app/src/main/scala/mdr/pdb/app/Routes.scala b/app/src/main/scala/mdr/pdb/app/Routes.scala new file mode 100644 index 0000000..2cc860c --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/Routes.scala @@ -0,0 +1,198 @@ +package mdr.pdb.app + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.* +import org.scalajs.dom +import zio.json.{*, given} +import mdr.pdb.OsobniCislo + +import scala.scalajs.js +import mdr.pdb.UserInfo +import mdr.pdb.Parameter +import mdr.pdb.ParameterCriteria +import mdr.pdb.app.Page.Titled + +// enum is not working with Waypoints' SplitRender collectStatic +sealed abstract class Page( + val id: String, + val title: String, + val parent: Option[Page] +) { + val path: Vector[Page] = + parent match + case None => Vector(this) + case Some(p) => p.path :+ this + + val isRoot: Boolean = parent.isEmpty +} + +object Page: + + case class Titled[V](value: V, title: Option[String] = None): + val show: String = title.getOrElse(value.toString) + + case object Directory extends Page("directory", "Adresář", None) + + case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) + + case class Detail(osobniCislo: Titled[OsobniCislo]) + extends Page("user", osobniCislo.show, Some(Directory)) + + object Detail { + def apply(o: UserInfo): Detail = Detail( + Titled(o.personalNumber, Some(o.name)) + ) + } + + case class DetailParametru( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String] + ) extends Page( + "parameter", + parametr.show, + Some(Detail(osobniCislo)) + ) + + object DetailParametru { + def apply(o: UserInfo, p: Parameter): DetailParametru = + DetailParametru( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)) + ) + } + + case class DetailKriteria( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String], + kriterium: Titled[String] + ) extends Page( + "criteria", + kriterium.show, + Some(DetailParametru(osobniCislo, parametr)) + ) + + object DetailKriteria { + def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = + DetailKriteria( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)), + Titled(k.id, Some(k.id)) + ) + } + + case class UpravDukazKriteria( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String], + kriterium: Titled[String] + ) extends Page( + "addProof", + "Důkaz", + Some(DetailKriteria(osobniCislo, parametr, kriterium)) + ) + + object UpravDukazKriteria { + def apply( + o: UserInfo, + p: Parameter, + k: ParameterCriteria + ): UpravDukazKriteria = + UpravDukazKriteria( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)), + Titled(k.id, Some(k.id)) + ) + } + + case class NotFound(url: String) extends Page("404", "404", Some(Directory)) + + case class UnhandledError( + errorName: Option[String], + errorMessage: Option[String] + ) extends Page("500", "Unexpected error", Some(Directory)) + +object Routes: + given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) + given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) + given [V: JsonEncoder]: JsonEncoder[Titled[V]] = + DeriveJsonEncoder.gen[Titled[V]] + given [V: JsonDecoder]: JsonDecoder[Titled[V]] = + DeriveJsonDecoder.gen[Titled[V]] + given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] + given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] + + val base = + js.`import`.meta.env.BASE_URL + .asInstanceOf[String] + "app" + + val homePage: Page = Page.Directory + + given router: Router[Page] = Router[Page]( + routes = List( + Route.static(homePage, root / endOfSegments, basePath = base), + Route.static( + Page.Dashboard, + root / "dashboard" / endOfSegments, + basePath = base + ), + Route[Page.Detail, String]( + encode = _.osobniCislo.value.toString, + decode = osc => Page.Detail(Titled(OsobniCislo(osc))), + root / "osoba" / segment[String] / endOfSegments, + basePath = base + ), + Route[Page.DetailParametru, (String, String)]( + encode = p => (p.osobniCislo.value.toString, p.parametr.value), + decode = + p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / endOfSegments, + basePath = base + ), + Route[Page.DetailKriteria, (String, String, String)]( + encode = p => + ( + p.osobniCislo.value.toString, + p.parametr.value, + p.kriterium.value.replaceAll("\\.", "--") + ), + decode = p => + Page.DetailKriteria( + Titled(OsobniCislo(p._1)), + Titled(p._2), + Titled(p._3.replaceAll("--", ".")) + ), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / "kriterium" / segment[String] / endOfSegments, + basePath = base + ), + Route[Page.UpravDukazKriteria, (String, String, String)]( + encode = p => + ( + p.osobniCislo.value.toString, + p.parametr.value, + p.kriterium.value.replaceAll("\\.", "--") + ), + decode = p => + Page.UpravDukazKriteria( + Titled(OsobniCislo(p._1)), + Titled(p._2), + Titled(p._3.replaceAll("--", ".")) + ), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / "kriterium" / segment[String] / "edit" / endOfSegments, + basePath = base + ) + ), + serializePage = _.toJson, + deserializePage = _.fromJson[Page] + .fold(s => throw IllegalStateException(s), identity), + getPageTitle = _.title, + routeFallback = url => Page.NotFound(url), + deserializeFallback = _ => Page.Dashboard + )( + $popStateEvent = windowEvents.onPopState, + owner = unsafeWindowOwner + ) diff --git a/app/src/main/scala/mdr/pdb/app/actions.scala b/app/src/main/scala/mdr/pdb/app/actions.scala new file mode 100644 index 0000000..d078b3a --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/actions.scala @@ -0,0 +1,21 @@ +package mdr.pdb.app + +import mdr.pdb.OsobniCislo +import mdr.pdb.UserInfo +import mdr.pdb.Parameter +import mdr.pdb.ParameterCriteria + +sealed trait Action + +case object FetchDirectory extends Action +case class FetchUserDetails(osc: OsobniCislo) extends Action +case class FetchParameters(osc: OsobniCislo) extends Action +case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action +case class FetchParameterCriteria( + osc: OsobniCislo, + paramId: String, + critId: String, + page: (UserInfo, Parameter, ParameterCriteria) => Page +) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action +case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/mdr/pdb/app/components/AppPage.scala new file mode 100644 index 0000000..130c817 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/AppPage.scala @@ -0,0 +1,82 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.Page +import mdr.pdb.{UserProfile, UserInfo, OsobniCislo} +import com.raquo.waypoint.Router +import mdr.pdb.app.Action +import mdr.pdb.app.NavigateTo +import mdr.pdb.UserFunction + +object AppPage: + // TODO: pages by logged in user + val pages: List[Page] = List(Page.Directory, Page.Dashboard) + + import NavigationBar.{Logo, MenuItem} + + val logo = Logo( + "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", + "Workflow" + ) + + // TODO: menu items by user profile + val userMenu = + List( + MenuItem("Your Profile"), + MenuItem("Settings"), + MenuItem("Sign out") + ) + + // TODO: load user profile + val $userProfile = Var( + UserProfile( + "tom", + UserInfo( + OsobniCislo("1031"), + "tom", + "Tom", + "Cook", + None, + None, + Some("tom@example.com"), + Some("+420 222 866 180"), + Some( + UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") + ), + Nil, + None + ) + ) + ) + + val $userInfo = $userProfile.signal.map(_.userInfo) + + type ViewModel = Option[HtmlElement] + def apply( + actionBus: Observer[Action] + )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using + router: Router[Page] + ): HtmlElement = + PageLayout(actionBus)( + $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => + PageLayout.ViewModel( + NavigationBar.ViewModel( + u, + pages.map(p => + NavigationBar.Link( + () => + PageLink( + p, + actionBus + ), + p == cp + ) + ), + userMenu, + logo + ), + c + ) + ), + mods + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala new file mode 100644 index 0000000..9375398 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala @@ -0,0 +1,143 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.tailwind.list.IconText.ViewModel +import fiftyforms.ui.components.tailwind.Icons +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.Page + +import com.raquo.laminar.api.L.{*, given} +import io.laminext.syntax.core.{*, given} +import mdr.pdb.app.Action + +object Breadcrumbs: + + private def slash = + import svg.{*, given} + svg( + cls := "flex-shrink-0 h-5 w-5 text-gray-300", + xmlns := "http://www.w3.org/2000/svg", + fill := "currentColor", + viewBox := "0 0 20 20", + ariaHidden := true, + path( + d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" + ) + ) + + def backIcon = + Icons.solid + .`arrow-narrow-left`() + .amend( + svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" + ) + + object Link: + case class ViewModel( + page: Page, + icon: Option[SvgElement], + extraClasses: String, + text: String, + textClasses: Option[String] = None + ) + + val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" + + def shortHome(p: Page) = ViewModel( + p, + Some(backIcon), + "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", + "Zpět na úvodní stránku", + None + ) + + def fullHome(p: Page) = ViewModel( + p, + Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), + "text-gray-400 hover:text-gray-500", + "Domů", + Some("sr-only") + ) + + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + PageLink + .container($m.map(_.page), actionBus) + .amend( + cls <-- $m.map(_.extraClasses), + child.maybe <-- $m.map(_.icon), + span( + cls <-- $m.map(_.textClasses.getOrElse("")), + child.text <-- $m.map(_.text) + ) + ) + + object FullBreadcrumbs: + type ViewModel = Page + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + ol( + role := "list", + cls := "flex items-center space-x-4", + children <-- $m.map(_.path).split(_.id)((_, _, $p) => + li( + div( + cls := "flex items-center", + child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), + Link( + $p.map(p => + if (p.isRoot) then Link.fullHome(p) + else + Link.ViewModel( + p, + None, + s"ml-4 max-w-xs truncate ${Link.baseClasses}", + p.title + ) + ), + actionBus + ) + ) + ) + ) + ) + + object ShortBreadcrumbs: + type ViewModel = Page + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + Link( + $m.map { p => + val target = p.parent.getOrElse(p) + if target.isRoot then Link.shortHome(target) + else + Link.ViewModel( + target, + Some(backIcon), + "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", + s"Zpět na ${target.title}" + ) + }, + actionBus + ) + + def apply(actionBus: Observer[Action])(using + router: Router[Page] + ): HtmlElement = + nav( + cls := "flex", + aria.label := "Breadcrumb", + div( + cls := "flex sm:hidden", + ShortBreadcrumbs(router.$currentPage, actionBus) + ), + div( + cls := "hidden sm:block", + FullBreadcrumbs(router.$currentPage, actionBus) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala new file mode 100644 index 0000000..4bb8b8d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala @@ -0,0 +1,242 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent +import fiftyforms.ui.components.tailwind.Icons +import fiftyforms.ui.components.tailwind.Avatar +import mdr.pdb.UserInfo +import io.laminext.syntax.core.* + +object NavigationBar: + + case class Logo(img: String, name: String) + case class Link(a: () => Anchor, active: Boolean) + case class MenuItem(title: String) + + case class ViewModel( + userInfo: UserInfo, + pages: List[Link], + userMenu: List[MenuItem], + logo: Logo + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + val $userInfo = $m.map(_.userInfo) + val mobileMenuOpen = Var(false) + + val desktopOnly = cls("hidden md:block") + val mobileOnly = cls("md:hidden") + + inline def avatarImage(size: Int = 8) = + Avatar($userInfo.map(_.img)).avatarImage(size) + + def notificationButton = button( + tpe := "button", + cls := List( + "bg-indigo-600", + "focus:outline-none", + "focus:ring-2", + "focus:ring-offset-2", + "focus:ring-offset-indigo-600", + "focus:ring-white", + "hover:text-white", + "p-1", + "rounded-full", + "text-indigo-200" + ), + span(cls := "sr-only", "View notifications"), + Icons.outline.bell() + ) + + def userProfile: HtmlElement = + val menuOpen = Var(false) + + def menuItem(item: MenuItem, idx: Int): HtmlElement = + a( + href := "#", + cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", + role := "menuitem", + tabIndex := -1, + idAttr := s"user-menu-item-$idx", + item.title + ) + + div( + cls := "ml-3 relative", + div( + button( + tpe := "button", + cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", + idAttr := "user-menu-button", + aria.expanded <-- menuOpen.signal, + aria.hasPopup := true, + span(cls := "sr-only", "Open user menu"), + child <-- avatarImage(), + onClick.preventDefault.mapTo( + !menuOpen.now() + ) --> menuOpen.writer + ) + ), + /* + * */ + div( + cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", + cls <-- menuOpen.signal.map { o => + if (o) "md:block" else "md:hidden" + }, + role := "menu", + aria.orientation := "vertical", + aria.labelledBy := "user-menu-button", + tabIndex := -1, + // : keyboard navigation + children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) + ) + ) + + def mobileProfile = + def menuItem(item: MenuItem): HtmlElement = + a( + href := "#", + cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", + item.title + ) + + div( + cls := "pt-4 pb-3 border-t border-indigo-700", + div( + cls := "flex items-center px-5", + div( + cls := "flex-shrink-0", + child <-- avatarImage(10) + ), + div( + cls := "ml-3", + div( + cls := "text-base font-medium text-white", + child.text <-- $userInfo.map(_.name) + ), + child.maybe <-- $userInfo.map( + _.email.map(e => + div( + cls := "text-sm font-medium text-indigo-300", + e + ) + ) + ) + ), + notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) + ), + div( + cls := "mt-3 px-2 space-y-1", + children <-- $m.map(_.userMenu.map(menuItem)) + ) + ) + + def pageLink(page: Link): Anchor = + page + .a() + .amend( + cls := "text-white px-3 py-2 rounded-md text-sm font-medium", + cls := Seq( + "bg-indigo-700" -> page.active, + "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active + ), + ariaCurrent := (if page.active then "page" else "false") + ) + + def logoImg: Image = + img( + cls := "h-8 w-8", + src <-- $m.map(_.logo.img), + alt <-- $m.map(_.logo.name) + ) + + def mobileMenuButton = button( + tpe := "button", + cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", + aria.controls := "mobile-menu", + aria.expanded <-- mobileMenuOpen.signal, + span(cls := "sr-only", "Open main menu"), + Icons.outline + .menu() + .amend( + svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") + ), + Icons.outline + .x() + .amend( + svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") + ), + onClick.preventDefault.mapTo( + !mobileMenuOpen.now() + ) --> mobileMenuOpen.writer + ) + + def navBarLeft = + div( + cls := "flex items-center", + div(cls := "flex-shrink-0", logoImg), + div( + desktopOnly, + div( + cls := "ml-10 flex items-baseline space-x-4", + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) + ) + ) + ) + + def navBarRight = + div( + desktopOnly, + div( + cls := "ml-4 flex items-center md:ml-6", + notificationButton, + userProfile + ) + ) + + def navBarMobile = + div( + cls := "-mr-2 flex", + mobileOnly, + mobileMenuButton + ) + + def navBar = + div( + cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", + div( + cls := "flex items-center justify-between h-16", + navBarLeft, + navBarRight, + navBarMobile + ) + ) + + def mobileMenu = + div( + mobileOnly, + cls <-- mobileMenuOpen.signal.map { o => + if (o) "block" else "hidden" + }, + idAttr := "mobile-menu", + div( + cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) + ), + mobileProfile + ) + + nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala new file mode 100644 index 0000000..c4cb10d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala @@ -0,0 +1,19 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.Page +import com.raquo.waypoint.Router +import mdr.pdb.app.Action + +object PageHeader: + def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = + header( + cls := "bg-white shadow-sm", + div( + cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", + h1( + cls := "text-lg leading-6 font-semibold text-gray-900", + Breadcrumbs(actionBus) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala new file mode 100644 index 0000000..aae4539 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala @@ -0,0 +1,30 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.Page +import mdr.pdb.app.Action +import fiftyforms.ui.components.tailwind.Loading +import io.laminext.syntax.core.* + +object PageLayout: + case class ViewModel( + navigation: NavigationBar.ViewModel, + content: Option[HtmlElement] + ) + def apply(actionBus: Observer[Action])( + $m: Signal[ViewModel], + mods: Modifier[HtmlElement]* + )(using router: Router[Page]): HtmlElement = + val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) + div( + cls := "h-full flex flex-col", + NavigationBar($m.map(_.navigation)), + child.maybe <-- router.$currentPage.map(_.isRoot) + .switch(None, Some(PageHeader(actionBus))), + main( + cls := "flex-grow-1 overflow-y-auto", + mods, + child <-- $maybeContent.map(_.getOrElse(Loading)) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/mdr/pdb/app/components/PageLink.scala new file mode 100644 index 0000000..e6e7052 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageLink.scala @@ -0,0 +1,40 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.LinkSupport.* +import mdr.pdb.app.Page +import mdr.pdb.app.Action +import com.raquo.waypoint.Router +import mdr.pdb.app.NavigateTo + +object PageLink: + type ViewModel = Page + def apply($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = + a(mods($m, actions), child.text <-- $m.map(_.title)) + + def apply(m: ViewModel, actions: Observer[Action])(using + router: Router[Page] + ): Anchor = apply(Val(m), actions) + + def container($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = a(mods($m, actions)) + + def container(m: ViewModel, actions: Observer[Action])(using + Router[Page] + ): Anchor = container(Val(m), actions) + + private def mods($m: Signal[Page], actions: Observer[Action])(using + router: Router[Page] + ): Modifier[Anchor] = + Seq( + href <-- $m.map(router.absoluteUrlForPage).recover { case _ => + Some("invalid url") + }, + composeEvents(onClick.noKeyMod.preventDefault)( + _.sample($m) + .map(NavigateTo.apply) + ) --> actions + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala new file mode 100644 index 0000000..7682772 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala @@ -0,0 +1,13 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.pages.dashboard.DashboardPage +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage + +class DashboardPageConnector(actionBus: Observer[Action])(using + router: Router[Page] +): + def apply: HtmlElement = + AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala new file mode 100644 index 0000000..76b9133 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala @@ -0,0 +1,76 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.Parameter +import mdr.pdb.UserInfo +import mdr.pdb.app.pages.detail.DetailParametruPage +import pages.detail.DetailKriteriaPage +import pages.detail.components.DukazyKriteria +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.ParameterCriteria + +object DetailKriteriaPageConnector { + trait AppState { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + } +} + +case class DetailKriteriaPageConnector( + state: DetailKriteriaPageConnector.AppState +)( + $page: Signal[Page.DetailKriteria] +)(using Router[Page]): + val $paramChangeSignal = + $page.splitOne(p => + (p.osobniCislo.value, p.parametr.value, p.kriterium.value) + )((x, _, _) => x) + val $pageChangeSignal = + $paramChangeSignal.map( + FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) + ) + + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + val $merged = + $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => + for { + da <- d + pa <- p + pb <- pa.find(_.id == pc._2) + ka <- pb.criteria.find(_.id == pc._3) + } yield (da, pb, ka) + ) + + def apply: HtmlElement = + AppPage(state.actionBus)( + $merged.map(_.map(buildModel)) + .split(_ => ())((_, s, $s) => + DetailKriteriaPage($s)(state.actionBus.contramap { + case DukazyKriteria.Add => + NavigateTo( + Page.UpravDukazKriteria( + Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), + Page.Titled(s.parametr.id, Some(s.parametr.nazev)), + Page.Titled(s.kriterium.id, Some(s.kriterium.id)) + ) + ) + }) + ), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: Parameter, + k: ParameterCriteria + ): DetailKriteriaPage.ViewModel = + DetailKriteriaPage.ViewModel( + o.toDetailOsoby, + p.toParametr(_ => a()), + k.toKriterium(_ => a()) + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DetailPageConnector.scala new file mode 100644 index 0000000..df3a6b7 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DetailPageConnector.scala @@ -0,0 +1,57 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.UserInfo +import pages.detail.DetailPage +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.pages.detail.components.DetailOsoby +import mdr.pdb.Parameter +import mdr.pdb.app.pages.detail.components.SeznamParametru +import fiftyforms.ui.components.tailwind.Color + +object DetailPageConnector { + trait AppState { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + } +} + +case class DetailPageConnector(state: DetailPageConnector.AppState)( + $page: Signal[Page.Detail] +)(using router: Router[Page]): + val $oscChangeSignal = $page.splitOne(_.osobniCislo.value)((osc, _, _) => osc) + val $pageChangeSignal = + $oscChangeSignal.flatMap(osc => + EventStream.fromSeq(Seq(FetchUserDetails(osc), FetchParameters(osc))) + ) + // TODO: filter the value based on the current osc + // OSC change will fetch new data, but still + // - we need to be sure that what we got is really what we ought to display + // - we want to display stale data accordingly (at least with loading indicator) + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + def apply: HtmlElement = + AppPage(state.actionBus)( + $data.combineWithFn($params)(_ zip _) + .map(_.map(buildModel)) + .split(_ => ())((_, _, s) => DetailPage(s)), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: List[Parameter] + ): DetailPage.ViewModel = + DetailPage.ViewModel( + o.toDetailOsoby, + p.map( + _.toParametr(param => + PageLink.container(Page.DetailParametru(o, param), state.actionBus) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DetailParametruPageConnector.scala new file mode 100644 index 0000000..fa7193d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DetailParametruPageConnector.scala @@ -0,0 +1,67 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.Parameter +import mdr.pdb.UserInfo +import mdr.pdb.app.pages.detail.DetailParametruPage +import pages.detail.DetailParametruPage +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.components.PageLink + +object DetailParametruPageConnector { + trait AppState { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + } +} + +case class DetailParametruPageConnector( + state: DetailParametruPageConnector.AppState +)( + $page: Signal[Page.DetailParametru] +)(using router: Router[Page]): + val $paramChangeSignal = + $page.splitOne(p => (p.osobniCislo.value, p.parametr.value))((x, _, _) => x) + val $pageChangeSignal = + $paramChangeSignal.map(FetchParameter(_, _)) + + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + val $merged = + $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => + for { + da <- d + pa <- p + pb <- pa.find(_.id == pc._2) + } yield (da, pb) + ) + + def apply: HtmlElement = + AppPage(state.actionBus)( + $merged.map(_.map(buildModel)) + .split(_ => ())((_, _, s) => DetailParametruPage(s)), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: Parameter + ): DetailParametruPage.ViewModel = + DetailParametruPage.ViewModel( + o.toDetailOsoby, + p.toParametr(p => + PageLink.container(Page.DetailParametru(o, p), state.actionBus) + ), + p.criteria.map( + _.toKriterium { c => + PageLink.container( + Page.DetailKriteria(o, p, c), + state.actionBus + ) + } + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DirectoryPageConnector.scala new file mode 100644 index 0000000..ab2c045 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DirectoryPageConnector.scala @@ -0,0 +1,34 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.UserInfo +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.components.AppPage + +case class DirectoryPageConnector( + $input: EventStream[List[UserInfo]], + actionBus: Observer[Action] +)(using router: Router[Page]): + val $data = $input.startWithNone + val $actionSignal = EventStream.fromValue(FetchDirectory) + + def apply: HtmlElement = + AppPage(actionBus)( + $data.split(_ => ())((_, _, s) => + pages.directory.DirectoryPage( + s.map( + _.map( + _.toUserRow(u => + PageLink.container( + Page.Detail(Page.Titled(u.personalNumber)), + actionBus + ) + ) + ) + ) + ) + ), + $actionSignal --> actionBus + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/mdr/pdb/app/connectors/conversions.scala new file mode 100644 index 0000000..f581796 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/conversions.scala @@ -0,0 +1,68 @@ +package mdr.pdb.app.connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.UserInfo +import mdr.pdb.app.pages.detail.components.DetailOsoby +import mdr.pdb.Parameter +import mdr.pdb.app.pages.detail.components.SeznamParametru +import mdr.pdb.ParameterCriteria +import mdr.pdb.app.pages.detail.components.SeznamKriterii +import mdr.pdb.app.pages.directory.components.UserRow +import mdr.pdb.app.pages.detail.components.DetailParametru +import mdr.pdb.app.pages.detail.components.DetailKriteria +import fiftyforms.ui.components.tailwind.Color + +extension (o: UserInfo) + def toDetailOsoby: DetailOsoby.ViewModel = + DetailOsoby.ViewModel( + o.personalNumber, + o.name, + o.email, + o.phone, + o.img, + o.mainFunction.map(f => + DetailOsoby.Funkce.ViewModel(f.name, f.dept, f.ou) + ), + o.userContracts.headOption.map(c => + DetailOsoby.PracovniPomer.ViewModel(c.rel, c.startDate, c.endDate) + ) + ) + +extension (param: Parameter) + def toParametr(container: Parameter => Anchor): DetailParametru.ViewModel = + DetailParametru.ViewModel( + id = param.id, + nazev = param.name, + popis = param.description, + status = "Nesplněno", + statusColor = Color.red, + a = container(param) + ) + +extension (crit: ParameterCriteria) + def toKriterium( + container: ParameterCriteria => Anchor + ): DetailKriteria.ViewModel = + DetailKriteria.ViewModel( + nazev = crit.criteriumText, + kapitola = crit.chapterId, + bod = crit.itemId, + status = "Nesplněno", + statusColor = Color.red, + splneno = false, + dukazy = Nil, + container = container(crit) + ) + +extension (user: UserInfo) + def toUserRow( + container: UserInfo => HtmlElement = (_: UserInfo) => div() + ): UserRow.ViewModel = + UserRow.ViewModel( + osobniCislo = user.personalNumber.toString, + celeJmeno = user.name, + prijmeni = user.surname, + hlavniFunkce = user.mainFunction.map(_.name), + img = user.img, + container = () => container(user) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/dashboard/DashboardPage.scala b/app/src/main/scala/mdr/pdb/app/pages/dashboard/DashboardPage.scala new file mode 100644 index 0000000..0eb8f7a --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/dashboard/DashboardPage.scala @@ -0,0 +1,11 @@ +package mdr.pdb.app.pages.dashboard + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.Page + +object DashboardPage: + + def render: HtmlElement = + div("Dashboard page") diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala new file mode 100644 index 0000000..ff3a25f --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala @@ -0,0 +1,31 @@ +package mdr.pdb.app.pages.detail + +import com.raquo.laminar.api.L.{*, given} + +import components._ + +object DetailKriteriaPage: + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametr: DetailParametru.ViewModel, + kriterium: DetailKriteria.ViewModel + ) + + def apply($m: Signal[ViewModel])( + action: Observer[DukazyKriteria.Action] + ): HtmlElement = + div( + cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + div( + DetailOsoby.header($m.map(_.osoba)), + DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") + ), + div( + DetailKriteria($m.map(_.kriterium)), + DukazyKriteria($m.map(_.kriterium.dukazy))(action) + ) + ) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala deleted file mode 100644 index a3b00af..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala +++ /dev/null @@ -1,103 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import scala.scalajs.js.annotation.JSExportTopLevel -import scala.scalajs.js.annotation.JSExport -import scala.scalajs.js.annotation.JSImport -import scala.scalajs.js -import org.scalajs.dom -import com.raquo.laminar.api.L.{*, given} -import scala.scalajs.js.Date -import com.raquo.waypoint.Router -import com.raquo.waypoint.SplitRender -import cz.e_bs.cmi.mdr.pdb.app.services.DataFetcher -import scala.scalajs.js.JSON -import zio.json._ -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.state.AppState - -@js.native -@JSImport("stylesheets/main.css", JSImport.Namespace) -object Css extends js.Any - -@js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native -@JSImport("params/pdb-params.json", JSImport.Default) -object pdbParams extends js.Object - -@JSExportTopLevel("app") -object Main: - - @JSExport - def main(args: Array[String]): Unit = - import Routes.given - onLoad { - setupAirstream() - val appContainer = dom.document.querySelector("#app") - val _ = - render( - appContainer, - renderPage(state.MockAppState(using unsafeWindowOwner, router)) - ) - } - - private def onLoad(f: => Unit): Unit = - documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) - - private def setupAirstream()(using router: Router[Page]): Unit = - AirstreamError.registerUnhandledErrorCallback(err => - router.forcePage( - Page.UnhandledError( - Some(err.getClass.getName), // TODO: Fill only in dev mode - Some(err.getMessage) - ) - ) - ) - - def renderPage(state: AppState)(using - router: Router[Page] - ): HtmlElement = - val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) - .collectSignal[Page.Detail]( - connectors - .DetailPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailParametru]( - connectors - .DetailParametruPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailKriteria]( - connectors - .DetailKriteriaPageConnector(state)(_) - .apply - ) - .collectSignal[Page.UpravDukazKriteria]( - pages.detail.UpravDukaz.Connector(state)(_).apply - ) - .collectStatic(Page.Dashboard)( - connectors.DashboardPageConnector(state.actionBus).apply - ) - .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) - ) - .collect[Page.UnhandledError](pg => - pages.errors - .UnhandledErrorPage( - pages.errors.UnhandledErrorPage - .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), - state.actionBus - ) - ) - .collectStatic(Page.Directory)( - connectors - .DirectoryPageConnector(state.users, state.actionBus) - .apply - ) - div(cls := "h-full", child <-- pageSplitter.$view) - - // Pull in the stylesheet - val css: Css.type = Css diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala deleted file mode 100644 index 3bbc8c0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ /dev/null @@ -1,198 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.* -import org.scalajs.dom -import zio.json.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import scala.scalajs.js -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.Page.Titled - -// enum is not working with Waypoints' SplitRender collectStatic -sealed abstract class Page( - val id: String, - val title: String, - val parent: Option[Page] -) { - val path: Vector[Page] = - parent match - case None => Vector(this) - case Some(p) => p.path :+ this - - val isRoot: Boolean = parent.isEmpty -} - -object Page: - - case class Titled[V](value: V, title: Option[String] = None): - val show: String = title.getOrElse(value.toString) - - case object Directory extends Page("directory", "Adresář", None) - - case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) - - case class Detail(osobniCislo: Titled[OsobniCislo]) - extends Page("user", osobniCislo.show, Some(Directory)) - - object Detail { - def apply(o: UserInfo): Detail = Detail( - Titled(o.personalNumber, Some(o.name)) - ) - } - - case class DetailParametru( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String] - ) extends Page( - "parameter", - parametr.show, - Some(Detail(osobniCislo)) - ) - - object DetailParametru { - def apply(o: UserInfo, p: Parameter): DetailParametru = - DetailParametru( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)) - ) - } - - case class DetailKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "criteria", - kriterium.show, - Some(DetailParametru(osobniCislo, parametr)) - ) - - object DetailKriteria { - def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = - DetailKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class UpravDukazKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "addProof", - "Důkaz", - Some(DetailKriteria(osobniCislo, parametr, kriterium)) - ) - - object UpravDukazKriteria { - def apply( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): UpravDukazKriteria = - UpravDukazKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class NotFound(url: String) extends Page("404", "404", Some(Directory)) - - case class UnhandledError( - errorName: Option[String], - errorMessage: Option[String] - ) extends Page("500", "Unexpected error", Some(Directory)) - -object Routes: - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) - given [V: JsonEncoder]: JsonEncoder[Titled[V]] = - DeriveJsonEncoder.gen[Titled[V]] - given [V: JsonDecoder]: JsonDecoder[Titled[V]] = - DeriveJsonDecoder.gen[Titled[V]] - given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] - given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] - - val base = - js.`import`.meta.env.BASE_URL - .asInstanceOf[String] + "app" - - val homePage: Page = Page.Directory - - given router: Router[Page] = Router[Page]( - routes = List( - Route.static(homePage, root / endOfSegments, basePath = base), - Route.static( - Page.Dashboard, - root / "dashboard" / endOfSegments, - basePath = base - ), - Route[Page.Detail, String]( - encode = _.osobniCislo.value.toString, - decode = osc => Page.Detail(Titled(OsobniCislo(osc))), - root / "osoba" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.DetailParametru, (String, String)]( - encode = p => (p.osobniCislo.value.toString, p.parametr.value), - decode = - p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / endOfSegments, - basePath = base - ), - Route[Page.DetailKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.DetailKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.UpravDukazKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.UpravDukazKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / "edit" / endOfSegments, - basePath = base - ) - ), - serializePage = _.toJson, - deserializePage = _.fromJson[Page] - .fold(s => throw IllegalStateException(s), identity), - getPageTitle = _.title, - routeFallback = url => Page.NotFound(url), - deserializeFallback = _ => Page.Dashboard - )( - $popStateEvent = windowEvents.onPopState, - owner = unsafeWindowOwner - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala deleted file mode 100644 index b24b82b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -sealed trait Action - -case object FetchDirectory extends Action -case class FetchUserDetails(osc: OsobniCislo) extends Action -case class FetchParameters(osc: OsobniCislo) extends Action -case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action -case class FetchParameterCriteria( - osc: OsobniCislo, - paramId: String, - critId: String, - page: (UserInfo, Parameter, ParameterCriteria) => Page -) extends Action -case class FetchAvailableFiles(osc: OsobniCislo) extends Action -case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala deleted file mode 100644 index c637d35..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala +++ /dev/null @@ -1,82 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.{UserProfile, UserInfo, OsobniCislo} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo -import cz.e_bs.cmi.mdr.pdb.UserFunction - -object AppPage: - // TODO: pages by logged in user - val pages: List[Page] = List(Page.Directory, Page.Dashboard) - - import NavigationBar.{Logo, MenuItem} - - val logo = Logo( - "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", - "Workflow" - ) - - // TODO: menu items by user profile - val userMenu = - List( - MenuItem("Your Profile"), - MenuItem("Settings"), - MenuItem("Sign out") - ) - - // TODO: load user profile - val $userProfile = Var( - UserProfile( - "tom", - UserInfo( - OsobniCislo("1031"), - "tom", - "Tom", - "Cook", - None, - None, - Some("tom@example.com"), - Some("+420 222 866 180"), - Some( - UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") - ), - Nil, - None - ) - ) - ) - - val $userInfo = $userProfile.signal.map(_.userInfo) - - type ViewModel = Option[HtmlElement] - def apply( - actionBus: Observer[Action] - )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using - router: Router[Page] - ): HtmlElement = - PageLayout(actionBus)( - $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => - PageLayout.ViewModel( - NavigationBar.ViewModel( - u, - pages.map(p => - NavigationBar.Link( - () => - PageLink( - p, - actionBus - ), - p == cp - ) - ), - userMenu, - logo - ), - c - ) - ), - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala deleted file mode 100644 index bf473c6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ /dev/null @@ -1,143 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden -import fiftyforms.ui.components.tailwind.list.IconText.ViewModel -import fiftyforms.ui.components.tailwind.Icons -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Action - -object Breadcrumbs: - - private def slash = - import svg.{*, given} - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-300", - xmlns := "http://www.w3.org/2000/svg", - fill := "currentColor", - viewBox := "0 0 20 20", - ariaHidden := true, - path( - d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" - ) - ) - - def backIcon = - Icons.solid - .`arrow-narrow-left`() - .amend( - svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" - ) - - object Link: - case class ViewModel( - page: Page, - icon: Option[SvgElement], - extraClasses: String, - text: String, - textClasses: Option[String] = None - ) - - val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" - - def shortHome(p: Page) = ViewModel( - p, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - "Zpět na úvodní stránku", - None - ) - - def fullHome(p: Page) = ViewModel( - p, - Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), - "text-gray-400 hover:text-gray-500", - "Domů", - Some("sr-only") - ) - - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - PageLink - .container($m.map(_.page), actionBus) - .amend( - cls <-- $m.map(_.extraClasses), - child.maybe <-- $m.map(_.icon), - span( - cls <-- $m.map(_.textClasses.getOrElse("")), - child.text <-- $m.map(_.text) - ) - ) - - object FullBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - ol( - role := "list", - cls := "flex items-center space-x-4", - children <-- $m.map(_.path).split(_.id)((_, _, $p) => - li( - div( - cls := "flex items-center", - child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), - Link( - $p.map(p => - if (p.isRoot) then Link.fullHome(p) - else - Link.ViewModel( - p, - None, - s"ml-4 max-w-xs truncate ${Link.baseClasses}", - p.title - ) - ), - actionBus - ) - ) - ) - ) - ) - - object ShortBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - Link( - $m.map { p => - val target = p.parent.getOrElse(p) - if target.isRoot then Link.shortHome(target) - else - Link.ViewModel( - target, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - s"Zpět na ${target.title}" - ) - }, - actionBus - ) - - def apply(actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - nav( - cls := "flex", - aria.label := "Breadcrumb", - div( - cls := "flex sm:hidden", - ShortBreadcrumbs(router.$currentPage, actionBus) - ), - div( - cls := "hidden sm:block", - FullBreadcrumbs(router.$currentPage, actionBus) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala deleted file mode 100644 index 0db2047..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ /dev/null @@ -1,242 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent -import fiftyforms.ui.components.tailwind.Icons -import fiftyforms.ui.components.tailwind.Avatar -import cz.e_bs.cmi.mdr.pdb.UserInfo -import io.laminext.syntax.core.* - -object NavigationBar: - - case class Logo(img: String, name: String) - case class Link(a: () => Anchor, active: Boolean) - case class MenuItem(title: String) - - case class ViewModel( - userInfo: UserInfo, - pages: List[Link], - userMenu: List[MenuItem], - logo: Logo - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - val $userInfo = $m.map(_.userInfo) - val mobileMenuOpen = Var(false) - - val desktopOnly = cls("hidden md:block") - val mobileOnly = cls("md:hidden") - - inline def avatarImage(size: Int = 8) = - Avatar($userInfo.map(_.img)).avatarImage(size) - - def notificationButton = button( - tpe := "button", - cls := List( - "bg-indigo-600", - "focus:outline-none", - "focus:ring-2", - "focus:ring-offset-2", - "focus:ring-offset-indigo-600", - "focus:ring-white", - "hover:text-white", - "p-1", - "rounded-full", - "text-indigo-200" - ), - span(cls := "sr-only", "View notifications"), - Icons.outline.bell() - ) - - def userProfile: HtmlElement = - val menuOpen = Var(false) - - def menuItem(item: MenuItem, idx: Int): HtmlElement = - a( - href := "#", - cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", - role := "menuitem", - tabIndex := -1, - idAttr := s"user-menu-item-$idx", - item.title - ) - - div( - cls := "ml-3 relative", - div( - button( - tpe := "button", - cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - idAttr := "user-menu-button", - aria.expanded <-- menuOpen.signal, - aria.hasPopup := true, - span(cls := "sr-only", "Open user menu"), - child <-- avatarImage(), - onClick.preventDefault.mapTo( - !menuOpen.now() - ) --> menuOpen.writer - ) - ), - /* - * */ - div( - cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", - cls <-- menuOpen.signal.map { o => - if (o) "md:block" else "md:hidden" - }, - role := "menu", - aria.orientation := "vertical", - aria.labelledBy := "user-menu-button", - tabIndex := -1, - // : keyboard navigation - children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) - ) - ) - - def mobileProfile = - def menuItem(item: MenuItem): HtmlElement = - a( - href := "#", - cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", - item.title - ) - - div( - cls := "pt-4 pb-3 border-t border-indigo-700", - div( - cls := "flex items-center px-5", - div( - cls := "flex-shrink-0", - child <-- avatarImage(10) - ), - div( - cls := "ml-3", - div( - cls := "text-base font-medium text-white", - child.text <-- $userInfo.map(_.name) - ), - child.maybe <-- $userInfo.map( - _.email.map(e => - div( - cls := "text-sm font-medium text-indigo-300", - e - ) - ) - ) - ), - notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) - ), - div( - cls := "mt-3 px-2 space-y-1", - children <-- $m.map(_.userMenu.map(menuItem)) - ) - ) - - def pageLink(page: Link): Anchor = - page - .a() - .amend( - cls := "text-white px-3 py-2 rounded-md text-sm font-medium", - cls := Seq( - "bg-indigo-700" -> page.active, - "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active - ), - ariaCurrent := (if page.active then "page" else "false") - ) - - def logoImg: Image = - img( - cls := "h-8 w-8", - src <-- $m.map(_.logo.img), - alt <-- $m.map(_.logo.name) - ) - - def mobileMenuButton = button( - tpe := "button", - cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - aria.controls := "mobile-menu", - aria.expanded <-- mobileMenuOpen.signal, - span(cls := "sr-only", "Open main menu"), - Icons.outline - .menu() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") - ), - Icons.outline - .x() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") - ), - onClick.preventDefault.mapTo( - !mobileMenuOpen.now() - ) --> mobileMenuOpen.writer - ) - - def navBarLeft = - div( - cls := "flex items-center", - div(cls := "flex-shrink-0", logoImg), - div( - desktopOnly, - div( - cls := "ml-10 flex items-baseline space-x-4", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ) - ) - ) - - def navBarRight = - div( - desktopOnly, - div( - cls := "ml-4 flex items-center md:ml-6", - notificationButton, - userProfile - ) - ) - - def navBarMobile = - div( - cls := "-mr-2 flex", - mobileOnly, - mobileMenuButton - ) - - def navBar = - div( - cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex items-center justify-between h-16", - navBarLeft, - navBarRight, - navBarMobile - ) - ) - - def mobileMenu = - div( - mobileOnly, - cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }, - idAttr := "mobile-menu", - div( - cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ), - mobileProfile - ) - - nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala deleted file mode 100644 index 75339df..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object PageHeader: - def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = - header( - cls := "bg-white shadow-sm", - div( - cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", - h1( - cls := "text-lg leading-6 font-semibold text-gray-900", - Breadcrumbs(actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala deleted file mode 100644 index 5096463..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ /dev/null @@ -1,30 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import fiftyforms.ui.components.tailwind.Loading -import io.laminext.syntax.core.* - -object PageLayout: - case class ViewModel( - navigation: NavigationBar.ViewModel, - content: Option[HtmlElement] - ) - def apply(actionBus: Observer[Action])( - $m: Signal[ViewModel], - mods: Modifier[HtmlElement]* - )(using router: Router[Page]): HtmlElement = - val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) - div( - cls := "h-full flex flex-col", - NavigationBar($m.map(_.navigation)), - child.maybe <-- router.$currentPage.map(_.isRoot) - .switch(None, Some(PageHeader(actionBus))), - main( - cls := "flex-grow-1 overflow-y-auto", - mods, - child <-- $maybeContent.map(_.getOrElse(Loading)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala deleted file mode 100644 index 62d820f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.LinkSupport.* -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo - -object PageLink: - type ViewModel = Page - def apply($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = - a(mods($m, actions), child.text <-- $m.map(_.title)) - - def apply(m: ViewModel, actions: Observer[Action])(using - router: Router[Page] - ): Anchor = apply(Val(m), actions) - - def container($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = a(mods($m, actions)) - - def container(m: ViewModel, actions: Observer[Action])(using - Router[Page] - ): Anchor = container(Val(m), actions) - - private def mods($m: Signal[Page], actions: Observer[Action])(using - router: Router[Page] - ): Modifier[Anchor] = - Seq( - href <-- $m.map(router.absoluteUrlForPage).recover { case _ => - Some("invalid url") - }, - composeEvents(onClick.noKeyMod.preventDefault)( - _.sample($m) - .map(NavigateTo.apply) - ) --> actions - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala deleted file mode 100644 index 87f825a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.pages.dashboard.DashboardPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -class DashboardPageConnector(actionBus: Observer[Action])(using - router: Router[Page] -): - def apply: HtmlElement = - AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala deleted file mode 100644 index e9b0f71..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala +++ /dev/null @@ -1,76 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailKriteriaPage -import pages.detail.components.DukazyKriteria -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -object DetailKriteriaPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailKriteriaPageConnector( - state: DetailKriteriaPageConnector.AppState -)( - $page: Signal[Page.DetailKriteria] -)(using Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => - (p.osobniCislo.value, p.parametr.value, p.kriterium.value) - )((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map( - FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) - ) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, s, $s) => - DetailKriteriaPage($s)(state.actionBus.contramap { - case DukazyKriteria.Add => - NavigateTo( - Page.UpravDukazKriteria( - Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), - Page.Titled(s.parametr.id, Some(s.parametr.nazev)), - Page.Titled(s.kriterium.id, Some(s.kriterium.id)) - ) - ) - }) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): DetailKriteriaPage.ViewModel = - DetailKriteriaPage.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala deleted file mode 100644 index 4d8024f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ /dev/null @@ -1,57 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import pages.detail.DetailPage -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import fiftyforms.ui.components.tailwind.Color - -object DetailPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailPageConnector(state: DetailPageConnector.AppState)( - $page: Signal[Page.Detail] -)(using router: Router[Page]): - val $oscChangeSignal = $page.splitOne(_.osobniCislo.value)((osc, _, _) => osc) - val $pageChangeSignal = - $oscChangeSignal.flatMap(osc => - EventStream.fromSeq(Seq(FetchUserDetails(osc), FetchParameters(osc))) - ) - // TODO: filter the value based on the current osc - // OSC change will fetch new data, but still - // - we need to be sure that what we got is really what we ought to display - // - we want to display stale data accordingly (at least with loading indicator) - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - def apply: HtmlElement = - AppPage(state.actionBus)( - $data.combineWithFn($params)(_ zip _) - .map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: List[Parameter] - ): DetailPage.ViewModel = - DetailPage.ViewModel( - o.toDetailOsoby, - p.map( - _.toParametr(param => - PageLink.container(Page.DetailParametru(o, param), state.actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala deleted file mode 100644 index fdaa7f8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailParametruPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink - -object DetailParametruPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailParametruPageConnector( - state: DetailParametruPageConnector.AppState -)( - $page: Signal[Page.DetailParametru] -)(using router: Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => (p.osobniCislo.value, p.parametr.value))((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map(FetchParameter(_, _)) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - } yield (da, pb) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailParametruPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter - ): DetailParametruPage.ViewModel = - DetailParametruPage.ViewModel( - o.toDetailOsoby, - p.toParametr(p => - PageLink.container(Page.DetailParametru(o, p), state.actionBus) - ), - p.criteria.map( - _.toKriterium { c => - PageLink.container( - Page.DetailKriteria(o, p, c), - state.actionBus - ) - } - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala deleted file mode 100644 index 049aa22..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -case class DirectoryPageConnector( - $input: EventStream[List[UserInfo]], - actionBus: Observer[Action] -)(using router: Router[Page]): - val $data = $input.startWithNone - val $actionSignal = EventStream.fromValue(FetchDirectory) - - def apply: HtmlElement = - AppPage(actionBus)( - $data.split(_ => ())((_, _, s) => - pages.directory.DirectoryPage( - s.map( - _.map( - _.toUserRow(u => - PageLink.container( - Page.Detail(Page.Titled(u.personalNumber)), - actionBus - ) - ) - ) - ) - ) - ), - $actionSignal --> actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala deleted file mode 100644 index 88e7450..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ /dev/null @@ -1,68 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii -import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria -import fiftyforms.ui.components.tailwind.Color - -extension (o: UserInfo) - def toDetailOsoby: DetailOsoby.ViewModel = - DetailOsoby.ViewModel( - o.personalNumber, - o.name, - o.email, - o.phone, - o.img, - o.mainFunction.map(f => - DetailOsoby.Funkce.ViewModel(f.name, f.dept, f.ou) - ), - o.userContracts.headOption.map(c => - DetailOsoby.PracovniPomer.ViewModel(c.rel, c.startDate, c.endDate) - ) - ) - -extension (param: Parameter) - def toParametr(container: Parameter => Anchor): DetailParametru.ViewModel = - DetailParametru.ViewModel( - id = param.id, - nazev = param.name, - popis = param.description, - status = "Nesplněno", - statusColor = Color.red, - a = container(param) - ) - -extension (crit: ParameterCriteria) - def toKriterium( - container: ParameterCriteria => Anchor - ): DetailKriteria.ViewModel = - DetailKriteria.ViewModel( - nazev = crit.criteriumText, - kapitola = crit.chapterId, - bod = crit.itemId, - status = "Nesplněno", - statusColor = Color.red, - splneno = false, - dukazy = Nil, - container = container(crit) - ) - -extension (user: UserInfo) - def toUserRow( - container: UserInfo => HtmlElement = (_: UserInfo) => div() - ): UserRow.ViewModel = - UserRow.ViewModel( - osobniCislo = user.personalNumber.toString, - celeJmeno = user.name, - prijmeni = user.surname, - hlavniFunkce = user.mainFunction.map(_.name), - img = user.img, - container = () => container(user) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala deleted file mode 100644 index 7d5b2c1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.dashboard - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.Page - -object DashboardPage: - - def render: HtmlElement = - div("Dashboard page") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala deleted file mode 100644 index 9ef0698..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala +++ /dev/null @@ -1,31 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailKriteriaPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply($m: Signal[ViewModel])( - action: Observer[DukazyKriteria.Action] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - DukazyKriteria($m.map(_.kriterium.dukazy))(action) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala deleted file mode 100644 index 3b316e1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object DetailPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametry: SeznamParametru.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby($m.map(_.osoba)), - SeznamParametru($m.map(_.parametry)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala deleted file mode 100644 index 17224cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala +++ /dev/null @@ -1,24 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailParametruPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriteria: SeznamKriterii.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby.header($m.map(_.osoba)), - DetailParametru($m.map(_.parametr)), - SeznamKriterii($m.map(_.kriteria)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala deleted file mode 100644 index dc912fa..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ /dev/null @@ -1,107 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb -package app -package pages.detail - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.UpravDukazForm -import fiftyforms.services.files.File - -object UpravDukaz: - - type ThisPage = Page.UpravDukazKriteria - type PageKey = (OsobniCislo, String, String) - - trait State { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def availableFiles: EventStream[List[File]] - def actionBus: Observer[Action] - } - - def keyOfPage(page: ThisPage): PageKey = - (page.osobniCislo.value, page.parametr.value, page.kriterium.value) - - def onChangeAction(key: PageKey): Action = - FetchParameterCriteria( - key._1, - key._2, - key._3, - Page.UpravDukazKriteria(_, _, _) - ) - - class Connector(state: State)($page: Signal[ThisPage])(using Router[Page]): - val $paramChangeSignal = $page.splitOne(keyOfPage)((x, _, _) => x) - val $pageChangeSignal = $paramChangeSignal.map(onChangeAction) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.split(_ => ())((_, s, $s) => - PageComponent( - $s.map(buildModel), - state.availableFiles, - state.actionBus.contramap { - case UpravDukazForm.Cancelled => - NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) - case UpravDukazForm.AvailableFilesRequested => - FetchAvailableFiles(s._1.personalNumber) - } - ) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): PageComponent.ViewModel = - import connectors.* - PageComponent.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) - - object PageComponent: - import components.* - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply( - $m: Signal[ViewModel], - availableFilesStream: EventStream[List[File]], - events: Observer[UpravDukazForm.Event] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - UpravDukazForm(availableFilesStream)(events) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala deleted file mode 100644 index ca311b0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.Icons -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Color - -object DetailKriteria: - case class ViewModel( - nazev: String, - kapitola: String, - bod: String, - status: String, - statusColor: Color, - splneno: Boolean, - dukazy: List[DukazKriteria.ViewModel], - container: HtmlElement = div() - ) { - val id = s"${kapitola}${bod}" - } - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h3( - cls := "text-xl leading-6 font-bold text-gray-900", - child.text <-- $m.map(_.nazev) - ), - h3( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - "Kapitola ", - child.text <-- $m.map(_.kapitola), - " bod ", - child <-- $m.map(_.bod) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala deleted file mode 100644 index ae90290..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala +++ /dev/null @@ -1,100 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Avatar - -object DetailOsoby: - - object Funkce: - case class ViewModel( - nazev: String, - stredisko: String, - voj: String - ) - - def render($m: Signal[ViewModel]): HtmlElement = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.nazev), - span( - cls := "hidden md:inline", - " @ ", - child.text <-- $m.map(_.stredisko), - ", ", - child.text <-- $m.map(_.voj) - ) - ) - - object PracovniPomer: - case class ViewModel( - druh: String, - pocatek: LocalDate, - konec: Option[LocalDate] - ) - - def render($m: Signal[ViewModel]): HtmlElement = - import fiftyforms.ui.components.tailwind.CustomAttrs.datetime - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.druh), - " od ", - time( - datetime <-- $m.map(_.pocatek.toString), - child.text <-- $m.map(_.pocatek.toString) - ) - ) - - case class ViewModel( - osobniCislo: OsobniCislo, - jmeno: String, - email: Option[String], - telefon: Option[String], - img: Option[String], - hlavniFunkce: Option[Funkce.ViewModel], - pracovniPomer: Option[PracovniPomer.ViewModel] - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-4", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(16) - ), - div( - h1( - cls := "text-2xl font-bold text-gray-900", - child.text <-- $m.map(_.jmeno) - ), - child.maybe <-- $m.map(_.hlavniFunkce).split(_ => ())((_, _, d) => - Funkce.render(d) - ), - child.maybe <-- $m.map(_.pracovniPomer).split(_ => ())((_, _, d) => - PracovniPomer.render(d) - ) - ) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-5", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(8) - ), - div( - h1( - cls := "text-lg font-medium text-gray-800", - child.text <-- $m.map(_.jmeno) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala deleted file mode 100644 index e11cb62..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color - -object DetailParametru: - case class ViewModel( - id: String, - nazev: String, - popis: String, - status: String, - statusColor: Color, - a: Anchor - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h2( - cls := "text-2xl leading-6 font-medium text-gray-900", - child.text <-- $m.map(_.nazev) - ), - p( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - child.text <-- $m.map(_.popis) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - h2( - cls := "text-lg leading-6 font-medium text-gray-600", - child.text <-- $m.map(_.nazev) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala deleted file mode 100644 index a00ede8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala +++ /dev/null @@ -1,177 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.CustomAttrs - -object DukazKriteria: - case class Osoba(osobniCislo: String, jmeno: String) - case class Udalost(osoba: Osoba, datum: LocalDate) - case class Dokument( - url: String, - nazev: String, - pridal: Udalost, - odebral: Option[Udalost] - ) - case class ViewModel( - dokumenty: List[Dokument], - autorizoval: Option[Udalost], - platiDo: Option[LocalDate], - poznámka: Option[String] - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-lg", - div( - cls := "px-4 py-5 sm:px-6", - h3( - cls := "text-lg leading-6 font-medium text-gray-900", - """Applicant Information""" - ), - p( - cls := "mt-1 max-w-2xl text-sm text-gray-500", - """Personal details and application.""" - ) - ), - div( - cls := "border-t border-gray-200 px-4 py-5 sm:px-6", - dl( - cls := "grid grid-cols-1 gap-x-4 gap-y-8 sm:grid-cols-2", - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Full name""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Margot Foster""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Application for""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Backend Developer""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Email address""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """margotfoster@example.com""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Salary expectation""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """$120,000""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """About""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Fugiat ipsum ipsum deserunt culpa aute sint do nostrud anim incididunt cillum culpa consequat. Excepteur qui ipsum aliquip consequat sint. Sit id mollit nulla mollit nostrud in ea officia proident. Irure nostrud pariatur mollit ad adipisicing reprehenderit deserunt qui eu.""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """Attachments""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - ul( - role := "list", - cls := "border border-gray-200 rounded-md divide-y divide-gray-200", - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """resume_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ), - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """coverletter_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala deleted file mode 100644 index 22f134f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala +++ /dev/null @@ -1,35 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object DukazyKriteria: - sealed trait Action - case object Add extends Action - - type ViewModel = List[DukazKriteria.ViewModel] - - def apply($m: Signal[ViewModel])(actions: Observer[Action]): HtmlElement = - div( - child.maybe <-- $m.map(m => - if (m.isEmpty) then Some(prazdnyDukaz(actions)) - else None - ), - children <-- $m.map(_.zipWithIndex).split(_._2)((_, _, $s) => - DukazKriteria($s.map(_._1)) - ) - ) - - private def prazdnyDukaz(actions: Observer[Action]): HtmlElement = - button( - tpe := "button", - onClick.preventDefault.mapTo(Add) --> actions, - cls := "relative block w-full border-2 border-gray-300 border-dashed rounded-lg p-12 text-center hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - Icons.outline - .`document-add`(12) - .amend(svg.cls := "mx-auto text-gray-400"), - span( - cls := "mt-2 block text-sm font-medium text-gray-900", - "Přidat důkaz" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala deleted file mode 100644 index f628697..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.list.StackedList -import fiftyforms.ui.components.tailwind.list.ListRow -import fiftyforms.ui.components.tailwind.list.RowTag -import fiftyforms.ui.components.tailwind.list.RowNext -import java.time.LocalDate - -object SeznamKriterii: - type ViewModel = List[DetailKriteria.ViewModel] - - private val kritList = new StackedList[DetailKriteria.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - kritList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = - p(cls := "text-sm text-gray-500", s"${i.kapitola}${i.bod}"), - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.container - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala deleted file mode 100644 index 2bfe4bc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.list.{ - StackedList, - ListRow, - RowTag, - PropList, - IconText, - RowNext -} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.LinkSupport.* - -object SeznamParametru: - type ViewModel = List[DetailParametru.ViewModel] - - private val parametrList = new StackedList[DetailParametru.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - parametrList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = emptyNode, - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.a - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala deleted file mode 100644 index f048e9d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ /dev/null @@ -1,101 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.form.* -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement -import fiftyforms.services.files.components.tailwind.FilePicker -import cz.e_bs.cmi.mdr.pdb.frontend.AutorizujDukaz -import cz.e_bs.cmi.mdr.pdb.frontend.DocumentRef -import fiftyforms.services.files.File - -object UpravDukazForm: - sealed trait Event - case object Cancelled extends Event - case object AvailableFilesRequested extends Event - def apply(availableFilesStream: EventStream[List[File]])( - updates: Observer[Event] - ): HtmlElement = - val (filesStream, filesObserver) = - EventStream.withObserver[FilePicker.Event] - val files = Var[List[File]](Nil) - def submitButtons: HtmlElement = - div( - cls := "pt-5", - div( - cls := "flex justify-end", - button( - tpe := "button", - cls := "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zrušit", - onClick.mapTo(Cancelled) --> updates - ), - button( - tpe := "submit", - cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - disabled <-- files.signal.map(_.isEmpty), - "Autorizovat" - ) - ) - ) - - div( - filesStream.collect { case FilePicker.SelectionUpdated(files) => - files.to(List) - } --> files.writer, - filesStream.collect { case FilePicker.AvailableFilesRequested => - AvailableFilesRequested - } --> updates, - cls := "bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6", - Form( - Form.Body( - Form.Section( - FormHeader( - FormHeader.ViewModel( - "Doložení kritéria", - "Sestavte doklady poskytující důkaz kritéria a potvrďte odesláním formuláře. Případné limitace či jiné relevantní údaje vepište do pole Komentář." - ) - ), - FormFields( - FormRow( - "dokumenty", - "Dokumenty", - FilePicker(files.signal, availableFilesStream)(filesObserver) - .amend(idAttr := "dokumenty", cls("max-w-lg")) - ).toHtml, - FormRow( - "platnost", - "Platnost", - input( - idAttr := "platnost", - name := "platnost", - tpe := "date", - autoComplete := "date", - cls := "block max-w-lg w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" - ) - ).toHtml, - FormRow( - "komentar", - "Komentář", - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - textArea( - idAttr := "komentar", - name := "about", - rows := 3, - cls := "max-w-lg shadow-sm block w-full focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border border-gray-300 rounded-md" - ), - p( - cls := "mt-2 text-sm text-gray-500", - "Doplňte prosím potřebné informace související s doložením kritéria, včetně případných limitací." - ) - ) - ).toHtml - ) - ) - ), - submitButtons - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala deleted file mode 100644 index 41033f6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -import components._ - -object DirectoryPage: - - type ViewModel = List[UserRow.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - val (actionsStream, actionObserver) = - EventStream.withObserver[SearchForm.Action] - val $filter = actionsStream - .collect { - case SearchForm.ClearFilter => None - case SearchForm.SetFilter(t) => Some(t) - } - .startWith(None) - val byLetter = for { - d <- $m - f <- $filter - } yield for { - (letter, users) <- d - .filter { user => - f match - case None => true - case Some(t) => user.search.contains(t) - } - .groupBy(_.prijmeni.head) - .to(List) - .sortBy(_._1) - } yield (letter.toString, users.sortBy(_.prijmeni)) - - div( - cls := "h-full max-w-7xl mx-auto order-first flex flex-col flex-shrink-0", - SearchForm(actionObserver), - Directory(byLetter) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala deleted file mode 100644 index 73f915d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -object Directory: - - object Header: - type ViewModel = String - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500", - h3(child.text <-- $m) - ) - - object UserList: - type ViewModel = List[UserRow.ViewModel] - def apply($m: Signal[ViewModel]): HtmlElement = - ul( - role := "list", - cls := "relative z-0 divide-y divide-gray-200", - children <-- $m.split(_.osobniCislo)((_, _, s) => UserRow(s)) - ) - - type ViewModel = List[(String, List[UserRow.ViewModel])] - def apply($m: Signal[ViewModel]): HtmlElement = - nav( - cls := "flex-1 min-h-0 overflow-y-auto", - aria.label := "Directory", - children <-- $m.split(_._1)((_, _, s) => - div( - cls := "relative", - Header(s.map(_._1)), - UserList(s.map(_._2)) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala deleted file mode 100644 index e182910..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala +++ /dev/null @@ -1,54 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object SearchForm: - sealed trait Action - case object SubmitFilter extends Action - sealed trait FilterAction extends Action - case object ClearFilter extends FilterAction - case class SetFilter(value: String) extends FilterAction - - def apply(actions: Observer[Action]): HtmlElement = - div( - cls := "px-6 pt-4 pb-4", - form( - cls := "flex space-x-4", - onSubmit.mapTo(SubmitFilter) --> actions, - div( - cls := "flex-1 min-w-0", - label( - forId := "search", - cls := "sr-only", - "Hledat" - ), - div( - cls := "relative rounded-md shadow-sm", - div( - cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", - Icons.solid.search().amend(svg.cls := "text-gray-400") - ), - input( - tpe := "search", - name := "search", - idAttr := "search", - cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", - placeholder := "Hledat", - composeEvents(onInput.mapToValue.setAsValue.map(SetFilter(_)))( - _.throttle(500) - ) --> actions - ) - ) - ), - button( - tpe := "submit", - cls := "inline-flex justify-center px-3.5 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500", - Icons.solid.filter().amend(svg.cls := "text-gray-400"), - span( - cls := "sr-only", - "Hledat" - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala deleted file mode 100644 index 82f7723..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala +++ /dev/null @@ -1,52 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.airstream.core.Signal -import fiftyforms.ui.components.tailwind.Avatar - -object UserRow: - case class ViewModel( - osobniCislo: String, - celeJmeno: String, - prijmeni: String, - hlavniFunkce: Option[String], - img: Option[String], - container: () => HtmlElement = () => div() - ) { - val search = prijmeni.toLowerCase - } - - def apply($m: Signal[ViewModel]): HtmlElement = - inline def avatarImage = - Avatar($m.map(_.img)).avatarImage(10) - - li( - div( - cls := "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500", - div( - cls := "flex-shrink-0", - child <-- avatarImage - ), - div( - cls := "flex-1 min-w-0", - child <-- $m.map { o => - o.container() - .amend( - cls := "focus:outline-none", - span( - cls := "absolute inset-0", - aria.hidden := true - ), - p( - cls := "text-sm font-medium text-gray-900", - o.celeJmeno - ), - p( - cls := "text-sm text-gray-500 truncate", - o.hlavniFunkce - ) - ) - } - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala deleted file mode 100644 index 467c5f5..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala +++ /dev/null @@ -1,99 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router - -object ErrorPage: - case class ViewModel( - homePage: Page, - errorName: String, - title: String, - subTitle: String - ) - - def apply(m: ViewModel, actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - val ViewModel(homePage, errorName, title, subTitle) = m - div( - cls := "min-h-full pt-16 pb-12 flex flex-col bg-white", - main( - cls := "flex-grow flex flex-col justify-center max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex-shrink-0 flex justify-center", - a( - href := "/", - cls := "inline-flex", - span( - cls := "sr-only", - """Workflow""" - ), - img( - cls := "h-12 w-auto", - src := "https://tailwindui.com/img/logos/workflow-mark.svg?color=indigo&shade=600", - alt := "" - ) - ) - ), - div( - cls := "py-16", - div( - cls := "text-center", - p( - cls := "text-sm font-semibold text-indigo-600 uppercase tracking-wide", - errorName - ), - h1( - cls := "mt-2 text-4xl font-extrabold text-gray-900 tracking-tight sm:text-5xl", - title - ), - p( - cls := "mt-2 text-base text-gray-500", - subTitle - ), - div( - cls := "mt-6", - PageLink - .container(homePage, actionBus) - .amend( - cls := "text-base font-medium text-indigo-600 hover:text-indigo-500", - """Go back home""" - ) - ) - ) - ) - ), - footer( - cls := "flex-shrink-0 max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", - nav( - cls := "flex justify-center space-x-4", - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Contact Support""" - ), - span( - cls := "inline-block border-l border-gray-300", - aria.hidden := true - ), - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Status""" - ), - span( - cls := "inline-block border-l border-gray-300", - aria.hidden := true - ), - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Twitter""" - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala deleted file mode 100644 index bd9baec..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action - -object NotFoundPage: - def apply(homePage: Page, url: String, actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - ErrorPage( - ErrorPage.ViewModel( - homePage, - "404 error", - "Page not found.", - s"Sorry, but page $url doesn't exist." - ), - actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala deleted file mode 100644 index 476fe3c..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action - -object UnhandledErrorPage: - - case class ViewModel( - homePage: Page, - errorName: Option[String], - errorMessage: Option[String] - ) - - def apply(m: ViewModel, actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - ErrorPage( - ErrorPage.ViewModel( - m.homePage, - "Unexpected error occurred", - m.errorName.getOrElse( - "Uh oh!" - ), // TODO: translations, better text than uh oh - m.errorMessage.getOrElse( - "This wasn't supposed to happen! Please try again." - ) - ), - actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala deleted file mode 100644 index 25f059f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala +++ /dev/null @@ -1,6 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.services - -import com.raquo.airstream.core.Observer - -trait DataFetcher[K, A]: - def fetch(id: K, sink: Observer[A]): Unit diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala deleted file mode 100644 index a469830..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala +++ /dev/null @@ -1,120 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package state - -import com.raquo.airstream.core.EventStream -import cz.e_bs.cmi.mdr.pdb.{UserInfo, OsobniCislo} -import com.raquo.airstream.core.Observer -import scala.scalajs.js -import scala.scalajs.js.JSON -import zio.json._ -import com.raquo.airstream.eventbus.EventBus -import com.raquo.airstream.ownership.Owner -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.UserFunction -import cz.e_bs.cmi.mdr.pdb.UserContract -import fiftyforms.services.files.File - -trait AppState - extends connectors.DetailPageConnector.AppState - with connectors.DetailParametruPageConnector.AppState - with connectors.DetailKriteriaPageConnector.AppState - with pages.detail.UpravDukaz.State: - def users: EventStream[List[UserInfo]] - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - -class MockAppState(implicit owner: Owner, router: Router[Page]) - extends AppState: - - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonDecoder[UserFunction] = DeriveJsonDecoder.gen - given JsonDecoder[UserContract] = DeriveJsonDecoder.gen - given JsonDecoder[UserInfo] = DeriveJsonDecoder.gen - - given JsonDecoder[ParameterCriteria] = DeriveJsonDecoder.gen - given JsonDecoder[Parameter] = DeriveJsonDecoder.gen - - private val actions = EventBus[Action]() - private val (parametersStream, pushParameters) = - EventStream.withCallback[List[Parameter]] - private val (usersStream, pushUsers) = - EventStream.withCallback[List[UserInfo]] - private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] - private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] - - private val mockData: List[UserInfo] = - mockUsers - .asInstanceOf[js.Dictionary[js.Object]] - .values - // TODO: is there a more efficient way to parse from JS object directly? - .map { o => - val parsed = JSON.stringify(o).fromJson[UserInfo] - parsed.left.foreach(org.scalajs.dom.console.log(_)) - parsed - } - .collect { case Right(u) => - u - } - .toList - - private val mockParameters: List[Parameter] = - pdbParams - .asInstanceOf[js.Dictionary[js.Object]] - .values - .map(o => JSON.stringify(o).fromJson[Parameter]) - .collect { case Right(p) => p } - .toList - - // TODO: Extract to separate event handler - actions.events.foreach { - case FetchDirectory => pushUsers(mockData) - case FetchUserDetails(osc) => - mockData.find(_.personalNumber == osc).foreach { o => - pushDetails(o) - router.replaceState(Page.Detail(o)) - } - case FetchParameters(osc) => - pushParameters(mockParameters) - case FetchParameter(osc, paramId) => - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(Page.DetailParametru(o, p)) - case FetchParameterCriteria(osc, paramId, critId, page) => - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - c <- p.criteria.find(_.id == critId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(page(o, p, c)) - case NavigateTo(page) => router.pushState(page) - case FetchAvailableFiles(osc) => - pushFiles( - List( - File("https://tc163.cmi.cz/here", "Example file") - ) - ) - } - - override def users: EventStream[List[UserInfo]] = - usersStream.debugWithName("users") - - override def details: EventStream[UserInfo] = - detailsStream.debugWithName("details") - - override def parameters: EventStream[List[Parameter]] = - parametersStream.debugWithName("parameters") - - override def availableFiles: EventStream[List[File]] = - filesStream.debugWithName("available files") - - override def actionBus: Observer[Action] = - actions.writer.debugWithName("actions writer") diff --git a/app/src/main/scala/mdr/pdb/app/Main.scala b/app/src/main/scala/mdr/pdb/app/Main.scala new file mode 100644 index 0000000..081b4ee --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/Main.scala @@ -0,0 +1,103 @@ +package mdr.pdb.app + +import scala.scalajs.js.annotation.JSExportTopLevel +import scala.scalajs.js.annotation.JSExport +import scala.scalajs.js.annotation.JSImport +import scala.scalajs.js +import org.scalajs.dom +import com.raquo.laminar.api.L.{*, given} +import scala.scalajs.js.Date +import com.raquo.waypoint.Router +import com.raquo.waypoint.SplitRender +import mdr.pdb.app.services.DataFetcher +import scala.scalajs.js.JSON +import zio.json._ +import mdr.pdb.UserInfo +import mdr.pdb.app.state.AppState + +@js.native +@JSImport("stylesheets/main.css", JSImport.Namespace) +object Css extends js.Any + +@js.native +@JSImport("data/users.json", JSImport.Default) +object mockUsers extends js.Object + +@js.native +@JSImport("params/pdb-params.json", JSImport.Default) +object pdbParams extends js.Object + +@JSExportTopLevel("app") +object Main: + + @JSExport + def main(args: Array[String]): Unit = + import Routes.given + onLoad { + setupAirstream() + val appContainer = dom.document.querySelector("#app") + val _ = + render( + appContainer, + renderPage(state.MockAppState(using unsafeWindowOwner, router)) + ) + } + + private def onLoad(f: => Unit): Unit = + documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) + + private def setupAirstream()(using router: Router[Page]): Unit = + AirstreamError.registerUnhandledErrorCallback(err => + router.forcePage( + Page.UnhandledError( + Some(err.getClass.getName), // TODO: Fill only in dev mode + Some(err.getMessage) + ) + ) + ) + + def renderPage(state: AppState)(using + router: Router[Page] + ): HtmlElement = + val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) + .collectSignal[Page.Detail]( + connectors + .DetailPageConnector(state)(_) + .apply + ) + .collectSignal[Page.DetailParametru]( + connectors + .DetailParametruPageConnector(state)(_) + .apply + ) + .collectSignal[Page.DetailKriteria]( + connectors + .DetailKriteriaPageConnector(state)(_) + .apply + ) + .collectSignal[Page.UpravDukazKriteria]( + pages.detail.UpravDukaz.Connector(state)(_).apply + ) + .collectStatic(Page.Dashboard)( + connectors.DashboardPageConnector(state.actionBus).apply + ) + .collect[Page.NotFound](pg => + pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) + ) + .collect[Page.UnhandledError](pg => + pages.errors + .UnhandledErrorPage( + pages.errors.UnhandledErrorPage + .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), + state.actionBus + ) + ) + .collectStatic(Page.Directory)( + connectors + .DirectoryPageConnector(state.users, state.actionBus) + .apply + ) + div(cls := "h-full", child <-- pageSplitter.$view) + + // Pull in the stylesheet + val css: Css.type = Css diff --git a/app/src/main/scala/mdr/pdb/app/Routes.scala b/app/src/main/scala/mdr/pdb/app/Routes.scala new file mode 100644 index 0000000..2cc860c --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/Routes.scala @@ -0,0 +1,198 @@ +package mdr.pdb.app + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.* +import org.scalajs.dom +import zio.json.{*, given} +import mdr.pdb.OsobniCislo + +import scala.scalajs.js +import mdr.pdb.UserInfo +import mdr.pdb.Parameter +import mdr.pdb.ParameterCriteria +import mdr.pdb.app.Page.Titled + +// enum is not working with Waypoints' SplitRender collectStatic +sealed abstract class Page( + val id: String, + val title: String, + val parent: Option[Page] +) { + val path: Vector[Page] = + parent match + case None => Vector(this) + case Some(p) => p.path :+ this + + val isRoot: Boolean = parent.isEmpty +} + +object Page: + + case class Titled[V](value: V, title: Option[String] = None): + val show: String = title.getOrElse(value.toString) + + case object Directory extends Page("directory", "Adresář", None) + + case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) + + case class Detail(osobniCislo: Titled[OsobniCislo]) + extends Page("user", osobniCislo.show, Some(Directory)) + + object Detail { + def apply(o: UserInfo): Detail = Detail( + Titled(o.personalNumber, Some(o.name)) + ) + } + + case class DetailParametru( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String] + ) extends Page( + "parameter", + parametr.show, + Some(Detail(osobniCislo)) + ) + + object DetailParametru { + def apply(o: UserInfo, p: Parameter): DetailParametru = + DetailParametru( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)) + ) + } + + case class DetailKriteria( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String], + kriterium: Titled[String] + ) extends Page( + "criteria", + kriterium.show, + Some(DetailParametru(osobniCislo, parametr)) + ) + + object DetailKriteria { + def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = + DetailKriteria( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)), + Titled(k.id, Some(k.id)) + ) + } + + case class UpravDukazKriteria( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String], + kriterium: Titled[String] + ) extends Page( + "addProof", + "Důkaz", + Some(DetailKriteria(osobniCislo, parametr, kriterium)) + ) + + object UpravDukazKriteria { + def apply( + o: UserInfo, + p: Parameter, + k: ParameterCriteria + ): UpravDukazKriteria = + UpravDukazKriteria( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)), + Titled(k.id, Some(k.id)) + ) + } + + case class NotFound(url: String) extends Page("404", "404", Some(Directory)) + + case class UnhandledError( + errorName: Option[String], + errorMessage: Option[String] + ) extends Page("500", "Unexpected error", Some(Directory)) + +object Routes: + given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) + given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) + given [V: JsonEncoder]: JsonEncoder[Titled[V]] = + DeriveJsonEncoder.gen[Titled[V]] + given [V: JsonDecoder]: JsonDecoder[Titled[V]] = + DeriveJsonDecoder.gen[Titled[V]] + given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] + given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] + + val base = + js.`import`.meta.env.BASE_URL + .asInstanceOf[String] + "app" + + val homePage: Page = Page.Directory + + given router: Router[Page] = Router[Page]( + routes = List( + Route.static(homePage, root / endOfSegments, basePath = base), + Route.static( + Page.Dashboard, + root / "dashboard" / endOfSegments, + basePath = base + ), + Route[Page.Detail, String]( + encode = _.osobniCislo.value.toString, + decode = osc => Page.Detail(Titled(OsobniCislo(osc))), + root / "osoba" / segment[String] / endOfSegments, + basePath = base + ), + Route[Page.DetailParametru, (String, String)]( + encode = p => (p.osobniCislo.value.toString, p.parametr.value), + decode = + p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / endOfSegments, + basePath = base + ), + Route[Page.DetailKriteria, (String, String, String)]( + encode = p => + ( + p.osobniCislo.value.toString, + p.parametr.value, + p.kriterium.value.replaceAll("\\.", "--") + ), + decode = p => + Page.DetailKriteria( + Titled(OsobniCislo(p._1)), + Titled(p._2), + Titled(p._3.replaceAll("--", ".")) + ), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / "kriterium" / segment[String] / endOfSegments, + basePath = base + ), + Route[Page.UpravDukazKriteria, (String, String, String)]( + encode = p => + ( + p.osobniCislo.value.toString, + p.parametr.value, + p.kriterium.value.replaceAll("\\.", "--") + ), + decode = p => + Page.UpravDukazKriteria( + Titled(OsobniCislo(p._1)), + Titled(p._2), + Titled(p._3.replaceAll("--", ".")) + ), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / "kriterium" / segment[String] / "edit" / endOfSegments, + basePath = base + ) + ), + serializePage = _.toJson, + deserializePage = _.fromJson[Page] + .fold(s => throw IllegalStateException(s), identity), + getPageTitle = _.title, + routeFallback = url => Page.NotFound(url), + deserializeFallback = _ => Page.Dashboard + )( + $popStateEvent = windowEvents.onPopState, + owner = unsafeWindowOwner + ) diff --git a/app/src/main/scala/mdr/pdb/app/actions.scala b/app/src/main/scala/mdr/pdb/app/actions.scala new file mode 100644 index 0000000..d078b3a --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/actions.scala @@ -0,0 +1,21 @@ +package mdr.pdb.app + +import mdr.pdb.OsobniCislo +import mdr.pdb.UserInfo +import mdr.pdb.Parameter +import mdr.pdb.ParameterCriteria + +sealed trait Action + +case object FetchDirectory extends Action +case class FetchUserDetails(osc: OsobniCislo) extends Action +case class FetchParameters(osc: OsobniCislo) extends Action +case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action +case class FetchParameterCriteria( + osc: OsobniCislo, + paramId: String, + critId: String, + page: (UserInfo, Parameter, ParameterCriteria) => Page +) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action +case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/mdr/pdb/app/components/AppPage.scala new file mode 100644 index 0000000..130c817 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/AppPage.scala @@ -0,0 +1,82 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.Page +import mdr.pdb.{UserProfile, UserInfo, OsobniCislo} +import com.raquo.waypoint.Router +import mdr.pdb.app.Action +import mdr.pdb.app.NavigateTo +import mdr.pdb.UserFunction + +object AppPage: + // TODO: pages by logged in user + val pages: List[Page] = List(Page.Directory, Page.Dashboard) + + import NavigationBar.{Logo, MenuItem} + + val logo = Logo( + "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", + "Workflow" + ) + + // TODO: menu items by user profile + val userMenu = + List( + MenuItem("Your Profile"), + MenuItem("Settings"), + MenuItem("Sign out") + ) + + // TODO: load user profile + val $userProfile = Var( + UserProfile( + "tom", + UserInfo( + OsobniCislo("1031"), + "tom", + "Tom", + "Cook", + None, + None, + Some("tom@example.com"), + Some("+420 222 866 180"), + Some( + UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") + ), + Nil, + None + ) + ) + ) + + val $userInfo = $userProfile.signal.map(_.userInfo) + + type ViewModel = Option[HtmlElement] + def apply( + actionBus: Observer[Action] + )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using + router: Router[Page] + ): HtmlElement = + PageLayout(actionBus)( + $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => + PageLayout.ViewModel( + NavigationBar.ViewModel( + u, + pages.map(p => + NavigationBar.Link( + () => + PageLink( + p, + actionBus + ), + p == cp + ) + ), + userMenu, + logo + ), + c + ) + ), + mods + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala new file mode 100644 index 0000000..9375398 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala @@ -0,0 +1,143 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.tailwind.list.IconText.ViewModel +import fiftyforms.ui.components.tailwind.Icons +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.Page + +import com.raquo.laminar.api.L.{*, given} +import io.laminext.syntax.core.{*, given} +import mdr.pdb.app.Action + +object Breadcrumbs: + + private def slash = + import svg.{*, given} + svg( + cls := "flex-shrink-0 h-5 w-5 text-gray-300", + xmlns := "http://www.w3.org/2000/svg", + fill := "currentColor", + viewBox := "0 0 20 20", + ariaHidden := true, + path( + d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" + ) + ) + + def backIcon = + Icons.solid + .`arrow-narrow-left`() + .amend( + svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" + ) + + object Link: + case class ViewModel( + page: Page, + icon: Option[SvgElement], + extraClasses: String, + text: String, + textClasses: Option[String] = None + ) + + val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" + + def shortHome(p: Page) = ViewModel( + p, + Some(backIcon), + "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", + "Zpět na úvodní stránku", + None + ) + + def fullHome(p: Page) = ViewModel( + p, + Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), + "text-gray-400 hover:text-gray-500", + "Domů", + Some("sr-only") + ) + + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + PageLink + .container($m.map(_.page), actionBus) + .amend( + cls <-- $m.map(_.extraClasses), + child.maybe <-- $m.map(_.icon), + span( + cls <-- $m.map(_.textClasses.getOrElse("")), + child.text <-- $m.map(_.text) + ) + ) + + object FullBreadcrumbs: + type ViewModel = Page + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + ol( + role := "list", + cls := "flex items-center space-x-4", + children <-- $m.map(_.path).split(_.id)((_, _, $p) => + li( + div( + cls := "flex items-center", + child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), + Link( + $p.map(p => + if (p.isRoot) then Link.fullHome(p) + else + Link.ViewModel( + p, + None, + s"ml-4 max-w-xs truncate ${Link.baseClasses}", + p.title + ) + ), + actionBus + ) + ) + ) + ) + ) + + object ShortBreadcrumbs: + type ViewModel = Page + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + Link( + $m.map { p => + val target = p.parent.getOrElse(p) + if target.isRoot then Link.shortHome(target) + else + Link.ViewModel( + target, + Some(backIcon), + "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", + s"Zpět na ${target.title}" + ) + }, + actionBus + ) + + def apply(actionBus: Observer[Action])(using + router: Router[Page] + ): HtmlElement = + nav( + cls := "flex", + aria.label := "Breadcrumb", + div( + cls := "flex sm:hidden", + ShortBreadcrumbs(router.$currentPage, actionBus) + ), + div( + cls := "hidden sm:block", + FullBreadcrumbs(router.$currentPage, actionBus) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala new file mode 100644 index 0000000..4bb8b8d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala @@ -0,0 +1,242 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent +import fiftyforms.ui.components.tailwind.Icons +import fiftyforms.ui.components.tailwind.Avatar +import mdr.pdb.UserInfo +import io.laminext.syntax.core.* + +object NavigationBar: + + case class Logo(img: String, name: String) + case class Link(a: () => Anchor, active: Boolean) + case class MenuItem(title: String) + + case class ViewModel( + userInfo: UserInfo, + pages: List[Link], + userMenu: List[MenuItem], + logo: Logo + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + val $userInfo = $m.map(_.userInfo) + val mobileMenuOpen = Var(false) + + val desktopOnly = cls("hidden md:block") + val mobileOnly = cls("md:hidden") + + inline def avatarImage(size: Int = 8) = + Avatar($userInfo.map(_.img)).avatarImage(size) + + def notificationButton = button( + tpe := "button", + cls := List( + "bg-indigo-600", + "focus:outline-none", + "focus:ring-2", + "focus:ring-offset-2", + "focus:ring-offset-indigo-600", + "focus:ring-white", + "hover:text-white", + "p-1", + "rounded-full", + "text-indigo-200" + ), + span(cls := "sr-only", "View notifications"), + Icons.outline.bell() + ) + + def userProfile: HtmlElement = + val menuOpen = Var(false) + + def menuItem(item: MenuItem, idx: Int): HtmlElement = + a( + href := "#", + cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", + role := "menuitem", + tabIndex := -1, + idAttr := s"user-menu-item-$idx", + item.title + ) + + div( + cls := "ml-3 relative", + div( + button( + tpe := "button", + cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", + idAttr := "user-menu-button", + aria.expanded <-- menuOpen.signal, + aria.hasPopup := true, + span(cls := "sr-only", "Open user menu"), + child <-- avatarImage(), + onClick.preventDefault.mapTo( + !menuOpen.now() + ) --> menuOpen.writer + ) + ), + /* + * */ + div( + cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", + cls <-- menuOpen.signal.map { o => + if (o) "md:block" else "md:hidden" + }, + role := "menu", + aria.orientation := "vertical", + aria.labelledBy := "user-menu-button", + tabIndex := -1, + // : keyboard navigation + children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) + ) + ) + + def mobileProfile = + def menuItem(item: MenuItem): HtmlElement = + a( + href := "#", + cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", + item.title + ) + + div( + cls := "pt-4 pb-3 border-t border-indigo-700", + div( + cls := "flex items-center px-5", + div( + cls := "flex-shrink-0", + child <-- avatarImage(10) + ), + div( + cls := "ml-3", + div( + cls := "text-base font-medium text-white", + child.text <-- $userInfo.map(_.name) + ), + child.maybe <-- $userInfo.map( + _.email.map(e => + div( + cls := "text-sm font-medium text-indigo-300", + e + ) + ) + ) + ), + notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) + ), + div( + cls := "mt-3 px-2 space-y-1", + children <-- $m.map(_.userMenu.map(menuItem)) + ) + ) + + def pageLink(page: Link): Anchor = + page + .a() + .amend( + cls := "text-white px-3 py-2 rounded-md text-sm font-medium", + cls := Seq( + "bg-indigo-700" -> page.active, + "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active + ), + ariaCurrent := (if page.active then "page" else "false") + ) + + def logoImg: Image = + img( + cls := "h-8 w-8", + src <-- $m.map(_.logo.img), + alt <-- $m.map(_.logo.name) + ) + + def mobileMenuButton = button( + tpe := "button", + cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", + aria.controls := "mobile-menu", + aria.expanded <-- mobileMenuOpen.signal, + span(cls := "sr-only", "Open main menu"), + Icons.outline + .menu() + .amend( + svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") + ), + Icons.outline + .x() + .amend( + svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") + ), + onClick.preventDefault.mapTo( + !mobileMenuOpen.now() + ) --> mobileMenuOpen.writer + ) + + def navBarLeft = + div( + cls := "flex items-center", + div(cls := "flex-shrink-0", logoImg), + div( + desktopOnly, + div( + cls := "ml-10 flex items-baseline space-x-4", + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) + ) + ) + ) + + def navBarRight = + div( + desktopOnly, + div( + cls := "ml-4 flex items-center md:ml-6", + notificationButton, + userProfile + ) + ) + + def navBarMobile = + div( + cls := "-mr-2 flex", + mobileOnly, + mobileMenuButton + ) + + def navBar = + div( + cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", + div( + cls := "flex items-center justify-between h-16", + navBarLeft, + navBarRight, + navBarMobile + ) + ) + + def mobileMenu = + div( + mobileOnly, + cls <-- mobileMenuOpen.signal.map { o => + if (o) "block" else "hidden" + }, + idAttr := "mobile-menu", + div( + cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) + ), + mobileProfile + ) + + nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala new file mode 100644 index 0000000..c4cb10d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala @@ -0,0 +1,19 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.Page +import com.raquo.waypoint.Router +import mdr.pdb.app.Action + +object PageHeader: + def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = + header( + cls := "bg-white shadow-sm", + div( + cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", + h1( + cls := "text-lg leading-6 font-semibold text-gray-900", + Breadcrumbs(actionBus) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala new file mode 100644 index 0000000..aae4539 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala @@ -0,0 +1,30 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.Page +import mdr.pdb.app.Action +import fiftyforms.ui.components.tailwind.Loading +import io.laminext.syntax.core.* + +object PageLayout: + case class ViewModel( + navigation: NavigationBar.ViewModel, + content: Option[HtmlElement] + ) + def apply(actionBus: Observer[Action])( + $m: Signal[ViewModel], + mods: Modifier[HtmlElement]* + )(using router: Router[Page]): HtmlElement = + val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) + div( + cls := "h-full flex flex-col", + NavigationBar($m.map(_.navigation)), + child.maybe <-- router.$currentPage.map(_.isRoot) + .switch(None, Some(PageHeader(actionBus))), + main( + cls := "flex-grow-1 overflow-y-auto", + mods, + child <-- $maybeContent.map(_.getOrElse(Loading)) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/mdr/pdb/app/components/PageLink.scala new file mode 100644 index 0000000..e6e7052 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageLink.scala @@ -0,0 +1,40 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.LinkSupport.* +import mdr.pdb.app.Page +import mdr.pdb.app.Action +import com.raquo.waypoint.Router +import mdr.pdb.app.NavigateTo + +object PageLink: + type ViewModel = Page + def apply($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = + a(mods($m, actions), child.text <-- $m.map(_.title)) + + def apply(m: ViewModel, actions: Observer[Action])(using + router: Router[Page] + ): Anchor = apply(Val(m), actions) + + def container($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = a(mods($m, actions)) + + def container(m: ViewModel, actions: Observer[Action])(using + Router[Page] + ): Anchor = container(Val(m), actions) + + private def mods($m: Signal[Page], actions: Observer[Action])(using + router: Router[Page] + ): Modifier[Anchor] = + Seq( + href <-- $m.map(router.absoluteUrlForPage).recover { case _ => + Some("invalid url") + }, + composeEvents(onClick.noKeyMod.preventDefault)( + _.sample($m) + .map(NavigateTo.apply) + ) --> actions + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala new file mode 100644 index 0000000..7682772 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala @@ -0,0 +1,13 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.pages.dashboard.DashboardPage +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage + +class DashboardPageConnector(actionBus: Observer[Action])(using + router: Router[Page] +): + def apply: HtmlElement = + AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala new file mode 100644 index 0000000..76b9133 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala @@ -0,0 +1,76 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.Parameter +import mdr.pdb.UserInfo +import mdr.pdb.app.pages.detail.DetailParametruPage +import pages.detail.DetailKriteriaPage +import pages.detail.components.DukazyKriteria +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.ParameterCriteria + +object DetailKriteriaPageConnector { + trait AppState { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + } +} + +case class DetailKriteriaPageConnector( + state: DetailKriteriaPageConnector.AppState +)( + $page: Signal[Page.DetailKriteria] +)(using Router[Page]): + val $paramChangeSignal = + $page.splitOne(p => + (p.osobniCislo.value, p.parametr.value, p.kriterium.value) + )((x, _, _) => x) + val $pageChangeSignal = + $paramChangeSignal.map( + FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) + ) + + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + val $merged = + $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => + for { + da <- d + pa <- p + pb <- pa.find(_.id == pc._2) + ka <- pb.criteria.find(_.id == pc._3) + } yield (da, pb, ka) + ) + + def apply: HtmlElement = + AppPage(state.actionBus)( + $merged.map(_.map(buildModel)) + .split(_ => ())((_, s, $s) => + DetailKriteriaPage($s)(state.actionBus.contramap { + case DukazyKriteria.Add => + NavigateTo( + Page.UpravDukazKriteria( + Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), + Page.Titled(s.parametr.id, Some(s.parametr.nazev)), + Page.Titled(s.kriterium.id, Some(s.kriterium.id)) + ) + ) + }) + ), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: Parameter, + k: ParameterCriteria + ): DetailKriteriaPage.ViewModel = + DetailKriteriaPage.ViewModel( + o.toDetailOsoby, + p.toParametr(_ => a()), + k.toKriterium(_ => a()) + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DetailPageConnector.scala new file mode 100644 index 0000000..df3a6b7 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DetailPageConnector.scala @@ -0,0 +1,57 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.UserInfo +import pages.detail.DetailPage +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.pages.detail.components.DetailOsoby +import mdr.pdb.Parameter +import mdr.pdb.app.pages.detail.components.SeznamParametru +import fiftyforms.ui.components.tailwind.Color + +object DetailPageConnector { + trait AppState { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + } +} + +case class DetailPageConnector(state: DetailPageConnector.AppState)( + $page: Signal[Page.Detail] +)(using router: Router[Page]): + val $oscChangeSignal = $page.splitOne(_.osobniCislo.value)((osc, _, _) => osc) + val $pageChangeSignal = + $oscChangeSignal.flatMap(osc => + EventStream.fromSeq(Seq(FetchUserDetails(osc), FetchParameters(osc))) + ) + // TODO: filter the value based on the current osc + // OSC change will fetch new data, but still + // - we need to be sure that what we got is really what we ought to display + // - we want to display stale data accordingly (at least with loading indicator) + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + def apply: HtmlElement = + AppPage(state.actionBus)( + $data.combineWithFn($params)(_ zip _) + .map(_.map(buildModel)) + .split(_ => ())((_, _, s) => DetailPage(s)), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: List[Parameter] + ): DetailPage.ViewModel = + DetailPage.ViewModel( + o.toDetailOsoby, + p.map( + _.toParametr(param => + PageLink.container(Page.DetailParametru(o, param), state.actionBus) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DetailParametruPageConnector.scala new file mode 100644 index 0000000..fa7193d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DetailParametruPageConnector.scala @@ -0,0 +1,67 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.Parameter +import mdr.pdb.UserInfo +import mdr.pdb.app.pages.detail.DetailParametruPage +import pages.detail.DetailParametruPage +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.components.PageLink + +object DetailParametruPageConnector { + trait AppState { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + } +} + +case class DetailParametruPageConnector( + state: DetailParametruPageConnector.AppState +)( + $page: Signal[Page.DetailParametru] +)(using router: Router[Page]): + val $paramChangeSignal = + $page.splitOne(p => (p.osobniCislo.value, p.parametr.value))((x, _, _) => x) + val $pageChangeSignal = + $paramChangeSignal.map(FetchParameter(_, _)) + + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + val $merged = + $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => + for { + da <- d + pa <- p + pb <- pa.find(_.id == pc._2) + } yield (da, pb) + ) + + def apply: HtmlElement = + AppPage(state.actionBus)( + $merged.map(_.map(buildModel)) + .split(_ => ())((_, _, s) => DetailParametruPage(s)), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: Parameter + ): DetailParametruPage.ViewModel = + DetailParametruPage.ViewModel( + o.toDetailOsoby, + p.toParametr(p => + PageLink.container(Page.DetailParametru(o, p), state.actionBus) + ), + p.criteria.map( + _.toKriterium { c => + PageLink.container( + Page.DetailKriteria(o, p, c), + state.actionBus + ) + } + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DirectoryPageConnector.scala new file mode 100644 index 0000000..ab2c045 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DirectoryPageConnector.scala @@ -0,0 +1,34 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.UserInfo +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.components.AppPage + +case class DirectoryPageConnector( + $input: EventStream[List[UserInfo]], + actionBus: Observer[Action] +)(using router: Router[Page]): + val $data = $input.startWithNone + val $actionSignal = EventStream.fromValue(FetchDirectory) + + def apply: HtmlElement = + AppPage(actionBus)( + $data.split(_ => ())((_, _, s) => + pages.directory.DirectoryPage( + s.map( + _.map( + _.toUserRow(u => + PageLink.container( + Page.Detail(Page.Titled(u.personalNumber)), + actionBus + ) + ) + ) + ) + ) + ), + $actionSignal --> actionBus + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/mdr/pdb/app/connectors/conversions.scala new file mode 100644 index 0000000..f581796 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/conversions.scala @@ -0,0 +1,68 @@ +package mdr.pdb.app.connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.UserInfo +import mdr.pdb.app.pages.detail.components.DetailOsoby +import mdr.pdb.Parameter +import mdr.pdb.app.pages.detail.components.SeznamParametru +import mdr.pdb.ParameterCriteria +import mdr.pdb.app.pages.detail.components.SeznamKriterii +import mdr.pdb.app.pages.directory.components.UserRow +import mdr.pdb.app.pages.detail.components.DetailParametru +import mdr.pdb.app.pages.detail.components.DetailKriteria +import fiftyforms.ui.components.tailwind.Color + +extension (o: UserInfo) + def toDetailOsoby: DetailOsoby.ViewModel = + DetailOsoby.ViewModel( + o.personalNumber, + o.name, + o.email, + o.phone, + o.img, + o.mainFunction.map(f => + DetailOsoby.Funkce.ViewModel(f.name, f.dept, f.ou) + ), + o.userContracts.headOption.map(c => + DetailOsoby.PracovniPomer.ViewModel(c.rel, c.startDate, c.endDate) + ) + ) + +extension (param: Parameter) + def toParametr(container: Parameter => Anchor): DetailParametru.ViewModel = + DetailParametru.ViewModel( + id = param.id, + nazev = param.name, + popis = param.description, + status = "Nesplněno", + statusColor = Color.red, + a = container(param) + ) + +extension (crit: ParameterCriteria) + def toKriterium( + container: ParameterCriteria => Anchor + ): DetailKriteria.ViewModel = + DetailKriteria.ViewModel( + nazev = crit.criteriumText, + kapitola = crit.chapterId, + bod = crit.itemId, + status = "Nesplněno", + statusColor = Color.red, + splneno = false, + dukazy = Nil, + container = container(crit) + ) + +extension (user: UserInfo) + def toUserRow( + container: UserInfo => HtmlElement = (_: UserInfo) => div() + ): UserRow.ViewModel = + UserRow.ViewModel( + osobniCislo = user.personalNumber.toString, + celeJmeno = user.name, + prijmeni = user.surname, + hlavniFunkce = user.mainFunction.map(_.name), + img = user.img, + container = () => container(user) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/dashboard/DashboardPage.scala b/app/src/main/scala/mdr/pdb/app/pages/dashboard/DashboardPage.scala new file mode 100644 index 0000000..0eb8f7a --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/dashboard/DashboardPage.scala @@ -0,0 +1,11 @@ +package mdr.pdb.app.pages.dashboard + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.Page + +object DashboardPage: + + def render: HtmlElement = + div("Dashboard page") diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala new file mode 100644 index 0000000..ff3a25f --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala @@ -0,0 +1,31 @@ +package mdr.pdb.app.pages.detail + +import com.raquo.laminar.api.L.{*, given} + +import components._ + +object DetailKriteriaPage: + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametr: DetailParametru.ViewModel, + kriterium: DetailKriteria.ViewModel + ) + + def apply($m: Signal[ViewModel])( + action: Observer[DukazyKriteria.Action] + ): HtmlElement = + div( + cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + div( + DetailOsoby.header($m.map(_.osoba)), + DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") + ), + div( + DetailKriteria($m.map(_.kriterium)), + DukazyKriteria($m.map(_.kriterium.dukazy))(action) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/DetailPage.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailPage.scala new file mode 100644 index 0000000..b591cc9 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailPage.scala @@ -0,0 +1,25 @@ +package mdr.pdb.app.pages.detail + +import com.raquo.laminar.api.L.{*, given} + +import components._ +import mdr.pdb.app.Page +import com.raquo.waypoint.Router +import mdr.pdb.app.Action + +object DetailPage: + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametry: SeznamParametru.ViewModel + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + DetailOsoby($m.map(_.osoba)), + SeznamParametru($m.map(_.parametry)) + ) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala deleted file mode 100644 index a3b00af..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala +++ /dev/null @@ -1,103 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import scala.scalajs.js.annotation.JSExportTopLevel -import scala.scalajs.js.annotation.JSExport -import scala.scalajs.js.annotation.JSImport -import scala.scalajs.js -import org.scalajs.dom -import com.raquo.laminar.api.L.{*, given} -import scala.scalajs.js.Date -import com.raquo.waypoint.Router -import com.raquo.waypoint.SplitRender -import cz.e_bs.cmi.mdr.pdb.app.services.DataFetcher -import scala.scalajs.js.JSON -import zio.json._ -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.state.AppState - -@js.native -@JSImport("stylesheets/main.css", JSImport.Namespace) -object Css extends js.Any - -@js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native -@JSImport("params/pdb-params.json", JSImport.Default) -object pdbParams extends js.Object - -@JSExportTopLevel("app") -object Main: - - @JSExport - def main(args: Array[String]): Unit = - import Routes.given - onLoad { - setupAirstream() - val appContainer = dom.document.querySelector("#app") - val _ = - render( - appContainer, - renderPage(state.MockAppState(using unsafeWindowOwner, router)) - ) - } - - private def onLoad(f: => Unit): Unit = - documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) - - private def setupAirstream()(using router: Router[Page]): Unit = - AirstreamError.registerUnhandledErrorCallback(err => - router.forcePage( - Page.UnhandledError( - Some(err.getClass.getName), // TODO: Fill only in dev mode - Some(err.getMessage) - ) - ) - ) - - def renderPage(state: AppState)(using - router: Router[Page] - ): HtmlElement = - val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) - .collectSignal[Page.Detail]( - connectors - .DetailPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailParametru]( - connectors - .DetailParametruPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailKriteria]( - connectors - .DetailKriteriaPageConnector(state)(_) - .apply - ) - .collectSignal[Page.UpravDukazKriteria]( - pages.detail.UpravDukaz.Connector(state)(_).apply - ) - .collectStatic(Page.Dashboard)( - connectors.DashboardPageConnector(state.actionBus).apply - ) - .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) - ) - .collect[Page.UnhandledError](pg => - pages.errors - .UnhandledErrorPage( - pages.errors.UnhandledErrorPage - .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), - state.actionBus - ) - ) - .collectStatic(Page.Directory)( - connectors - .DirectoryPageConnector(state.users, state.actionBus) - .apply - ) - div(cls := "h-full", child <-- pageSplitter.$view) - - // Pull in the stylesheet - val css: Css.type = Css diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala deleted file mode 100644 index 3bbc8c0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ /dev/null @@ -1,198 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.* -import org.scalajs.dom -import zio.json.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import scala.scalajs.js -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.Page.Titled - -// enum is not working with Waypoints' SplitRender collectStatic -sealed abstract class Page( - val id: String, - val title: String, - val parent: Option[Page] -) { - val path: Vector[Page] = - parent match - case None => Vector(this) - case Some(p) => p.path :+ this - - val isRoot: Boolean = parent.isEmpty -} - -object Page: - - case class Titled[V](value: V, title: Option[String] = None): - val show: String = title.getOrElse(value.toString) - - case object Directory extends Page("directory", "Adresář", None) - - case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) - - case class Detail(osobniCislo: Titled[OsobniCislo]) - extends Page("user", osobniCislo.show, Some(Directory)) - - object Detail { - def apply(o: UserInfo): Detail = Detail( - Titled(o.personalNumber, Some(o.name)) - ) - } - - case class DetailParametru( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String] - ) extends Page( - "parameter", - parametr.show, - Some(Detail(osobniCislo)) - ) - - object DetailParametru { - def apply(o: UserInfo, p: Parameter): DetailParametru = - DetailParametru( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)) - ) - } - - case class DetailKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "criteria", - kriterium.show, - Some(DetailParametru(osobniCislo, parametr)) - ) - - object DetailKriteria { - def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = - DetailKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class UpravDukazKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "addProof", - "Důkaz", - Some(DetailKriteria(osobniCislo, parametr, kriterium)) - ) - - object UpravDukazKriteria { - def apply( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): UpravDukazKriteria = - UpravDukazKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class NotFound(url: String) extends Page("404", "404", Some(Directory)) - - case class UnhandledError( - errorName: Option[String], - errorMessage: Option[String] - ) extends Page("500", "Unexpected error", Some(Directory)) - -object Routes: - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) - given [V: JsonEncoder]: JsonEncoder[Titled[V]] = - DeriveJsonEncoder.gen[Titled[V]] - given [V: JsonDecoder]: JsonDecoder[Titled[V]] = - DeriveJsonDecoder.gen[Titled[V]] - given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] - given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] - - val base = - js.`import`.meta.env.BASE_URL - .asInstanceOf[String] + "app" - - val homePage: Page = Page.Directory - - given router: Router[Page] = Router[Page]( - routes = List( - Route.static(homePage, root / endOfSegments, basePath = base), - Route.static( - Page.Dashboard, - root / "dashboard" / endOfSegments, - basePath = base - ), - Route[Page.Detail, String]( - encode = _.osobniCislo.value.toString, - decode = osc => Page.Detail(Titled(OsobniCislo(osc))), - root / "osoba" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.DetailParametru, (String, String)]( - encode = p => (p.osobniCislo.value.toString, p.parametr.value), - decode = - p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / endOfSegments, - basePath = base - ), - Route[Page.DetailKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.DetailKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.UpravDukazKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.UpravDukazKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / "edit" / endOfSegments, - basePath = base - ) - ), - serializePage = _.toJson, - deserializePage = _.fromJson[Page] - .fold(s => throw IllegalStateException(s), identity), - getPageTitle = _.title, - routeFallback = url => Page.NotFound(url), - deserializeFallback = _ => Page.Dashboard - )( - $popStateEvent = windowEvents.onPopState, - owner = unsafeWindowOwner - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala deleted file mode 100644 index b24b82b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -sealed trait Action - -case object FetchDirectory extends Action -case class FetchUserDetails(osc: OsobniCislo) extends Action -case class FetchParameters(osc: OsobniCislo) extends Action -case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action -case class FetchParameterCriteria( - osc: OsobniCislo, - paramId: String, - critId: String, - page: (UserInfo, Parameter, ParameterCriteria) => Page -) extends Action -case class FetchAvailableFiles(osc: OsobniCislo) extends Action -case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala deleted file mode 100644 index c637d35..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala +++ /dev/null @@ -1,82 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.{UserProfile, UserInfo, OsobniCislo} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo -import cz.e_bs.cmi.mdr.pdb.UserFunction - -object AppPage: - // TODO: pages by logged in user - val pages: List[Page] = List(Page.Directory, Page.Dashboard) - - import NavigationBar.{Logo, MenuItem} - - val logo = Logo( - "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", - "Workflow" - ) - - // TODO: menu items by user profile - val userMenu = - List( - MenuItem("Your Profile"), - MenuItem("Settings"), - MenuItem("Sign out") - ) - - // TODO: load user profile - val $userProfile = Var( - UserProfile( - "tom", - UserInfo( - OsobniCislo("1031"), - "tom", - "Tom", - "Cook", - None, - None, - Some("tom@example.com"), - Some("+420 222 866 180"), - Some( - UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") - ), - Nil, - None - ) - ) - ) - - val $userInfo = $userProfile.signal.map(_.userInfo) - - type ViewModel = Option[HtmlElement] - def apply( - actionBus: Observer[Action] - )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using - router: Router[Page] - ): HtmlElement = - PageLayout(actionBus)( - $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => - PageLayout.ViewModel( - NavigationBar.ViewModel( - u, - pages.map(p => - NavigationBar.Link( - () => - PageLink( - p, - actionBus - ), - p == cp - ) - ), - userMenu, - logo - ), - c - ) - ), - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala deleted file mode 100644 index bf473c6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ /dev/null @@ -1,143 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden -import fiftyforms.ui.components.tailwind.list.IconText.ViewModel -import fiftyforms.ui.components.tailwind.Icons -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Action - -object Breadcrumbs: - - private def slash = - import svg.{*, given} - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-300", - xmlns := "http://www.w3.org/2000/svg", - fill := "currentColor", - viewBox := "0 0 20 20", - ariaHidden := true, - path( - d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" - ) - ) - - def backIcon = - Icons.solid - .`arrow-narrow-left`() - .amend( - svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" - ) - - object Link: - case class ViewModel( - page: Page, - icon: Option[SvgElement], - extraClasses: String, - text: String, - textClasses: Option[String] = None - ) - - val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" - - def shortHome(p: Page) = ViewModel( - p, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - "Zpět na úvodní stránku", - None - ) - - def fullHome(p: Page) = ViewModel( - p, - Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), - "text-gray-400 hover:text-gray-500", - "Domů", - Some("sr-only") - ) - - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - PageLink - .container($m.map(_.page), actionBus) - .amend( - cls <-- $m.map(_.extraClasses), - child.maybe <-- $m.map(_.icon), - span( - cls <-- $m.map(_.textClasses.getOrElse("")), - child.text <-- $m.map(_.text) - ) - ) - - object FullBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - ol( - role := "list", - cls := "flex items-center space-x-4", - children <-- $m.map(_.path).split(_.id)((_, _, $p) => - li( - div( - cls := "flex items-center", - child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), - Link( - $p.map(p => - if (p.isRoot) then Link.fullHome(p) - else - Link.ViewModel( - p, - None, - s"ml-4 max-w-xs truncate ${Link.baseClasses}", - p.title - ) - ), - actionBus - ) - ) - ) - ) - ) - - object ShortBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - Link( - $m.map { p => - val target = p.parent.getOrElse(p) - if target.isRoot then Link.shortHome(target) - else - Link.ViewModel( - target, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - s"Zpět na ${target.title}" - ) - }, - actionBus - ) - - def apply(actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - nav( - cls := "flex", - aria.label := "Breadcrumb", - div( - cls := "flex sm:hidden", - ShortBreadcrumbs(router.$currentPage, actionBus) - ), - div( - cls := "hidden sm:block", - FullBreadcrumbs(router.$currentPage, actionBus) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala deleted file mode 100644 index 0db2047..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ /dev/null @@ -1,242 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent -import fiftyforms.ui.components.tailwind.Icons -import fiftyforms.ui.components.tailwind.Avatar -import cz.e_bs.cmi.mdr.pdb.UserInfo -import io.laminext.syntax.core.* - -object NavigationBar: - - case class Logo(img: String, name: String) - case class Link(a: () => Anchor, active: Boolean) - case class MenuItem(title: String) - - case class ViewModel( - userInfo: UserInfo, - pages: List[Link], - userMenu: List[MenuItem], - logo: Logo - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - val $userInfo = $m.map(_.userInfo) - val mobileMenuOpen = Var(false) - - val desktopOnly = cls("hidden md:block") - val mobileOnly = cls("md:hidden") - - inline def avatarImage(size: Int = 8) = - Avatar($userInfo.map(_.img)).avatarImage(size) - - def notificationButton = button( - tpe := "button", - cls := List( - "bg-indigo-600", - "focus:outline-none", - "focus:ring-2", - "focus:ring-offset-2", - "focus:ring-offset-indigo-600", - "focus:ring-white", - "hover:text-white", - "p-1", - "rounded-full", - "text-indigo-200" - ), - span(cls := "sr-only", "View notifications"), - Icons.outline.bell() - ) - - def userProfile: HtmlElement = - val menuOpen = Var(false) - - def menuItem(item: MenuItem, idx: Int): HtmlElement = - a( - href := "#", - cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", - role := "menuitem", - tabIndex := -1, - idAttr := s"user-menu-item-$idx", - item.title - ) - - div( - cls := "ml-3 relative", - div( - button( - tpe := "button", - cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - idAttr := "user-menu-button", - aria.expanded <-- menuOpen.signal, - aria.hasPopup := true, - span(cls := "sr-only", "Open user menu"), - child <-- avatarImage(), - onClick.preventDefault.mapTo( - !menuOpen.now() - ) --> menuOpen.writer - ) - ), - /* - * */ - div( - cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", - cls <-- menuOpen.signal.map { o => - if (o) "md:block" else "md:hidden" - }, - role := "menu", - aria.orientation := "vertical", - aria.labelledBy := "user-menu-button", - tabIndex := -1, - // : keyboard navigation - children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) - ) - ) - - def mobileProfile = - def menuItem(item: MenuItem): HtmlElement = - a( - href := "#", - cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", - item.title - ) - - div( - cls := "pt-4 pb-3 border-t border-indigo-700", - div( - cls := "flex items-center px-5", - div( - cls := "flex-shrink-0", - child <-- avatarImage(10) - ), - div( - cls := "ml-3", - div( - cls := "text-base font-medium text-white", - child.text <-- $userInfo.map(_.name) - ), - child.maybe <-- $userInfo.map( - _.email.map(e => - div( - cls := "text-sm font-medium text-indigo-300", - e - ) - ) - ) - ), - notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) - ), - div( - cls := "mt-3 px-2 space-y-1", - children <-- $m.map(_.userMenu.map(menuItem)) - ) - ) - - def pageLink(page: Link): Anchor = - page - .a() - .amend( - cls := "text-white px-3 py-2 rounded-md text-sm font-medium", - cls := Seq( - "bg-indigo-700" -> page.active, - "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active - ), - ariaCurrent := (if page.active then "page" else "false") - ) - - def logoImg: Image = - img( - cls := "h-8 w-8", - src <-- $m.map(_.logo.img), - alt <-- $m.map(_.logo.name) - ) - - def mobileMenuButton = button( - tpe := "button", - cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - aria.controls := "mobile-menu", - aria.expanded <-- mobileMenuOpen.signal, - span(cls := "sr-only", "Open main menu"), - Icons.outline - .menu() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") - ), - Icons.outline - .x() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") - ), - onClick.preventDefault.mapTo( - !mobileMenuOpen.now() - ) --> mobileMenuOpen.writer - ) - - def navBarLeft = - div( - cls := "flex items-center", - div(cls := "flex-shrink-0", logoImg), - div( - desktopOnly, - div( - cls := "ml-10 flex items-baseline space-x-4", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ) - ) - ) - - def navBarRight = - div( - desktopOnly, - div( - cls := "ml-4 flex items-center md:ml-6", - notificationButton, - userProfile - ) - ) - - def navBarMobile = - div( - cls := "-mr-2 flex", - mobileOnly, - mobileMenuButton - ) - - def navBar = - div( - cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex items-center justify-between h-16", - navBarLeft, - navBarRight, - navBarMobile - ) - ) - - def mobileMenu = - div( - mobileOnly, - cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }, - idAttr := "mobile-menu", - div( - cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ), - mobileProfile - ) - - nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala deleted file mode 100644 index 75339df..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object PageHeader: - def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = - header( - cls := "bg-white shadow-sm", - div( - cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", - h1( - cls := "text-lg leading-6 font-semibold text-gray-900", - Breadcrumbs(actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala deleted file mode 100644 index 5096463..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ /dev/null @@ -1,30 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import fiftyforms.ui.components.tailwind.Loading -import io.laminext.syntax.core.* - -object PageLayout: - case class ViewModel( - navigation: NavigationBar.ViewModel, - content: Option[HtmlElement] - ) - def apply(actionBus: Observer[Action])( - $m: Signal[ViewModel], - mods: Modifier[HtmlElement]* - )(using router: Router[Page]): HtmlElement = - val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) - div( - cls := "h-full flex flex-col", - NavigationBar($m.map(_.navigation)), - child.maybe <-- router.$currentPage.map(_.isRoot) - .switch(None, Some(PageHeader(actionBus))), - main( - cls := "flex-grow-1 overflow-y-auto", - mods, - child <-- $maybeContent.map(_.getOrElse(Loading)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala deleted file mode 100644 index 62d820f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.LinkSupport.* -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo - -object PageLink: - type ViewModel = Page - def apply($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = - a(mods($m, actions), child.text <-- $m.map(_.title)) - - def apply(m: ViewModel, actions: Observer[Action])(using - router: Router[Page] - ): Anchor = apply(Val(m), actions) - - def container($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = a(mods($m, actions)) - - def container(m: ViewModel, actions: Observer[Action])(using - Router[Page] - ): Anchor = container(Val(m), actions) - - private def mods($m: Signal[Page], actions: Observer[Action])(using - router: Router[Page] - ): Modifier[Anchor] = - Seq( - href <-- $m.map(router.absoluteUrlForPage).recover { case _ => - Some("invalid url") - }, - composeEvents(onClick.noKeyMod.preventDefault)( - _.sample($m) - .map(NavigateTo.apply) - ) --> actions - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala deleted file mode 100644 index 87f825a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.pages.dashboard.DashboardPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -class DashboardPageConnector(actionBus: Observer[Action])(using - router: Router[Page] -): - def apply: HtmlElement = - AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala deleted file mode 100644 index e9b0f71..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala +++ /dev/null @@ -1,76 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailKriteriaPage -import pages.detail.components.DukazyKriteria -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -object DetailKriteriaPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailKriteriaPageConnector( - state: DetailKriteriaPageConnector.AppState -)( - $page: Signal[Page.DetailKriteria] -)(using Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => - (p.osobniCislo.value, p.parametr.value, p.kriterium.value) - )((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map( - FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) - ) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, s, $s) => - DetailKriteriaPage($s)(state.actionBus.contramap { - case DukazyKriteria.Add => - NavigateTo( - Page.UpravDukazKriteria( - Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), - Page.Titled(s.parametr.id, Some(s.parametr.nazev)), - Page.Titled(s.kriterium.id, Some(s.kriterium.id)) - ) - ) - }) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): DetailKriteriaPage.ViewModel = - DetailKriteriaPage.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala deleted file mode 100644 index 4d8024f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ /dev/null @@ -1,57 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import pages.detail.DetailPage -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import fiftyforms.ui.components.tailwind.Color - -object DetailPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailPageConnector(state: DetailPageConnector.AppState)( - $page: Signal[Page.Detail] -)(using router: Router[Page]): - val $oscChangeSignal = $page.splitOne(_.osobniCislo.value)((osc, _, _) => osc) - val $pageChangeSignal = - $oscChangeSignal.flatMap(osc => - EventStream.fromSeq(Seq(FetchUserDetails(osc), FetchParameters(osc))) - ) - // TODO: filter the value based on the current osc - // OSC change will fetch new data, but still - // - we need to be sure that what we got is really what we ought to display - // - we want to display stale data accordingly (at least with loading indicator) - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - def apply: HtmlElement = - AppPage(state.actionBus)( - $data.combineWithFn($params)(_ zip _) - .map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: List[Parameter] - ): DetailPage.ViewModel = - DetailPage.ViewModel( - o.toDetailOsoby, - p.map( - _.toParametr(param => - PageLink.container(Page.DetailParametru(o, param), state.actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala deleted file mode 100644 index fdaa7f8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailParametruPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink - -object DetailParametruPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailParametruPageConnector( - state: DetailParametruPageConnector.AppState -)( - $page: Signal[Page.DetailParametru] -)(using router: Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => (p.osobniCislo.value, p.parametr.value))((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map(FetchParameter(_, _)) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - } yield (da, pb) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailParametruPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter - ): DetailParametruPage.ViewModel = - DetailParametruPage.ViewModel( - o.toDetailOsoby, - p.toParametr(p => - PageLink.container(Page.DetailParametru(o, p), state.actionBus) - ), - p.criteria.map( - _.toKriterium { c => - PageLink.container( - Page.DetailKriteria(o, p, c), - state.actionBus - ) - } - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala deleted file mode 100644 index 049aa22..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -case class DirectoryPageConnector( - $input: EventStream[List[UserInfo]], - actionBus: Observer[Action] -)(using router: Router[Page]): - val $data = $input.startWithNone - val $actionSignal = EventStream.fromValue(FetchDirectory) - - def apply: HtmlElement = - AppPage(actionBus)( - $data.split(_ => ())((_, _, s) => - pages.directory.DirectoryPage( - s.map( - _.map( - _.toUserRow(u => - PageLink.container( - Page.Detail(Page.Titled(u.personalNumber)), - actionBus - ) - ) - ) - ) - ) - ), - $actionSignal --> actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala deleted file mode 100644 index 88e7450..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ /dev/null @@ -1,68 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii -import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria -import fiftyforms.ui.components.tailwind.Color - -extension (o: UserInfo) - def toDetailOsoby: DetailOsoby.ViewModel = - DetailOsoby.ViewModel( - o.personalNumber, - o.name, - o.email, - o.phone, - o.img, - o.mainFunction.map(f => - DetailOsoby.Funkce.ViewModel(f.name, f.dept, f.ou) - ), - o.userContracts.headOption.map(c => - DetailOsoby.PracovniPomer.ViewModel(c.rel, c.startDate, c.endDate) - ) - ) - -extension (param: Parameter) - def toParametr(container: Parameter => Anchor): DetailParametru.ViewModel = - DetailParametru.ViewModel( - id = param.id, - nazev = param.name, - popis = param.description, - status = "Nesplněno", - statusColor = Color.red, - a = container(param) - ) - -extension (crit: ParameterCriteria) - def toKriterium( - container: ParameterCriteria => Anchor - ): DetailKriteria.ViewModel = - DetailKriteria.ViewModel( - nazev = crit.criteriumText, - kapitola = crit.chapterId, - bod = crit.itemId, - status = "Nesplněno", - statusColor = Color.red, - splneno = false, - dukazy = Nil, - container = container(crit) - ) - -extension (user: UserInfo) - def toUserRow( - container: UserInfo => HtmlElement = (_: UserInfo) => div() - ): UserRow.ViewModel = - UserRow.ViewModel( - osobniCislo = user.personalNumber.toString, - celeJmeno = user.name, - prijmeni = user.surname, - hlavniFunkce = user.mainFunction.map(_.name), - img = user.img, - container = () => container(user) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala deleted file mode 100644 index 7d5b2c1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.dashboard - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.Page - -object DashboardPage: - - def render: HtmlElement = - div("Dashboard page") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala deleted file mode 100644 index 9ef0698..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala +++ /dev/null @@ -1,31 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailKriteriaPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply($m: Signal[ViewModel])( - action: Observer[DukazyKriteria.Action] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - DukazyKriteria($m.map(_.kriterium.dukazy))(action) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala deleted file mode 100644 index 3b316e1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object DetailPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametry: SeznamParametru.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby($m.map(_.osoba)), - SeznamParametru($m.map(_.parametry)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala deleted file mode 100644 index 17224cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala +++ /dev/null @@ -1,24 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailParametruPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriteria: SeznamKriterii.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby.header($m.map(_.osoba)), - DetailParametru($m.map(_.parametr)), - SeznamKriterii($m.map(_.kriteria)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala deleted file mode 100644 index dc912fa..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ /dev/null @@ -1,107 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb -package app -package pages.detail - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.UpravDukazForm -import fiftyforms.services.files.File - -object UpravDukaz: - - type ThisPage = Page.UpravDukazKriteria - type PageKey = (OsobniCislo, String, String) - - trait State { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def availableFiles: EventStream[List[File]] - def actionBus: Observer[Action] - } - - def keyOfPage(page: ThisPage): PageKey = - (page.osobniCislo.value, page.parametr.value, page.kriterium.value) - - def onChangeAction(key: PageKey): Action = - FetchParameterCriteria( - key._1, - key._2, - key._3, - Page.UpravDukazKriteria(_, _, _) - ) - - class Connector(state: State)($page: Signal[ThisPage])(using Router[Page]): - val $paramChangeSignal = $page.splitOne(keyOfPage)((x, _, _) => x) - val $pageChangeSignal = $paramChangeSignal.map(onChangeAction) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.split(_ => ())((_, s, $s) => - PageComponent( - $s.map(buildModel), - state.availableFiles, - state.actionBus.contramap { - case UpravDukazForm.Cancelled => - NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) - case UpravDukazForm.AvailableFilesRequested => - FetchAvailableFiles(s._1.personalNumber) - } - ) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): PageComponent.ViewModel = - import connectors.* - PageComponent.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) - - object PageComponent: - import components.* - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply( - $m: Signal[ViewModel], - availableFilesStream: EventStream[List[File]], - events: Observer[UpravDukazForm.Event] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - UpravDukazForm(availableFilesStream)(events) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala deleted file mode 100644 index ca311b0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.Icons -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Color - -object DetailKriteria: - case class ViewModel( - nazev: String, - kapitola: String, - bod: String, - status: String, - statusColor: Color, - splneno: Boolean, - dukazy: List[DukazKriteria.ViewModel], - container: HtmlElement = div() - ) { - val id = s"${kapitola}${bod}" - } - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h3( - cls := "text-xl leading-6 font-bold text-gray-900", - child.text <-- $m.map(_.nazev) - ), - h3( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - "Kapitola ", - child.text <-- $m.map(_.kapitola), - " bod ", - child <-- $m.map(_.bod) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala deleted file mode 100644 index ae90290..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala +++ /dev/null @@ -1,100 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Avatar - -object DetailOsoby: - - object Funkce: - case class ViewModel( - nazev: String, - stredisko: String, - voj: String - ) - - def render($m: Signal[ViewModel]): HtmlElement = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.nazev), - span( - cls := "hidden md:inline", - " @ ", - child.text <-- $m.map(_.stredisko), - ", ", - child.text <-- $m.map(_.voj) - ) - ) - - object PracovniPomer: - case class ViewModel( - druh: String, - pocatek: LocalDate, - konec: Option[LocalDate] - ) - - def render($m: Signal[ViewModel]): HtmlElement = - import fiftyforms.ui.components.tailwind.CustomAttrs.datetime - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.druh), - " od ", - time( - datetime <-- $m.map(_.pocatek.toString), - child.text <-- $m.map(_.pocatek.toString) - ) - ) - - case class ViewModel( - osobniCislo: OsobniCislo, - jmeno: String, - email: Option[String], - telefon: Option[String], - img: Option[String], - hlavniFunkce: Option[Funkce.ViewModel], - pracovniPomer: Option[PracovniPomer.ViewModel] - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-4", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(16) - ), - div( - h1( - cls := "text-2xl font-bold text-gray-900", - child.text <-- $m.map(_.jmeno) - ), - child.maybe <-- $m.map(_.hlavniFunkce).split(_ => ())((_, _, d) => - Funkce.render(d) - ), - child.maybe <-- $m.map(_.pracovniPomer).split(_ => ())((_, _, d) => - PracovniPomer.render(d) - ) - ) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-5", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(8) - ), - div( - h1( - cls := "text-lg font-medium text-gray-800", - child.text <-- $m.map(_.jmeno) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala deleted file mode 100644 index e11cb62..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color - -object DetailParametru: - case class ViewModel( - id: String, - nazev: String, - popis: String, - status: String, - statusColor: Color, - a: Anchor - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h2( - cls := "text-2xl leading-6 font-medium text-gray-900", - child.text <-- $m.map(_.nazev) - ), - p( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - child.text <-- $m.map(_.popis) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - h2( - cls := "text-lg leading-6 font-medium text-gray-600", - child.text <-- $m.map(_.nazev) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala deleted file mode 100644 index a00ede8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala +++ /dev/null @@ -1,177 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.CustomAttrs - -object DukazKriteria: - case class Osoba(osobniCislo: String, jmeno: String) - case class Udalost(osoba: Osoba, datum: LocalDate) - case class Dokument( - url: String, - nazev: String, - pridal: Udalost, - odebral: Option[Udalost] - ) - case class ViewModel( - dokumenty: List[Dokument], - autorizoval: Option[Udalost], - platiDo: Option[LocalDate], - poznámka: Option[String] - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-lg", - div( - cls := "px-4 py-5 sm:px-6", - h3( - cls := "text-lg leading-6 font-medium text-gray-900", - """Applicant Information""" - ), - p( - cls := "mt-1 max-w-2xl text-sm text-gray-500", - """Personal details and application.""" - ) - ), - div( - cls := "border-t border-gray-200 px-4 py-5 sm:px-6", - dl( - cls := "grid grid-cols-1 gap-x-4 gap-y-8 sm:grid-cols-2", - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Full name""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Margot Foster""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Application for""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Backend Developer""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Email address""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """margotfoster@example.com""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Salary expectation""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """$120,000""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """About""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Fugiat ipsum ipsum deserunt culpa aute sint do nostrud anim incididunt cillum culpa consequat. Excepteur qui ipsum aliquip consequat sint. Sit id mollit nulla mollit nostrud in ea officia proident. Irure nostrud pariatur mollit ad adipisicing reprehenderit deserunt qui eu.""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """Attachments""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - ul( - role := "list", - cls := "border border-gray-200 rounded-md divide-y divide-gray-200", - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """resume_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ), - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """coverletter_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala deleted file mode 100644 index 22f134f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala +++ /dev/null @@ -1,35 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object DukazyKriteria: - sealed trait Action - case object Add extends Action - - type ViewModel = List[DukazKriteria.ViewModel] - - def apply($m: Signal[ViewModel])(actions: Observer[Action]): HtmlElement = - div( - child.maybe <-- $m.map(m => - if (m.isEmpty) then Some(prazdnyDukaz(actions)) - else None - ), - children <-- $m.map(_.zipWithIndex).split(_._2)((_, _, $s) => - DukazKriteria($s.map(_._1)) - ) - ) - - private def prazdnyDukaz(actions: Observer[Action]): HtmlElement = - button( - tpe := "button", - onClick.preventDefault.mapTo(Add) --> actions, - cls := "relative block w-full border-2 border-gray-300 border-dashed rounded-lg p-12 text-center hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - Icons.outline - .`document-add`(12) - .amend(svg.cls := "mx-auto text-gray-400"), - span( - cls := "mt-2 block text-sm font-medium text-gray-900", - "Přidat důkaz" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala deleted file mode 100644 index f628697..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.list.StackedList -import fiftyforms.ui.components.tailwind.list.ListRow -import fiftyforms.ui.components.tailwind.list.RowTag -import fiftyforms.ui.components.tailwind.list.RowNext -import java.time.LocalDate - -object SeznamKriterii: - type ViewModel = List[DetailKriteria.ViewModel] - - private val kritList = new StackedList[DetailKriteria.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - kritList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = - p(cls := "text-sm text-gray-500", s"${i.kapitola}${i.bod}"), - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.container - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala deleted file mode 100644 index 2bfe4bc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.list.{ - StackedList, - ListRow, - RowTag, - PropList, - IconText, - RowNext -} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.LinkSupport.* - -object SeznamParametru: - type ViewModel = List[DetailParametru.ViewModel] - - private val parametrList = new StackedList[DetailParametru.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - parametrList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = emptyNode, - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.a - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala deleted file mode 100644 index f048e9d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ /dev/null @@ -1,101 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.form.* -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement -import fiftyforms.services.files.components.tailwind.FilePicker -import cz.e_bs.cmi.mdr.pdb.frontend.AutorizujDukaz -import cz.e_bs.cmi.mdr.pdb.frontend.DocumentRef -import fiftyforms.services.files.File - -object UpravDukazForm: - sealed trait Event - case object Cancelled extends Event - case object AvailableFilesRequested extends Event - def apply(availableFilesStream: EventStream[List[File]])( - updates: Observer[Event] - ): HtmlElement = - val (filesStream, filesObserver) = - EventStream.withObserver[FilePicker.Event] - val files = Var[List[File]](Nil) - def submitButtons: HtmlElement = - div( - cls := "pt-5", - div( - cls := "flex justify-end", - button( - tpe := "button", - cls := "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zrušit", - onClick.mapTo(Cancelled) --> updates - ), - button( - tpe := "submit", - cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - disabled <-- files.signal.map(_.isEmpty), - "Autorizovat" - ) - ) - ) - - div( - filesStream.collect { case FilePicker.SelectionUpdated(files) => - files.to(List) - } --> files.writer, - filesStream.collect { case FilePicker.AvailableFilesRequested => - AvailableFilesRequested - } --> updates, - cls := "bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6", - Form( - Form.Body( - Form.Section( - FormHeader( - FormHeader.ViewModel( - "Doložení kritéria", - "Sestavte doklady poskytující důkaz kritéria a potvrďte odesláním formuláře. Případné limitace či jiné relevantní údaje vepište do pole Komentář." - ) - ), - FormFields( - FormRow( - "dokumenty", - "Dokumenty", - FilePicker(files.signal, availableFilesStream)(filesObserver) - .amend(idAttr := "dokumenty", cls("max-w-lg")) - ).toHtml, - FormRow( - "platnost", - "Platnost", - input( - idAttr := "platnost", - name := "platnost", - tpe := "date", - autoComplete := "date", - cls := "block max-w-lg w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" - ) - ).toHtml, - FormRow( - "komentar", - "Komentář", - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - textArea( - idAttr := "komentar", - name := "about", - rows := 3, - cls := "max-w-lg shadow-sm block w-full focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border border-gray-300 rounded-md" - ), - p( - cls := "mt-2 text-sm text-gray-500", - "Doplňte prosím potřebné informace související s doložením kritéria, včetně případných limitací." - ) - ) - ).toHtml - ) - ) - ), - submitButtons - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala deleted file mode 100644 index 41033f6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -import components._ - -object DirectoryPage: - - type ViewModel = List[UserRow.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - val (actionsStream, actionObserver) = - EventStream.withObserver[SearchForm.Action] - val $filter = actionsStream - .collect { - case SearchForm.ClearFilter => None - case SearchForm.SetFilter(t) => Some(t) - } - .startWith(None) - val byLetter = for { - d <- $m - f <- $filter - } yield for { - (letter, users) <- d - .filter { user => - f match - case None => true - case Some(t) => user.search.contains(t) - } - .groupBy(_.prijmeni.head) - .to(List) - .sortBy(_._1) - } yield (letter.toString, users.sortBy(_.prijmeni)) - - div( - cls := "h-full max-w-7xl mx-auto order-first flex flex-col flex-shrink-0", - SearchForm(actionObserver), - Directory(byLetter) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala deleted file mode 100644 index 73f915d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -object Directory: - - object Header: - type ViewModel = String - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500", - h3(child.text <-- $m) - ) - - object UserList: - type ViewModel = List[UserRow.ViewModel] - def apply($m: Signal[ViewModel]): HtmlElement = - ul( - role := "list", - cls := "relative z-0 divide-y divide-gray-200", - children <-- $m.split(_.osobniCislo)((_, _, s) => UserRow(s)) - ) - - type ViewModel = List[(String, List[UserRow.ViewModel])] - def apply($m: Signal[ViewModel]): HtmlElement = - nav( - cls := "flex-1 min-h-0 overflow-y-auto", - aria.label := "Directory", - children <-- $m.split(_._1)((_, _, s) => - div( - cls := "relative", - Header(s.map(_._1)), - UserList(s.map(_._2)) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala deleted file mode 100644 index e182910..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala +++ /dev/null @@ -1,54 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object SearchForm: - sealed trait Action - case object SubmitFilter extends Action - sealed trait FilterAction extends Action - case object ClearFilter extends FilterAction - case class SetFilter(value: String) extends FilterAction - - def apply(actions: Observer[Action]): HtmlElement = - div( - cls := "px-6 pt-4 pb-4", - form( - cls := "flex space-x-4", - onSubmit.mapTo(SubmitFilter) --> actions, - div( - cls := "flex-1 min-w-0", - label( - forId := "search", - cls := "sr-only", - "Hledat" - ), - div( - cls := "relative rounded-md shadow-sm", - div( - cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", - Icons.solid.search().amend(svg.cls := "text-gray-400") - ), - input( - tpe := "search", - name := "search", - idAttr := "search", - cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", - placeholder := "Hledat", - composeEvents(onInput.mapToValue.setAsValue.map(SetFilter(_)))( - _.throttle(500) - ) --> actions - ) - ) - ), - button( - tpe := "submit", - cls := "inline-flex justify-center px-3.5 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500", - Icons.solid.filter().amend(svg.cls := "text-gray-400"), - span( - cls := "sr-only", - "Hledat" - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala deleted file mode 100644 index 82f7723..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala +++ /dev/null @@ -1,52 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.airstream.core.Signal -import fiftyforms.ui.components.tailwind.Avatar - -object UserRow: - case class ViewModel( - osobniCislo: String, - celeJmeno: String, - prijmeni: String, - hlavniFunkce: Option[String], - img: Option[String], - container: () => HtmlElement = () => div() - ) { - val search = prijmeni.toLowerCase - } - - def apply($m: Signal[ViewModel]): HtmlElement = - inline def avatarImage = - Avatar($m.map(_.img)).avatarImage(10) - - li( - div( - cls := "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500", - div( - cls := "flex-shrink-0", - child <-- avatarImage - ), - div( - cls := "flex-1 min-w-0", - child <-- $m.map { o => - o.container() - .amend( - cls := "focus:outline-none", - span( - cls := "absolute inset-0", - aria.hidden := true - ), - p( - cls := "text-sm font-medium text-gray-900", - o.celeJmeno - ), - p( - cls := "text-sm text-gray-500 truncate", - o.hlavniFunkce - ) - ) - } - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala deleted file mode 100644 index 467c5f5..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala +++ /dev/null @@ -1,99 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router - -object ErrorPage: - case class ViewModel( - homePage: Page, - errorName: String, - title: String, - subTitle: String - ) - - def apply(m: ViewModel, actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - val ViewModel(homePage, errorName, title, subTitle) = m - div( - cls := "min-h-full pt-16 pb-12 flex flex-col bg-white", - main( - cls := "flex-grow flex flex-col justify-center max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex-shrink-0 flex justify-center", - a( - href := "/", - cls := "inline-flex", - span( - cls := "sr-only", - """Workflow""" - ), - img( - cls := "h-12 w-auto", - src := "https://tailwindui.com/img/logos/workflow-mark.svg?color=indigo&shade=600", - alt := "" - ) - ) - ), - div( - cls := "py-16", - div( - cls := "text-center", - p( - cls := "text-sm font-semibold text-indigo-600 uppercase tracking-wide", - errorName - ), - h1( - cls := "mt-2 text-4xl font-extrabold text-gray-900 tracking-tight sm:text-5xl", - title - ), - p( - cls := "mt-2 text-base text-gray-500", - subTitle - ), - div( - cls := "mt-6", - PageLink - .container(homePage, actionBus) - .amend( - cls := "text-base font-medium text-indigo-600 hover:text-indigo-500", - """Go back home""" - ) - ) - ) - ) - ), - footer( - cls := "flex-shrink-0 max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", - nav( - cls := "flex justify-center space-x-4", - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Contact Support""" - ), - span( - cls := "inline-block border-l border-gray-300", - aria.hidden := true - ), - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Status""" - ), - span( - cls := "inline-block border-l border-gray-300", - aria.hidden := true - ), - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Twitter""" - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala deleted file mode 100644 index bd9baec..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action - -object NotFoundPage: - def apply(homePage: Page, url: String, actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - ErrorPage( - ErrorPage.ViewModel( - homePage, - "404 error", - "Page not found.", - s"Sorry, but page $url doesn't exist." - ), - actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala deleted file mode 100644 index 476fe3c..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action - -object UnhandledErrorPage: - - case class ViewModel( - homePage: Page, - errorName: Option[String], - errorMessage: Option[String] - ) - - def apply(m: ViewModel, actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - ErrorPage( - ErrorPage.ViewModel( - m.homePage, - "Unexpected error occurred", - m.errorName.getOrElse( - "Uh oh!" - ), // TODO: translations, better text than uh oh - m.errorMessage.getOrElse( - "This wasn't supposed to happen! Please try again." - ) - ), - actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala deleted file mode 100644 index 25f059f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala +++ /dev/null @@ -1,6 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.services - -import com.raquo.airstream.core.Observer - -trait DataFetcher[K, A]: - def fetch(id: K, sink: Observer[A]): Unit diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala deleted file mode 100644 index a469830..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala +++ /dev/null @@ -1,120 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package state - -import com.raquo.airstream.core.EventStream -import cz.e_bs.cmi.mdr.pdb.{UserInfo, OsobniCislo} -import com.raquo.airstream.core.Observer -import scala.scalajs.js -import scala.scalajs.js.JSON -import zio.json._ -import com.raquo.airstream.eventbus.EventBus -import com.raquo.airstream.ownership.Owner -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.UserFunction -import cz.e_bs.cmi.mdr.pdb.UserContract -import fiftyforms.services.files.File - -trait AppState - extends connectors.DetailPageConnector.AppState - with connectors.DetailParametruPageConnector.AppState - with connectors.DetailKriteriaPageConnector.AppState - with pages.detail.UpravDukaz.State: - def users: EventStream[List[UserInfo]] - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - -class MockAppState(implicit owner: Owner, router: Router[Page]) - extends AppState: - - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonDecoder[UserFunction] = DeriveJsonDecoder.gen - given JsonDecoder[UserContract] = DeriveJsonDecoder.gen - given JsonDecoder[UserInfo] = DeriveJsonDecoder.gen - - given JsonDecoder[ParameterCriteria] = DeriveJsonDecoder.gen - given JsonDecoder[Parameter] = DeriveJsonDecoder.gen - - private val actions = EventBus[Action]() - private val (parametersStream, pushParameters) = - EventStream.withCallback[List[Parameter]] - private val (usersStream, pushUsers) = - EventStream.withCallback[List[UserInfo]] - private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] - private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] - - private val mockData: List[UserInfo] = - mockUsers - .asInstanceOf[js.Dictionary[js.Object]] - .values - // TODO: is there a more efficient way to parse from JS object directly? - .map { o => - val parsed = JSON.stringify(o).fromJson[UserInfo] - parsed.left.foreach(org.scalajs.dom.console.log(_)) - parsed - } - .collect { case Right(u) => - u - } - .toList - - private val mockParameters: List[Parameter] = - pdbParams - .asInstanceOf[js.Dictionary[js.Object]] - .values - .map(o => JSON.stringify(o).fromJson[Parameter]) - .collect { case Right(p) => p } - .toList - - // TODO: Extract to separate event handler - actions.events.foreach { - case FetchDirectory => pushUsers(mockData) - case FetchUserDetails(osc) => - mockData.find(_.personalNumber == osc).foreach { o => - pushDetails(o) - router.replaceState(Page.Detail(o)) - } - case FetchParameters(osc) => - pushParameters(mockParameters) - case FetchParameter(osc, paramId) => - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(Page.DetailParametru(o, p)) - case FetchParameterCriteria(osc, paramId, critId, page) => - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - c <- p.criteria.find(_.id == critId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(page(o, p, c)) - case NavigateTo(page) => router.pushState(page) - case FetchAvailableFiles(osc) => - pushFiles( - List( - File("https://tc163.cmi.cz/here", "Example file") - ) - ) - } - - override def users: EventStream[List[UserInfo]] = - usersStream.debugWithName("users") - - override def details: EventStream[UserInfo] = - detailsStream.debugWithName("details") - - override def parameters: EventStream[List[Parameter]] = - parametersStream.debugWithName("parameters") - - override def availableFiles: EventStream[List[File]] = - filesStream.debugWithName("available files") - - override def actionBus: Observer[Action] = - actions.writer.debugWithName("actions writer") diff --git a/app/src/main/scala/mdr/pdb/app/Main.scala b/app/src/main/scala/mdr/pdb/app/Main.scala new file mode 100644 index 0000000..081b4ee --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/Main.scala @@ -0,0 +1,103 @@ +package mdr.pdb.app + +import scala.scalajs.js.annotation.JSExportTopLevel +import scala.scalajs.js.annotation.JSExport +import scala.scalajs.js.annotation.JSImport +import scala.scalajs.js +import org.scalajs.dom +import com.raquo.laminar.api.L.{*, given} +import scala.scalajs.js.Date +import com.raquo.waypoint.Router +import com.raquo.waypoint.SplitRender +import mdr.pdb.app.services.DataFetcher +import scala.scalajs.js.JSON +import zio.json._ +import mdr.pdb.UserInfo +import mdr.pdb.app.state.AppState + +@js.native +@JSImport("stylesheets/main.css", JSImport.Namespace) +object Css extends js.Any + +@js.native +@JSImport("data/users.json", JSImport.Default) +object mockUsers extends js.Object + +@js.native +@JSImport("params/pdb-params.json", JSImport.Default) +object pdbParams extends js.Object + +@JSExportTopLevel("app") +object Main: + + @JSExport + def main(args: Array[String]): Unit = + import Routes.given + onLoad { + setupAirstream() + val appContainer = dom.document.querySelector("#app") + val _ = + render( + appContainer, + renderPage(state.MockAppState(using unsafeWindowOwner, router)) + ) + } + + private def onLoad(f: => Unit): Unit = + documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) + + private def setupAirstream()(using router: Router[Page]): Unit = + AirstreamError.registerUnhandledErrorCallback(err => + router.forcePage( + Page.UnhandledError( + Some(err.getClass.getName), // TODO: Fill only in dev mode + Some(err.getMessage) + ) + ) + ) + + def renderPage(state: AppState)(using + router: Router[Page] + ): HtmlElement = + val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) + .collectSignal[Page.Detail]( + connectors + .DetailPageConnector(state)(_) + .apply + ) + .collectSignal[Page.DetailParametru]( + connectors + .DetailParametruPageConnector(state)(_) + .apply + ) + .collectSignal[Page.DetailKriteria]( + connectors + .DetailKriteriaPageConnector(state)(_) + .apply + ) + .collectSignal[Page.UpravDukazKriteria]( + pages.detail.UpravDukaz.Connector(state)(_).apply + ) + .collectStatic(Page.Dashboard)( + connectors.DashboardPageConnector(state.actionBus).apply + ) + .collect[Page.NotFound](pg => + pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) + ) + .collect[Page.UnhandledError](pg => + pages.errors + .UnhandledErrorPage( + pages.errors.UnhandledErrorPage + .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), + state.actionBus + ) + ) + .collectStatic(Page.Directory)( + connectors + .DirectoryPageConnector(state.users, state.actionBus) + .apply + ) + div(cls := "h-full", child <-- pageSplitter.$view) + + // Pull in the stylesheet + val css: Css.type = Css diff --git a/app/src/main/scala/mdr/pdb/app/Routes.scala b/app/src/main/scala/mdr/pdb/app/Routes.scala new file mode 100644 index 0000000..2cc860c --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/Routes.scala @@ -0,0 +1,198 @@ +package mdr.pdb.app + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.* +import org.scalajs.dom +import zio.json.{*, given} +import mdr.pdb.OsobniCislo + +import scala.scalajs.js +import mdr.pdb.UserInfo +import mdr.pdb.Parameter +import mdr.pdb.ParameterCriteria +import mdr.pdb.app.Page.Titled + +// enum is not working with Waypoints' SplitRender collectStatic +sealed abstract class Page( + val id: String, + val title: String, + val parent: Option[Page] +) { + val path: Vector[Page] = + parent match + case None => Vector(this) + case Some(p) => p.path :+ this + + val isRoot: Boolean = parent.isEmpty +} + +object Page: + + case class Titled[V](value: V, title: Option[String] = None): + val show: String = title.getOrElse(value.toString) + + case object Directory extends Page("directory", "Adresář", None) + + case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) + + case class Detail(osobniCislo: Titled[OsobniCislo]) + extends Page("user", osobniCislo.show, Some(Directory)) + + object Detail { + def apply(o: UserInfo): Detail = Detail( + Titled(o.personalNumber, Some(o.name)) + ) + } + + case class DetailParametru( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String] + ) extends Page( + "parameter", + parametr.show, + Some(Detail(osobniCislo)) + ) + + object DetailParametru { + def apply(o: UserInfo, p: Parameter): DetailParametru = + DetailParametru( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)) + ) + } + + case class DetailKriteria( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String], + kriterium: Titled[String] + ) extends Page( + "criteria", + kriterium.show, + Some(DetailParametru(osobniCislo, parametr)) + ) + + object DetailKriteria { + def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = + DetailKriteria( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)), + Titled(k.id, Some(k.id)) + ) + } + + case class UpravDukazKriteria( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String], + kriterium: Titled[String] + ) extends Page( + "addProof", + "Důkaz", + Some(DetailKriteria(osobniCislo, parametr, kriterium)) + ) + + object UpravDukazKriteria { + def apply( + o: UserInfo, + p: Parameter, + k: ParameterCriteria + ): UpravDukazKriteria = + UpravDukazKriteria( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)), + Titled(k.id, Some(k.id)) + ) + } + + case class NotFound(url: String) extends Page("404", "404", Some(Directory)) + + case class UnhandledError( + errorName: Option[String], + errorMessage: Option[String] + ) extends Page("500", "Unexpected error", Some(Directory)) + +object Routes: + given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) + given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) + given [V: JsonEncoder]: JsonEncoder[Titled[V]] = + DeriveJsonEncoder.gen[Titled[V]] + given [V: JsonDecoder]: JsonDecoder[Titled[V]] = + DeriveJsonDecoder.gen[Titled[V]] + given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] + given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] + + val base = + js.`import`.meta.env.BASE_URL + .asInstanceOf[String] + "app" + + val homePage: Page = Page.Directory + + given router: Router[Page] = Router[Page]( + routes = List( + Route.static(homePage, root / endOfSegments, basePath = base), + Route.static( + Page.Dashboard, + root / "dashboard" / endOfSegments, + basePath = base + ), + Route[Page.Detail, String]( + encode = _.osobniCislo.value.toString, + decode = osc => Page.Detail(Titled(OsobniCislo(osc))), + root / "osoba" / segment[String] / endOfSegments, + basePath = base + ), + Route[Page.DetailParametru, (String, String)]( + encode = p => (p.osobniCislo.value.toString, p.parametr.value), + decode = + p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / endOfSegments, + basePath = base + ), + Route[Page.DetailKriteria, (String, String, String)]( + encode = p => + ( + p.osobniCislo.value.toString, + p.parametr.value, + p.kriterium.value.replaceAll("\\.", "--") + ), + decode = p => + Page.DetailKriteria( + Titled(OsobniCislo(p._1)), + Titled(p._2), + Titled(p._3.replaceAll("--", ".")) + ), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / "kriterium" / segment[String] / endOfSegments, + basePath = base + ), + Route[Page.UpravDukazKriteria, (String, String, String)]( + encode = p => + ( + p.osobniCislo.value.toString, + p.parametr.value, + p.kriterium.value.replaceAll("\\.", "--") + ), + decode = p => + Page.UpravDukazKriteria( + Titled(OsobniCislo(p._1)), + Titled(p._2), + Titled(p._3.replaceAll("--", ".")) + ), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / "kriterium" / segment[String] / "edit" / endOfSegments, + basePath = base + ) + ), + serializePage = _.toJson, + deserializePage = _.fromJson[Page] + .fold(s => throw IllegalStateException(s), identity), + getPageTitle = _.title, + routeFallback = url => Page.NotFound(url), + deserializeFallback = _ => Page.Dashboard + )( + $popStateEvent = windowEvents.onPopState, + owner = unsafeWindowOwner + ) diff --git a/app/src/main/scala/mdr/pdb/app/actions.scala b/app/src/main/scala/mdr/pdb/app/actions.scala new file mode 100644 index 0000000..d078b3a --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/actions.scala @@ -0,0 +1,21 @@ +package mdr.pdb.app + +import mdr.pdb.OsobniCislo +import mdr.pdb.UserInfo +import mdr.pdb.Parameter +import mdr.pdb.ParameterCriteria + +sealed trait Action + +case object FetchDirectory extends Action +case class FetchUserDetails(osc: OsobniCislo) extends Action +case class FetchParameters(osc: OsobniCislo) extends Action +case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action +case class FetchParameterCriteria( + osc: OsobniCislo, + paramId: String, + critId: String, + page: (UserInfo, Parameter, ParameterCriteria) => Page +) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action +case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/mdr/pdb/app/components/AppPage.scala new file mode 100644 index 0000000..130c817 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/AppPage.scala @@ -0,0 +1,82 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.Page +import mdr.pdb.{UserProfile, UserInfo, OsobniCislo} +import com.raquo.waypoint.Router +import mdr.pdb.app.Action +import mdr.pdb.app.NavigateTo +import mdr.pdb.UserFunction + +object AppPage: + // TODO: pages by logged in user + val pages: List[Page] = List(Page.Directory, Page.Dashboard) + + import NavigationBar.{Logo, MenuItem} + + val logo = Logo( + "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", + "Workflow" + ) + + // TODO: menu items by user profile + val userMenu = + List( + MenuItem("Your Profile"), + MenuItem("Settings"), + MenuItem("Sign out") + ) + + // TODO: load user profile + val $userProfile = Var( + UserProfile( + "tom", + UserInfo( + OsobniCislo("1031"), + "tom", + "Tom", + "Cook", + None, + None, + Some("tom@example.com"), + Some("+420 222 866 180"), + Some( + UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") + ), + Nil, + None + ) + ) + ) + + val $userInfo = $userProfile.signal.map(_.userInfo) + + type ViewModel = Option[HtmlElement] + def apply( + actionBus: Observer[Action] + )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using + router: Router[Page] + ): HtmlElement = + PageLayout(actionBus)( + $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => + PageLayout.ViewModel( + NavigationBar.ViewModel( + u, + pages.map(p => + NavigationBar.Link( + () => + PageLink( + p, + actionBus + ), + p == cp + ) + ), + userMenu, + logo + ), + c + ) + ), + mods + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala new file mode 100644 index 0000000..9375398 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala @@ -0,0 +1,143 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.tailwind.list.IconText.ViewModel +import fiftyforms.ui.components.tailwind.Icons +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.Page + +import com.raquo.laminar.api.L.{*, given} +import io.laminext.syntax.core.{*, given} +import mdr.pdb.app.Action + +object Breadcrumbs: + + private def slash = + import svg.{*, given} + svg( + cls := "flex-shrink-0 h-5 w-5 text-gray-300", + xmlns := "http://www.w3.org/2000/svg", + fill := "currentColor", + viewBox := "0 0 20 20", + ariaHidden := true, + path( + d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" + ) + ) + + def backIcon = + Icons.solid + .`arrow-narrow-left`() + .amend( + svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" + ) + + object Link: + case class ViewModel( + page: Page, + icon: Option[SvgElement], + extraClasses: String, + text: String, + textClasses: Option[String] = None + ) + + val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" + + def shortHome(p: Page) = ViewModel( + p, + Some(backIcon), + "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", + "Zpět na úvodní stránku", + None + ) + + def fullHome(p: Page) = ViewModel( + p, + Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), + "text-gray-400 hover:text-gray-500", + "Domů", + Some("sr-only") + ) + + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + PageLink + .container($m.map(_.page), actionBus) + .amend( + cls <-- $m.map(_.extraClasses), + child.maybe <-- $m.map(_.icon), + span( + cls <-- $m.map(_.textClasses.getOrElse("")), + child.text <-- $m.map(_.text) + ) + ) + + object FullBreadcrumbs: + type ViewModel = Page + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + ol( + role := "list", + cls := "flex items-center space-x-4", + children <-- $m.map(_.path).split(_.id)((_, _, $p) => + li( + div( + cls := "flex items-center", + child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), + Link( + $p.map(p => + if (p.isRoot) then Link.fullHome(p) + else + Link.ViewModel( + p, + None, + s"ml-4 max-w-xs truncate ${Link.baseClasses}", + p.title + ) + ), + actionBus + ) + ) + ) + ) + ) + + object ShortBreadcrumbs: + type ViewModel = Page + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + Link( + $m.map { p => + val target = p.parent.getOrElse(p) + if target.isRoot then Link.shortHome(target) + else + Link.ViewModel( + target, + Some(backIcon), + "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", + s"Zpět na ${target.title}" + ) + }, + actionBus + ) + + def apply(actionBus: Observer[Action])(using + router: Router[Page] + ): HtmlElement = + nav( + cls := "flex", + aria.label := "Breadcrumb", + div( + cls := "flex sm:hidden", + ShortBreadcrumbs(router.$currentPage, actionBus) + ), + div( + cls := "hidden sm:block", + FullBreadcrumbs(router.$currentPage, actionBus) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala new file mode 100644 index 0000000..4bb8b8d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala @@ -0,0 +1,242 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent +import fiftyforms.ui.components.tailwind.Icons +import fiftyforms.ui.components.tailwind.Avatar +import mdr.pdb.UserInfo +import io.laminext.syntax.core.* + +object NavigationBar: + + case class Logo(img: String, name: String) + case class Link(a: () => Anchor, active: Boolean) + case class MenuItem(title: String) + + case class ViewModel( + userInfo: UserInfo, + pages: List[Link], + userMenu: List[MenuItem], + logo: Logo + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + val $userInfo = $m.map(_.userInfo) + val mobileMenuOpen = Var(false) + + val desktopOnly = cls("hidden md:block") + val mobileOnly = cls("md:hidden") + + inline def avatarImage(size: Int = 8) = + Avatar($userInfo.map(_.img)).avatarImage(size) + + def notificationButton = button( + tpe := "button", + cls := List( + "bg-indigo-600", + "focus:outline-none", + "focus:ring-2", + "focus:ring-offset-2", + "focus:ring-offset-indigo-600", + "focus:ring-white", + "hover:text-white", + "p-1", + "rounded-full", + "text-indigo-200" + ), + span(cls := "sr-only", "View notifications"), + Icons.outline.bell() + ) + + def userProfile: HtmlElement = + val menuOpen = Var(false) + + def menuItem(item: MenuItem, idx: Int): HtmlElement = + a( + href := "#", + cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", + role := "menuitem", + tabIndex := -1, + idAttr := s"user-menu-item-$idx", + item.title + ) + + div( + cls := "ml-3 relative", + div( + button( + tpe := "button", + cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", + idAttr := "user-menu-button", + aria.expanded <-- menuOpen.signal, + aria.hasPopup := true, + span(cls := "sr-only", "Open user menu"), + child <-- avatarImage(), + onClick.preventDefault.mapTo( + !menuOpen.now() + ) --> menuOpen.writer + ) + ), + /* + * */ + div( + cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", + cls <-- menuOpen.signal.map { o => + if (o) "md:block" else "md:hidden" + }, + role := "menu", + aria.orientation := "vertical", + aria.labelledBy := "user-menu-button", + tabIndex := -1, + // : keyboard navigation + children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) + ) + ) + + def mobileProfile = + def menuItem(item: MenuItem): HtmlElement = + a( + href := "#", + cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", + item.title + ) + + div( + cls := "pt-4 pb-3 border-t border-indigo-700", + div( + cls := "flex items-center px-5", + div( + cls := "flex-shrink-0", + child <-- avatarImage(10) + ), + div( + cls := "ml-3", + div( + cls := "text-base font-medium text-white", + child.text <-- $userInfo.map(_.name) + ), + child.maybe <-- $userInfo.map( + _.email.map(e => + div( + cls := "text-sm font-medium text-indigo-300", + e + ) + ) + ) + ), + notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) + ), + div( + cls := "mt-3 px-2 space-y-1", + children <-- $m.map(_.userMenu.map(menuItem)) + ) + ) + + def pageLink(page: Link): Anchor = + page + .a() + .amend( + cls := "text-white px-3 py-2 rounded-md text-sm font-medium", + cls := Seq( + "bg-indigo-700" -> page.active, + "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active + ), + ariaCurrent := (if page.active then "page" else "false") + ) + + def logoImg: Image = + img( + cls := "h-8 w-8", + src <-- $m.map(_.logo.img), + alt <-- $m.map(_.logo.name) + ) + + def mobileMenuButton = button( + tpe := "button", + cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", + aria.controls := "mobile-menu", + aria.expanded <-- mobileMenuOpen.signal, + span(cls := "sr-only", "Open main menu"), + Icons.outline + .menu() + .amend( + svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") + ), + Icons.outline + .x() + .amend( + svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") + ), + onClick.preventDefault.mapTo( + !mobileMenuOpen.now() + ) --> mobileMenuOpen.writer + ) + + def navBarLeft = + div( + cls := "flex items-center", + div(cls := "flex-shrink-0", logoImg), + div( + desktopOnly, + div( + cls := "ml-10 flex items-baseline space-x-4", + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) + ) + ) + ) + + def navBarRight = + div( + desktopOnly, + div( + cls := "ml-4 flex items-center md:ml-6", + notificationButton, + userProfile + ) + ) + + def navBarMobile = + div( + cls := "-mr-2 flex", + mobileOnly, + mobileMenuButton + ) + + def navBar = + div( + cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", + div( + cls := "flex items-center justify-between h-16", + navBarLeft, + navBarRight, + navBarMobile + ) + ) + + def mobileMenu = + div( + mobileOnly, + cls <-- mobileMenuOpen.signal.map { o => + if (o) "block" else "hidden" + }, + idAttr := "mobile-menu", + div( + cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) + ), + mobileProfile + ) + + nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala new file mode 100644 index 0000000..c4cb10d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala @@ -0,0 +1,19 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.Page +import com.raquo.waypoint.Router +import mdr.pdb.app.Action + +object PageHeader: + def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = + header( + cls := "bg-white shadow-sm", + div( + cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", + h1( + cls := "text-lg leading-6 font-semibold text-gray-900", + Breadcrumbs(actionBus) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala new file mode 100644 index 0000000..aae4539 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala @@ -0,0 +1,30 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.Page +import mdr.pdb.app.Action +import fiftyforms.ui.components.tailwind.Loading +import io.laminext.syntax.core.* + +object PageLayout: + case class ViewModel( + navigation: NavigationBar.ViewModel, + content: Option[HtmlElement] + ) + def apply(actionBus: Observer[Action])( + $m: Signal[ViewModel], + mods: Modifier[HtmlElement]* + )(using router: Router[Page]): HtmlElement = + val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) + div( + cls := "h-full flex flex-col", + NavigationBar($m.map(_.navigation)), + child.maybe <-- router.$currentPage.map(_.isRoot) + .switch(None, Some(PageHeader(actionBus))), + main( + cls := "flex-grow-1 overflow-y-auto", + mods, + child <-- $maybeContent.map(_.getOrElse(Loading)) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/mdr/pdb/app/components/PageLink.scala new file mode 100644 index 0000000..e6e7052 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageLink.scala @@ -0,0 +1,40 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.LinkSupport.* +import mdr.pdb.app.Page +import mdr.pdb.app.Action +import com.raquo.waypoint.Router +import mdr.pdb.app.NavigateTo + +object PageLink: + type ViewModel = Page + def apply($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = + a(mods($m, actions), child.text <-- $m.map(_.title)) + + def apply(m: ViewModel, actions: Observer[Action])(using + router: Router[Page] + ): Anchor = apply(Val(m), actions) + + def container($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = a(mods($m, actions)) + + def container(m: ViewModel, actions: Observer[Action])(using + Router[Page] + ): Anchor = container(Val(m), actions) + + private def mods($m: Signal[Page], actions: Observer[Action])(using + router: Router[Page] + ): Modifier[Anchor] = + Seq( + href <-- $m.map(router.absoluteUrlForPage).recover { case _ => + Some("invalid url") + }, + composeEvents(onClick.noKeyMod.preventDefault)( + _.sample($m) + .map(NavigateTo.apply) + ) --> actions + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala new file mode 100644 index 0000000..7682772 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala @@ -0,0 +1,13 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.pages.dashboard.DashboardPage +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage + +class DashboardPageConnector(actionBus: Observer[Action])(using + router: Router[Page] +): + def apply: HtmlElement = + AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala new file mode 100644 index 0000000..76b9133 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala @@ -0,0 +1,76 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.Parameter +import mdr.pdb.UserInfo +import mdr.pdb.app.pages.detail.DetailParametruPage +import pages.detail.DetailKriteriaPage +import pages.detail.components.DukazyKriteria +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.ParameterCriteria + +object DetailKriteriaPageConnector { + trait AppState { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + } +} + +case class DetailKriteriaPageConnector( + state: DetailKriteriaPageConnector.AppState +)( + $page: Signal[Page.DetailKriteria] +)(using Router[Page]): + val $paramChangeSignal = + $page.splitOne(p => + (p.osobniCislo.value, p.parametr.value, p.kriterium.value) + )((x, _, _) => x) + val $pageChangeSignal = + $paramChangeSignal.map( + FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) + ) + + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + val $merged = + $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => + for { + da <- d + pa <- p + pb <- pa.find(_.id == pc._2) + ka <- pb.criteria.find(_.id == pc._3) + } yield (da, pb, ka) + ) + + def apply: HtmlElement = + AppPage(state.actionBus)( + $merged.map(_.map(buildModel)) + .split(_ => ())((_, s, $s) => + DetailKriteriaPage($s)(state.actionBus.contramap { + case DukazyKriteria.Add => + NavigateTo( + Page.UpravDukazKriteria( + Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), + Page.Titled(s.parametr.id, Some(s.parametr.nazev)), + Page.Titled(s.kriterium.id, Some(s.kriterium.id)) + ) + ) + }) + ), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: Parameter, + k: ParameterCriteria + ): DetailKriteriaPage.ViewModel = + DetailKriteriaPage.ViewModel( + o.toDetailOsoby, + p.toParametr(_ => a()), + k.toKriterium(_ => a()) + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DetailPageConnector.scala new file mode 100644 index 0000000..df3a6b7 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DetailPageConnector.scala @@ -0,0 +1,57 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.UserInfo +import pages.detail.DetailPage +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.pages.detail.components.DetailOsoby +import mdr.pdb.Parameter +import mdr.pdb.app.pages.detail.components.SeznamParametru +import fiftyforms.ui.components.tailwind.Color + +object DetailPageConnector { + trait AppState { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + } +} + +case class DetailPageConnector(state: DetailPageConnector.AppState)( + $page: Signal[Page.Detail] +)(using router: Router[Page]): + val $oscChangeSignal = $page.splitOne(_.osobniCislo.value)((osc, _, _) => osc) + val $pageChangeSignal = + $oscChangeSignal.flatMap(osc => + EventStream.fromSeq(Seq(FetchUserDetails(osc), FetchParameters(osc))) + ) + // TODO: filter the value based on the current osc + // OSC change will fetch new data, but still + // - we need to be sure that what we got is really what we ought to display + // - we want to display stale data accordingly (at least with loading indicator) + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + def apply: HtmlElement = + AppPage(state.actionBus)( + $data.combineWithFn($params)(_ zip _) + .map(_.map(buildModel)) + .split(_ => ())((_, _, s) => DetailPage(s)), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: List[Parameter] + ): DetailPage.ViewModel = + DetailPage.ViewModel( + o.toDetailOsoby, + p.map( + _.toParametr(param => + PageLink.container(Page.DetailParametru(o, param), state.actionBus) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DetailParametruPageConnector.scala new file mode 100644 index 0000000..fa7193d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DetailParametruPageConnector.scala @@ -0,0 +1,67 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.Parameter +import mdr.pdb.UserInfo +import mdr.pdb.app.pages.detail.DetailParametruPage +import pages.detail.DetailParametruPage +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.components.PageLink + +object DetailParametruPageConnector { + trait AppState { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + } +} + +case class DetailParametruPageConnector( + state: DetailParametruPageConnector.AppState +)( + $page: Signal[Page.DetailParametru] +)(using router: Router[Page]): + val $paramChangeSignal = + $page.splitOne(p => (p.osobniCislo.value, p.parametr.value))((x, _, _) => x) + val $pageChangeSignal = + $paramChangeSignal.map(FetchParameter(_, _)) + + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + val $merged = + $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => + for { + da <- d + pa <- p + pb <- pa.find(_.id == pc._2) + } yield (da, pb) + ) + + def apply: HtmlElement = + AppPage(state.actionBus)( + $merged.map(_.map(buildModel)) + .split(_ => ())((_, _, s) => DetailParametruPage(s)), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: Parameter + ): DetailParametruPage.ViewModel = + DetailParametruPage.ViewModel( + o.toDetailOsoby, + p.toParametr(p => + PageLink.container(Page.DetailParametru(o, p), state.actionBus) + ), + p.criteria.map( + _.toKriterium { c => + PageLink.container( + Page.DetailKriteria(o, p, c), + state.actionBus + ) + } + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DirectoryPageConnector.scala new file mode 100644 index 0000000..ab2c045 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DirectoryPageConnector.scala @@ -0,0 +1,34 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.UserInfo +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.components.AppPage + +case class DirectoryPageConnector( + $input: EventStream[List[UserInfo]], + actionBus: Observer[Action] +)(using router: Router[Page]): + val $data = $input.startWithNone + val $actionSignal = EventStream.fromValue(FetchDirectory) + + def apply: HtmlElement = + AppPage(actionBus)( + $data.split(_ => ())((_, _, s) => + pages.directory.DirectoryPage( + s.map( + _.map( + _.toUserRow(u => + PageLink.container( + Page.Detail(Page.Titled(u.personalNumber)), + actionBus + ) + ) + ) + ) + ) + ), + $actionSignal --> actionBus + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/mdr/pdb/app/connectors/conversions.scala new file mode 100644 index 0000000..f581796 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/conversions.scala @@ -0,0 +1,68 @@ +package mdr.pdb.app.connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.UserInfo +import mdr.pdb.app.pages.detail.components.DetailOsoby +import mdr.pdb.Parameter +import mdr.pdb.app.pages.detail.components.SeznamParametru +import mdr.pdb.ParameterCriteria +import mdr.pdb.app.pages.detail.components.SeznamKriterii +import mdr.pdb.app.pages.directory.components.UserRow +import mdr.pdb.app.pages.detail.components.DetailParametru +import mdr.pdb.app.pages.detail.components.DetailKriteria +import fiftyforms.ui.components.tailwind.Color + +extension (o: UserInfo) + def toDetailOsoby: DetailOsoby.ViewModel = + DetailOsoby.ViewModel( + o.personalNumber, + o.name, + o.email, + o.phone, + o.img, + o.mainFunction.map(f => + DetailOsoby.Funkce.ViewModel(f.name, f.dept, f.ou) + ), + o.userContracts.headOption.map(c => + DetailOsoby.PracovniPomer.ViewModel(c.rel, c.startDate, c.endDate) + ) + ) + +extension (param: Parameter) + def toParametr(container: Parameter => Anchor): DetailParametru.ViewModel = + DetailParametru.ViewModel( + id = param.id, + nazev = param.name, + popis = param.description, + status = "Nesplněno", + statusColor = Color.red, + a = container(param) + ) + +extension (crit: ParameterCriteria) + def toKriterium( + container: ParameterCriteria => Anchor + ): DetailKriteria.ViewModel = + DetailKriteria.ViewModel( + nazev = crit.criteriumText, + kapitola = crit.chapterId, + bod = crit.itemId, + status = "Nesplněno", + statusColor = Color.red, + splneno = false, + dukazy = Nil, + container = container(crit) + ) + +extension (user: UserInfo) + def toUserRow( + container: UserInfo => HtmlElement = (_: UserInfo) => div() + ): UserRow.ViewModel = + UserRow.ViewModel( + osobniCislo = user.personalNumber.toString, + celeJmeno = user.name, + prijmeni = user.surname, + hlavniFunkce = user.mainFunction.map(_.name), + img = user.img, + container = () => container(user) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/dashboard/DashboardPage.scala b/app/src/main/scala/mdr/pdb/app/pages/dashboard/DashboardPage.scala new file mode 100644 index 0000000..0eb8f7a --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/dashboard/DashboardPage.scala @@ -0,0 +1,11 @@ +package mdr.pdb.app.pages.dashboard + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.Page + +object DashboardPage: + + def render: HtmlElement = + div("Dashboard page") diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala new file mode 100644 index 0000000..ff3a25f --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala @@ -0,0 +1,31 @@ +package mdr.pdb.app.pages.detail + +import com.raquo.laminar.api.L.{*, given} + +import components._ + +object DetailKriteriaPage: + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametr: DetailParametru.ViewModel, + kriterium: DetailKriteria.ViewModel + ) + + def apply($m: Signal[ViewModel])( + action: Observer[DukazyKriteria.Action] + ): HtmlElement = + div( + cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + div( + DetailOsoby.header($m.map(_.osoba)), + DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") + ), + div( + DetailKriteria($m.map(_.kriterium)), + DukazyKriteria($m.map(_.kriterium.dukazy))(action) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/DetailPage.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailPage.scala new file mode 100644 index 0000000..b591cc9 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailPage.scala @@ -0,0 +1,25 @@ +package mdr.pdb.app.pages.detail + +import com.raquo.laminar.api.L.{*, given} + +import components._ +import mdr.pdb.app.Page +import com.raquo.waypoint.Router +import mdr.pdb.app.Action + +object DetailPage: + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametry: SeznamParametru.ViewModel + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + DetailOsoby($m.map(_.osoba)), + SeznamParametru($m.map(_.parametry)) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/DetailParametruPage.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailParametruPage.scala new file mode 100644 index 0000000..3322279 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailParametruPage.scala @@ -0,0 +1,24 @@ +package mdr.pdb.app.pages.detail + +import com.raquo.laminar.api.L.{*, given} + +import components._ + +object DetailParametruPage: + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametr: DetailParametru.ViewModel, + kriteria: SeznamKriterii.ViewModel + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + DetailOsoby.header($m.map(_.osoba)), + DetailParametru($m.map(_.parametr)), + SeznamKriterii($m.map(_.kriteria)) + ) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala deleted file mode 100644 index a3b00af..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala +++ /dev/null @@ -1,103 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import scala.scalajs.js.annotation.JSExportTopLevel -import scala.scalajs.js.annotation.JSExport -import scala.scalajs.js.annotation.JSImport -import scala.scalajs.js -import org.scalajs.dom -import com.raquo.laminar.api.L.{*, given} -import scala.scalajs.js.Date -import com.raquo.waypoint.Router -import com.raquo.waypoint.SplitRender -import cz.e_bs.cmi.mdr.pdb.app.services.DataFetcher -import scala.scalajs.js.JSON -import zio.json._ -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.state.AppState - -@js.native -@JSImport("stylesheets/main.css", JSImport.Namespace) -object Css extends js.Any - -@js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native -@JSImport("params/pdb-params.json", JSImport.Default) -object pdbParams extends js.Object - -@JSExportTopLevel("app") -object Main: - - @JSExport - def main(args: Array[String]): Unit = - import Routes.given - onLoad { - setupAirstream() - val appContainer = dom.document.querySelector("#app") - val _ = - render( - appContainer, - renderPage(state.MockAppState(using unsafeWindowOwner, router)) - ) - } - - private def onLoad(f: => Unit): Unit = - documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) - - private def setupAirstream()(using router: Router[Page]): Unit = - AirstreamError.registerUnhandledErrorCallback(err => - router.forcePage( - Page.UnhandledError( - Some(err.getClass.getName), // TODO: Fill only in dev mode - Some(err.getMessage) - ) - ) - ) - - def renderPage(state: AppState)(using - router: Router[Page] - ): HtmlElement = - val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) - .collectSignal[Page.Detail]( - connectors - .DetailPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailParametru]( - connectors - .DetailParametruPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailKriteria]( - connectors - .DetailKriteriaPageConnector(state)(_) - .apply - ) - .collectSignal[Page.UpravDukazKriteria]( - pages.detail.UpravDukaz.Connector(state)(_).apply - ) - .collectStatic(Page.Dashboard)( - connectors.DashboardPageConnector(state.actionBus).apply - ) - .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) - ) - .collect[Page.UnhandledError](pg => - pages.errors - .UnhandledErrorPage( - pages.errors.UnhandledErrorPage - .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), - state.actionBus - ) - ) - .collectStatic(Page.Directory)( - connectors - .DirectoryPageConnector(state.users, state.actionBus) - .apply - ) - div(cls := "h-full", child <-- pageSplitter.$view) - - // Pull in the stylesheet - val css: Css.type = Css diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala deleted file mode 100644 index 3bbc8c0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ /dev/null @@ -1,198 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.* -import org.scalajs.dom -import zio.json.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import scala.scalajs.js -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.Page.Titled - -// enum is not working with Waypoints' SplitRender collectStatic -sealed abstract class Page( - val id: String, - val title: String, - val parent: Option[Page] -) { - val path: Vector[Page] = - parent match - case None => Vector(this) - case Some(p) => p.path :+ this - - val isRoot: Boolean = parent.isEmpty -} - -object Page: - - case class Titled[V](value: V, title: Option[String] = None): - val show: String = title.getOrElse(value.toString) - - case object Directory extends Page("directory", "Adresář", None) - - case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) - - case class Detail(osobniCislo: Titled[OsobniCislo]) - extends Page("user", osobniCislo.show, Some(Directory)) - - object Detail { - def apply(o: UserInfo): Detail = Detail( - Titled(o.personalNumber, Some(o.name)) - ) - } - - case class DetailParametru( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String] - ) extends Page( - "parameter", - parametr.show, - Some(Detail(osobniCislo)) - ) - - object DetailParametru { - def apply(o: UserInfo, p: Parameter): DetailParametru = - DetailParametru( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)) - ) - } - - case class DetailKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "criteria", - kriterium.show, - Some(DetailParametru(osobniCislo, parametr)) - ) - - object DetailKriteria { - def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = - DetailKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class UpravDukazKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "addProof", - "Důkaz", - Some(DetailKriteria(osobniCislo, parametr, kriterium)) - ) - - object UpravDukazKriteria { - def apply( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): UpravDukazKriteria = - UpravDukazKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class NotFound(url: String) extends Page("404", "404", Some(Directory)) - - case class UnhandledError( - errorName: Option[String], - errorMessage: Option[String] - ) extends Page("500", "Unexpected error", Some(Directory)) - -object Routes: - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) - given [V: JsonEncoder]: JsonEncoder[Titled[V]] = - DeriveJsonEncoder.gen[Titled[V]] - given [V: JsonDecoder]: JsonDecoder[Titled[V]] = - DeriveJsonDecoder.gen[Titled[V]] - given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] - given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] - - val base = - js.`import`.meta.env.BASE_URL - .asInstanceOf[String] + "app" - - val homePage: Page = Page.Directory - - given router: Router[Page] = Router[Page]( - routes = List( - Route.static(homePage, root / endOfSegments, basePath = base), - Route.static( - Page.Dashboard, - root / "dashboard" / endOfSegments, - basePath = base - ), - Route[Page.Detail, String]( - encode = _.osobniCislo.value.toString, - decode = osc => Page.Detail(Titled(OsobniCislo(osc))), - root / "osoba" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.DetailParametru, (String, String)]( - encode = p => (p.osobniCislo.value.toString, p.parametr.value), - decode = - p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / endOfSegments, - basePath = base - ), - Route[Page.DetailKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.DetailKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.UpravDukazKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.UpravDukazKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / "edit" / endOfSegments, - basePath = base - ) - ), - serializePage = _.toJson, - deserializePage = _.fromJson[Page] - .fold(s => throw IllegalStateException(s), identity), - getPageTitle = _.title, - routeFallback = url => Page.NotFound(url), - deserializeFallback = _ => Page.Dashboard - )( - $popStateEvent = windowEvents.onPopState, - owner = unsafeWindowOwner - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala deleted file mode 100644 index b24b82b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -sealed trait Action - -case object FetchDirectory extends Action -case class FetchUserDetails(osc: OsobniCislo) extends Action -case class FetchParameters(osc: OsobniCislo) extends Action -case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action -case class FetchParameterCriteria( - osc: OsobniCislo, - paramId: String, - critId: String, - page: (UserInfo, Parameter, ParameterCriteria) => Page -) extends Action -case class FetchAvailableFiles(osc: OsobniCislo) extends Action -case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala deleted file mode 100644 index c637d35..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala +++ /dev/null @@ -1,82 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.{UserProfile, UserInfo, OsobniCislo} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo -import cz.e_bs.cmi.mdr.pdb.UserFunction - -object AppPage: - // TODO: pages by logged in user - val pages: List[Page] = List(Page.Directory, Page.Dashboard) - - import NavigationBar.{Logo, MenuItem} - - val logo = Logo( - "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", - "Workflow" - ) - - // TODO: menu items by user profile - val userMenu = - List( - MenuItem("Your Profile"), - MenuItem("Settings"), - MenuItem("Sign out") - ) - - // TODO: load user profile - val $userProfile = Var( - UserProfile( - "tom", - UserInfo( - OsobniCislo("1031"), - "tom", - "Tom", - "Cook", - None, - None, - Some("tom@example.com"), - Some("+420 222 866 180"), - Some( - UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") - ), - Nil, - None - ) - ) - ) - - val $userInfo = $userProfile.signal.map(_.userInfo) - - type ViewModel = Option[HtmlElement] - def apply( - actionBus: Observer[Action] - )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using - router: Router[Page] - ): HtmlElement = - PageLayout(actionBus)( - $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => - PageLayout.ViewModel( - NavigationBar.ViewModel( - u, - pages.map(p => - NavigationBar.Link( - () => - PageLink( - p, - actionBus - ), - p == cp - ) - ), - userMenu, - logo - ), - c - ) - ), - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala deleted file mode 100644 index bf473c6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ /dev/null @@ -1,143 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden -import fiftyforms.ui.components.tailwind.list.IconText.ViewModel -import fiftyforms.ui.components.tailwind.Icons -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Action - -object Breadcrumbs: - - private def slash = - import svg.{*, given} - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-300", - xmlns := "http://www.w3.org/2000/svg", - fill := "currentColor", - viewBox := "0 0 20 20", - ariaHidden := true, - path( - d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" - ) - ) - - def backIcon = - Icons.solid - .`arrow-narrow-left`() - .amend( - svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" - ) - - object Link: - case class ViewModel( - page: Page, - icon: Option[SvgElement], - extraClasses: String, - text: String, - textClasses: Option[String] = None - ) - - val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" - - def shortHome(p: Page) = ViewModel( - p, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - "Zpět na úvodní stránku", - None - ) - - def fullHome(p: Page) = ViewModel( - p, - Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), - "text-gray-400 hover:text-gray-500", - "Domů", - Some("sr-only") - ) - - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - PageLink - .container($m.map(_.page), actionBus) - .amend( - cls <-- $m.map(_.extraClasses), - child.maybe <-- $m.map(_.icon), - span( - cls <-- $m.map(_.textClasses.getOrElse("")), - child.text <-- $m.map(_.text) - ) - ) - - object FullBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - ol( - role := "list", - cls := "flex items-center space-x-4", - children <-- $m.map(_.path).split(_.id)((_, _, $p) => - li( - div( - cls := "flex items-center", - child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), - Link( - $p.map(p => - if (p.isRoot) then Link.fullHome(p) - else - Link.ViewModel( - p, - None, - s"ml-4 max-w-xs truncate ${Link.baseClasses}", - p.title - ) - ), - actionBus - ) - ) - ) - ) - ) - - object ShortBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - Link( - $m.map { p => - val target = p.parent.getOrElse(p) - if target.isRoot then Link.shortHome(target) - else - Link.ViewModel( - target, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - s"Zpět na ${target.title}" - ) - }, - actionBus - ) - - def apply(actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - nav( - cls := "flex", - aria.label := "Breadcrumb", - div( - cls := "flex sm:hidden", - ShortBreadcrumbs(router.$currentPage, actionBus) - ), - div( - cls := "hidden sm:block", - FullBreadcrumbs(router.$currentPage, actionBus) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala deleted file mode 100644 index 0db2047..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ /dev/null @@ -1,242 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent -import fiftyforms.ui.components.tailwind.Icons -import fiftyforms.ui.components.tailwind.Avatar -import cz.e_bs.cmi.mdr.pdb.UserInfo -import io.laminext.syntax.core.* - -object NavigationBar: - - case class Logo(img: String, name: String) - case class Link(a: () => Anchor, active: Boolean) - case class MenuItem(title: String) - - case class ViewModel( - userInfo: UserInfo, - pages: List[Link], - userMenu: List[MenuItem], - logo: Logo - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - val $userInfo = $m.map(_.userInfo) - val mobileMenuOpen = Var(false) - - val desktopOnly = cls("hidden md:block") - val mobileOnly = cls("md:hidden") - - inline def avatarImage(size: Int = 8) = - Avatar($userInfo.map(_.img)).avatarImage(size) - - def notificationButton = button( - tpe := "button", - cls := List( - "bg-indigo-600", - "focus:outline-none", - "focus:ring-2", - "focus:ring-offset-2", - "focus:ring-offset-indigo-600", - "focus:ring-white", - "hover:text-white", - "p-1", - "rounded-full", - "text-indigo-200" - ), - span(cls := "sr-only", "View notifications"), - Icons.outline.bell() - ) - - def userProfile: HtmlElement = - val menuOpen = Var(false) - - def menuItem(item: MenuItem, idx: Int): HtmlElement = - a( - href := "#", - cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", - role := "menuitem", - tabIndex := -1, - idAttr := s"user-menu-item-$idx", - item.title - ) - - div( - cls := "ml-3 relative", - div( - button( - tpe := "button", - cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - idAttr := "user-menu-button", - aria.expanded <-- menuOpen.signal, - aria.hasPopup := true, - span(cls := "sr-only", "Open user menu"), - child <-- avatarImage(), - onClick.preventDefault.mapTo( - !menuOpen.now() - ) --> menuOpen.writer - ) - ), - /* - * */ - div( - cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", - cls <-- menuOpen.signal.map { o => - if (o) "md:block" else "md:hidden" - }, - role := "menu", - aria.orientation := "vertical", - aria.labelledBy := "user-menu-button", - tabIndex := -1, - // : keyboard navigation - children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) - ) - ) - - def mobileProfile = - def menuItem(item: MenuItem): HtmlElement = - a( - href := "#", - cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", - item.title - ) - - div( - cls := "pt-4 pb-3 border-t border-indigo-700", - div( - cls := "flex items-center px-5", - div( - cls := "flex-shrink-0", - child <-- avatarImage(10) - ), - div( - cls := "ml-3", - div( - cls := "text-base font-medium text-white", - child.text <-- $userInfo.map(_.name) - ), - child.maybe <-- $userInfo.map( - _.email.map(e => - div( - cls := "text-sm font-medium text-indigo-300", - e - ) - ) - ) - ), - notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) - ), - div( - cls := "mt-3 px-2 space-y-1", - children <-- $m.map(_.userMenu.map(menuItem)) - ) - ) - - def pageLink(page: Link): Anchor = - page - .a() - .amend( - cls := "text-white px-3 py-2 rounded-md text-sm font-medium", - cls := Seq( - "bg-indigo-700" -> page.active, - "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active - ), - ariaCurrent := (if page.active then "page" else "false") - ) - - def logoImg: Image = - img( - cls := "h-8 w-8", - src <-- $m.map(_.logo.img), - alt <-- $m.map(_.logo.name) - ) - - def mobileMenuButton = button( - tpe := "button", - cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - aria.controls := "mobile-menu", - aria.expanded <-- mobileMenuOpen.signal, - span(cls := "sr-only", "Open main menu"), - Icons.outline - .menu() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") - ), - Icons.outline - .x() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") - ), - onClick.preventDefault.mapTo( - !mobileMenuOpen.now() - ) --> mobileMenuOpen.writer - ) - - def navBarLeft = - div( - cls := "flex items-center", - div(cls := "flex-shrink-0", logoImg), - div( - desktopOnly, - div( - cls := "ml-10 flex items-baseline space-x-4", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ) - ) - ) - - def navBarRight = - div( - desktopOnly, - div( - cls := "ml-4 flex items-center md:ml-6", - notificationButton, - userProfile - ) - ) - - def navBarMobile = - div( - cls := "-mr-2 flex", - mobileOnly, - mobileMenuButton - ) - - def navBar = - div( - cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex items-center justify-between h-16", - navBarLeft, - navBarRight, - navBarMobile - ) - ) - - def mobileMenu = - div( - mobileOnly, - cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }, - idAttr := "mobile-menu", - div( - cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ), - mobileProfile - ) - - nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala deleted file mode 100644 index 75339df..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object PageHeader: - def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = - header( - cls := "bg-white shadow-sm", - div( - cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", - h1( - cls := "text-lg leading-6 font-semibold text-gray-900", - Breadcrumbs(actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala deleted file mode 100644 index 5096463..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ /dev/null @@ -1,30 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import fiftyforms.ui.components.tailwind.Loading -import io.laminext.syntax.core.* - -object PageLayout: - case class ViewModel( - navigation: NavigationBar.ViewModel, - content: Option[HtmlElement] - ) - def apply(actionBus: Observer[Action])( - $m: Signal[ViewModel], - mods: Modifier[HtmlElement]* - )(using router: Router[Page]): HtmlElement = - val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) - div( - cls := "h-full flex flex-col", - NavigationBar($m.map(_.navigation)), - child.maybe <-- router.$currentPage.map(_.isRoot) - .switch(None, Some(PageHeader(actionBus))), - main( - cls := "flex-grow-1 overflow-y-auto", - mods, - child <-- $maybeContent.map(_.getOrElse(Loading)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala deleted file mode 100644 index 62d820f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.LinkSupport.* -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo - -object PageLink: - type ViewModel = Page - def apply($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = - a(mods($m, actions), child.text <-- $m.map(_.title)) - - def apply(m: ViewModel, actions: Observer[Action])(using - router: Router[Page] - ): Anchor = apply(Val(m), actions) - - def container($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = a(mods($m, actions)) - - def container(m: ViewModel, actions: Observer[Action])(using - Router[Page] - ): Anchor = container(Val(m), actions) - - private def mods($m: Signal[Page], actions: Observer[Action])(using - router: Router[Page] - ): Modifier[Anchor] = - Seq( - href <-- $m.map(router.absoluteUrlForPage).recover { case _ => - Some("invalid url") - }, - composeEvents(onClick.noKeyMod.preventDefault)( - _.sample($m) - .map(NavigateTo.apply) - ) --> actions - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala deleted file mode 100644 index 87f825a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.pages.dashboard.DashboardPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -class DashboardPageConnector(actionBus: Observer[Action])(using - router: Router[Page] -): - def apply: HtmlElement = - AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala deleted file mode 100644 index e9b0f71..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala +++ /dev/null @@ -1,76 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailKriteriaPage -import pages.detail.components.DukazyKriteria -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -object DetailKriteriaPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailKriteriaPageConnector( - state: DetailKriteriaPageConnector.AppState -)( - $page: Signal[Page.DetailKriteria] -)(using Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => - (p.osobniCislo.value, p.parametr.value, p.kriterium.value) - )((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map( - FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) - ) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, s, $s) => - DetailKriteriaPage($s)(state.actionBus.contramap { - case DukazyKriteria.Add => - NavigateTo( - Page.UpravDukazKriteria( - Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), - Page.Titled(s.parametr.id, Some(s.parametr.nazev)), - Page.Titled(s.kriterium.id, Some(s.kriterium.id)) - ) - ) - }) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): DetailKriteriaPage.ViewModel = - DetailKriteriaPage.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala deleted file mode 100644 index 4d8024f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ /dev/null @@ -1,57 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import pages.detail.DetailPage -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import fiftyforms.ui.components.tailwind.Color - -object DetailPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailPageConnector(state: DetailPageConnector.AppState)( - $page: Signal[Page.Detail] -)(using router: Router[Page]): - val $oscChangeSignal = $page.splitOne(_.osobniCislo.value)((osc, _, _) => osc) - val $pageChangeSignal = - $oscChangeSignal.flatMap(osc => - EventStream.fromSeq(Seq(FetchUserDetails(osc), FetchParameters(osc))) - ) - // TODO: filter the value based on the current osc - // OSC change will fetch new data, but still - // - we need to be sure that what we got is really what we ought to display - // - we want to display stale data accordingly (at least with loading indicator) - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - def apply: HtmlElement = - AppPage(state.actionBus)( - $data.combineWithFn($params)(_ zip _) - .map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: List[Parameter] - ): DetailPage.ViewModel = - DetailPage.ViewModel( - o.toDetailOsoby, - p.map( - _.toParametr(param => - PageLink.container(Page.DetailParametru(o, param), state.actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala deleted file mode 100644 index fdaa7f8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailParametruPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink - -object DetailParametruPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailParametruPageConnector( - state: DetailParametruPageConnector.AppState -)( - $page: Signal[Page.DetailParametru] -)(using router: Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => (p.osobniCislo.value, p.parametr.value))((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map(FetchParameter(_, _)) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - } yield (da, pb) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailParametruPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter - ): DetailParametruPage.ViewModel = - DetailParametruPage.ViewModel( - o.toDetailOsoby, - p.toParametr(p => - PageLink.container(Page.DetailParametru(o, p), state.actionBus) - ), - p.criteria.map( - _.toKriterium { c => - PageLink.container( - Page.DetailKriteria(o, p, c), - state.actionBus - ) - } - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala deleted file mode 100644 index 049aa22..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -case class DirectoryPageConnector( - $input: EventStream[List[UserInfo]], - actionBus: Observer[Action] -)(using router: Router[Page]): - val $data = $input.startWithNone - val $actionSignal = EventStream.fromValue(FetchDirectory) - - def apply: HtmlElement = - AppPage(actionBus)( - $data.split(_ => ())((_, _, s) => - pages.directory.DirectoryPage( - s.map( - _.map( - _.toUserRow(u => - PageLink.container( - Page.Detail(Page.Titled(u.personalNumber)), - actionBus - ) - ) - ) - ) - ) - ), - $actionSignal --> actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala deleted file mode 100644 index 88e7450..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ /dev/null @@ -1,68 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii -import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria -import fiftyforms.ui.components.tailwind.Color - -extension (o: UserInfo) - def toDetailOsoby: DetailOsoby.ViewModel = - DetailOsoby.ViewModel( - o.personalNumber, - o.name, - o.email, - o.phone, - o.img, - o.mainFunction.map(f => - DetailOsoby.Funkce.ViewModel(f.name, f.dept, f.ou) - ), - o.userContracts.headOption.map(c => - DetailOsoby.PracovniPomer.ViewModel(c.rel, c.startDate, c.endDate) - ) - ) - -extension (param: Parameter) - def toParametr(container: Parameter => Anchor): DetailParametru.ViewModel = - DetailParametru.ViewModel( - id = param.id, - nazev = param.name, - popis = param.description, - status = "Nesplněno", - statusColor = Color.red, - a = container(param) - ) - -extension (crit: ParameterCriteria) - def toKriterium( - container: ParameterCriteria => Anchor - ): DetailKriteria.ViewModel = - DetailKriteria.ViewModel( - nazev = crit.criteriumText, - kapitola = crit.chapterId, - bod = crit.itemId, - status = "Nesplněno", - statusColor = Color.red, - splneno = false, - dukazy = Nil, - container = container(crit) - ) - -extension (user: UserInfo) - def toUserRow( - container: UserInfo => HtmlElement = (_: UserInfo) => div() - ): UserRow.ViewModel = - UserRow.ViewModel( - osobniCislo = user.personalNumber.toString, - celeJmeno = user.name, - prijmeni = user.surname, - hlavniFunkce = user.mainFunction.map(_.name), - img = user.img, - container = () => container(user) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala deleted file mode 100644 index 7d5b2c1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.dashboard - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.Page - -object DashboardPage: - - def render: HtmlElement = - div("Dashboard page") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala deleted file mode 100644 index 9ef0698..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala +++ /dev/null @@ -1,31 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailKriteriaPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply($m: Signal[ViewModel])( - action: Observer[DukazyKriteria.Action] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - DukazyKriteria($m.map(_.kriterium.dukazy))(action) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala deleted file mode 100644 index 3b316e1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object DetailPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametry: SeznamParametru.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby($m.map(_.osoba)), - SeznamParametru($m.map(_.parametry)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala deleted file mode 100644 index 17224cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala +++ /dev/null @@ -1,24 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailParametruPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriteria: SeznamKriterii.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby.header($m.map(_.osoba)), - DetailParametru($m.map(_.parametr)), - SeznamKriterii($m.map(_.kriteria)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala deleted file mode 100644 index dc912fa..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ /dev/null @@ -1,107 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb -package app -package pages.detail - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.UpravDukazForm -import fiftyforms.services.files.File - -object UpravDukaz: - - type ThisPage = Page.UpravDukazKriteria - type PageKey = (OsobniCislo, String, String) - - trait State { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def availableFiles: EventStream[List[File]] - def actionBus: Observer[Action] - } - - def keyOfPage(page: ThisPage): PageKey = - (page.osobniCislo.value, page.parametr.value, page.kriterium.value) - - def onChangeAction(key: PageKey): Action = - FetchParameterCriteria( - key._1, - key._2, - key._3, - Page.UpravDukazKriteria(_, _, _) - ) - - class Connector(state: State)($page: Signal[ThisPage])(using Router[Page]): - val $paramChangeSignal = $page.splitOne(keyOfPage)((x, _, _) => x) - val $pageChangeSignal = $paramChangeSignal.map(onChangeAction) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.split(_ => ())((_, s, $s) => - PageComponent( - $s.map(buildModel), - state.availableFiles, - state.actionBus.contramap { - case UpravDukazForm.Cancelled => - NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) - case UpravDukazForm.AvailableFilesRequested => - FetchAvailableFiles(s._1.personalNumber) - } - ) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): PageComponent.ViewModel = - import connectors.* - PageComponent.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) - - object PageComponent: - import components.* - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply( - $m: Signal[ViewModel], - availableFilesStream: EventStream[List[File]], - events: Observer[UpravDukazForm.Event] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - UpravDukazForm(availableFilesStream)(events) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala deleted file mode 100644 index ca311b0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.Icons -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Color - -object DetailKriteria: - case class ViewModel( - nazev: String, - kapitola: String, - bod: String, - status: String, - statusColor: Color, - splneno: Boolean, - dukazy: List[DukazKriteria.ViewModel], - container: HtmlElement = div() - ) { - val id = s"${kapitola}${bod}" - } - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h3( - cls := "text-xl leading-6 font-bold text-gray-900", - child.text <-- $m.map(_.nazev) - ), - h3( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - "Kapitola ", - child.text <-- $m.map(_.kapitola), - " bod ", - child <-- $m.map(_.bod) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala deleted file mode 100644 index ae90290..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala +++ /dev/null @@ -1,100 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Avatar - -object DetailOsoby: - - object Funkce: - case class ViewModel( - nazev: String, - stredisko: String, - voj: String - ) - - def render($m: Signal[ViewModel]): HtmlElement = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.nazev), - span( - cls := "hidden md:inline", - " @ ", - child.text <-- $m.map(_.stredisko), - ", ", - child.text <-- $m.map(_.voj) - ) - ) - - object PracovniPomer: - case class ViewModel( - druh: String, - pocatek: LocalDate, - konec: Option[LocalDate] - ) - - def render($m: Signal[ViewModel]): HtmlElement = - import fiftyforms.ui.components.tailwind.CustomAttrs.datetime - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.druh), - " od ", - time( - datetime <-- $m.map(_.pocatek.toString), - child.text <-- $m.map(_.pocatek.toString) - ) - ) - - case class ViewModel( - osobniCislo: OsobniCislo, - jmeno: String, - email: Option[String], - telefon: Option[String], - img: Option[String], - hlavniFunkce: Option[Funkce.ViewModel], - pracovniPomer: Option[PracovniPomer.ViewModel] - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-4", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(16) - ), - div( - h1( - cls := "text-2xl font-bold text-gray-900", - child.text <-- $m.map(_.jmeno) - ), - child.maybe <-- $m.map(_.hlavniFunkce).split(_ => ())((_, _, d) => - Funkce.render(d) - ), - child.maybe <-- $m.map(_.pracovniPomer).split(_ => ())((_, _, d) => - PracovniPomer.render(d) - ) - ) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-5", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(8) - ), - div( - h1( - cls := "text-lg font-medium text-gray-800", - child.text <-- $m.map(_.jmeno) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala deleted file mode 100644 index e11cb62..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color - -object DetailParametru: - case class ViewModel( - id: String, - nazev: String, - popis: String, - status: String, - statusColor: Color, - a: Anchor - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h2( - cls := "text-2xl leading-6 font-medium text-gray-900", - child.text <-- $m.map(_.nazev) - ), - p( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - child.text <-- $m.map(_.popis) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - h2( - cls := "text-lg leading-6 font-medium text-gray-600", - child.text <-- $m.map(_.nazev) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala deleted file mode 100644 index a00ede8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala +++ /dev/null @@ -1,177 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.CustomAttrs - -object DukazKriteria: - case class Osoba(osobniCislo: String, jmeno: String) - case class Udalost(osoba: Osoba, datum: LocalDate) - case class Dokument( - url: String, - nazev: String, - pridal: Udalost, - odebral: Option[Udalost] - ) - case class ViewModel( - dokumenty: List[Dokument], - autorizoval: Option[Udalost], - platiDo: Option[LocalDate], - poznámka: Option[String] - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-lg", - div( - cls := "px-4 py-5 sm:px-6", - h3( - cls := "text-lg leading-6 font-medium text-gray-900", - """Applicant Information""" - ), - p( - cls := "mt-1 max-w-2xl text-sm text-gray-500", - """Personal details and application.""" - ) - ), - div( - cls := "border-t border-gray-200 px-4 py-5 sm:px-6", - dl( - cls := "grid grid-cols-1 gap-x-4 gap-y-8 sm:grid-cols-2", - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Full name""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Margot Foster""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Application for""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Backend Developer""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Email address""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """margotfoster@example.com""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Salary expectation""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """$120,000""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """About""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Fugiat ipsum ipsum deserunt culpa aute sint do nostrud anim incididunt cillum culpa consequat. Excepteur qui ipsum aliquip consequat sint. Sit id mollit nulla mollit nostrud in ea officia proident. Irure nostrud pariatur mollit ad adipisicing reprehenderit deserunt qui eu.""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """Attachments""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - ul( - role := "list", - cls := "border border-gray-200 rounded-md divide-y divide-gray-200", - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """resume_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ), - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """coverletter_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala deleted file mode 100644 index 22f134f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala +++ /dev/null @@ -1,35 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object DukazyKriteria: - sealed trait Action - case object Add extends Action - - type ViewModel = List[DukazKriteria.ViewModel] - - def apply($m: Signal[ViewModel])(actions: Observer[Action]): HtmlElement = - div( - child.maybe <-- $m.map(m => - if (m.isEmpty) then Some(prazdnyDukaz(actions)) - else None - ), - children <-- $m.map(_.zipWithIndex).split(_._2)((_, _, $s) => - DukazKriteria($s.map(_._1)) - ) - ) - - private def prazdnyDukaz(actions: Observer[Action]): HtmlElement = - button( - tpe := "button", - onClick.preventDefault.mapTo(Add) --> actions, - cls := "relative block w-full border-2 border-gray-300 border-dashed rounded-lg p-12 text-center hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - Icons.outline - .`document-add`(12) - .amend(svg.cls := "mx-auto text-gray-400"), - span( - cls := "mt-2 block text-sm font-medium text-gray-900", - "Přidat důkaz" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala deleted file mode 100644 index f628697..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.list.StackedList -import fiftyforms.ui.components.tailwind.list.ListRow -import fiftyforms.ui.components.tailwind.list.RowTag -import fiftyforms.ui.components.tailwind.list.RowNext -import java.time.LocalDate - -object SeznamKriterii: - type ViewModel = List[DetailKriteria.ViewModel] - - private val kritList = new StackedList[DetailKriteria.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - kritList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = - p(cls := "text-sm text-gray-500", s"${i.kapitola}${i.bod}"), - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.container - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala deleted file mode 100644 index 2bfe4bc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.list.{ - StackedList, - ListRow, - RowTag, - PropList, - IconText, - RowNext -} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.LinkSupport.* - -object SeznamParametru: - type ViewModel = List[DetailParametru.ViewModel] - - private val parametrList = new StackedList[DetailParametru.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - parametrList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = emptyNode, - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.a - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala deleted file mode 100644 index f048e9d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ /dev/null @@ -1,101 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.form.* -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement -import fiftyforms.services.files.components.tailwind.FilePicker -import cz.e_bs.cmi.mdr.pdb.frontend.AutorizujDukaz -import cz.e_bs.cmi.mdr.pdb.frontend.DocumentRef -import fiftyforms.services.files.File - -object UpravDukazForm: - sealed trait Event - case object Cancelled extends Event - case object AvailableFilesRequested extends Event - def apply(availableFilesStream: EventStream[List[File]])( - updates: Observer[Event] - ): HtmlElement = - val (filesStream, filesObserver) = - EventStream.withObserver[FilePicker.Event] - val files = Var[List[File]](Nil) - def submitButtons: HtmlElement = - div( - cls := "pt-5", - div( - cls := "flex justify-end", - button( - tpe := "button", - cls := "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zrušit", - onClick.mapTo(Cancelled) --> updates - ), - button( - tpe := "submit", - cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - disabled <-- files.signal.map(_.isEmpty), - "Autorizovat" - ) - ) - ) - - div( - filesStream.collect { case FilePicker.SelectionUpdated(files) => - files.to(List) - } --> files.writer, - filesStream.collect { case FilePicker.AvailableFilesRequested => - AvailableFilesRequested - } --> updates, - cls := "bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6", - Form( - Form.Body( - Form.Section( - FormHeader( - FormHeader.ViewModel( - "Doložení kritéria", - "Sestavte doklady poskytující důkaz kritéria a potvrďte odesláním formuláře. Případné limitace či jiné relevantní údaje vepište do pole Komentář." - ) - ), - FormFields( - FormRow( - "dokumenty", - "Dokumenty", - FilePicker(files.signal, availableFilesStream)(filesObserver) - .amend(idAttr := "dokumenty", cls("max-w-lg")) - ).toHtml, - FormRow( - "platnost", - "Platnost", - input( - idAttr := "platnost", - name := "platnost", - tpe := "date", - autoComplete := "date", - cls := "block max-w-lg w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" - ) - ).toHtml, - FormRow( - "komentar", - "Komentář", - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - textArea( - idAttr := "komentar", - name := "about", - rows := 3, - cls := "max-w-lg shadow-sm block w-full focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border border-gray-300 rounded-md" - ), - p( - cls := "mt-2 text-sm text-gray-500", - "Doplňte prosím potřebné informace související s doložením kritéria, včetně případných limitací." - ) - ) - ).toHtml - ) - ) - ), - submitButtons - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala deleted file mode 100644 index 41033f6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -import components._ - -object DirectoryPage: - - type ViewModel = List[UserRow.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - val (actionsStream, actionObserver) = - EventStream.withObserver[SearchForm.Action] - val $filter = actionsStream - .collect { - case SearchForm.ClearFilter => None - case SearchForm.SetFilter(t) => Some(t) - } - .startWith(None) - val byLetter = for { - d <- $m - f <- $filter - } yield for { - (letter, users) <- d - .filter { user => - f match - case None => true - case Some(t) => user.search.contains(t) - } - .groupBy(_.prijmeni.head) - .to(List) - .sortBy(_._1) - } yield (letter.toString, users.sortBy(_.prijmeni)) - - div( - cls := "h-full max-w-7xl mx-auto order-first flex flex-col flex-shrink-0", - SearchForm(actionObserver), - Directory(byLetter) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala deleted file mode 100644 index 73f915d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -object Directory: - - object Header: - type ViewModel = String - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500", - h3(child.text <-- $m) - ) - - object UserList: - type ViewModel = List[UserRow.ViewModel] - def apply($m: Signal[ViewModel]): HtmlElement = - ul( - role := "list", - cls := "relative z-0 divide-y divide-gray-200", - children <-- $m.split(_.osobniCislo)((_, _, s) => UserRow(s)) - ) - - type ViewModel = List[(String, List[UserRow.ViewModel])] - def apply($m: Signal[ViewModel]): HtmlElement = - nav( - cls := "flex-1 min-h-0 overflow-y-auto", - aria.label := "Directory", - children <-- $m.split(_._1)((_, _, s) => - div( - cls := "relative", - Header(s.map(_._1)), - UserList(s.map(_._2)) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala deleted file mode 100644 index e182910..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala +++ /dev/null @@ -1,54 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object SearchForm: - sealed trait Action - case object SubmitFilter extends Action - sealed trait FilterAction extends Action - case object ClearFilter extends FilterAction - case class SetFilter(value: String) extends FilterAction - - def apply(actions: Observer[Action]): HtmlElement = - div( - cls := "px-6 pt-4 pb-4", - form( - cls := "flex space-x-4", - onSubmit.mapTo(SubmitFilter) --> actions, - div( - cls := "flex-1 min-w-0", - label( - forId := "search", - cls := "sr-only", - "Hledat" - ), - div( - cls := "relative rounded-md shadow-sm", - div( - cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", - Icons.solid.search().amend(svg.cls := "text-gray-400") - ), - input( - tpe := "search", - name := "search", - idAttr := "search", - cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", - placeholder := "Hledat", - composeEvents(onInput.mapToValue.setAsValue.map(SetFilter(_)))( - _.throttle(500) - ) --> actions - ) - ) - ), - button( - tpe := "submit", - cls := "inline-flex justify-center px-3.5 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500", - Icons.solid.filter().amend(svg.cls := "text-gray-400"), - span( - cls := "sr-only", - "Hledat" - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala deleted file mode 100644 index 82f7723..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala +++ /dev/null @@ -1,52 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.airstream.core.Signal -import fiftyforms.ui.components.tailwind.Avatar - -object UserRow: - case class ViewModel( - osobniCislo: String, - celeJmeno: String, - prijmeni: String, - hlavniFunkce: Option[String], - img: Option[String], - container: () => HtmlElement = () => div() - ) { - val search = prijmeni.toLowerCase - } - - def apply($m: Signal[ViewModel]): HtmlElement = - inline def avatarImage = - Avatar($m.map(_.img)).avatarImage(10) - - li( - div( - cls := "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500", - div( - cls := "flex-shrink-0", - child <-- avatarImage - ), - div( - cls := "flex-1 min-w-0", - child <-- $m.map { o => - o.container() - .amend( - cls := "focus:outline-none", - span( - cls := "absolute inset-0", - aria.hidden := true - ), - p( - cls := "text-sm font-medium text-gray-900", - o.celeJmeno - ), - p( - cls := "text-sm text-gray-500 truncate", - o.hlavniFunkce - ) - ) - } - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala deleted file mode 100644 index 467c5f5..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala +++ /dev/null @@ -1,99 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router - -object ErrorPage: - case class ViewModel( - homePage: Page, - errorName: String, - title: String, - subTitle: String - ) - - def apply(m: ViewModel, actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - val ViewModel(homePage, errorName, title, subTitle) = m - div( - cls := "min-h-full pt-16 pb-12 flex flex-col bg-white", - main( - cls := "flex-grow flex flex-col justify-center max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex-shrink-0 flex justify-center", - a( - href := "/", - cls := "inline-flex", - span( - cls := "sr-only", - """Workflow""" - ), - img( - cls := "h-12 w-auto", - src := "https://tailwindui.com/img/logos/workflow-mark.svg?color=indigo&shade=600", - alt := "" - ) - ) - ), - div( - cls := "py-16", - div( - cls := "text-center", - p( - cls := "text-sm font-semibold text-indigo-600 uppercase tracking-wide", - errorName - ), - h1( - cls := "mt-2 text-4xl font-extrabold text-gray-900 tracking-tight sm:text-5xl", - title - ), - p( - cls := "mt-2 text-base text-gray-500", - subTitle - ), - div( - cls := "mt-6", - PageLink - .container(homePage, actionBus) - .amend( - cls := "text-base font-medium text-indigo-600 hover:text-indigo-500", - """Go back home""" - ) - ) - ) - ) - ), - footer( - cls := "flex-shrink-0 max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", - nav( - cls := "flex justify-center space-x-4", - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Contact Support""" - ), - span( - cls := "inline-block border-l border-gray-300", - aria.hidden := true - ), - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Status""" - ), - span( - cls := "inline-block border-l border-gray-300", - aria.hidden := true - ), - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Twitter""" - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala deleted file mode 100644 index bd9baec..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action - -object NotFoundPage: - def apply(homePage: Page, url: String, actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - ErrorPage( - ErrorPage.ViewModel( - homePage, - "404 error", - "Page not found.", - s"Sorry, but page $url doesn't exist." - ), - actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala deleted file mode 100644 index 476fe3c..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action - -object UnhandledErrorPage: - - case class ViewModel( - homePage: Page, - errorName: Option[String], - errorMessage: Option[String] - ) - - def apply(m: ViewModel, actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - ErrorPage( - ErrorPage.ViewModel( - m.homePage, - "Unexpected error occurred", - m.errorName.getOrElse( - "Uh oh!" - ), // TODO: translations, better text than uh oh - m.errorMessage.getOrElse( - "This wasn't supposed to happen! Please try again." - ) - ), - actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala deleted file mode 100644 index 25f059f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala +++ /dev/null @@ -1,6 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.services - -import com.raquo.airstream.core.Observer - -trait DataFetcher[K, A]: - def fetch(id: K, sink: Observer[A]): Unit diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala deleted file mode 100644 index a469830..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala +++ /dev/null @@ -1,120 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package state - -import com.raquo.airstream.core.EventStream -import cz.e_bs.cmi.mdr.pdb.{UserInfo, OsobniCislo} -import com.raquo.airstream.core.Observer -import scala.scalajs.js -import scala.scalajs.js.JSON -import zio.json._ -import com.raquo.airstream.eventbus.EventBus -import com.raquo.airstream.ownership.Owner -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.UserFunction -import cz.e_bs.cmi.mdr.pdb.UserContract -import fiftyforms.services.files.File - -trait AppState - extends connectors.DetailPageConnector.AppState - with connectors.DetailParametruPageConnector.AppState - with connectors.DetailKriteriaPageConnector.AppState - with pages.detail.UpravDukaz.State: - def users: EventStream[List[UserInfo]] - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - -class MockAppState(implicit owner: Owner, router: Router[Page]) - extends AppState: - - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonDecoder[UserFunction] = DeriveJsonDecoder.gen - given JsonDecoder[UserContract] = DeriveJsonDecoder.gen - given JsonDecoder[UserInfo] = DeriveJsonDecoder.gen - - given JsonDecoder[ParameterCriteria] = DeriveJsonDecoder.gen - given JsonDecoder[Parameter] = DeriveJsonDecoder.gen - - private val actions = EventBus[Action]() - private val (parametersStream, pushParameters) = - EventStream.withCallback[List[Parameter]] - private val (usersStream, pushUsers) = - EventStream.withCallback[List[UserInfo]] - private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] - private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] - - private val mockData: List[UserInfo] = - mockUsers - .asInstanceOf[js.Dictionary[js.Object]] - .values - // TODO: is there a more efficient way to parse from JS object directly? - .map { o => - val parsed = JSON.stringify(o).fromJson[UserInfo] - parsed.left.foreach(org.scalajs.dom.console.log(_)) - parsed - } - .collect { case Right(u) => - u - } - .toList - - private val mockParameters: List[Parameter] = - pdbParams - .asInstanceOf[js.Dictionary[js.Object]] - .values - .map(o => JSON.stringify(o).fromJson[Parameter]) - .collect { case Right(p) => p } - .toList - - // TODO: Extract to separate event handler - actions.events.foreach { - case FetchDirectory => pushUsers(mockData) - case FetchUserDetails(osc) => - mockData.find(_.personalNumber == osc).foreach { o => - pushDetails(o) - router.replaceState(Page.Detail(o)) - } - case FetchParameters(osc) => - pushParameters(mockParameters) - case FetchParameter(osc, paramId) => - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(Page.DetailParametru(o, p)) - case FetchParameterCriteria(osc, paramId, critId, page) => - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - c <- p.criteria.find(_.id == critId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(page(o, p, c)) - case NavigateTo(page) => router.pushState(page) - case FetchAvailableFiles(osc) => - pushFiles( - List( - File("https://tc163.cmi.cz/here", "Example file") - ) - ) - } - - override def users: EventStream[List[UserInfo]] = - usersStream.debugWithName("users") - - override def details: EventStream[UserInfo] = - detailsStream.debugWithName("details") - - override def parameters: EventStream[List[Parameter]] = - parametersStream.debugWithName("parameters") - - override def availableFiles: EventStream[List[File]] = - filesStream.debugWithName("available files") - - override def actionBus: Observer[Action] = - actions.writer.debugWithName("actions writer") diff --git a/app/src/main/scala/mdr/pdb/app/Main.scala b/app/src/main/scala/mdr/pdb/app/Main.scala new file mode 100644 index 0000000..081b4ee --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/Main.scala @@ -0,0 +1,103 @@ +package mdr.pdb.app + +import scala.scalajs.js.annotation.JSExportTopLevel +import scala.scalajs.js.annotation.JSExport +import scala.scalajs.js.annotation.JSImport +import scala.scalajs.js +import org.scalajs.dom +import com.raquo.laminar.api.L.{*, given} +import scala.scalajs.js.Date +import com.raquo.waypoint.Router +import com.raquo.waypoint.SplitRender +import mdr.pdb.app.services.DataFetcher +import scala.scalajs.js.JSON +import zio.json._ +import mdr.pdb.UserInfo +import mdr.pdb.app.state.AppState + +@js.native +@JSImport("stylesheets/main.css", JSImport.Namespace) +object Css extends js.Any + +@js.native +@JSImport("data/users.json", JSImport.Default) +object mockUsers extends js.Object + +@js.native +@JSImport("params/pdb-params.json", JSImport.Default) +object pdbParams extends js.Object + +@JSExportTopLevel("app") +object Main: + + @JSExport + def main(args: Array[String]): Unit = + import Routes.given + onLoad { + setupAirstream() + val appContainer = dom.document.querySelector("#app") + val _ = + render( + appContainer, + renderPage(state.MockAppState(using unsafeWindowOwner, router)) + ) + } + + private def onLoad(f: => Unit): Unit = + documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) + + private def setupAirstream()(using router: Router[Page]): Unit = + AirstreamError.registerUnhandledErrorCallback(err => + router.forcePage( + Page.UnhandledError( + Some(err.getClass.getName), // TODO: Fill only in dev mode + Some(err.getMessage) + ) + ) + ) + + def renderPage(state: AppState)(using + router: Router[Page] + ): HtmlElement = + val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) + .collectSignal[Page.Detail]( + connectors + .DetailPageConnector(state)(_) + .apply + ) + .collectSignal[Page.DetailParametru]( + connectors + .DetailParametruPageConnector(state)(_) + .apply + ) + .collectSignal[Page.DetailKriteria]( + connectors + .DetailKriteriaPageConnector(state)(_) + .apply + ) + .collectSignal[Page.UpravDukazKriteria]( + pages.detail.UpravDukaz.Connector(state)(_).apply + ) + .collectStatic(Page.Dashboard)( + connectors.DashboardPageConnector(state.actionBus).apply + ) + .collect[Page.NotFound](pg => + pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) + ) + .collect[Page.UnhandledError](pg => + pages.errors + .UnhandledErrorPage( + pages.errors.UnhandledErrorPage + .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), + state.actionBus + ) + ) + .collectStatic(Page.Directory)( + connectors + .DirectoryPageConnector(state.users, state.actionBus) + .apply + ) + div(cls := "h-full", child <-- pageSplitter.$view) + + // Pull in the stylesheet + val css: Css.type = Css diff --git a/app/src/main/scala/mdr/pdb/app/Routes.scala b/app/src/main/scala/mdr/pdb/app/Routes.scala new file mode 100644 index 0000000..2cc860c --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/Routes.scala @@ -0,0 +1,198 @@ +package mdr.pdb.app + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.* +import org.scalajs.dom +import zio.json.{*, given} +import mdr.pdb.OsobniCislo + +import scala.scalajs.js +import mdr.pdb.UserInfo +import mdr.pdb.Parameter +import mdr.pdb.ParameterCriteria +import mdr.pdb.app.Page.Titled + +// enum is not working with Waypoints' SplitRender collectStatic +sealed abstract class Page( + val id: String, + val title: String, + val parent: Option[Page] +) { + val path: Vector[Page] = + parent match + case None => Vector(this) + case Some(p) => p.path :+ this + + val isRoot: Boolean = parent.isEmpty +} + +object Page: + + case class Titled[V](value: V, title: Option[String] = None): + val show: String = title.getOrElse(value.toString) + + case object Directory extends Page("directory", "Adresář", None) + + case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) + + case class Detail(osobniCislo: Titled[OsobniCislo]) + extends Page("user", osobniCislo.show, Some(Directory)) + + object Detail { + def apply(o: UserInfo): Detail = Detail( + Titled(o.personalNumber, Some(o.name)) + ) + } + + case class DetailParametru( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String] + ) extends Page( + "parameter", + parametr.show, + Some(Detail(osobniCislo)) + ) + + object DetailParametru { + def apply(o: UserInfo, p: Parameter): DetailParametru = + DetailParametru( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)) + ) + } + + case class DetailKriteria( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String], + kriterium: Titled[String] + ) extends Page( + "criteria", + kriterium.show, + Some(DetailParametru(osobniCislo, parametr)) + ) + + object DetailKriteria { + def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = + DetailKriteria( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)), + Titled(k.id, Some(k.id)) + ) + } + + case class UpravDukazKriteria( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String], + kriterium: Titled[String] + ) extends Page( + "addProof", + "Důkaz", + Some(DetailKriteria(osobniCislo, parametr, kriterium)) + ) + + object UpravDukazKriteria { + def apply( + o: UserInfo, + p: Parameter, + k: ParameterCriteria + ): UpravDukazKriteria = + UpravDukazKriteria( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)), + Titled(k.id, Some(k.id)) + ) + } + + case class NotFound(url: String) extends Page("404", "404", Some(Directory)) + + case class UnhandledError( + errorName: Option[String], + errorMessage: Option[String] + ) extends Page("500", "Unexpected error", Some(Directory)) + +object Routes: + given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) + given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) + given [V: JsonEncoder]: JsonEncoder[Titled[V]] = + DeriveJsonEncoder.gen[Titled[V]] + given [V: JsonDecoder]: JsonDecoder[Titled[V]] = + DeriveJsonDecoder.gen[Titled[V]] + given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] + given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] + + val base = + js.`import`.meta.env.BASE_URL + .asInstanceOf[String] + "app" + + val homePage: Page = Page.Directory + + given router: Router[Page] = Router[Page]( + routes = List( + Route.static(homePage, root / endOfSegments, basePath = base), + Route.static( + Page.Dashboard, + root / "dashboard" / endOfSegments, + basePath = base + ), + Route[Page.Detail, String]( + encode = _.osobniCislo.value.toString, + decode = osc => Page.Detail(Titled(OsobniCislo(osc))), + root / "osoba" / segment[String] / endOfSegments, + basePath = base + ), + Route[Page.DetailParametru, (String, String)]( + encode = p => (p.osobniCislo.value.toString, p.parametr.value), + decode = + p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / endOfSegments, + basePath = base + ), + Route[Page.DetailKriteria, (String, String, String)]( + encode = p => + ( + p.osobniCislo.value.toString, + p.parametr.value, + p.kriterium.value.replaceAll("\\.", "--") + ), + decode = p => + Page.DetailKriteria( + Titled(OsobniCislo(p._1)), + Titled(p._2), + Titled(p._3.replaceAll("--", ".")) + ), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / "kriterium" / segment[String] / endOfSegments, + basePath = base + ), + Route[Page.UpravDukazKriteria, (String, String, String)]( + encode = p => + ( + p.osobniCislo.value.toString, + p.parametr.value, + p.kriterium.value.replaceAll("\\.", "--") + ), + decode = p => + Page.UpravDukazKriteria( + Titled(OsobniCislo(p._1)), + Titled(p._2), + Titled(p._3.replaceAll("--", ".")) + ), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / "kriterium" / segment[String] / "edit" / endOfSegments, + basePath = base + ) + ), + serializePage = _.toJson, + deserializePage = _.fromJson[Page] + .fold(s => throw IllegalStateException(s), identity), + getPageTitle = _.title, + routeFallback = url => Page.NotFound(url), + deserializeFallback = _ => Page.Dashboard + )( + $popStateEvent = windowEvents.onPopState, + owner = unsafeWindowOwner + ) diff --git a/app/src/main/scala/mdr/pdb/app/actions.scala b/app/src/main/scala/mdr/pdb/app/actions.scala new file mode 100644 index 0000000..d078b3a --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/actions.scala @@ -0,0 +1,21 @@ +package mdr.pdb.app + +import mdr.pdb.OsobniCislo +import mdr.pdb.UserInfo +import mdr.pdb.Parameter +import mdr.pdb.ParameterCriteria + +sealed trait Action + +case object FetchDirectory extends Action +case class FetchUserDetails(osc: OsobniCislo) extends Action +case class FetchParameters(osc: OsobniCislo) extends Action +case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action +case class FetchParameterCriteria( + osc: OsobniCislo, + paramId: String, + critId: String, + page: (UserInfo, Parameter, ParameterCriteria) => Page +) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action +case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/mdr/pdb/app/components/AppPage.scala new file mode 100644 index 0000000..130c817 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/AppPage.scala @@ -0,0 +1,82 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.Page +import mdr.pdb.{UserProfile, UserInfo, OsobniCislo} +import com.raquo.waypoint.Router +import mdr.pdb.app.Action +import mdr.pdb.app.NavigateTo +import mdr.pdb.UserFunction + +object AppPage: + // TODO: pages by logged in user + val pages: List[Page] = List(Page.Directory, Page.Dashboard) + + import NavigationBar.{Logo, MenuItem} + + val logo = Logo( + "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", + "Workflow" + ) + + // TODO: menu items by user profile + val userMenu = + List( + MenuItem("Your Profile"), + MenuItem("Settings"), + MenuItem("Sign out") + ) + + // TODO: load user profile + val $userProfile = Var( + UserProfile( + "tom", + UserInfo( + OsobniCislo("1031"), + "tom", + "Tom", + "Cook", + None, + None, + Some("tom@example.com"), + Some("+420 222 866 180"), + Some( + UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") + ), + Nil, + None + ) + ) + ) + + val $userInfo = $userProfile.signal.map(_.userInfo) + + type ViewModel = Option[HtmlElement] + def apply( + actionBus: Observer[Action] + )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using + router: Router[Page] + ): HtmlElement = + PageLayout(actionBus)( + $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => + PageLayout.ViewModel( + NavigationBar.ViewModel( + u, + pages.map(p => + NavigationBar.Link( + () => + PageLink( + p, + actionBus + ), + p == cp + ) + ), + userMenu, + logo + ), + c + ) + ), + mods + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala new file mode 100644 index 0000000..9375398 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala @@ -0,0 +1,143 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.tailwind.list.IconText.ViewModel +import fiftyforms.ui.components.tailwind.Icons +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.Page + +import com.raquo.laminar.api.L.{*, given} +import io.laminext.syntax.core.{*, given} +import mdr.pdb.app.Action + +object Breadcrumbs: + + private def slash = + import svg.{*, given} + svg( + cls := "flex-shrink-0 h-5 w-5 text-gray-300", + xmlns := "http://www.w3.org/2000/svg", + fill := "currentColor", + viewBox := "0 0 20 20", + ariaHidden := true, + path( + d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" + ) + ) + + def backIcon = + Icons.solid + .`arrow-narrow-left`() + .amend( + svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" + ) + + object Link: + case class ViewModel( + page: Page, + icon: Option[SvgElement], + extraClasses: String, + text: String, + textClasses: Option[String] = None + ) + + val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" + + def shortHome(p: Page) = ViewModel( + p, + Some(backIcon), + "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", + "Zpět na úvodní stránku", + None + ) + + def fullHome(p: Page) = ViewModel( + p, + Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), + "text-gray-400 hover:text-gray-500", + "Domů", + Some("sr-only") + ) + + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + PageLink + .container($m.map(_.page), actionBus) + .amend( + cls <-- $m.map(_.extraClasses), + child.maybe <-- $m.map(_.icon), + span( + cls <-- $m.map(_.textClasses.getOrElse("")), + child.text <-- $m.map(_.text) + ) + ) + + object FullBreadcrumbs: + type ViewModel = Page + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + ol( + role := "list", + cls := "flex items-center space-x-4", + children <-- $m.map(_.path).split(_.id)((_, _, $p) => + li( + div( + cls := "flex items-center", + child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), + Link( + $p.map(p => + if (p.isRoot) then Link.fullHome(p) + else + Link.ViewModel( + p, + None, + s"ml-4 max-w-xs truncate ${Link.baseClasses}", + p.title + ) + ), + actionBus + ) + ) + ) + ) + ) + + object ShortBreadcrumbs: + type ViewModel = Page + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + Link( + $m.map { p => + val target = p.parent.getOrElse(p) + if target.isRoot then Link.shortHome(target) + else + Link.ViewModel( + target, + Some(backIcon), + "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", + s"Zpět na ${target.title}" + ) + }, + actionBus + ) + + def apply(actionBus: Observer[Action])(using + router: Router[Page] + ): HtmlElement = + nav( + cls := "flex", + aria.label := "Breadcrumb", + div( + cls := "flex sm:hidden", + ShortBreadcrumbs(router.$currentPage, actionBus) + ), + div( + cls := "hidden sm:block", + FullBreadcrumbs(router.$currentPage, actionBus) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala new file mode 100644 index 0000000..4bb8b8d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala @@ -0,0 +1,242 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent +import fiftyforms.ui.components.tailwind.Icons +import fiftyforms.ui.components.tailwind.Avatar +import mdr.pdb.UserInfo +import io.laminext.syntax.core.* + +object NavigationBar: + + case class Logo(img: String, name: String) + case class Link(a: () => Anchor, active: Boolean) + case class MenuItem(title: String) + + case class ViewModel( + userInfo: UserInfo, + pages: List[Link], + userMenu: List[MenuItem], + logo: Logo + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + val $userInfo = $m.map(_.userInfo) + val mobileMenuOpen = Var(false) + + val desktopOnly = cls("hidden md:block") + val mobileOnly = cls("md:hidden") + + inline def avatarImage(size: Int = 8) = + Avatar($userInfo.map(_.img)).avatarImage(size) + + def notificationButton = button( + tpe := "button", + cls := List( + "bg-indigo-600", + "focus:outline-none", + "focus:ring-2", + "focus:ring-offset-2", + "focus:ring-offset-indigo-600", + "focus:ring-white", + "hover:text-white", + "p-1", + "rounded-full", + "text-indigo-200" + ), + span(cls := "sr-only", "View notifications"), + Icons.outline.bell() + ) + + def userProfile: HtmlElement = + val menuOpen = Var(false) + + def menuItem(item: MenuItem, idx: Int): HtmlElement = + a( + href := "#", + cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", + role := "menuitem", + tabIndex := -1, + idAttr := s"user-menu-item-$idx", + item.title + ) + + div( + cls := "ml-3 relative", + div( + button( + tpe := "button", + cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", + idAttr := "user-menu-button", + aria.expanded <-- menuOpen.signal, + aria.hasPopup := true, + span(cls := "sr-only", "Open user menu"), + child <-- avatarImage(), + onClick.preventDefault.mapTo( + !menuOpen.now() + ) --> menuOpen.writer + ) + ), + /* + * */ + div( + cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", + cls <-- menuOpen.signal.map { o => + if (o) "md:block" else "md:hidden" + }, + role := "menu", + aria.orientation := "vertical", + aria.labelledBy := "user-menu-button", + tabIndex := -1, + // : keyboard navigation + children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) + ) + ) + + def mobileProfile = + def menuItem(item: MenuItem): HtmlElement = + a( + href := "#", + cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", + item.title + ) + + div( + cls := "pt-4 pb-3 border-t border-indigo-700", + div( + cls := "flex items-center px-5", + div( + cls := "flex-shrink-0", + child <-- avatarImage(10) + ), + div( + cls := "ml-3", + div( + cls := "text-base font-medium text-white", + child.text <-- $userInfo.map(_.name) + ), + child.maybe <-- $userInfo.map( + _.email.map(e => + div( + cls := "text-sm font-medium text-indigo-300", + e + ) + ) + ) + ), + notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) + ), + div( + cls := "mt-3 px-2 space-y-1", + children <-- $m.map(_.userMenu.map(menuItem)) + ) + ) + + def pageLink(page: Link): Anchor = + page + .a() + .amend( + cls := "text-white px-3 py-2 rounded-md text-sm font-medium", + cls := Seq( + "bg-indigo-700" -> page.active, + "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active + ), + ariaCurrent := (if page.active then "page" else "false") + ) + + def logoImg: Image = + img( + cls := "h-8 w-8", + src <-- $m.map(_.logo.img), + alt <-- $m.map(_.logo.name) + ) + + def mobileMenuButton = button( + tpe := "button", + cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", + aria.controls := "mobile-menu", + aria.expanded <-- mobileMenuOpen.signal, + span(cls := "sr-only", "Open main menu"), + Icons.outline + .menu() + .amend( + svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") + ), + Icons.outline + .x() + .amend( + svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") + ), + onClick.preventDefault.mapTo( + !mobileMenuOpen.now() + ) --> mobileMenuOpen.writer + ) + + def navBarLeft = + div( + cls := "flex items-center", + div(cls := "flex-shrink-0", logoImg), + div( + desktopOnly, + div( + cls := "ml-10 flex items-baseline space-x-4", + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) + ) + ) + ) + + def navBarRight = + div( + desktopOnly, + div( + cls := "ml-4 flex items-center md:ml-6", + notificationButton, + userProfile + ) + ) + + def navBarMobile = + div( + cls := "-mr-2 flex", + mobileOnly, + mobileMenuButton + ) + + def navBar = + div( + cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", + div( + cls := "flex items-center justify-between h-16", + navBarLeft, + navBarRight, + navBarMobile + ) + ) + + def mobileMenu = + div( + mobileOnly, + cls <-- mobileMenuOpen.signal.map { o => + if (o) "block" else "hidden" + }, + idAttr := "mobile-menu", + div( + cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) + ), + mobileProfile + ) + + nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala new file mode 100644 index 0000000..c4cb10d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala @@ -0,0 +1,19 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.Page +import com.raquo.waypoint.Router +import mdr.pdb.app.Action + +object PageHeader: + def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = + header( + cls := "bg-white shadow-sm", + div( + cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", + h1( + cls := "text-lg leading-6 font-semibold text-gray-900", + Breadcrumbs(actionBus) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala new file mode 100644 index 0000000..aae4539 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala @@ -0,0 +1,30 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.Page +import mdr.pdb.app.Action +import fiftyforms.ui.components.tailwind.Loading +import io.laminext.syntax.core.* + +object PageLayout: + case class ViewModel( + navigation: NavigationBar.ViewModel, + content: Option[HtmlElement] + ) + def apply(actionBus: Observer[Action])( + $m: Signal[ViewModel], + mods: Modifier[HtmlElement]* + )(using router: Router[Page]): HtmlElement = + val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) + div( + cls := "h-full flex flex-col", + NavigationBar($m.map(_.navigation)), + child.maybe <-- router.$currentPage.map(_.isRoot) + .switch(None, Some(PageHeader(actionBus))), + main( + cls := "flex-grow-1 overflow-y-auto", + mods, + child <-- $maybeContent.map(_.getOrElse(Loading)) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/mdr/pdb/app/components/PageLink.scala new file mode 100644 index 0000000..e6e7052 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageLink.scala @@ -0,0 +1,40 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.LinkSupport.* +import mdr.pdb.app.Page +import mdr.pdb.app.Action +import com.raquo.waypoint.Router +import mdr.pdb.app.NavigateTo + +object PageLink: + type ViewModel = Page + def apply($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = + a(mods($m, actions), child.text <-- $m.map(_.title)) + + def apply(m: ViewModel, actions: Observer[Action])(using + router: Router[Page] + ): Anchor = apply(Val(m), actions) + + def container($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = a(mods($m, actions)) + + def container(m: ViewModel, actions: Observer[Action])(using + Router[Page] + ): Anchor = container(Val(m), actions) + + private def mods($m: Signal[Page], actions: Observer[Action])(using + router: Router[Page] + ): Modifier[Anchor] = + Seq( + href <-- $m.map(router.absoluteUrlForPage).recover { case _ => + Some("invalid url") + }, + composeEvents(onClick.noKeyMod.preventDefault)( + _.sample($m) + .map(NavigateTo.apply) + ) --> actions + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala new file mode 100644 index 0000000..7682772 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala @@ -0,0 +1,13 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.pages.dashboard.DashboardPage +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage + +class DashboardPageConnector(actionBus: Observer[Action])(using + router: Router[Page] +): + def apply: HtmlElement = + AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala new file mode 100644 index 0000000..76b9133 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala @@ -0,0 +1,76 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.Parameter +import mdr.pdb.UserInfo +import mdr.pdb.app.pages.detail.DetailParametruPage +import pages.detail.DetailKriteriaPage +import pages.detail.components.DukazyKriteria +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.ParameterCriteria + +object DetailKriteriaPageConnector { + trait AppState { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + } +} + +case class DetailKriteriaPageConnector( + state: DetailKriteriaPageConnector.AppState +)( + $page: Signal[Page.DetailKriteria] +)(using Router[Page]): + val $paramChangeSignal = + $page.splitOne(p => + (p.osobniCislo.value, p.parametr.value, p.kriterium.value) + )((x, _, _) => x) + val $pageChangeSignal = + $paramChangeSignal.map( + FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) + ) + + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + val $merged = + $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => + for { + da <- d + pa <- p + pb <- pa.find(_.id == pc._2) + ka <- pb.criteria.find(_.id == pc._3) + } yield (da, pb, ka) + ) + + def apply: HtmlElement = + AppPage(state.actionBus)( + $merged.map(_.map(buildModel)) + .split(_ => ())((_, s, $s) => + DetailKriteriaPage($s)(state.actionBus.contramap { + case DukazyKriteria.Add => + NavigateTo( + Page.UpravDukazKriteria( + Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), + Page.Titled(s.parametr.id, Some(s.parametr.nazev)), + Page.Titled(s.kriterium.id, Some(s.kriterium.id)) + ) + ) + }) + ), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: Parameter, + k: ParameterCriteria + ): DetailKriteriaPage.ViewModel = + DetailKriteriaPage.ViewModel( + o.toDetailOsoby, + p.toParametr(_ => a()), + k.toKriterium(_ => a()) + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DetailPageConnector.scala new file mode 100644 index 0000000..df3a6b7 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DetailPageConnector.scala @@ -0,0 +1,57 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.UserInfo +import pages.detail.DetailPage +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.pages.detail.components.DetailOsoby +import mdr.pdb.Parameter +import mdr.pdb.app.pages.detail.components.SeznamParametru +import fiftyforms.ui.components.tailwind.Color + +object DetailPageConnector { + trait AppState { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + } +} + +case class DetailPageConnector(state: DetailPageConnector.AppState)( + $page: Signal[Page.Detail] +)(using router: Router[Page]): + val $oscChangeSignal = $page.splitOne(_.osobniCislo.value)((osc, _, _) => osc) + val $pageChangeSignal = + $oscChangeSignal.flatMap(osc => + EventStream.fromSeq(Seq(FetchUserDetails(osc), FetchParameters(osc))) + ) + // TODO: filter the value based on the current osc + // OSC change will fetch new data, but still + // - we need to be sure that what we got is really what we ought to display + // - we want to display stale data accordingly (at least with loading indicator) + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + def apply: HtmlElement = + AppPage(state.actionBus)( + $data.combineWithFn($params)(_ zip _) + .map(_.map(buildModel)) + .split(_ => ())((_, _, s) => DetailPage(s)), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: List[Parameter] + ): DetailPage.ViewModel = + DetailPage.ViewModel( + o.toDetailOsoby, + p.map( + _.toParametr(param => + PageLink.container(Page.DetailParametru(o, param), state.actionBus) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DetailParametruPageConnector.scala new file mode 100644 index 0000000..fa7193d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DetailParametruPageConnector.scala @@ -0,0 +1,67 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.Parameter +import mdr.pdb.UserInfo +import mdr.pdb.app.pages.detail.DetailParametruPage +import pages.detail.DetailParametruPage +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.components.PageLink + +object DetailParametruPageConnector { + trait AppState { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + } +} + +case class DetailParametruPageConnector( + state: DetailParametruPageConnector.AppState +)( + $page: Signal[Page.DetailParametru] +)(using router: Router[Page]): + val $paramChangeSignal = + $page.splitOne(p => (p.osobniCislo.value, p.parametr.value))((x, _, _) => x) + val $pageChangeSignal = + $paramChangeSignal.map(FetchParameter(_, _)) + + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + val $merged = + $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => + for { + da <- d + pa <- p + pb <- pa.find(_.id == pc._2) + } yield (da, pb) + ) + + def apply: HtmlElement = + AppPage(state.actionBus)( + $merged.map(_.map(buildModel)) + .split(_ => ())((_, _, s) => DetailParametruPage(s)), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: Parameter + ): DetailParametruPage.ViewModel = + DetailParametruPage.ViewModel( + o.toDetailOsoby, + p.toParametr(p => + PageLink.container(Page.DetailParametru(o, p), state.actionBus) + ), + p.criteria.map( + _.toKriterium { c => + PageLink.container( + Page.DetailKriteria(o, p, c), + state.actionBus + ) + } + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DirectoryPageConnector.scala new file mode 100644 index 0000000..ab2c045 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DirectoryPageConnector.scala @@ -0,0 +1,34 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.UserInfo +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.components.AppPage + +case class DirectoryPageConnector( + $input: EventStream[List[UserInfo]], + actionBus: Observer[Action] +)(using router: Router[Page]): + val $data = $input.startWithNone + val $actionSignal = EventStream.fromValue(FetchDirectory) + + def apply: HtmlElement = + AppPage(actionBus)( + $data.split(_ => ())((_, _, s) => + pages.directory.DirectoryPage( + s.map( + _.map( + _.toUserRow(u => + PageLink.container( + Page.Detail(Page.Titled(u.personalNumber)), + actionBus + ) + ) + ) + ) + ) + ), + $actionSignal --> actionBus + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/mdr/pdb/app/connectors/conversions.scala new file mode 100644 index 0000000..f581796 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/conversions.scala @@ -0,0 +1,68 @@ +package mdr.pdb.app.connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.UserInfo +import mdr.pdb.app.pages.detail.components.DetailOsoby +import mdr.pdb.Parameter +import mdr.pdb.app.pages.detail.components.SeznamParametru +import mdr.pdb.ParameterCriteria +import mdr.pdb.app.pages.detail.components.SeznamKriterii +import mdr.pdb.app.pages.directory.components.UserRow +import mdr.pdb.app.pages.detail.components.DetailParametru +import mdr.pdb.app.pages.detail.components.DetailKriteria +import fiftyforms.ui.components.tailwind.Color + +extension (o: UserInfo) + def toDetailOsoby: DetailOsoby.ViewModel = + DetailOsoby.ViewModel( + o.personalNumber, + o.name, + o.email, + o.phone, + o.img, + o.mainFunction.map(f => + DetailOsoby.Funkce.ViewModel(f.name, f.dept, f.ou) + ), + o.userContracts.headOption.map(c => + DetailOsoby.PracovniPomer.ViewModel(c.rel, c.startDate, c.endDate) + ) + ) + +extension (param: Parameter) + def toParametr(container: Parameter => Anchor): DetailParametru.ViewModel = + DetailParametru.ViewModel( + id = param.id, + nazev = param.name, + popis = param.description, + status = "Nesplněno", + statusColor = Color.red, + a = container(param) + ) + +extension (crit: ParameterCriteria) + def toKriterium( + container: ParameterCriteria => Anchor + ): DetailKriteria.ViewModel = + DetailKriteria.ViewModel( + nazev = crit.criteriumText, + kapitola = crit.chapterId, + bod = crit.itemId, + status = "Nesplněno", + statusColor = Color.red, + splneno = false, + dukazy = Nil, + container = container(crit) + ) + +extension (user: UserInfo) + def toUserRow( + container: UserInfo => HtmlElement = (_: UserInfo) => div() + ): UserRow.ViewModel = + UserRow.ViewModel( + osobniCislo = user.personalNumber.toString, + celeJmeno = user.name, + prijmeni = user.surname, + hlavniFunkce = user.mainFunction.map(_.name), + img = user.img, + container = () => container(user) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/dashboard/DashboardPage.scala b/app/src/main/scala/mdr/pdb/app/pages/dashboard/DashboardPage.scala new file mode 100644 index 0000000..0eb8f7a --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/dashboard/DashboardPage.scala @@ -0,0 +1,11 @@ +package mdr.pdb.app.pages.dashboard + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.Page + +object DashboardPage: + + def render: HtmlElement = + div("Dashboard page") diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala new file mode 100644 index 0000000..ff3a25f --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala @@ -0,0 +1,31 @@ +package mdr.pdb.app.pages.detail + +import com.raquo.laminar.api.L.{*, given} + +import components._ + +object DetailKriteriaPage: + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametr: DetailParametru.ViewModel, + kriterium: DetailKriteria.ViewModel + ) + + def apply($m: Signal[ViewModel])( + action: Observer[DukazyKriteria.Action] + ): HtmlElement = + div( + cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + div( + DetailOsoby.header($m.map(_.osoba)), + DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") + ), + div( + DetailKriteria($m.map(_.kriterium)), + DukazyKriteria($m.map(_.kriterium.dukazy))(action) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/DetailPage.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailPage.scala new file mode 100644 index 0000000..b591cc9 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailPage.scala @@ -0,0 +1,25 @@ +package mdr.pdb.app.pages.detail + +import com.raquo.laminar.api.L.{*, given} + +import components._ +import mdr.pdb.app.Page +import com.raquo.waypoint.Router +import mdr.pdb.app.Action + +object DetailPage: + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametry: SeznamParametru.ViewModel + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + DetailOsoby($m.map(_.osoba)), + SeznamParametru($m.map(_.parametry)) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/DetailParametruPage.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailParametruPage.scala new file mode 100644 index 0000000..3322279 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailParametruPage.scala @@ -0,0 +1,24 @@ +package mdr.pdb.app.pages.detail + +import com.raquo.laminar.api.L.{*, given} + +import components._ + +object DetailParametruPage: + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametr: DetailParametru.ViewModel, + kriteria: SeznamKriterii.ViewModel + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + DetailOsoby.header($m.map(_.osoba)), + DetailParametru($m.map(_.parametr)), + SeznamKriterii($m.map(_.kriteria)) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala new file mode 100644 index 0000000..b58618a --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala @@ -0,0 +1,107 @@ +package mdr.pdb +package app +package pages.detail + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.pages.detail.components.UpravDukazForm +import fiftyforms.services.files.File + +object UpravDukaz: + + type ThisPage = Page.UpravDukazKriteria + type PageKey = (OsobniCislo, String, String) + + trait State { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def availableFiles: EventStream[List[File]] + def actionBus: Observer[Action] + } + + def keyOfPage(page: ThisPage): PageKey = + (page.osobniCislo.value, page.parametr.value, page.kriterium.value) + + def onChangeAction(key: PageKey): Action = + FetchParameterCriteria( + key._1, + key._2, + key._3, + Page.UpravDukazKriteria(_, _, _) + ) + + class Connector(state: State)($page: Signal[ThisPage])(using Router[Page]): + val $paramChangeSignal = $page.splitOne(keyOfPage)((x, _, _) => x) + val $pageChangeSignal = $paramChangeSignal.map(onChangeAction) + + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + val $merged = + $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => + for { + da <- d + pa <- p + pb <- pa.find(_.id == pc._2) + ka <- pb.criteria.find(_.id == pc._3) + } yield (da, pb, ka) + ) + + def apply: HtmlElement = + AppPage(state.actionBus)( + $merged.split(_ => ())((_, s, $s) => + PageComponent( + $s.map(buildModel), + state.availableFiles, + state.actionBus.contramap { + case UpravDukazForm.Cancelled => + NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) + case UpravDukazForm.AvailableFilesRequested => + FetchAvailableFiles(s._1.personalNumber) + } + ) + ), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: Parameter, + k: ParameterCriteria + ): PageComponent.ViewModel = + import connectors.* + PageComponent.ViewModel( + o.toDetailOsoby, + p.toParametr(_ => a()), + k.toKriterium(_ => a()) + ) + + object PageComponent: + import components.* + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametr: DetailParametru.ViewModel, + kriterium: DetailKriteria.ViewModel + ) + + def apply( + $m: Signal[ViewModel], + availableFilesStream: EventStream[List[File]], + events: Observer[UpravDukazForm.Event] + ): HtmlElement = + div( + cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + div( + DetailOsoby.header($m.map(_.osoba)), + DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") + ), + div( + DetailKriteria($m.map(_.kriterium)), + UpravDukazForm(availableFilesStream)(events) + ) + ) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala deleted file mode 100644 index a3b00af..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala +++ /dev/null @@ -1,103 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import scala.scalajs.js.annotation.JSExportTopLevel -import scala.scalajs.js.annotation.JSExport -import scala.scalajs.js.annotation.JSImport -import scala.scalajs.js -import org.scalajs.dom -import com.raquo.laminar.api.L.{*, given} -import scala.scalajs.js.Date -import com.raquo.waypoint.Router -import com.raquo.waypoint.SplitRender -import cz.e_bs.cmi.mdr.pdb.app.services.DataFetcher -import scala.scalajs.js.JSON -import zio.json._ -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.state.AppState - -@js.native -@JSImport("stylesheets/main.css", JSImport.Namespace) -object Css extends js.Any - -@js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native -@JSImport("params/pdb-params.json", JSImport.Default) -object pdbParams extends js.Object - -@JSExportTopLevel("app") -object Main: - - @JSExport - def main(args: Array[String]): Unit = - import Routes.given - onLoad { - setupAirstream() - val appContainer = dom.document.querySelector("#app") - val _ = - render( - appContainer, - renderPage(state.MockAppState(using unsafeWindowOwner, router)) - ) - } - - private def onLoad(f: => Unit): Unit = - documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) - - private def setupAirstream()(using router: Router[Page]): Unit = - AirstreamError.registerUnhandledErrorCallback(err => - router.forcePage( - Page.UnhandledError( - Some(err.getClass.getName), // TODO: Fill only in dev mode - Some(err.getMessage) - ) - ) - ) - - def renderPage(state: AppState)(using - router: Router[Page] - ): HtmlElement = - val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) - .collectSignal[Page.Detail]( - connectors - .DetailPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailParametru]( - connectors - .DetailParametruPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailKriteria]( - connectors - .DetailKriteriaPageConnector(state)(_) - .apply - ) - .collectSignal[Page.UpravDukazKriteria]( - pages.detail.UpravDukaz.Connector(state)(_).apply - ) - .collectStatic(Page.Dashboard)( - connectors.DashboardPageConnector(state.actionBus).apply - ) - .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) - ) - .collect[Page.UnhandledError](pg => - pages.errors - .UnhandledErrorPage( - pages.errors.UnhandledErrorPage - .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), - state.actionBus - ) - ) - .collectStatic(Page.Directory)( - connectors - .DirectoryPageConnector(state.users, state.actionBus) - .apply - ) - div(cls := "h-full", child <-- pageSplitter.$view) - - // Pull in the stylesheet - val css: Css.type = Css diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala deleted file mode 100644 index 3bbc8c0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ /dev/null @@ -1,198 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.* -import org.scalajs.dom -import zio.json.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import scala.scalajs.js -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.Page.Titled - -// enum is not working with Waypoints' SplitRender collectStatic -sealed abstract class Page( - val id: String, - val title: String, - val parent: Option[Page] -) { - val path: Vector[Page] = - parent match - case None => Vector(this) - case Some(p) => p.path :+ this - - val isRoot: Boolean = parent.isEmpty -} - -object Page: - - case class Titled[V](value: V, title: Option[String] = None): - val show: String = title.getOrElse(value.toString) - - case object Directory extends Page("directory", "Adresář", None) - - case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) - - case class Detail(osobniCislo: Titled[OsobniCislo]) - extends Page("user", osobniCislo.show, Some(Directory)) - - object Detail { - def apply(o: UserInfo): Detail = Detail( - Titled(o.personalNumber, Some(o.name)) - ) - } - - case class DetailParametru( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String] - ) extends Page( - "parameter", - parametr.show, - Some(Detail(osobniCislo)) - ) - - object DetailParametru { - def apply(o: UserInfo, p: Parameter): DetailParametru = - DetailParametru( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)) - ) - } - - case class DetailKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "criteria", - kriterium.show, - Some(DetailParametru(osobniCislo, parametr)) - ) - - object DetailKriteria { - def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = - DetailKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class UpravDukazKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "addProof", - "Důkaz", - Some(DetailKriteria(osobniCislo, parametr, kriterium)) - ) - - object UpravDukazKriteria { - def apply( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): UpravDukazKriteria = - UpravDukazKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class NotFound(url: String) extends Page("404", "404", Some(Directory)) - - case class UnhandledError( - errorName: Option[String], - errorMessage: Option[String] - ) extends Page("500", "Unexpected error", Some(Directory)) - -object Routes: - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) - given [V: JsonEncoder]: JsonEncoder[Titled[V]] = - DeriveJsonEncoder.gen[Titled[V]] - given [V: JsonDecoder]: JsonDecoder[Titled[V]] = - DeriveJsonDecoder.gen[Titled[V]] - given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] - given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] - - val base = - js.`import`.meta.env.BASE_URL - .asInstanceOf[String] + "app" - - val homePage: Page = Page.Directory - - given router: Router[Page] = Router[Page]( - routes = List( - Route.static(homePage, root / endOfSegments, basePath = base), - Route.static( - Page.Dashboard, - root / "dashboard" / endOfSegments, - basePath = base - ), - Route[Page.Detail, String]( - encode = _.osobniCislo.value.toString, - decode = osc => Page.Detail(Titled(OsobniCislo(osc))), - root / "osoba" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.DetailParametru, (String, String)]( - encode = p => (p.osobniCislo.value.toString, p.parametr.value), - decode = - p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / endOfSegments, - basePath = base - ), - Route[Page.DetailKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.DetailKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.UpravDukazKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.UpravDukazKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / "edit" / endOfSegments, - basePath = base - ) - ), - serializePage = _.toJson, - deserializePage = _.fromJson[Page] - .fold(s => throw IllegalStateException(s), identity), - getPageTitle = _.title, - routeFallback = url => Page.NotFound(url), - deserializeFallback = _ => Page.Dashboard - )( - $popStateEvent = windowEvents.onPopState, - owner = unsafeWindowOwner - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala deleted file mode 100644 index b24b82b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -sealed trait Action - -case object FetchDirectory extends Action -case class FetchUserDetails(osc: OsobniCislo) extends Action -case class FetchParameters(osc: OsobniCislo) extends Action -case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action -case class FetchParameterCriteria( - osc: OsobniCislo, - paramId: String, - critId: String, - page: (UserInfo, Parameter, ParameterCriteria) => Page -) extends Action -case class FetchAvailableFiles(osc: OsobniCislo) extends Action -case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala deleted file mode 100644 index c637d35..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala +++ /dev/null @@ -1,82 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.{UserProfile, UserInfo, OsobniCislo} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo -import cz.e_bs.cmi.mdr.pdb.UserFunction - -object AppPage: - // TODO: pages by logged in user - val pages: List[Page] = List(Page.Directory, Page.Dashboard) - - import NavigationBar.{Logo, MenuItem} - - val logo = Logo( - "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", - "Workflow" - ) - - // TODO: menu items by user profile - val userMenu = - List( - MenuItem("Your Profile"), - MenuItem("Settings"), - MenuItem("Sign out") - ) - - // TODO: load user profile - val $userProfile = Var( - UserProfile( - "tom", - UserInfo( - OsobniCislo("1031"), - "tom", - "Tom", - "Cook", - None, - None, - Some("tom@example.com"), - Some("+420 222 866 180"), - Some( - UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") - ), - Nil, - None - ) - ) - ) - - val $userInfo = $userProfile.signal.map(_.userInfo) - - type ViewModel = Option[HtmlElement] - def apply( - actionBus: Observer[Action] - )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using - router: Router[Page] - ): HtmlElement = - PageLayout(actionBus)( - $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => - PageLayout.ViewModel( - NavigationBar.ViewModel( - u, - pages.map(p => - NavigationBar.Link( - () => - PageLink( - p, - actionBus - ), - p == cp - ) - ), - userMenu, - logo - ), - c - ) - ), - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala deleted file mode 100644 index bf473c6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ /dev/null @@ -1,143 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden -import fiftyforms.ui.components.tailwind.list.IconText.ViewModel -import fiftyforms.ui.components.tailwind.Icons -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Action - -object Breadcrumbs: - - private def slash = - import svg.{*, given} - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-300", - xmlns := "http://www.w3.org/2000/svg", - fill := "currentColor", - viewBox := "0 0 20 20", - ariaHidden := true, - path( - d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" - ) - ) - - def backIcon = - Icons.solid - .`arrow-narrow-left`() - .amend( - svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" - ) - - object Link: - case class ViewModel( - page: Page, - icon: Option[SvgElement], - extraClasses: String, - text: String, - textClasses: Option[String] = None - ) - - val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" - - def shortHome(p: Page) = ViewModel( - p, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - "Zpět na úvodní stránku", - None - ) - - def fullHome(p: Page) = ViewModel( - p, - Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), - "text-gray-400 hover:text-gray-500", - "Domů", - Some("sr-only") - ) - - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - PageLink - .container($m.map(_.page), actionBus) - .amend( - cls <-- $m.map(_.extraClasses), - child.maybe <-- $m.map(_.icon), - span( - cls <-- $m.map(_.textClasses.getOrElse("")), - child.text <-- $m.map(_.text) - ) - ) - - object FullBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - ol( - role := "list", - cls := "flex items-center space-x-4", - children <-- $m.map(_.path).split(_.id)((_, _, $p) => - li( - div( - cls := "flex items-center", - child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), - Link( - $p.map(p => - if (p.isRoot) then Link.fullHome(p) - else - Link.ViewModel( - p, - None, - s"ml-4 max-w-xs truncate ${Link.baseClasses}", - p.title - ) - ), - actionBus - ) - ) - ) - ) - ) - - object ShortBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - Link( - $m.map { p => - val target = p.parent.getOrElse(p) - if target.isRoot then Link.shortHome(target) - else - Link.ViewModel( - target, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - s"Zpět na ${target.title}" - ) - }, - actionBus - ) - - def apply(actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - nav( - cls := "flex", - aria.label := "Breadcrumb", - div( - cls := "flex sm:hidden", - ShortBreadcrumbs(router.$currentPage, actionBus) - ), - div( - cls := "hidden sm:block", - FullBreadcrumbs(router.$currentPage, actionBus) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala deleted file mode 100644 index 0db2047..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ /dev/null @@ -1,242 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent -import fiftyforms.ui.components.tailwind.Icons -import fiftyforms.ui.components.tailwind.Avatar -import cz.e_bs.cmi.mdr.pdb.UserInfo -import io.laminext.syntax.core.* - -object NavigationBar: - - case class Logo(img: String, name: String) - case class Link(a: () => Anchor, active: Boolean) - case class MenuItem(title: String) - - case class ViewModel( - userInfo: UserInfo, - pages: List[Link], - userMenu: List[MenuItem], - logo: Logo - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - val $userInfo = $m.map(_.userInfo) - val mobileMenuOpen = Var(false) - - val desktopOnly = cls("hidden md:block") - val mobileOnly = cls("md:hidden") - - inline def avatarImage(size: Int = 8) = - Avatar($userInfo.map(_.img)).avatarImage(size) - - def notificationButton = button( - tpe := "button", - cls := List( - "bg-indigo-600", - "focus:outline-none", - "focus:ring-2", - "focus:ring-offset-2", - "focus:ring-offset-indigo-600", - "focus:ring-white", - "hover:text-white", - "p-1", - "rounded-full", - "text-indigo-200" - ), - span(cls := "sr-only", "View notifications"), - Icons.outline.bell() - ) - - def userProfile: HtmlElement = - val menuOpen = Var(false) - - def menuItem(item: MenuItem, idx: Int): HtmlElement = - a( - href := "#", - cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", - role := "menuitem", - tabIndex := -1, - idAttr := s"user-menu-item-$idx", - item.title - ) - - div( - cls := "ml-3 relative", - div( - button( - tpe := "button", - cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - idAttr := "user-menu-button", - aria.expanded <-- menuOpen.signal, - aria.hasPopup := true, - span(cls := "sr-only", "Open user menu"), - child <-- avatarImage(), - onClick.preventDefault.mapTo( - !menuOpen.now() - ) --> menuOpen.writer - ) - ), - /* - * */ - div( - cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", - cls <-- menuOpen.signal.map { o => - if (o) "md:block" else "md:hidden" - }, - role := "menu", - aria.orientation := "vertical", - aria.labelledBy := "user-menu-button", - tabIndex := -1, - // : keyboard navigation - children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) - ) - ) - - def mobileProfile = - def menuItem(item: MenuItem): HtmlElement = - a( - href := "#", - cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", - item.title - ) - - div( - cls := "pt-4 pb-3 border-t border-indigo-700", - div( - cls := "flex items-center px-5", - div( - cls := "flex-shrink-0", - child <-- avatarImage(10) - ), - div( - cls := "ml-3", - div( - cls := "text-base font-medium text-white", - child.text <-- $userInfo.map(_.name) - ), - child.maybe <-- $userInfo.map( - _.email.map(e => - div( - cls := "text-sm font-medium text-indigo-300", - e - ) - ) - ) - ), - notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) - ), - div( - cls := "mt-3 px-2 space-y-1", - children <-- $m.map(_.userMenu.map(menuItem)) - ) - ) - - def pageLink(page: Link): Anchor = - page - .a() - .amend( - cls := "text-white px-3 py-2 rounded-md text-sm font-medium", - cls := Seq( - "bg-indigo-700" -> page.active, - "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active - ), - ariaCurrent := (if page.active then "page" else "false") - ) - - def logoImg: Image = - img( - cls := "h-8 w-8", - src <-- $m.map(_.logo.img), - alt <-- $m.map(_.logo.name) - ) - - def mobileMenuButton = button( - tpe := "button", - cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - aria.controls := "mobile-menu", - aria.expanded <-- mobileMenuOpen.signal, - span(cls := "sr-only", "Open main menu"), - Icons.outline - .menu() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") - ), - Icons.outline - .x() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") - ), - onClick.preventDefault.mapTo( - !mobileMenuOpen.now() - ) --> mobileMenuOpen.writer - ) - - def navBarLeft = - div( - cls := "flex items-center", - div(cls := "flex-shrink-0", logoImg), - div( - desktopOnly, - div( - cls := "ml-10 flex items-baseline space-x-4", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ) - ) - ) - - def navBarRight = - div( - desktopOnly, - div( - cls := "ml-4 flex items-center md:ml-6", - notificationButton, - userProfile - ) - ) - - def navBarMobile = - div( - cls := "-mr-2 flex", - mobileOnly, - mobileMenuButton - ) - - def navBar = - div( - cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex items-center justify-between h-16", - navBarLeft, - navBarRight, - navBarMobile - ) - ) - - def mobileMenu = - div( - mobileOnly, - cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }, - idAttr := "mobile-menu", - div( - cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ), - mobileProfile - ) - - nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala deleted file mode 100644 index 75339df..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object PageHeader: - def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = - header( - cls := "bg-white shadow-sm", - div( - cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", - h1( - cls := "text-lg leading-6 font-semibold text-gray-900", - Breadcrumbs(actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala deleted file mode 100644 index 5096463..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ /dev/null @@ -1,30 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import fiftyforms.ui.components.tailwind.Loading -import io.laminext.syntax.core.* - -object PageLayout: - case class ViewModel( - navigation: NavigationBar.ViewModel, - content: Option[HtmlElement] - ) - def apply(actionBus: Observer[Action])( - $m: Signal[ViewModel], - mods: Modifier[HtmlElement]* - )(using router: Router[Page]): HtmlElement = - val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) - div( - cls := "h-full flex flex-col", - NavigationBar($m.map(_.navigation)), - child.maybe <-- router.$currentPage.map(_.isRoot) - .switch(None, Some(PageHeader(actionBus))), - main( - cls := "flex-grow-1 overflow-y-auto", - mods, - child <-- $maybeContent.map(_.getOrElse(Loading)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala deleted file mode 100644 index 62d820f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.LinkSupport.* -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo - -object PageLink: - type ViewModel = Page - def apply($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = - a(mods($m, actions), child.text <-- $m.map(_.title)) - - def apply(m: ViewModel, actions: Observer[Action])(using - router: Router[Page] - ): Anchor = apply(Val(m), actions) - - def container($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = a(mods($m, actions)) - - def container(m: ViewModel, actions: Observer[Action])(using - Router[Page] - ): Anchor = container(Val(m), actions) - - private def mods($m: Signal[Page], actions: Observer[Action])(using - router: Router[Page] - ): Modifier[Anchor] = - Seq( - href <-- $m.map(router.absoluteUrlForPage).recover { case _ => - Some("invalid url") - }, - composeEvents(onClick.noKeyMod.preventDefault)( - _.sample($m) - .map(NavigateTo.apply) - ) --> actions - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala deleted file mode 100644 index 87f825a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.pages.dashboard.DashboardPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -class DashboardPageConnector(actionBus: Observer[Action])(using - router: Router[Page] -): - def apply: HtmlElement = - AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala deleted file mode 100644 index e9b0f71..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala +++ /dev/null @@ -1,76 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailKriteriaPage -import pages.detail.components.DukazyKriteria -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -object DetailKriteriaPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailKriteriaPageConnector( - state: DetailKriteriaPageConnector.AppState -)( - $page: Signal[Page.DetailKriteria] -)(using Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => - (p.osobniCislo.value, p.parametr.value, p.kriterium.value) - )((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map( - FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) - ) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, s, $s) => - DetailKriteriaPage($s)(state.actionBus.contramap { - case DukazyKriteria.Add => - NavigateTo( - Page.UpravDukazKriteria( - Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), - Page.Titled(s.parametr.id, Some(s.parametr.nazev)), - Page.Titled(s.kriterium.id, Some(s.kriterium.id)) - ) - ) - }) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): DetailKriteriaPage.ViewModel = - DetailKriteriaPage.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala deleted file mode 100644 index 4d8024f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ /dev/null @@ -1,57 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import pages.detail.DetailPage -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import fiftyforms.ui.components.tailwind.Color - -object DetailPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailPageConnector(state: DetailPageConnector.AppState)( - $page: Signal[Page.Detail] -)(using router: Router[Page]): - val $oscChangeSignal = $page.splitOne(_.osobniCislo.value)((osc, _, _) => osc) - val $pageChangeSignal = - $oscChangeSignal.flatMap(osc => - EventStream.fromSeq(Seq(FetchUserDetails(osc), FetchParameters(osc))) - ) - // TODO: filter the value based on the current osc - // OSC change will fetch new data, but still - // - we need to be sure that what we got is really what we ought to display - // - we want to display stale data accordingly (at least with loading indicator) - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - def apply: HtmlElement = - AppPage(state.actionBus)( - $data.combineWithFn($params)(_ zip _) - .map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: List[Parameter] - ): DetailPage.ViewModel = - DetailPage.ViewModel( - o.toDetailOsoby, - p.map( - _.toParametr(param => - PageLink.container(Page.DetailParametru(o, param), state.actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala deleted file mode 100644 index fdaa7f8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailParametruPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink - -object DetailParametruPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailParametruPageConnector( - state: DetailParametruPageConnector.AppState -)( - $page: Signal[Page.DetailParametru] -)(using router: Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => (p.osobniCislo.value, p.parametr.value))((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map(FetchParameter(_, _)) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - } yield (da, pb) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailParametruPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter - ): DetailParametruPage.ViewModel = - DetailParametruPage.ViewModel( - o.toDetailOsoby, - p.toParametr(p => - PageLink.container(Page.DetailParametru(o, p), state.actionBus) - ), - p.criteria.map( - _.toKriterium { c => - PageLink.container( - Page.DetailKriteria(o, p, c), - state.actionBus - ) - } - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala deleted file mode 100644 index 049aa22..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -case class DirectoryPageConnector( - $input: EventStream[List[UserInfo]], - actionBus: Observer[Action] -)(using router: Router[Page]): - val $data = $input.startWithNone - val $actionSignal = EventStream.fromValue(FetchDirectory) - - def apply: HtmlElement = - AppPage(actionBus)( - $data.split(_ => ())((_, _, s) => - pages.directory.DirectoryPage( - s.map( - _.map( - _.toUserRow(u => - PageLink.container( - Page.Detail(Page.Titled(u.personalNumber)), - actionBus - ) - ) - ) - ) - ) - ), - $actionSignal --> actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala deleted file mode 100644 index 88e7450..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ /dev/null @@ -1,68 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii -import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria -import fiftyforms.ui.components.tailwind.Color - -extension (o: UserInfo) - def toDetailOsoby: DetailOsoby.ViewModel = - DetailOsoby.ViewModel( - o.personalNumber, - o.name, - o.email, - o.phone, - o.img, - o.mainFunction.map(f => - DetailOsoby.Funkce.ViewModel(f.name, f.dept, f.ou) - ), - o.userContracts.headOption.map(c => - DetailOsoby.PracovniPomer.ViewModel(c.rel, c.startDate, c.endDate) - ) - ) - -extension (param: Parameter) - def toParametr(container: Parameter => Anchor): DetailParametru.ViewModel = - DetailParametru.ViewModel( - id = param.id, - nazev = param.name, - popis = param.description, - status = "Nesplněno", - statusColor = Color.red, - a = container(param) - ) - -extension (crit: ParameterCriteria) - def toKriterium( - container: ParameterCriteria => Anchor - ): DetailKriteria.ViewModel = - DetailKriteria.ViewModel( - nazev = crit.criteriumText, - kapitola = crit.chapterId, - bod = crit.itemId, - status = "Nesplněno", - statusColor = Color.red, - splneno = false, - dukazy = Nil, - container = container(crit) - ) - -extension (user: UserInfo) - def toUserRow( - container: UserInfo => HtmlElement = (_: UserInfo) => div() - ): UserRow.ViewModel = - UserRow.ViewModel( - osobniCislo = user.personalNumber.toString, - celeJmeno = user.name, - prijmeni = user.surname, - hlavniFunkce = user.mainFunction.map(_.name), - img = user.img, - container = () => container(user) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala deleted file mode 100644 index 7d5b2c1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.dashboard - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.Page - -object DashboardPage: - - def render: HtmlElement = - div("Dashboard page") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala deleted file mode 100644 index 9ef0698..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala +++ /dev/null @@ -1,31 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailKriteriaPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply($m: Signal[ViewModel])( - action: Observer[DukazyKriteria.Action] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - DukazyKriteria($m.map(_.kriterium.dukazy))(action) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala deleted file mode 100644 index 3b316e1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object DetailPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametry: SeznamParametru.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby($m.map(_.osoba)), - SeznamParametru($m.map(_.parametry)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala deleted file mode 100644 index 17224cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala +++ /dev/null @@ -1,24 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailParametruPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriteria: SeznamKriterii.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby.header($m.map(_.osoba)), - DetailParametru($m.map(_.parametr)), - SeznamKriterii($m.map(_.kriteria)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala deleted file mode 100644 index dc912fa..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ /dev/null @@ -1,107 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb -package app -package pages.detail - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.UpravDukazForm -import fiftyforms.services.files.File - -object UpravDukaz: - - type ThisPage = Page.UpravDukazKriteria - type PageKey = (OsobniCislo, String, String) - - trait State { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def availableFiles: EventStream[List[File]] - def actionBus: Observer[Action] - } - - def keyOfPage(page: ThisPage): PageKey = - (page.osobniCislo.value, page.parametr.value, page.kriterium.value) - - def onChangeAction(key: PageKey): Action = - FetchParameterCriteria( - key._1, - key._2, - key._3, - Page.UpravDukazKriteria(_, _, _) - ) - - class Connector(state: State)($page: Signal[ThisPage])(using Router[Page]): - val $paramChangeSignal = $page.splitOne(keyOfPage)((x, _, _) => x) - val $pageChangeSignal = $paramChangeSignal.map(onChangeAction) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.split(_ => ())((_, s, $s) => - PageComponent( - $s.map(buildModel), - state.availableFiles, - state.actionBus.contramap { - case UpravDukazForm.Cancelled => - NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) - case UpravDukazForm.AvailableFilesRequested => - FetchAvailableFiles(s._1.personalNumber) - } - ) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): PageComponent.ViewModel = - import connectors.* - PageComponent.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) - - object PageComponent: - import components.* - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply( - $m: Signal[ViewModel], - availableFilesStream: EventStream[List[File]], - events: Observer[UpravDukazForm.Event] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - UpravDukazForm(availableFilesStream)(events) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala deleted file mode 100644 index ca311b0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.Icons -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Color - -object DetailKriteria: - case class ViewModel( - nazev: String, - kapitola: String, - bod: String, - status: String, - statusColor: Color, - splneno: Boolean, - dukazy: List[DukazKriteria.ViewModel], - container: HtmlElement = div() - ) { - val id = s"${kapitola}${bod}" - } - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h3( - cls := "text-xl leading-6 font-bold text-gray-900", - child.text <-- $m.map(_.nazev) - ), - h3( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - "Kapitola ", - child.text <-- $m.map(_.kapitola), - " bod ", - child <-- $m.map(_.bod) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala deleted file mode 100644 index ae90290..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala +++ /dev/null @@ -1,100 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Avatar - -object DetailOsoby: - - object Funkce: - case class ViewModel( - nazev: String, - stredisko: String, - voj: String - ) - - def render($m: Signal[ViewModel]): HtmlElement = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.nazev), - span( - cls := "hidden md:inline", - " @ ", - child.text <-- $m.map(_.stredisko), - ", ", - child.text <-- $m.map(_.voj) - ) - ) - - object PracovniPomer: - case class ViewModel( - druh: String, - pocatek: LocalDate, - konec: Option[LocalDate] - ) - - def render($m: Signal[ViewModel]): HtmlElement = - import fiftyforms.ui.components.tailwind.CustomAttrs.datetime - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.druh), - " od ", - time( - datetime <-- $m.map(_.pocatek.toString), - child.text <-- $m.map(_.pocatek.toString) - ) - ) - - case class ViewModel( - osobniCislo: OsobniCislo, - jmeno: String, - email: Option[String], - telefon: Option[String], - img: Option[String], - hlavniFunkce: Option[Funkce.ViewModel], - pracovniPomer: Option[PracovniPomer.ViewModel] - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-4", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(16) - ), - div( - h1( - cls := "text-2xl font-bold text-gray-900", - child.text <-- $m.map(_.jmeno) - ), - child.maybe <-- $m.map(_.hlavniFunkce).split(_ => ())((_, _, d) => - Funkce.render(d) - ), - child.maybe <-- $m.map(_.pracovniPomer).split(_ => ())((_, _, d) => - PracovniPomer.render(d) - ) - ) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-5", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(8) - ), - div( - h1( - cls := "text-lg font-medium text-gray-800", - child.text <-- $m.map(_.jmeno) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala deleted file mode 100644 index e11cb62..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color - -object DetailParametru: - case class ViewModel( - id: String, - nazev: String, - popis: String, - status: String, - statusColor: Color, - a: Anchor - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h2( - cls := "text-2xl leading-6 font-medium text-gray-900", - child.text <-- $m.map(_.nazev) - ), - p( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - child.text <-- $m.map(_.popis) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - h2( - cls := "text-lg leading-6 font-medium text-gray-600", - child.text <-- $m.map(_.nazev) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala deleted file mode 100644 index a00ede8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala +++ /dev/null @@ -1,177 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.CustomAttrs - -object DukazKriteria: - case class Osoba(osobniCislo: String, jmeno: String) - case class Udalost(osoba: Osoba, datum: LocalDate) - case class Dokument( - url: String, - nazev: String, - pridal: Udalost, - odebral: Option[Udalost] - ) - case class ViewModel( - dokumenty: List[Dokument], - autorizoval: Option[Udalost], - platiDo: Option[LocalDate], - poznámka: Option[String] - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-lg", - div( - cls := "px-4 py-5 sm:px-6", - h3( - cls := "text-lg leading-6 font-medium text-gray-900", - """Applicant Information""" - ), - p( - cls := "mt-1 max-w-2xl text-sm text-gray-500", - """Personal details and application.""" - ) - ), - div( - cls := "border-t border-gray-200 px-4 py-5 sm:px-6", - dl( - cls := "grid grid-cols-1 gap-x-4 gap-y-8 sm:grid-cols-2", - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Full name""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Margot Foster""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Application for""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Backend Developer""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Email address""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """margotfoster@example.com""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Salary expectation""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """$120,000""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """About""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Fugiat ipsum ipsum deserunt culpa aute sint do nostrud anim incididunt cillum culpa consequat. Excepteur qui ipsum aliquip consequat sint. Sit id mollit nulla mollit nostrud in ea officia proident. Irure nostrud pariatur mollit ad adipisicing reprehenderit deserunt qui eu.""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """Attachments""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - ul( - role := "list", - cls := "border border-gray-200 rounded-md divide-y divide-gray-200", - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """resume_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ), - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """coverletter_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala deleted file mode 100644 index 22f134f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala +++ /dev/null @@ -1,35 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object DukazyKriteria: - sealed trait Action - case object Add extends Action - - type ViewModel = List[DukazKriteria.ViewModel] - - def apply($m: Signal[ViewModel])(actions: Observer[Action]): HtmlElement = - div( - child.maybe <-- $m.map(m => - if (m.isEmpty) then Some(prazdnyDukaz(actions)) - else None - ), - children <-- $m.map(_.zipWithIndex).split(_._2)((_, _, $s) => - DukazKriteria($s.map(_._1)) - ) - ) - - private def prazdnyDukaz(actions: Observer[Action]): HtmlElement = - button( - tpe := "button", - onClick.preventDefault.mapTo(Add) --> actions, - cls := "relative block w-full border-2 border-gray-300 border-dashed rounded-lg p-12 text-center hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - Icons.outline - .`document-add`(12) - .amend(svg.cls := "mx-auto text-gray-400"), - span( - cls := "mt-2 block text-sm font-medium text-gray-900", - "Přidat důkaz" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala deleted file mode 100644 index f628697..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.list.StackedList -import fiftyforms.ui.components.tailwind.list.ListRow -import fiftyforms.ui.components.tailwind.list.RowTag -import fiftyforms.ui.components.tailwind.list.RowNext -import java.time.LocalDate - -object SeznamKriterii: - type ViewModel = List[DetailKriteria.ViewModel] - - private val kritList = new StackedList[DetailKriteria.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - kritList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = - p(cls := "text-sm text-gray-500", s"${i.kapitola}${i.bod}"), - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.container - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala deleted file mode 100644 index 2bfe4bc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.list.{ - StackedList, - ListRow, - RowTag, - PropList, - IconText, - RowNext -} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.LinkSupport.* - -object SeznamParametru: - type ViewModel = List[DetailParametru.ViewModel] - - private val parametrList = new StackedList[DetailParametru.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - parametrList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = emptyNode, - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.a - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala deleted file mode 100644 index f048e9d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ /dev/null @@ -1,101 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.form.* -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement -import fiftyforms.services.files.components.tailwind.FilePicker -import cz.e_bs.cmi.mdr.pdb.frontend.AutorizujDukaz -import cz.e_bs.cmi.mdr.pdb.frontend.DocumentRef -import fiftyforms.services.files.File - -object UpravDukazForm: - sealed trait Event - case object Cancelled extends Event - case object AvailableFilesRequested extends Event - def apply(availableFilesStream: EventStream[List[File]])( - updates: Observer[Event] - ): HtmlElement = - val (filesStream, filesObserver) = - EventStream.withObserver[FilePicker.Event] - val files = Var[List[File]](Nil) - def submitButtons: HtmlElement = - div( - cls := "pt-5", - div( - cls := "flex justify-end", - button( - tpe := "button", - cls := "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zrušit", - onClick.mapTo(Cancelled) --> updates - ), - button( - tpe := "submit", - cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - disabled <-- files.signal.map(_.isEmpty), - "Autorizovat" - ) - ) - ) - - div( - filesStream.collect { case FilePicker.SelectionUpdated(files) => - files.to(List) - } --> files.writer, - filesStream.collect { case FilePicker.AvailableFilesRequested => - AvailableFilesRequested - } --> updates, - cls := "bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6", - Form( - Form.Body( - Form.Section( - FormHeader( - FormHeader.ViewModel( - "Doložení kritéria", - "Sestavte doklady poskytující důkaz kritéria a potvrďte odesláním formuláře. Případné limitace či jiné relevantní údaje vepište do pole Komentář." - ) - ), - FormFields( - FormRow( - "dokumenty", - "Dokumenty", - FilePicker(files.signal, availableFilesStream)(filesObserver) - .amend(idAttr := "dokumenty", cls("max-w-lg")) - ).toHtml, - FormRow( - "platnost", - "Platnost", - input( - idAttr := "platnost", - name := "platnost", - tpe := "date", - autoComplete := "date", - cls := "block max-w-lg w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" - ) - ).toHtml, - FormRow( - "komentar", - "Komentář", - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - textArea( - idAttr := "komentar", - name := "about", - rows := 3, - cls := "max-w-lg shadow-sm block w-full focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border border-gray-300 rounded-md" - ), - p( - cls := "mt-2 text-sm text-gray-500", - "Doplňte prosím potřebné informace související s doložením kritéria, včetně případných limitací." - ) - ) - ).toHtml - ) - ) - ), - submitButtons - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala deleted file mode 100644 index 41033f6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -import components._ - -object DirectoryPage: - - type ViewModel = List[UserRow.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - val (actionsStream, actionObserver) = - EventStream.withObserver[SearchForm.Action] - val $filter = actionsStream - .collect { - case SearchForm.ClearFilter => None - case SearchForm.SetFilter(t) => Some(t) - } - .startWith(None) - val byLetter = for { - d <- $m - f <- $filter - } yield for { - (letter, users) <- d - .filter { user => - f match - case None => true - case Some(t) => user.search.contains(t) - } - .groupBy(_.prijmeni.head) - .to(List) - .sortBy(_._1) - } yield (letter.toString, users.sortBy(_.prijmeni)) - - div( - cls := "h-full max-w-7xl mx-auto order-first flex flex-col flex-shrink-0", - SearchForm(actionObserver), - Directory(byLetter) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala deleted file mode 100644 index 73f915d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -object Directory: - - object Header: - type ViewModel = String - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500", - h3(child.text <-- $m) - ) - - object UserList: - type ViewModel = List[UserRow.ViewModel] - def apply($m: Signal[ViewModel]): HtmlElement = - ul( - role := "list", - cls := "relative z-0 divide-y divide-gray-200", - children <-- $m.split(_.osobniCislo)((_, _, s) => UserRow(s)) - ) - - type ViewModel = List[(String, List[UserRow.ViewModel])] - def apply($m: Signal[ViewModel]): HtmlElement = - nav( - cls := "flex-1 min-h-0 overflow-y-auto", - aria.label := "Directory", - children <-- $m.split(_._1)((_, _, s) => - div( - cls := "relative", - Header(s.map(_._1)), - UserList(s.map(_._2)) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala deleted file mode 100644 index e182910..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala +++ /dev/null @@ -1,54 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object SearchForm: - sealed trait Action - case object SubmitFilter extends Action - sealed trait FilterAction extends Action - case object ClearFilter extends FilterAction - case class SetFilter(value: String) extends FilterAction - - def apply(actions: Observer[Action]): HtmlElement = - div( - cls := "px-6 pt-4 pb-4", - form( - cls := "flex space-x-4", - onSubmit.mapTo(SubmitFilter) --> actions, - div( - cls := "flex-1 min-w-0", - label( - forId := "search", - cls := "sr-only", - "Hledat" - ), - div( - cls := "relative rounded-md shadow-sm", - div( - cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", - Icons.solid.search().amend(svg.cls := "text-gray-400") - ), - input( - tpe := "search", - name := "search", - idAttr := "search", - cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", - placeholder := "Hledat", - composeEvents(onInput.mapToValue.setAsValue.map(SetFilter(_)))( - _.throttle(500) - ) --> actions - ) - ) - ), - button( - tpe := "submit", - cls := "inline-flex justify-center px-3.5 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500", - Icons.solid.filter().amend(svg.cls := "text-gray-400"), - span( - cls := "sr-only", - "Hledat" - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala deleted file mode 100644 index 82f7723..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala +++ /dev/null @@ -1,52 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.airstream.core.Signal -import fiftyforms.ui.components.tailwind.Avatar - -object UserRow: - case class ViewModel( - osobniCislo: String, - celeJmeno: String, - prijmeni: String, - hlavniFunkce: Option[String], - img: Option[String], - container: () => HtmlElement = () => div() - ) { - val search = prijmeni.toLowerCase - } - - def apply($m: Signal[ViewModel]): HtmlElement = - inline def avatarImage = - Avatar($m.map(_.img)).avatarImage(10) - - li( - div( - cls := "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500", - div( - cls := "flex-shrink-0", - child <-- avatarImage - ), - div( - cls := "flex-1 min-w-0", - child <-- $m.map { o => - o.container() - .amend( - cls := "focus:outline-none", - span( - cls := "absolute inset-0", - aria.hidden := true - ), - p( - cls := "text-sm font-medium text-gray-900", - o.celeJmeno - ), - p( - cls := "text-sm text-gray-500 truncate", - o.hlavniFunkce - ) - ) - } - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala deleted file mode 100644 index 467c5f5..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala +++ /dev/null @@ -1,99 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router - -object ErrorPage: - case class ViewModel( - homePage: Page, - errorName: String, - title: String, - subTitle: String - ) - - def apply(m: ViewModel, actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - val ViewModel(homePage, errorName, title, subTitle) = m - div( - cls := "min-h-full pt-16 pb-12 flex flex-col bg-white", - main( - cls := "flex-grow flex flex-col justify-center max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex-shrink-0 flex justify-center", - a( - href := "/", - cls := "inline-flex", - span( - cls := "sr-only", - """Workflow""" - ), - img( - cls := "h-12 w-auto", - src := "https://tailwindui.com/img/logos/workflow-mark.svg?color=indigo&shade=600", - alt := "" - ) - ) - ), - div( - cls := "py-16", - div( - cls := "text-center", - p( - cls := "text-sm font-semibold text-indigo-600 uppercase tracking-wide", - errorName - ), - h1( - cls := "mt-2 text-4xl font-extrabold text-gray-900 tracking-tight sm:text-5xl", - title - ), - p( - cls := "mt-2 text-base text-gray-500", - subTitle - ), - div( - cls := "mt-6", - PageLink - .container(homePage, actionBus) - .amend( - cls := "text-base font-medium text-indigo-600 hover:text-indigo-500", - """Go back home""" - ) - ) - ) - ) - ), - footer( - cls := "flex-shrink-0 max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", - nav( - cls := "flex justify-center space-x-4", - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Contact Support""" - ), - span( - cls := "inline-block border-l border-gray-300", - aria.hidden := true - ), - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Status""" - ), - span( - cls := "inline-block border-l border-gray-300", - aria.hidden := true - ), - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Twitter""" - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala deleted file mode 100644 index bd9baec..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action - -object NotFoundPage: - def apply(homePage: Page, url: String, actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - ErrorPage( - ErrorPage.ViewModel( - homePage, - "404 error", - "Page not found.", - s"Sorry, but page $url doesn't exist." - ), - actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala deleted file mode 100644 index 476fe3c..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action - -object UnhandledErrorPage: - - case class ViewModel( - homePage: Page, - errorName: Option[String], - errorMessage: Option[String] - ) - - def apply(m: ViewModel, actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - ErrorPage( - ErrorPage.ViewModel( - m.homePage, - "Unexpected error occurred", - m.errorName.getOrElse( - "Uh oh!" - ), // TODO: translations, better text than uh oh - m.errorMessage.getOrElse( - "This wasn't supposed to happen! Please try again." - ) - ), - actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala deleted file mode 100644 index 25f059f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala +++ /dev/null @@ -1,6 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.services - -import com.raquo.airstream.core.Observer - -trait DataFetcher[K, A]: - def fetch(id: K, sink: Observer[A]): Unit diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala deleted file mode 100644 index a469830..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala +++ /dev/null @@ -1,120 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package state - -import com.raquo.airstream.core.EventStream -import cz.e_bs.cmi.mdr.pdb.{UserInfo, OsobniCislo} -import com.raquo.airstream.core.Observer -import scala.scalajs.js -import scala.scalajs.js.JSON -import zio.json._ -import com.raquo.airstream.eventbus.EventBus -import com.raquo.airstream.ownership.Owner -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.UserFunction -import cz.e_bs.cmi.mdr.pdb.UserContract -import fiftyforms.services.files.File - -trait AppState - extends connectors.DetailPageConnector.AppState - with connectors.DetailParametruPageConnector.AppState - with connectors.DetailKriteriaPageConnector.AppState - with pages.detail.UpravDukaz.State: - def users: EventStream[List[UserInfo]] - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - -class MockAppState(implicit owner: Owner, router: Router[Page]) - extends AppState: - - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonDecoder[UserFunction] = DeriveJsonDecoder.gen - given JsonDecoder[UserContract] = DeriveJsonDecoder.gen - given JsonDecoder[UserInfo] = DeriveJsonDecoder.gen - - given JsonDecoder[ParameterCriteria] = DeriveJsonDecoder.gen - given JsonDecoder[Parameter] = DeriveJsonDecoder.gen - - private val actions = EventBus[Action]() - private val (parametersStream, pushParameters) = - EventStream.withCallback[List[Parameter]] - private val (usersStream, pushUsers) = - EventStream.withCallback[List[UserInfo]] - private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] - private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] - - private val mockData: List[UserInfo] = - mockUsers - .asInstanceOf[js.Dictionary[js.Object]] - .values - // TODO: is there a more efficient way to parse from JS object directly? - .map { o => - val parsed = JSON.stringify(o).fromJson[UserInfo] - parsed.left.foreach(org.scalajs.dom.console.log(_)) - parsed - } - .collect { case Right(u) => - u - } - .toList - - private val mockParameters: List[Parameter] = - pdbParams - .asInstanceOf[js.Dictionary[js.Object]] - .values - .map(o => JSON.stringify(o).fromJson[Parameter]) - .collect { case Right(p) => p } - .toList - - // TODO: Extract to separate event handler - actions.events.foreach { - case FetchDirectory => pushUsers(mockData) - case FetchUserDetails(osc) => - mockData.find(_.personalNumber == osc).foreach { o => - pushDetails(o) - router.replaceState(Page.Detail(o)) - } - case FetchParameters(osc) => - pushParameters(mockParameters) - case FetchParameter(osc, paramId) => - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(Page.DetailParametru(o, p)) - case FetchParameterCriteria(osc, paramId, critId, page) => - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - c <- p.criteria.find(_.id == critId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(page(o, p, c)) - case NavigateTo(page) => router.pushState(page) - case FetchAvailableFiles(osc) => - pushFiles( - List( - File("https://tc163.cmi.cz/here", "Example file") - ) - ) - } - - override def users: EventStream[List[UserInfo]] = - usersStream.debugWithName("users") - - override def details: EventStream[UserInfo] = - detailsStream.debugWithName("details") - - override def parameters: EventStream[List[Parameter]] = - parametersStream.debugWithName("parameters") - - override def availableFiles: EventStream[List[File]] = - filesStream.debugWithName("available files") - - override def actionBus: Observer[Action] = - actions.writer.debugWithName("actions writer") diff --git a/app/src/main/scala/mdr/pdb/app/Main.scala b/app/src/main/scala/mdr/pdb/app/Main.scala new file mode 100644 index 0000000..081b4ee --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/Main.scala @@ -0,0 +1,103 @@ +package mdr.pdb.app + +import scala.scalajs.js.annotation.JSExportTopLevel +import scala.scalajs.js.annotation.JSExport +import scala.scalajs.js.annotation.JSImport +import scala.scalajs.js +import org.scalajs.dom +import com.raquo.laminar.api.L.{*, given} +import scala.scalajs.js.Date +import com.raquo.waypoint.Router +import com.raquo.waypoint.SplitRender +import mdr.pdb.app.services.DataFetcher +import scala.scalajs.js.JSON +import zio.json._ +import mdr.pdb.UserInfo +import mdr.pdb.app.state.AppState + +@js.native +@JSImport("stylesheets/main.css", JSImport.Namespace) +object Css extends js.Any + +@js.native +@JSImport("data/users.json", JSImport.Default) +object mockUsers extends js.Object + +@js.native +@JSImport("params/pdb-params.json", JSImport.Default) +object pdbParams extends js.Object + +@JSExportTopLevel("app") +object Main: + + @JSExport + def main(args: Array[String]): Unit = + import Routes.given + onLoad { + setupAirstream() + val appContainer = dom.document.querySelector("#app") + val _ = + render( + appContainer, + renderPage(state.MockAppState(using unsafeWindowOwner, router)) + ) + } + + private def onLoad(f: => Unit): Unit = + documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) + + private def setupAirstream()(using router: Router[Page]): Unit = + AirstreamError.registerUnhandledErrorCallback(err => + router.forcePage( + Page.UnhandledError( + Some(err.getClass.getName), // TODO: Fill only in dev mode + Some(err.getMessage) + ) + ) + ) + + def renderPage(state: AppState)(using + router: Router[Page] + ): HtmlElement = + val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) + .collectSignal[Page.Detail]( + connectors + .DetailPageConnector(state)(_) + .apply + ) + .collectSignal[Page.DetailParametru]( + connectors + .DetailParametruPageConnector(state)(_) + .apply + ) + .collectSignal[Page.DetailKriteria]( + connectors + .DetailKriteriaPageConnector(state)(_) + .apply + ) + .collectSignal[Page.UpravDukazKriteria]( + pages.detail.UpravDukaz.Connector(state)(_).apply + ) + .collectStatic(Page.Dashboard)( + connectors.DashboardPageConnector(state.actionBus).apply + ) + .collect[Page.NotFound](pg => + pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) + ) + .collect[Page.UnhandledError](pg => + pages.errors + .UnhandledErrorPage( + pages.errors.UnhandledErrorPage + .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), + state.actionBus + ) + ) + .collectStatic(Page.Directory)( + connectors + .DirectoryPageConnector(state.users, state.actionBus) + .apply + ) + div(cls := "h-full", child <-- pageSplitter.$view) + + // Pull in the stylesheet + val css: Css.type = Css diff --git a/app/src/main/scala/mdr/pdb/app/Routes.scala b/app/src/main/scala/mdr/pdb/app/Routes.scala new file mode 100644 index 0000000..2cc860c --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/Routes.scala @@ -0,0 +1,198 @@ +package mdr.pdb.app + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.* +import org.scalajs.dom +import zio.json.{*, given} +import mdr.pdb.OsobniCislo + +import scala.scalajs.js +import mdr.pdb.UserInfo +import mdr.pdb.Parameter +import mdr.pdb.ParameterCriteria +import mdr.pdb.app.Page.Titled + +// enum is not working with Waypoints' SplitRender collectStatic +sealed abstract class Page( + val id: String, + val title: String, + val parent: Option[Page] +) { + val path: Vector[Page] = + parent match + case None => Vector(this) + case Some(p) => p.path :+ this + + val isRoot: Boolean = parent.isEmpty +} + +object Page: + + case class Titled[V](value: V, title: Option[String] = None): + val show: String = title.getOrElse(value.toString) + + case object Directory extends Page("directory", "Adresář", None) + + case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) + + case class Detail(osobniCislo: Titled[OsobniCislo]) + extends Page("user", osobniCislo.show, Some(Directory)) + + object Detail { + def apply(o: UserInfo): Detail = Detail( + Titled(o.personalNumber, Some(o.name)) + ) + } + + case class DetailParametru( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String] + ) extends Page( + "parameter", + parametr.show, + Some(Detail(osobniCislo)) + ) + + object DetailParametru { + def apply(o: UserInfo, p: Parameter): DetailParametru = + DetailParametru( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)) + ) + } + + case class DetailKriteria( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String], + kriterium: Titled[String] + ) extends Page( + "criteria", + kriterium.show, + Some(DetailParametru(osobniCislo, parametr)) + ) + + object DetailKriteria { + def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = + DetailKriteria( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)), + Titled(k.id, Some(k.id)) + ) + } + + case class UpravDukazKriteria( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String], + kriterium: Titled[String] + ) extends Page( + "addProof", + "Důkaz", + Some(DetailKriteria(osobniCislo, parametr, kriterium)) + ) + + object UpravDukazKriteria { + def apply( + o: UserInfo, + p: Parameter, + k: ParameterCriteria + ): UpravDukazKriteria = + UpravDukazKriteria( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)), + Titled(k.id, Some(k.id)) + ) + } + + case class NotFound(url: String) extends Page("404", "404", Some(Directory)) + + case class UnhandledError( + errorName: Option[String], + errorMessage: Option[String] + ) extends Page("500", "Unexpected error", Some(Directory)) + +object Routes: + given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) + given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) + given [V: JsonEncoder]: JsonEncoder[Titled[V]] = + DeriveJsonEncoder.gen[Titled[V]] + given [V: JsonDecoder]: JsonDecoder[Titled[V]] = + DeriveJsonDecoder.gen[Titled[V]] + given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] + given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] + + val base = + js.`import`.meta.env.BASE_URL + .asInstanceOf[String] + "app" + + val homePage: Page = Page.Directory + + given router: Router[Page] = Router[Page]( + routes = List( + Route.static(homePage, root / endOfSegments, basePath = base), + Route.static( + Page.Dashboard, + root / "dashboard" / endOfSegments, + basePath = base + ), + Route[Page.Detail, String]( + encode = _.osobniCislo.value.toString, + decode = osc => Page.Detail(Titled(OsobniCislo(osc))), + root / "osoba" / segment[String] / endOfSegments, + basePath = base + ), + Route[Page.DetailParametru, (String, String)]( + encode = p => (p.osobniCislo.value.toString, p.parametr.value), + decode = + p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / endOfSegments, + basePath = base + ), + Route[Page.DetailKriteria, (String, String, String)]( + encode = p => + ( + p.osobniCislo.value.toString, + p.parametr.value, + p.kriterium.value.replaceAll("\\.", "--") + ), + decode = p => + Page.DetailKriteria( + Titled(OsobniCislo(p._1)), + Titled(p._2), + Titled(p._3.replaceAll("--", ".")) + ), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / "kriterium" / segment[String] / endOfSegments, + basePath = base + ), + Route[Page.UpravDukazKriteria, (String, String, String)]( + encode = p => + ( + p.osobniCislo.value.toString, + p.parametr.value, + p.kriterium.value.replaceAll("\\.", "--") + ), + decode = p => + Page.UpravDukazKriteria( + Titled(OsobniCislo(p._1)), + Titled(p._2), + Titled(p._3.replaceAll("--", ".")) + ), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / "kriterium" / segment[String] / "edit" / endOfSegments, + basePath = base + ) + ), + serializePage = _.toJson, + deserializePage = _.fromJson[Page] + .fold(s => throw IllegalStateException(s), identity), + getPageTitle = _.title, + routeFallback = url => Page.NotFound(url), + deserializeFallback = _ => Page.Dashboard + )( + $popStateEvent = windowEvents.onPopState, + owner = unsafeWindowOwner + ) diff --git a/app/src/main/scala/mdr/pdb/app/actions.scala b/app/src/main/scala/mdr/pdb/app/actions.scala new file mode 100644 index 0000000..d078b3a --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/actions.scala @@ -0,0 +1,21 @@ +package mdr.pdb.app + +import mdr.pdb.OsobniCislo +import mdr.pdb.UserInfo +import mdr.pdb.Parameter +import mdr.pdb.ParameterCriteria + +sealed trait Action + +case object FetchDirectory extends Action +case class FetchUserDetails(osc: OsobniCislo) extends Action +case class FetchParameters(osc: OsobniCislo) extends Action +case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action +case class FetchParameterCriteria( + osc: OsobniCislo, + paramId: String, + critId: String, + page: (UserInfo, Parameter, ParameterCriteria) => Page +) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action +case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/mdr/pdb/app/components/AppPage.scala new file mode 100644 index 0000000..130c817 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/AppPage.scala @@ -0,0 +1,82 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.Page +import mdr.pdb.{UserProfile, UserInfo, OsobniCislo} +import com.raquo.waypoint.Router +import mdr.pdb.app.Action +import mdr.pdb.app.NavigateTo +import mdr.pdb.UserFunction + +object AppPage: + // TODO: pages by logged in user + val pages: List[Page] = List(Page.Directory, Page.Dashboard) + + import NavigationBar.{Logo, MenuItem} + + val logo = Logo( + "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", + "Workflow" + ) + + // TODO: menu items by user profile + val userMenu = + List( + MenuItem("Your Profile"), + MenuItem("Settings"), + MenuItem("Sign out") + ) + + // TODO: load user profile + val $userProfile = Var( + UserProfile( + "tom", + UserInfo( + OsobniCislo("1031"), + "tom", + "Tom", + "Cook", + None, + None, + Some("tom@example.com"), + Some("+420 222 866 180"), + Some( + UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") + ), + Nil, + None + ) + ) + ) + + val $userInfo = $userProfile.signal.map(_.userInfo) + + type ViewModel = Option[HtmlElement] + def apply( + actionBus: Observer[Action] + )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using + router: Router[Page] + ): HtmlElement = + PageLayout(actionBus)( + $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => + PageLayout.ViewModel( + NavigationBar.ViewModel( + u, + pages.map(p => + NavigationBar.Link( + () => + PageLink( + p, + actionBus + ), + p == cp + ) + ), + userMenu, + logo + ), + c + ) + ), + mods + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala new file mode 100644 index 0000000..9375398 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala @@ -0,0 +1,143 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.tailwind.list.IconText.ViewModel +import fiftyforms.ui.components.tailwind.Icons +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.Page + +import com.raquo.laminar.api.L.{*, given} +import io.laminext.syntax.core.{*, given} +import mdr.pdb.app.Action + +object Breadcrumbs: + + private def slash = + import svg.{*, given} + svg( + cls := "flex-shrink-0 h-5 w-5 text-gray-300", + xmlns := "http://www.w3.org/2000/svg", + fill := "currentColor", + viewBox := "0 0 20 20", + ariaHidden := true, + path( + d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" + ) + ) + + def backIcon = + Icons.solid + .`arrow-narrow-left`() + .amend( + svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" + ) + + object Link: + case class ViewModel( + page: Page, + icon: Option[SvgElement], + extraClasses: String, + text: String, + textClasses: Option[String] = None + ) + + val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" + + def shortHome(p: Page) = ViewModel( + p, + Some(backIcon), + "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", + "Zpět na úvodní stránku", + None + ) + + def fullHome(p: Page) = ViewModel( + p, + Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), + "text-gray-400 hover:text-gray-500", + "Domů", + Some("sr-only") + ) + + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + PageLink + .container($m.map(_.page), actionBus) + .amend( + cls <-- $m.map(_.extraClasses), + child.maybe <-- $m.map(_.icon), + span( + cls <-- $m.map(_.textClasses.getOrElse("")), + child.text <-- $m.map(_.text) + ) + ) + + object FullBreadcrumbs: + type ViewModel = Page + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + ol( + role := "list", + cls := "flex items-center space-x-4", + children <-- $m.map(_.path).split(_.id)((_, _, $p) => + li( + div( + cls := "flex items-center", + child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), + Link( + $p.map(p => + if (p.isRoot) then Link.fullHome(p) + else + Link.ViewModel( + p, + None, + s"ml-4 max-w-xs truncate ${Link.baseClasses}", + p.title + ) + ), + actionBus + ) + ) + ) + ) + ) + + object ShortBreadcrumbs: + type ViewModel = Page + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + Link( + $m.map { p => + val target = p.parent.getOrElse(p) + if target.isRoot then Link.shortHome(target) + else + Link.ViewModel( + target, + Some(backIcon), + "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", + s"Zpět na ${target.title}" + ) + }, + actionBus + ) + + def apply(actionBus: Observer[Action])(using + router: Router[Page] + ): HtmlElement = + nav( + cls := "flex", + aria.label := "Breadcrumb", + div( + cls := "flex sm:hidden", + ShortBreadcrumbs(router.$currentPage, actionBus) + ), + div( + cls := "hidden sm:block", + FullBreadcrumbs(router.$currentPage, actionBus) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala new file mode 100644 index 0000000..4bb8b8d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala @@ -0,0 +1,242 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent +import fiftyforms.ui.components.tailwind.Icons +import fiftyforms.ui.components.tailwind.Avatar +import mdr.pdb.UserInfo +import io.laminext.syntax.core.* + +object NavigationBar: + + case class Logo(img: String, name: String) + case class Link(a: () => Anchor, active: Boolean) + case class MenuItem(title: String) + + case class ViewModel( + userInfo: UserInfo, + pages: List[Link], + userMenu: List[MenuItem], + logo: Logo + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + val $userInfo = $m.map(_.userInfo) + val mobileMenuOpen = Var(false) + + val desktopOnly = cls("hidden md:block") + val mobileOnly = cls("md:hidden") + + inline def avatarImage(size: Int = 8) = + Avatar($userInfo.map(_.img)).avatarImage(size) + + def notificationButton = button( + tpe := "button", + cls := List( + "bg-indigo-600", + "focus:outline-none", + "focus:ring-2", + "focus:ring-offset-2", + "focus:ring-offset-indigo-600", + "focus:ring-white", + "hover:text-white", + "p-1", + "rounded-full", + "text-indigo-200" + ), + span(cls := "sr-only", "View notifications"), + Icons.outline.bell() + ) + + def userProfile: HtmlElement = + val menuOpen = Var(false) + + def menuItem(item: MenuItem, idx: Int): HtmlElement = + a( + href := "#", + cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", + role := "menuitem", + tabIndex := -1, + idAttr := s"user-menu-item-$idx", + item.title + ) + + div( + cls := "ml-3 relative", + div( + button( + tpe := "button", + cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", + idAttr := "user-menu-button", + aria.expanded <-- menuOpen.signal, + aria.hasPopup := true, + span(cls := "sr-only", "Open user menu"), + child <-- avatarImage(), + onClick.preventDefault.mapTo( + !menuOpen.now() + ) --> menuOpen.writer + ) + ), + /* + * */ + div( + cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", + cls <-- menuOpen.signal.map { o => + if (o) "md:block" else "md:hidden" + }, + role := "menu", + aria.orientation := "vertical", + aria.labelledBy := "user-menu-button", + tabIndex := -1, + // : keyboard navigation + children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) + ) + ) + + def mobileProfile = + def menuItem(item: MenuItem): HtmlElement = + a( + href := "#", + cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", + item.title + ) + + div( + cls := "pt-4 pb-3 border-t border-indigo-700", + div( + cls := "flex items-center px-5", + div( + cls := "flex-shrink-0", + child <-- avatarImage(10) + ), + div( + cls := "ml-3", + div( + cls := "text-base font-medium text-white", + child.text <-- $userInfo.map(_.name) + ), + child.maybe <-- $userInfo.map( + _.email.map(e => + div( + cls := "text-sm font-medium text-indigo-300", + e + ) + ) + ) + ), + notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) + ), + div( + cls := "mt-3 px-2 space-y-1", + children <-- $m.map(_.userMenu.map(menuItem)) + ) + ) + + def pageLink(page: Link): Anchor = + page + .a() + .amend( + cls := "text-white px-3 py-2 rounded-md text-sm font-medium", + cls := Seq( + "bg-indigo-700" -> page.active, + "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active + ), + ariaCurrent := (if page.active then "page" else "false") + ) + + def logoImg: Image = + img( + cls := "h-8 w-8", + src <-- $m.map(_.logo.img), + alt <-- $m.map(_.logo.name) + ) + + def mobileMenuButton = button( + tpe := "button", + cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", + aria.controls := "mobile-menu", + aria.expanded <-- mobileMenuOpen.signal, + span(cls := "sr-only", "Open main menu"), + Icons.outline + .menu() + .amend( + svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") + ), + Icons.outline + .x() + .amend( + svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") + ), + onClick.preventDefault.mapTo( + !mobileMenuOpen.now() + ) --> mobileMenuOpen.writer + ) + + def navBarLeft = + div( + cls := "flex items-center", + div(cls := "flex-shrink-0", logoImg), + div( + desktopOnly, + div( + cls := "ml-10 flex items-baseline space-x-4", + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) + ) + ) + ) + + def navBarRight = + div( + desktopOnly, + div( + cls := "ml-4 flex items-center md:ml-6", + notificationButton, + userProfile + ) + ) + + def navBarMobile = + div( + cls := "-mr-2 flex", + mobileOnly, + mobileMenuButton + ) + + def navBar = + div( + cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", + div( + cls := "flex items-center justify-between h-16", + navBarLeft, + navBarRight, + navBarMobile + ) + ) + + def mobileMenu = + div( + mobileOnly, + cls <-- mobileMenuOpen.signal.map { o => + if (o) "block" else "hidden" + }, + idAttr := "mobile-menu", + div( + cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) + ), + mobileProfile + ) + + nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala new file mode 100644 index 0000000..c4cb10d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala @@ -0,0 +1,19 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.Page +import com.raquo.waypoint.Router +import mdr.pdb.app.Action + +object PageHeader: + def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = + header( + cls := "bg-white shadow-sm", + div( + cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", + h1( + cls := "text-lg leading-6 font-semibold text-gray-900", + Breadcrumbs(actionBus) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala new file mode 100644 index 0000000..aae4539 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala @@ -0,0 +1,30 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.Page +import mdr.pdb.app.Action +import fiftyforms.ui.components.tailwind.Loading +import io.laminext.syntax.core.* + +object PageLayout: + case class ViewModel( + navigation: NavigationBar.ViewModel, + content: Option[HtmlElement] + ) + def apply(actionBus: Observer[Action])( + $m: Signal[ViewModel], + mods: Modifier[HtmlElement]* + )(using router: Router[Page]): HtmlElement = + val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) + div( + cls := "h-full flex flex-col", + NavigationBar($m.map(_.navigation)), + child.maybe <-- router.$currentPage.map(_.isRoot) + .switch(None, Some(PageHeader(actionBus))), + main( + cls := "flex-grow-1 overflow-y-auto", + mods, + child <-- $maybeContent.map(_.getOrElse(Loading)) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/mdr/pdb/app/components/PageLink.scala new file mode 100644 index 0000000..e6e7052 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageLink.scala @@ -0,0 +1,40 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.LinkSupport.* +import mdr.pdb.app.Page +import mdr.pdb.app.Action +import com.raquo.waypoint.Router +import mdr.pdb.app.NavigateTo + +object PageLink: + type ViewModel = Page + def apply($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = + a(mods($m, actions), child.text <-- $m.map(_.title)) + + def apply(m: ViewModel, actions: Observer[Action])(using + router: Router[Page] + ): Anchor = apply(Val(m), actions) + + def container($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = a(mods($m, actions)) + + def container(m: ViewModel, actions: Observer[Action])(using + Router[Page] + ): Anchor = container(Val(m), actions) + + private def mods($m: Signal[Page], actions: Observer[Action])(using + router: Router[Page] + ): Modifier[Anchor] = + Seq( + href <-- $m.map(router.absoluteUrlForPage).recover { case _ => + Some("invalid url") + }, + composeEvents(onClick.noKeyMod.preventDefault)( + _.sample($m) + .map(NavigateTo.apply) + ) --> actions + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala new file mode 100644 index 0000000..7682772 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala @@ -0,0 +1,13 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.pages.dashboard.DashboardPage +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage + +class DashboardPageConnector(actionBus: Observer[Action])(using + router: Router[Page] +): + def apply: HtmlElement = + AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala new file mode 100644 index 0000000..76b9133 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala @@ -0,0 +1,76 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.Parameter +import mdr.pdb.UserInfo +import mdr.pdb.app.pages.detail.DetailParametruPage +import pages.detail.DetailKriteriaPage +import pages.detail.components.DukazyKriteria +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.ParameterCriteria + +object DetailKriteriaPageConnector { + trait AppState { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + } +} + +case class DetailKriteriaPageConnector( + state: DetailKriteriaPageConnector.AppState +)( + $page: Signal[Page.DetailKriteria] +)(using Router[Page]): + val $paramChangeSignal = + $page.splitOne(p => + (p.osobniCislo.value, p.parametr.value, p.kriterium.value) + )((x, _, _) => x) + val $pageChangeSignal = + $paramChangeSignal.map( + FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) + ) + + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + val $merged = + $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => + for { + da <- d + pa <- p + pb <- pa.find(_.id == pc._2) + ka <- pb.criteria.find(_.id == pc._3) + } yield (da, pb, ka) + ) + + def apply: HtmlElement = + AppPage(state.actionBus)( + $merged.map(_.map(buildModel)) + .split(_ => ())((_, s, $s) => + DetailKriteriaPage($s)(state.actionBus.contramap { + case DukazyKriteria.Add => + NavigateTo( + Page.UpravDukazKriteria( + Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), + Page.Titled(s.parametr.id, Some(s.parametr.nazev)), + Page.Titled(s.kriterium.id, Some(s.kriterium.id)) + ) + ) + }) + ), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: Parameter, + k: ParameterCriteria + ): DetailKriteriaPage.ViewModel = + DetailKriteriaPage.ViewModel( + o.toDetailOsoby, + p.toParametr(_ => a()), + k.toKriterium(_ => a()) + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DetailPageConnector.scala new file mode 100644 index 0000000..df3a6b7 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DetailPageConnector.scala @@ -0,0 +1,57 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.UserInfo +import pages.detail.DetailPage +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.pages.detail.components.DetailOsoby +import mdr.pdb.Parameter +import mdr.pdb.app.pages.detail.components.SeznamParametru +import fiftyforms.ui.components.tailwind.Color + +object DetailPageConnector { + trait AppState { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + } +} + +case class DetailPageConnector(state: DetailPageConnector.AppState)( + $page: Signal[Page.Detail] +)(using router: Router[Page]): + val $oscChangeSignal = $page.splitOne(_.osobniCislo.value)((osc, _, _) => osc) + val $pageChangeSignal = + $oscChangeSignal.flatMap(osc => + EventStream.fromSeq(Seq(FetchUserDetails(osc), FetchParameters(osc))) + ) + // TODO: filter the value based on the current osc + // OSC change will fetch new data, but still + // - we need to be sure that what we got is really what we ought to display + // - we want to display stale data accordingly (at least with loading indicator) + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + def apply: HtmlElement = + AppPage(state.actionBus)( + $data.combineWithFn($params)(_ zip _) + .map(_.map(buildModel)) + .split(_ => ())((_, _, s) => DetailPage(s)), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: List[Parameter] + ): DetailPage.ViewModel = + DetailPage.ViewModel( + o.toDetailOsoby, + p.map( + _.toParametr(param => + PageLink.container(Page.DetailParametru(o, param), state.actionBus) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DetailParametruPageConnector.scala new file mode 100644 index 0000000..fa7193d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DetailParametruPageConnector.scala @@ -0,0 +1,67 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.Parameter +import mdr.pdb.UserInfo +import mdr.pdb.app.pages.detail.DetailParametruPage +import pages.detail.DetailParametruPage +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.components.PageLink + +object DetailParametruPageConnector { + trait AppState { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + } +} + +case class DetailParametruPageConnector( + state: DetailParametruPageConnector.AppState +)( + $page: Signal[Page.DetailParametru] +)(using router: Router[Page]): + val $paramChangeSignal = + $page.splitOne(p => (p.osobniCislo.value, p.parametr.value))((x, _, _) => x) + val $pageChangeSignal = + $paramChangeSignal.map(FetchParameter(_, _)) + + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + val $merged = + $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => + for { + da <- d + pa <- p + pb <- pa.find(_.id == pc._2) + } yield (da, pb) + ) + + def apply: HtmlElement = + AppPage(state.actionBus)( + $merged.map(_.map(buildModel)) + .split(_ => ())((_, _, s) => DetailParametruPage(s)), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: Parameter + ): DetailParametruPage.ViewModel = + DetailParametruPage.ViewModel( + o.toDetailOsoby, + p.toParametr(p => + PageLink.container(Page.DetailParametru(o, p), state.actionBus) + ), + p.criteria.map( + _.toKriterium { c => + PageLink.container( + Page.DetailKriteria(o, p, c), + state.actionBus + ) + } + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DirectoryPageConnector.scala new file mode 100644 index 0000000..ab2c045 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DirectoryPageConnector.scala @@ -0,0 +1,34 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.UserInfo +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.components.AppPage + +case class DirectoryPageConnector( + $input: EventStream[List[UserInfo]], + actionBus: Observer[Action] +)(using router: Router[Page]): + val $data = $input.startWithNone + val $actionSignal = EventStream.fromValue(FetchDirectory) + + def apply: HtmlElement = + AppPage(actionBus)( + $data.split(_ => ())((_, _, s) => + pages.directory.DirectoryPage( + s.map( + _.map( + _.toUserRow(u => + PageLink.container( + Page.Detail(Page.Titled(u.personalNumber)), + actionBus + ) + ) + ) + ) + ) + ), + $actionSignal --> actionBus + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/mdr/pdb/app/connectors/conversions.scala new file mode 100644 index 0000000..f581796 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/conversions.scala @@ -0,0 +1,68 @@ +package mdr.pdb.app.connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.UserInfo +import mdr.pdb.app.pages.detail.components.DetailOsoby +import mdr.pdb.Parameter +import mdr.pdb.app.pages.detail.components.SeznamParametru +import mdr.pdb.ParameterCriteria +import mdr.pdb.app.pages.detail.components.SeznamKriterii +import mdr.pdb.app.pages.directory.components.UserRow +import mdr.pdb.app.pages.detail.components.DetailParametru +import mdr.pdb.app.pages.detail.components.DetailKriteria +import fiftyforms.ui.components.tailwind.Color + +extension (o: UserInfo) + def toDetailOsoby: DetailOsoby.ViewModel = + DetailOsoby.ViewModel( + o.personalNumber, + o.name, + o.email, + o.phone, + o.img, + o.mainFunction.map(f => + DetailOsoby.Funkce.ViewModel(f.name, f.dept, f.ou) + ), + o.userContracts.headOption.map(c => + DetailOsoby.PracovniPomer.ViewModel(c.rel, c.startDate, c.endDate) + ) + ) + +extension (param: Parameter) + def toParametr(container: Parameter => Anchor): DetailParametru.ViewModel = + DetailParametru.ViewModel( + id = param.id, + nazev = param.name, + popis = param.description, + status = "Nesplněno", + statusColor = Color.red, + a = container(param) + ) + +extension (crit: ParameterCriteria) + def toKriterium( + container: ParameterCriteria => Anchor + ): DetailKriteria.ViewModel = + DetailKriteria.ViewModel( + nazev = crit.criteriumText, + kapitola = crit.chapterId, + bod = crit.itemId, + status = "Nesplněno", + statusColor = Color.red, + splneno = false, + dukazy = Nil, + container = container(crit) + ) + +extension (user: UserInfo) + def toUserRow( + container: UserInfo => HtmlElement = (_: UserInfo) => div() + ): UserRow.ViewModel = + UserRow.ViewModel( + osobniCislo = user.personalNumber.toString, + celeJmeno = user.name, + prijmeni = user.surname, + hlavniFunkce = user.mainFunction.map(_.name), + img = user.img, + container = () => container(user) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/dashboard/DashboardPage.scala b/app/src/main/scala/mdr/pdb/app/pages/dashboard/DashboardPage.scala new file mode 100644 index 0000000..0eb8f7a --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/dashboard/DashboardPage.scala @@ -0,0 +1,11 @@ +package mdr.pdb.app.pages.dashboard + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.Page + +object DashboardPage: + + def render: HtmlElement = + div("Dashboard page") diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala new file mode 100644 index 0000000..ff3a25f --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala @@ -0,0 +1,31 @@ +package mdr.pdb.app.pages.detail + +import com.raquo.laminar.api.L.{*, given} + +import components._ + +object DetailKriteriaPage: + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametr: DetailParametru.ViewModel, + kriterium: DetailKriteria.ViewModel + ) + + def apply($m: Signal[ViewModel])( + action: Observer[DukazyKriteria.Action] + ): HtmlElement = + div( + cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + div( + DetailOsoby.header($m.map(_.osoba)), + DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") + ), + div( + DetailKriteria($m.map(_.kriterium)), + DukazyKriteria($m.map(_.kriterium.dukazy))(action) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/DetailPage.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailPage.scala new file mode 100644 index 0000000..b591cc9 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailPage.scala @@ -0,0 +1,25 @@ +package mdr.pdb.app.pages.detail + +import com.raquo.laminar.api.L.{*, given} + +import components._ +import mdr.pdb.app.Page +import com.raquo.waypoint.Router +import mdr.pdb.app.Action + +object DetailPage: + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametry: SeznamParametru.ViewModel + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + DetailOsoby($m.map(_.osoba)), + SeznamParametru($m.map(_.parametry)) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/DetailParametruPage.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailParametruPage.scala new file mode 100644 index 0000000..3322279 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailParametruPage.scala @@ -0,0 +1,24 @@ +package mdr.pdb.app.pages.detail + +import com.raquo.laminar.api.L.{*, given} + +import components._ + +object DetailParametruPage: + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametr: DetailParametru.ViewModel, + kriteria: SeznamKriterii.ViewModel + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + DetailOsoby.header($m.map(_.osoba)), + DetailParametru($m.map(_.parametr)), + SeznamKriterii($m.map(_.kriteria)) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala new file mode 100644 index 0000000..b58618a --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala @@ -0,0 +1,107 @@ +package mdr.pdb +package app +package pages.detail + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.pages.detail.components.UpravDukazForm +import fiftyforms.services.files.File + +object UpravDukaz: + + type ThisPage = Page.UpravDukazKriteria + type PageKey = (OsobniCislo, String, String) + + trait State { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def availableFiles: EventStream[List[File]] + def actionBus: Observer[Action] + } + + def keyOfPage(page: ThisPage): PageKey = + (page.osobniCislo.value, page.parametr.value, page.kriterium.value) + + def onChangeAction(key: PageKey): Action = + FetchParameterCriteria( + key._1, + key._2, + key._3, + Page.UpravDukazKriteria(_, _, _) + ) + + class Connector(state: State)($page: Signal[ThisPage])(using Router[Page]): + val $paramChangeSignal = $page.splitOne(keyOfPage)((x, _, _) => x) + val $pageChangeSignal = $paramChangeSignal.map(onChangeAction) + + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + val $merged = + $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => + for { + da <- d + pa <- p + pb <- pa.find(_.id == pc._2) + ka <- pb.criteria.find(_.id == pc._3) + } yield (da, pb, ka) + ) + + def apply: HtmlElement = + AppPage(state.actionBus)( + $merged.split(_ => ())((_, s, $s) => + PageComponent( + $s.map(buildModel), + state.availableFiles, + state.actionBus.contramap { + case UpravDukazForm.Cancelled => + NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) + case UpravDukazForm.AvailableFilesRequested => + FetchAvailableFiles(s._1.personalNumber) + } + ) + ), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: Parameter, + k: ParameterCriteria + ): PageComponent.ViewModel = + import connectors.* + PageComponent.ViewModel( + o.toDetailOsoby, + p.toParametr(_ => a()), + k.toKriterium(_ => a()) + ) + + object PageComponent: + import components.* + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametr: DetailParametru.ViewModel, + kriterium: DetailKriteria.ViewModel + ) + + def apply( + $m: Signal[ViewModel], + availableFilesStream: EventStream[List[File]], + events: Observer[UpravDukazForm.Event] + ): HtmlElement = + div( + cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + div( + DetailOsoby.header($m.map(_.osoba)), + DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") + ), + div( + DetailKriteria($m.map(_.kriterium)), + UpravDukazForm(availableFilesStream)(events) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailKriteria.scala new file mode 100644 index 0000000..a3ab634 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailKriteria.scala @@ -0,0 +1,37 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.CustomAttrs +import fiftyforms.ui.components.tailwind.Icons +import java.time.LocalDate +import fiftyforms.ui.components.tailwind.Color + +object DetailKriteria: + case class ViewModel( + nazev: String, + kapitola: String, + bod: String, + status: String, + statusColor: Color, + splneno: Boolean, + dukazy: List[DukazKriteria.ViewModel], + container: HtmlElement = div() + ) { + val id = s"${kapitola}${bod}" + } + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "pb-5 border-b border-gray-200", + h3( + cls := "text-xl leading-6 font-bold text-gray-900", + child.text <-- $m.map(_.nazev) + ), + h3( + cls := "mt-2 max-w-4xl text-sm text-gray-500", + "Kapitola ", + child.text <-- $m.map(_.kapitola), + " bod ", + child <-- $m.map(_.bod) + ) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala deleted file mode 100644 index a3b00af..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala +++ /dev/null @@ -1,103 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import scala.scalajs.js.annotation.JSExportTopLevel -import scala.scalajs.js.annotation.JSExport -import scala.scalajs.js.annotation.JSImport -import scala.scalajs.js -import org.scalajs.dom -import com.raquo.laminar.api.L.{*, given} -import scala.scalajs.js.Date -import com.raquo.waypoint.Router -import com.raquo.waypoint.SplitRender -import cz.e_bs.cmi.mdr.pdb.app.services.DataFetcher -import scala.scalajs.js.JSON -import zio.json._ -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.state.AppState - -@js.native -@JSImport("stylesheets/main.css", JSImport.Namespace) -object Css extends js.Any - -@js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native -@JSImport("params/pdb-params.json", JSImport.Default) -object pdbParams extends js.Object - -@JSExportTopLevel("app") -object Main: - - @JSExport - def main(args: Array[String]): Unit = - import Routes.given - onLoad { - setupAirstream() - val appContainer = dom.document.querySelector("#app") - val _ = - render( - appContainer, - renderPage(state.MockAppState(using unsafeWindowOwner, router)) - ) - } - - private def onLoad(f: => Unit): Unit = - documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) - - private def setupAirstream()(using router: Router[Page]): Unit = - AirstreamError.registerUnhandledErrorCallback(err => - router.forcePage( - Page.UnhandledError( - Some(err.getClass.getName), // TODO: Fill only in dev mode - Some(err.getMessage) - ) - ) - ) - - def renderPage(state: AppState)(using - router: Router[Page] - ): HtmlElement = - val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) - .collectSignal[Page.Detail]( - connectors - .DetailPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailParametru]( - connectors - .DetailParametruPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailKriteria]( - connectors - .DetailKriteriaPageConnector(state)(_) - .apply - ) - .collectSignal[Page.UpravDukazKriteria]( - pages.detail.UpravDukaz.Connector(state)(_).apply - ) - .collectStatic(Page.Dashboard)( - connectors.DashboardPageConnector(state.actionBus).apply - ) - .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) - ) - .collect[Page.UnhandledError](pg => - pages.errors - .UnhandledErrorPage( - pages.errors.UnhandledErrorPage - .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), - state.actionBus - ) - ) - .collectStatic(Page.Directory)( - connectors - .DirectoryPageConnector(state.users, state.actionBus) - .apply - ) - div(cls := "h-full", child <-- pageSplitter.$view) - - // Pull in the stylesheet - val css: Css.type = Css diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala deleted file mode 100644 index 3bbc8c0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ /dev/null @@ -1,198 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.* -import org.scalajs.dom -import zio.json.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import scala.scalajs.js -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.Page.Titled - -// enum is not working with Waypoints' SplitRender collectStatic -sealed abstract class Page( - val id: String, - val title: String, - val parent: Option[Page] -) { - val path: Vector[Page] = - parent match - case None => Vector(this) - case Some(p) => p.path :+ this - - val isRoot: Boolean = parent.isEmpty -} - -object Page: - - case class Titled[V](value: V, title: Option[String] = None): - val show: String = title.getOrElse(value.toString) - - case object Directory extends Page("directory", "Adresář", None) - - case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) - - case class Detail(osobniCislo: Titled[OsobniCislo]) - extends Page("user", osobniCislo.show, Some(Directory)) - - object Detail { - def apply(o: UserInfo): Detail = Detail( - Titled(o.personalNumber, Some(o.name)) - ) - } - - case class DetailParametru( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String] - ) extends Page( - "parameter", - parametr.show, - Some(Detail(osobniCislo)) - ) - - object DetailParametru { - def apply(o: UserInfo, p: Parameter): DetailParametru = - DetailParametru( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)) - ) - } - - case class DetailKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "criteria", - kriterium.show, - Some(DetailParametru(osobniCislo, parametr)) - ) - - object DetailKriteria { - def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = - DetailKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class UpravDukazKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "addProof", - "Důkaz", - Some(DetailKriteria(osobniCislo, parametr, kriterium)) - ) - - object UpravDukazKriteria { - def apply( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): UpravDukazKriteria = - UpravDukazKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class NotFound(url: String) extends Page("404", "404", Some(Directory)) - - case class UnhandledError( - errorName: Option[String], - errorMessage: Option[String] - ) extends Page("500", "Unexpected error", Some(Directory)) - -object Routes: - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) - given [V: JsonEncoder]: JsonEncoder[Titled[V]] = - DeriveJsonEncoder.gen[Titled[V]] - given [V: JsonDecoder]: JsonDecoder[Titled[V]] = - DeriveJsonDecoder.gen[Titled[V]] - given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] - given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] - - val base = - js.`import`.meta.env.BASE_URL - .asInstanceOf[String] + "app" - - val homePage: Page = Page.Directory - - given router: Router[Page] = Router[Page]( - routes = List( - Route.static(homePage, root / endOfSegments, basePath = base), - Route.static( - Page.Dashboard, - root / "dashboard" / endOfSegments, - basePath = base - ), - Route[Page.Detail, String]( - encode = _.osobniCislo.value.toString, - decode = osc => Page.Detail(Titled(OsobniCislo(osc))), - root / "osoba" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.DetailParametru, (String, String)]( - encode = p => (p.osobniCislo.value.toString, p.parametr.value), - decode = - p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / endOfSegments, - basePath = base - ), - Route[Page.DetailKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.DetailKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.UpravDukazKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.UpravDukazKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / "edit" / endOfSegments, - basePath = base - ) - ), - serializePage = _.toJson, - deserializePage = _.fromJson[Page] - .fold(s => throw IllegalStateException(s), identity), - getPageTitle = _.title, - routeFallback = url => Page.NotFound(url), - deserializeFallback = _ => Page.Dashboard - )( - $popStateEvent = windowEvents.onPopState, - owner = unsafeWindowOwner - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala deleted file mode 100644 index b24b82b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -sealed trait Action - -case object FetchDirectory extends Action -case class FetchUserDetails(osc: OsobniCislo) extends Action -case class FetchParameters(osc: OsobniCislo) extends Action -case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action -case class FetchParameterCriteria( - osc: OsobniCislo, - paramId: String, - critId: String, - page: (UserInfo, Parameter, ParameterCriteria) => Page -) extends Action -case class FetchAvailableFiles(osc: OsobniCislo) extends Action -case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala deleted file mode 100644 index c637d35..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala +++ /dev/null @@ -1,82 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.{UserProfile, UserInfo, OsobniCislo} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo -import cz.e_bs.cmi.mdr.pdb.UserFunction - -object AppPage: - // TODO: pages by logged in user - val pages: List[Page] = List(Page.Directory, Page.Dashboard) - - import NavigationBar.{Logo, MenuItem} - - val logo = Logo( - "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", - "Workflow" - ) - - // TODO: menu items by user profile - val userMenu = - List( - MenuItem("Your Profile"), - MenuItem("Settings"), - MenuItem("Sign out") - ) - - // TODO: load user profile - val $userProfile = Var( - UserProfile( - "tom", - UserInfo( - OsobniCislo("1031"), - "tom", - "Tom", - "Cook", - None, - None, - Some("tom@example.com"), - Some("+420 222 866 180"), - Some( - UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") - ), - Nil, - None - ) - ) - ) - - val $userInfo = $userProfile.signal.map(_.userInfo) - - type ViewModel = Option[HtmlElement] - def apply( - actionBus: Observer[Action] - )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using - router: Router[Page] - ): HtmlElement = - PageLayout(actionBus)( - $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => - PageLayout.ViewModel( - NavigationBar.ViewModel( - u, - pages.map(p => - NavigationBar.Link( - () => - PageLink( - p, - actionBus - ), - p == cp - ) - ), - userMenu, - logo - ), - c - ) - ), - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala deleted file mode 100644 index bf473c6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ /dev/null @@ -1,143 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden -import fiftyforms.ui.components.tailwind.list.IconText.ViewModel -import fiftyforms.ui.components.tailwind.Icons -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Action - -object Breadcrumbs: - - private def slash = - import svg.{*, given} - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-300", - xmlns := "http://www.w3.org/2000/svg", - fill := "currentColor", - viewBox := "0 0 20 20", - ariaHidden := true, - path( - d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" - ) - ) - - def backIcon = - Icons.solid - .`arrow-narrow-left`() - .amend( - svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" - ) - - object Link: - case class ViewModel( - page: Page, - icon: Option[SvgElement], - extraClasses: String, - text: String, - textClasses: Option[String] = None - ) - - val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" - - def shortHome(p: Page) = ViewModel( - p, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - "Zpět na úvodní stránku", - None - ) - - def fullHome(p: Page) = ViewModel( - p, - Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), - "text-gray-400 hover:text-gray-500", - "Domů", - Some("sr-only") - ) - - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - PageLink - .container($m.map(_.page), actionBus) - .amend( - cls <-- $m.map(_.extraClasses), - child.maybe <-- $m.map(_.icon), - span( - cls <-- $m.map(_.textClasses.getOrElse("")), - child.text <-- $m.map(_.text) - ) - ) - - object FullBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - ol( - role := "list", - cls := "flex items-center space-x-4", - children <-- $m.map(_.path).split(_.id)((_, _, $p) => - li( - div( - cls := "flex items-center", - child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), - Link( - $p.map(p => - if (p.isRoot) then Link.fullHome(p) - else - Link.ViewModel( - p, - None, - s"ml-4 max-w-xs truncate ${Link.baseClasses}", - p.title - ) - ), - actionBus - ) - ) - ) - ) - ) - - object ShortBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - Link( - $m.map { p => - val target = p.parent.getOrElse(p) - if target.isRoot then Link.shortHome(target) - else - Link.ViewModel( - target, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - s"Zpět na ${target.title}" - ) - }, - actionBus - ) - - def apply(actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - nav( - cls := "flex", - aria.label := "Breadcrumb", - div( - cls := "flex sm:hidden", - ShortBreadcrumbs(router.$currentPage, actionBus) - ), - div( - cls := "hidden sm:block", - FullBreadcrumbs(router.$currentPage, actionBus) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala deleted file mode 100644 index 0db2047..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ /dev/null @@ -1,242 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent -import fiftyforms.ui.components.tailwind.Icons -import fiftyforms.ui.components.tailwind.Avatar -import cz.e_bs.cmi.mdr.pdb.UserInfo -import io.laminext.syntax.core.* - -object NavigationBar: - - case class Logo(img: String, name: String) - case class Link(a: () => Anchor, active: Boolean) - case class MenuItem(title: String) - - case class ViewModel( - userInfo: UserInfo, - pages: List[Link], - userMenu: List[MenuItem], - logo: Logo - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - val $userInfo = $m.map(_.userInfo) - val mobileMenuOpen = Var(false) - - val desktopOnly = cls("hidden md:block") - val mobileOnly = cls("md:hidden") - - inline def avatarImage(size: Int = 8) = - Avatar($userInfo.map(_.img)).avatarImage(size) - - def notificationButton = button( - tpe := "button", - cls := List( - "bg-indigo-600", - "focus:outline-none", - "focus:ring-2", - "focus:ring-offset-2", - "focus:ring-offset-indigo-600", - "focus:ring-white", - "hover:text-white", - "p-1", - "rounded-full", - "text-indigo-200" - ), - span(cls := "sr-only", "View notifications"), - Icons.outline.bell() - ) - - def userProfile: HtmlElement = - val menuOpen = Var(false) - - def menuItem(item: MenuItem, idx: Int): HtmlElement = - a( - href := "#", - cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", - role := "menuitem", - tabIndex := -1, - idAttr := s"user-menu-item-$idx", - item.title - ) - - div( - cls := "ml-3 relative", - div( - button( - tpe := "button", - cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - idAttr := "user-menu-button", - aria.expanded <-- menuOpen.signal, - aria.hasPopup := true, - span(cls := "sr-only", "Open user menu"), - child <-- avatarImage(), - onClick.preventDefault.mapTo( - !menuOpen.now() - ) --> menuOpen.writer - ) - ), - /* - * */ - div( - cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", - cls <-- menuOpen.signal.map { o => - if (o) "md:block" else "md:hidden" - }, - role := "menu", - aria.orientation := "vertical", - aria.labelledBy := "user-menu-button", - tabIndex := -1, - // : keyboard navigation - children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) - ) - ) - - def mobileProfile = - def menuItem(item: MenuItem): HtmlElement = - a( - href := "#", - cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", - item.title - ) - - div( - cls := "pt-4 pb-3 border-t border-indigo-700", - div( - cls := "flex items-center px-5", - div( - cls := "flex-shrink-0", - child <-- avatarImage(10) - ), - div( - cls := "ml-3", - div( - cls := "text-base font-medium text-white", - child.text <-- $userInfo.map(_.name) - ), - child.maybe <-- $userInfo.map( - _.email.map(e => - div( - cls := "text-sm font-medium text-indigo-300", - e - ) - ) - ) - ), - notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) - ), - div( - cls := "mt-3 px-2 space-y-1", - children <-- $m.map(_.userMenu.map(menuItem)) - ) - ) - - def pageLink(page: Link): Anchor = - page - .a() - .amend( - cls := "text-white px-3 py-2 rounded-md text-sm font-medium", - cls := Seq( - "bg-indigo-700" -> page.active, - "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active - ), - ariaCurrent := (if page.active then "page" else "false") - ) - - def logoImg: Image = - img( - cls := "h-8 w-8", - src <-- $m.map(_.logo.img), - alt <-- $m.map(_.logo.name) - ) - - def mobileMenuButton = button( - tpe := "button", - cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - aria.controls := "mobile-menu", - aria.expanded <-- mobileMenuOpen.signal, - span(cls := "sr-only", "Open main menu"), - Icons.outline - .menu() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") - ), - Icons.outline - .x() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") - ), - onClick.preventDefault.mapTo( - !mobileMenuOpen.now() - ) --> mobileMenuOpen.writer - ) - - def navBarLeft = - div( - cls := "flex items-center", - div(cls := "flex-shrink-0", logoImg), - div( - desktopOnly, - div( - cls := "ml-10 flex items-baseline space-x-4", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ) - ) - ) - - def navBarRight = - div( - desktopOnly, - div( - cls := "ml-4 flex items-center md:ml-6", - notificationButton, - userProfile - ) - ) - - def navBarMobile = - div( - cls := "-mr-2 flex", - mobileOnly, - mobileMenuButton - ) - - def navBar = - div( - cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex items-center justify-between h-16", - navBarLeft, - navBarRight, - navBarMobile - ) - ) - - def mobileMenu = - div( - mobileOnly, - cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }, - idAttr := "mobile-menu", - div( - cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ), - mobileProfile - ) - - nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala deleted file mode 100644 index 75339df..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object PageHeader: - def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = - header( - cls := "bg-white shadow-sm", - div( - cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", - h1( - cls := "text-lg leading-6 font-semibold text-gray-900", - Breadcrumbs(actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala deleted file mode 100644 index 5096463..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ /dev/null @@ -1,30 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import fiftyforms.ui.components.tailwind.Loading -import io.laminext.syntax.core.* - -object PageLayout: - case class ViewModel( - navigation: NavigationBar.ViewModel, - content: Option[HtmlElement] - ) - def apply(actionBus: Observer[Action])( - $m: Signal[ViewModel], - mods: Modifier[HtmlElement]* - )(using router: Router[Page]): HtmlElement = - val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) - div( - cls := "h-full flex flex-col", - NavigationBar($m.map(_.navigation)), - child.maybe <-- router.$currentPage.map(_.isRoot) - .switch(None, Some(PageHeader(actionBus))), - main( - cls := "flex-grow-1 overflow-y-auto", - mods, - child <-- $maybeContent.map(_.getOrElse(Loading)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala deleted file mode 100644 index 62d820f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.LinkSupport.* -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo - -object PageLink: - type ViewModel = Page - def apply($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = - a(mods($m, actions), child.text <-- $m.map(_.title)) - - def apply(m: ViewModel, actions: Observer[Action])(using - router: Router[Page] - ): Anchor = apply(Val(m), actions) - - def container($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = a(mods($m, actions)) - - def container(m: ViewModel, actions: Observer[Action])(using - Router[Page] - ): Anchor = container(Val(m), actions) - - private def mods($m: Signal[Page], actions: Observer[Action])(using - router: Router[Page] - ): Modifier[Anchor] = - Seq( - href <-- $m.map(router.absoluteUrlForPage).recover { case _ => - Some("invalid url") - }, - composeEvents(onClick.noKeyMod.preventDefault)( - _.sample($m) - .map(NavigateTo.apply) - ) --> actions - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala deleted file mode 100644 index 87f825a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.pages.dashboard.DashboardPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -class DashboardPageConnector(actionBus: Observer[Action])(using - router: Router[Page] -): - def apply: HtmlElement = - AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala deleted file mode 100644 index e9b0f71..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala +++ /dev/null @@ -1,76 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailKriteriaPage -import pages.detail.components.DukazyKriteria -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -object DetailKriteriaPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailKriteriaPageConnector( - state: DetailKriteriaPageConnector.AppState -)( - $page: Signal[Page.DetailKriteria] -)(using Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => - (p.osobniCislo.value, p.parametr.value, p.kriterium.value) - )((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map( - FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) - ) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, s, $s) => - DetailKriteriaPage($s)(state.actionBus.contramap { - case DukazyKriteria.Add => - NavigateTo( - Page.UpravDukazKriteria( - Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), - Page.Titled(s.parametr.id, Some(s.parametr.nazev)), - Page.Titled(s.kriterium.id, Some(s.kriterium.id)) - ) - ) - }) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): DetailKriteriaPage.ViewModel = - DetailKriteriaPage.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala deleted file mode 100644 index 4d8024f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ /dev/null @@ -1,57 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import pages.detail.DetailPage -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import fiftyforms.ui.components.tailwind.Color - -object DetailPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailPageConnector(state: DetailPageConnector.AppState)( - $page: Signal[Page.Detail] -)(using router: Router[Page]): - val $oscChangeSignal = $page.splitOne(_.osobniCislo.value)((osc, _, _) => osc) - val $pageChangeSignal = - $oscChangeSignal.flatMap(osc => - EventStream.fromSeq(Seq(FetchUserDetails(osc), FetchParameters(osc))) - ) - // TODO: filter the value based on the current osc - // OSC change will fetch new data, but still - // - we need to be sure that what we got is really what we ought to display - // - we want to display stale data accordingly (at least with loading indicator) - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - def apply: HtmlElement = - AppPage(state.actionBus)( - $data.combineWithFn($params)(_ zip _) - .map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: List[Parameter] - ): DetailPage.ViewModel = - DetailPage.ViewModel( - o.toDetailOsoby, - p.map( - _.toParametr(param => - PageLink.container(Page.DetailParametru(o, param), state.actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala deleted file mode 100644 index fdaa7f8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailParametruPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink - -object DetailParametruPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailParametruPageConnector( - state: DetailParametruPageConnector.AppState -)( - $page: Signal[Page.DetailParametru] -)(using router: Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => (p.osobniCislo.value, p.parametr.value))((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map(FetchParameter(_, _)) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - } yield (da, pb) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailParametruPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter - ): DetailParametruPage.ViewModel = - DetailParametruPage.ViewModel( - o.toDetailOsoby, - p.toParametr(p => - PageLink.container(Page.DetailParametru(o, p), state.actionBus) - ), - p.criteria.map( - _.toKriterium { c => - PageLink.container( - Page.DetailKriteria(o, p, c), - state.actionBus - ) - } - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala deleted file mode 100644 index 049aa22..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -case class DirectoryPageConnector( - $input: EventStream[List[UserInfo]], - actionBus: Observer[Action] -)(using router: Router[Page]): - val $data = $input.startWithNone - val $actionSignal = EventStream.fromValue(FetchDirectory) - - def apply: HtmlElement = - AppPage(actionBus)( - $data.split(_ => ())((_, _, s) => - pages.directory.DirectoryPage( - s.map( - _.map( - _.toUserRow(u => - PageLink.container( - Page.Detail(Page.Titled(u.personalNumber)), - actionBus - ) - ) - ) - ) - ) - ), - $actionSignal --> actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala deleted file mode 100644 index 88e7450..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ /dev/null @@ -1,68 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii -import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria -import fiftyforms.ui.components.tailwind.Color - -extension (o: UserInfo) - def toDetailOsoby: DetailOsoby.ViewModel = - DetailOsoby.ViewModel( - o.personalNumber, - o.name, - o.email, - o.phone, - o.img, - o.mainFunction.map(f => - DetailOsoby.Funkce.ViewModel(f.name, f.dept, f.ou) - ), - o.userContracts.headOption.map(c => - DetailOsoby.PracovniPomer.ViewModel(c.rel, c.startDate, c.endDate) - ) - ) - -extension (param: Parameter) - def toParametr(container: Parameter => Anchor): DetailParametru.ViewModel = - DetailParametru.ViewModel( - id = param.id, - nazev = param.name, - popis = param.description, - status = "Nesplněno", - statusColor = Color.red, - a = container(param) - ) - -extension (crit: ParameterCriteria) - def toKriterium( - container: ParameterCriteria => Anchor - ): DetailKriteria.ViewModel = - DetailKriteria.ViewModel( - nazev = crit.criteriumText, - kapitola = crit.chapterId, - bod = crit.itemId, - status = "Nesplněno", - statusColor = Color.red, - splneno = false, - dukazy = Nil, - container = container(crit) - ) - -extension (user: UserInfo) - def toUserRow( - container: UserInfo => HtmlElement = (_: UserInfo) => div() - ): UserRow.ViewModel = - UserRow.ViewModel( - osobniCislo = user.personalNumber.toString, - celeJmeno = user.name, - prijmeni = user.surname, - hlavniFunkce = user.mainFunction.map(_.name), - img = user.img, - container = () => container(user) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala deleted file mode 100644 index 7d5b2c1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.dashboard - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.Page - -object DashboardPage: - - def render: HtmlElement = - div("Dashboard page") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala deleted file mode 100644 index 9ef0698..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala +++ /dev/null @@ -1,31 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailKriteriaPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply($m: Signal[ViewModel])( - action: Observer[DukazyKriteria.Action] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - DukazyKriteria($m.map(_.kriterium.dukazy))(action) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala deleted file mode 100644 index 3b316e1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object DetailPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametry: SeznamParametru.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby($m.map(_.osoba)), - SeznamParametru($m.map(_.parametry)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala deleted file mode 100644 index 17224cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala +++ /dev/null @@ -1,24 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailParametruPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriteria: SeznamKriterii.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby.header($m.map(_.osoba)), - DetailParametru($m.map(_.parametr)), - SeznamKriterii($m.map(_.kriteria)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala deleted file mode 100644 index dc912fa..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ /dev/null @@ -1,107 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb -package app -package pages.detail - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.UpravDukazForm -import fiftyforms.services.files.File - -object UpravDukaz: - - type ThisPage = Page.UpravDukazKriteria - type PageKey = (OsobniCislo, String, String) - - trait State { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def availableFiles: EventStream[List[File]] - def actionBus: Observer[Action] - } - - def keyOfPage(page: ThisPage): PageKey = - (page.osobniCislo.value, page.parametr.value, page.kriterium.value) - - def onChangeAction(key: PageKey): Action = - FetchParameterCriteria( - key._1, - key._2, - key._3, - Page.UpravDukazKriteria(_, _, _) - ) - - class Connector(state: State)($page: Signal[ThisPage])(using Router[Page]): - val $paramChangeSignal = $page.splitOne(keyOfPage)((x, _, _) => x) - val $pageChangeSignal = $paramChangeSignal.map(onChangeAction) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.split(_ => ())((_, s, $s) => - PageComponent( - $s.map(buildModel), - state.availableFiles, - state.actionBus.contramap { - case UpravDukazForm.Cancelled => - NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) - case UpravDukazForm.AvailableFilesRequested => - FetchAvailableFiles(s._1.personalNumber) - } - ) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): PageComponent.ViewModel = - import connectors.* - PageComponent.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) - - object PageComponent: - import components.* - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply( - $m: Signal[ViewModel], - availableFilesStream: EventStream[List[File]], - events: Observer[UpravDukazForm.Event] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - UpravDukazForm(availableFilesStream)(events) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala deleted file mode 100644 index ca311b0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.Icons -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Color - -object DetailKriteria: - case class ViewModel( - nazev: String, - kapitola: String, - bod: String, - status: String, - statusColor: Color, - splneno: Boolean, - dukazy: List[DukazKriteria.ViewModel], - container: HtmlElement = div() - ) { - val id = s"${kapitola}${bod}" - } - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h3( - cls := "text-xl leading-6 font-bold text-gray-900", - child.text <-- $m.map(_.nazev) - ), - h3( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - "Kapitola ", - child.text <-- $m.map(_.kapitola), - " bod ", - child <-- $m.map(_.bod) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala deleted file mode 100644 index ae90290..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala +++ /dev/null @@ -1,100 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Avatar - -object DetailOsoby: - - object Funkce: - case class ViewModel( - nazev: String, - stredisko: String, - voj: String - ) - - def render($m: Signal[ViewModel]): HtmlElement = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.nazev), - span( - cls := "hidden md:inline", - " @ ", - child.text <-- $m.map(_.stredisko), - ", ", - child.text <-- $m.map(_.voj) - ) - ) - - object PracovniPomer: - case class ViewModel( - druh: String, - pocatek: LocalDate, - konec: Option[LocalDate] - ) - - def render($m: Signal[ViewModel]): HtmlElement = - import fiftyforms.ui.components.tailwind.CustomAttrs.datetime - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.druh), - " od ", - time( - datetime <-- $m.map(_.pocatek.toString), - child.text <-- $m.map(_.pocatek.toString) - ) - ) - - case class ViewModel( - osobniCislo: OsobniCislo, - jmeno: String, - email: Option[String], - telefon: Option[String], - img: Option[String], - hlavniFunkce: Option[Funkce.ViewModel], - pracovniPomer: Option[PracovniPomer.ViewModel] - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-4", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(16) - ), - div( - h1( - cls := "text-2xl font-bold text-gray-900", - child.text <-- $m.map(_.jmeno) - ), - child.maybe <-- $m.map(_.hlavniFunkce).split(_ => ())((_, _, d) => - Funkce.render(d) - ), - child.maybe <-- $m.map(_.pracovniPomer).split(_ => ())((_, _, d) => - PracovniPomer.render(d) - ) - ) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-5", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(8) - ), - div( - h1( - cls := "text-lg font-medium text-gray-800", - child.text <-- $m.map(_.jmeno) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala deleted file mode 100644 index e11cb62..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color - -object DetailParametru: - case class ViewModel( - id: String, - nazev: String, - popis: String, - status: String, - statusColor: Color, - a: Anchor - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h2( - cls := "text-2xl leading-6 font-medium text-gray-900", - child.text <-- $m.map(_.nazev) - ), - p( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - child.text <-- $m.map(_.popis) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - h2( - cls := "text-lg leading-6 font-medium text-gray-600", - child.text <-- $m.map(_.nazev) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala deleted file mode 100644 index a00ede8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala +++ /dev/null @@ -1,177 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.CustomAttrs - -object DukazKriteria: - case class Osoba(osobniCislo: String, jmeno: String) - case class Udalost(osoba: Osoba, datum: LocalDate) - case class Dokument( - url: String, - nazev: String, - pridal: Udalost, - odebral: Option[Udalost] - ) - case class ViewModel( - dokumenty: List[Dokument], - autorizoval: Option[Udalost], - platiDo: Option[LocalDate], - poznámka: Option[String] - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-lg", - div( - cls := "px-4 py-5 sm:px-6", - h3( - cls := "text-lg leading-6 font-medium text-gray-900", - """Applicant Information""" - ), - p( - cls := "mt-1 max-w-2xl text-sm text-gray-500", - """Personal details and application.""" - ) - ), - div( - cls := "border-t border-gray-200 px-4 py-5 sm:px-6", - dl( - cls := "grid grid-cols-1 gap-x-4 gap-y-8 sm:grid-cols-2", - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Full name""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Margot Foster""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Application for""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Backend Developer""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Email address""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """margotfoster@example.com""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Salary expectation""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """$120,000""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """About""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Fugiat ipsum ipsum deserunt culpa aute sint do nostrud anim incididunt cillum culpa consequat. Excepteur qui ipsum aliquip consequat sint. Sit id mollit nulla mollit nostrud in ea officia proident. Irure nostrud pariatur mollit ad adipisicing reprehenderit deserunt qui eu.""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """Attachments""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - ul( - role := "list", - cls := "border border-gray-200 rounded-md divide-y divide-gray-200", - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """resume_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ), - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """coverletter_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala deleted file mode 100644 index 22f134f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala +++ /dev/null @@ -1,35 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object DukazyKriteria: - sealed trait Action - case object Add extends Action - - type ViewModel = List[DukazKriteria.ViewModel] - - def apply($m: Signal[ViewModel])(actions: Observer[Action]): HtmlElement = - div( - child.maybe <-- $m.map(m => - if (m.isEmpty) then Some(prazdnyDukaz(actions)) - else None - ), - children <-- $m.map(_.zipWithIndex).split(_._2)((_, _, $s) => - DukazKriteria($s.map(_._1)) - ) - ) - - private def prazdnyDukaz(actions: Observer[Action]): HtmlElement = - button( - tpe := "button", - onClick.preventDefault.mapTo(Add) --> actions, - cls := "relative block w-full border-2 border-gray-300 border-dashed rounded-lg p-12 text-center hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - Icons.outline - .`document-add`(12) - .amend(svg.cls := "mx-auto text-gray-400"), - span( - cls := "mt-2 block text-sm font-medium text-gray-900", - "Přidat důkaz" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala deleted file mode 100644 index f628697..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.list.StackedList -import fiftyforms.ui.components.tailwind.list.ListRow -import fiftyforms.ui.components.tailwind.list.RowTag -import fiftyforms.ui.components.tailwind.list.RowNext -import java.time.LocalDate - -object SeznamKriterii: - type ViewModel = List[DetailKriteria.ViewModel] - - private val kritList = new StackedList[DetailKriteria.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - kritList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = - p(cls := "text-sm text-gray-500", s"${i.kapitola}${i.bod}"), - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.container - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala deleted file mode 100644 index 2bfe4bc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.list.{ - StackedList, - ListRow, - RowTag, - PropList, - IconText, - RowNext -} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.LinkSupport.* - -object SeznamParametru: - type ViewModel = List[DetailParametru.ViewModel] - - private val parametrList = new StackedList[DetailParametru.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - parametrList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = emptyNode, - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.a - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala deleted file mode 100644 index f048e9d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ /dev/null @@ -1,101 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.form.* -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement -import fiftyforms.services.files.components.tailwind.FilePicker -import cz.e_bs.cmi.mdr.pdb.frontend.AutorizujDukaz -import cz.e_bs.cmi.mdr.pdb.frontend.DocumentRef -import fiftyforms.services.files.File - -object UpravDukazForm: - sealed trait Event - case object Cancelled extends Event - case object AvailableFilesRequested extends Event - def apply(availableFilesStream: EventStream[List[File]])( - updates: Observer[Event] - ): HtmlElement = - val (filesStream, filesObserver) = - EventStream.withObserver[FilePicker.Event] - val files = Var[List[File]](Nil) - def submitButtons: HtmlElement = - div( - cls := "pt-5", - div( - cls := "flex justify-end", - button( - tpe := "button", - cls := "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zrušit", - onClick.mapTo(Cancelled) --> updates - ), - button( - tpe := "submit", - cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - disabled <-- files.signal.map(_.isEmpty), - "Autorizovat" - ) - ) - ) - - div( - filesStream.collect { case FilePicker.SelectionUpdated(files) => - files.to(List) - } --> files.writer, - filesStream.collect { case FilePicker.AvailableFilesRequested => - AvailableFilesRequested - } --> updates, - cls := "bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6", - Form( - Form.Body( - Form.Section( - FormHeader( - FormHeader.ViewModel( - "Doložení kritéria", - "Sestavte doklady poskytující důkaz kritéria a potvrďte odesláním formuláře. Případné limitace či jiné relevantní údaje vepište do pole Komentář." - ) - ), - FormFields( - FormRow( - "dokumenty", - "Dokumenty", - FilePicker(files.signal, availableFilesStream)(filesObserver) - .amend(idAttr := "dokumenty", cls("max-w-lg")) - ).toHtml, - FormRow( - "platnost", - "Platnost", - input( - idAttr := "platnost", - name := "platnost", - tpe := "date", - autoComplete := "date", - cls := "block max-w-lg w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" - ) - ).toHtml, - FormRow( - "komentar", - "Komentář", - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - textArea( - idAttr := "komentar", - name := "about", - rows := 3, - cls := "max-w-lg shadow-sm block w-full focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border border-gray-300 rounded-md" - ), - p( - cls := "mt-2 text-sm text-gray-500", - "Doplňte prosím potřebné informace související s doložením kritéria, včetně případných limitací." - ) - ) - ).toHtml - ) - ) - ), - submitButtons - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala deleted file mode 100644 index 41033f6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -import components._ - -object DirectoryPage: - - type ViewModel = List[UserRow.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - val (actionsStream, actionObserver) = - EventStream.withObserver[SearchForm.Action] - val $filter = actionsStream - .collect { - case SearchForm.ClearFilter => None - case SearchForm.SetFilter(t) => Some(t) - } - .startWith(None) - val byLetter = for { - d <- $m - f <- $filter - } yield for { - (letter, users) <- d - .filter { user => - f match - case None => true - case Some(t) => user.search.contains(t) - } - .groupBy(_.prijmeni.head) - .to(List) - .sortBy(_._1) - } yield (letter.toString, users.sortBy(_.prijmeni)) - - div( - cls := "h-full max-w-7xl mx-auto order-first flex flex-col flex-shrink-0", - SearchForm(actionObserver), - Directory(byLetter) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala deleted file mode 100644 index 73f915d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -object Directory: - - object Header: - type ViewModel = String - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500", - h3(child.text <-- $m) - ) - - object UserList: - type ViewModel = List[UserRow.ViewModel] - def apply($m: Signal[ViewModel]): HtmlElement = - ul( - role := "list", - cls := "relative z-0 divide-y divide-gray-200", - children <-- $m.split(_.osobniCislo)((_, _, s) => UserRow(s)) - ) - - type ViewModel = List[(String, List[UserRow.ViewModel])] - def apply($m: Signal[ViewModel]): HtmlElement = - nav( - cls := "flex-1 min-h-0 overflow-y-auto", - aria.label := "Directory", - children <-- $m.split(_._1)((_, _, s) => - div( - cls := "relative", - Header(s.map(_._1)), - UserList(s.map(_._2)) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala deleted file mode 100644 index e182910..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala +++ /dev/null @@ -1,54 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object SearchForm: - sealed trait Action - case object SubmitFilter extends Action - sealed trait FilterAction extends Action - case object ClearFilter extends FilterAction - case class SetFilter(value: String) extends FilterAction - - def apply(actions: Observer[Action]): HtmlElement = - div( - cls := "px-6 pt-4 pb-4", - form( - cls := "flex space-x-4", - onSubmit.mapTo(SubmitFilter) --> actions, - div( - cls := "flex-1 min-w-0", - label( - forId := "search", - cls := "sr-only", - "Hledat" - ), - div( - cls := "relative rounded-md shadow-sm", - div( - cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", - Icons.solid.search().amend(svg.cls := "text-gray-400") - ), - input( - tpe := "search", - name := "search", - idAttr := "search", - cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", - placeholder := "Hledat", - composeEvents(onInput.mapToValue.setAsValue.map(SetFilter(_)))( - _.throttle(500) - ) --> actions - ) - ) - ), - button( - tpe := "submit", - cls := "inline-flex justify-center px-3.5 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500", - Icons.solid.filter().amend(svg.cls := "text-gray-400"), - span( - cls := "sr-only", - "Hledat" - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala deleted file mode 100644 index 82f7723..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala +++ /dev/null @@ -1,52 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.airstream.core.Signal -import fiftyforms.ui.components.tailwind.Avatar - -object UserRow: - case class ViewModel( - osobniCislo: String, - celeJmeno: String, - prijmeni: String, - hlavniFunkce: Option[String], - img: Option[String], - container: () => HtmlElement = () => div() - ) { - val search = prijmeni.toLowerCase - } - - def apply($m: Signal[ViewModel]): HtmlElement = - inline def avatarImage = - Avatar($m.map(_.img)).avatarImage(10) - - li( - div( - cls := "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500", - div( - cls := "flex-shrink-0", - child <-- avatarImage - ), - div( - cls := "flex-1 min-w-0", - child <-- $m.map { o => - o.container() - .amend( - cls := "focus:outline-none", - span( - cls := "absolute inset-0", - aria.hidden := true - ), - p( - cls := "text-sm font-medium text-gray-900", - o.celeJmeno - ), - p( - cls := "text-sm text-gray-500 truncate", - o.hlavniFunkce - ) - ) - } - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala deleted file mode 100644 index 467c5f5..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala +++ /dev/null @@ -1,99 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router - -object ErrorPage: - case class ViewModel( - homePage: Page, - errorName: String, - title: String, - subTitle: String - ) - - def apply(m: ViewModel, actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - val ViewModel(homePage, errorName, title, subTitle) = m - div( - cls := "min-h-full pt-16 pb-12 flex flex-col bg-white", - main( - cls := "flex-grow flex flex-col justify-center max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex-shrink-0 flex justify-center", - a( - href := "/", - cls := "inline-flex", - span( - cls := "sr-only", - """Workflow""" - ), - img( - cls := "h-12 w-auto", - src := "https://tailwindui.com/img/logos/workflow-mark.svg?color=indigo&shade=600", - alt := "" - ) - ) - ), - div( - cls := "py-16", - div( - cls := "text-center", - p( - cls := "text-sm font-semibold text-indigo-600 uppercase tracking-wide", - errorName - ), - h1( - cls := "mt-2 text-4xl font-extrabold text-gray-900 tracking-tight sm:text-5xl", - title - ), - p( - cls := "mt-2 text-base text-gray-500", - subTitle - ), - div( - cls := "mt-6", - PageLink - .container(homePage, actionBus) - .amend( - cls := "text-base font-medium text-indigo-600 hover:text-indigo-500", - """Go back home""" - ) - ) - ) - ) - ), - footer( - cls := "flex-shrink-0 max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", - nav( - cls := "flex justify-center space-x-4", - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Contact Support""" - ), - span( - cls := "inline-block border-l border-gray-300", - aria.hidden := true - ), - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Status""" - ), - span( - cls := "inline-block border-l border-gray-300", - aria.hidden := true - ), - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Twitter""" - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala deleted file mode 100644 index bd9baec..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action - -object NotFoundPage: - def apply(homePage: Page, url: String, actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - ErrorPage( - ErrorPage.ViewModel( - homePage, - "404 error", - "Page not found.", - s"Sorry, but page $url doesn't exist." - ), - actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala deleted file mode 100644 index 476fe3c..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action - -object UnhandledErrorPage: - - case class ViewModel( - homePage: Page, - errorName: Option[String], - errorMessage: Option[String] - ) - - def apply(m: ViewModel, actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - ErrorPage( - ErrorPage.ViewModel( - m.homePage, - "Unexpected error occurred", - m.errorName.getOrElse( - "Uh oh!" - ), // TODO: translations, better text than uh oh - m.errorMessage.getOrElse( - "This wasn't supposed to happen! Please try again." - ) - ), - actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala deleted file mode 100644 index 25f059f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala +++ /dev/null @@ -1,6 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.services - -import com.raquo.airstream.core.Observer - -trait DataFetcher[K, A]: - def fetch(id: K, sink: Observer[A]): Unit diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala deleted file mode 100644 index a469830..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala +++ /dev/null @@ -1,120 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package state - -import com.raquo.airstream.core.EventStream -import cz.e_bs.cmi.mdr.pdb.{UserInfo, OsobniCislo} -import com.raquo.airstream.core.Observer -import scala.scalajs.js -import scala.scalajs.js.JSON -import zio.json._ -import com.raquo.airstream.eventbus.EventBus -import com.raquo.airstream.ownership.Owner -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.UserFunction -import cz.e_bs.cmi.mdr.pdb.UserContract -import fiftyforms.services.files.File - -trait AppState - extends connectors.DetailPageConnector.AppState - with connectors.DetailParametruPageConnector.AppState - with connectors.DetailKriteriaPageConnector.AppState - with pages.detail.UpravDukaz.State: - def users: EventStream[List[UserInfo]] - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - -class MockAppState(implicit owner: Owner, router: Router[Page]) - extends AppState: - - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonDecoder[UserFunction] = DeriveJsonDecoder.gen - given JsonDecoder[UserContract] = DeriveJsonDecoder.gen - given JsonDecoder[UserInfo] = DeriveJsonDecoder.gen - - given JsonDecoder[ParameterCriteria] = DeriveJsonDecoder.gen - given JsonDecoder[Parameter] = DeriveJsonDecoder.gen - - private val actions = EventBus[Action]() - private val (parametersStream, pushParameters) = - EventStream.withCallback[List[Parameter]] - private val (usersStream, pushUsers) = - EventStream.withCallback[List[UserInfo]] - private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] - private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] - - private val mockData: List[UserInfo] = - mockUsers - .asInstanceOf[js.Dictionary[js.Object]] - .values - // TODO: is there a more efficient way to parse from JS object directly? - .map { o => - val parsed = JSON.stringify(o).fromJson[UserInfo] - parsed.left.foreach(org.scalajs.dom.console.log(_)) - parsed - } - .collect { case Right(u) => - u - } - .toList - - private val mockParameters: List[Parameter] = - pdbParams - .asInstanceOf[js.Dictionary[js.Object]] - .values - .map(o => JSON.stringify(o).fromJson[Parameter]) - .collect { case Right(p) => p } - .toList - - // TODO: Extract to separate event handler - actions.events.foreach { - case FetchDirectory => pushUsers(mockData) - case FetchUserDetails(osc) => - mockData.find(_.personalNumber == osc).foreach { o => - pushDetails(o) - router.replaceState(Page.Detail(o)) - } - case FetchParameters(osc) => - pushParameters(mockParameters) - case FetchParameter(osc, paramId) => - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(Page.DetailParametru(o, p)) - case FetchParameterCriteria(osc, paramId, critId, page) => - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - c <- p.criteria.find(_.id == critId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(page(o, p, c)) - case NavigateTo(page) => router.pushState(page) - case FetchAvailableFiles(osc) => - pushFiles( - List( - File("https://tc163.cmi.cz/here", "Example file") - ) - ) - } - - override def users: EventStream[List[UserInfo]] = - usersStream.debugWithName("users") - - override def details: EventStream[UserInfo] = - detailsStream.debugWithName("details") - - override def parameters: EventStream[List[Parameter]] = - parametersStream.debugWithName("parameters") - - override def availableFiles: EventStream[List[File]] = - filesStream.debugWithName("available files") - - override def actionBus: Observer[Action] = - actions.writer.debugWithName("actions writer") diff --git a/app/src/main/scala/mdr/pdb/app/Main.scala b/app/src/main/scala/mdr/pdb/app/Main.scala new file mode 100644 index 0000000..081b4ee --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/Main.scala @@ -0,0 +1,103 @@ +package mdr.pdb.app + +import scala.scalajs.js.annotation.JSExportTopLevel +import scala.scalajs.js.annotation.JSExport +import scala.scalajs.js.annotation.JSImport +import scala.scalajs.js +import org.scalajs.dom +import com.raquo.laminar.api.L.{*, given} +import scala.scalajs.js.Date +import com.raquo.waypoint.Router +import com.raquo.waypoint.SplitRender +import mdr.pdb.app.services.DataFetcher +import scala.scalajs.js.JSON +import zio.json._ +import mdr.pdb.UserInfo +import mdr.pdb.app.state.AppState + +@js.native +@JSImport("stylesheets/main.css", JSImport.Namespace) +object Css extends js.Any + +@js.native +@JSImport("data/users.json", JSImport.Default) +object mockUsers extends js.Object + +@js.native +@JSImport("params/pdb-params.json", JSImport.Default) +object pdbParams extends js.Object + +@JSExportTopLevel("app") +object Main: + + @JSExport + def main(args: Array[String]): Unit = + import Routes.given + onLoad { + setupAirstream() + val appContainer = dom.document.querySelector("#app") + val _ = + render( + appContainer, + renderPage(state.MockAppState(using unsafeWindowOwner, router)) + ) + } + + private def onLoad(f: => Unit): Unit = + documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) + + private def setupAirstream()(using router: Router[Page]): Unit = + AirstreamError.registerUnhandledErrorCallback(err => + router.forcePage( + Page.UnhandledError( + Some(err.getClass.getName), // TODO: Fill only in dev mode + Some(err.getMessage) + ) + ) + ) + + def renderPage(state: AppState)(using + router: Router[Page] + ): HtmlElement = + val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) + .collectSignal[Page.Detail]( + connectors + .DetailPageConnector(state)(_) + .apply + ) + .collectSignal[Page.DetailParametru]( + connectors + .DetailParametruPageConnector(state)(_) + .apply + ) + .collectSignal[Page.DetailKriteria]( + connectors + .DetailKriteriaPageConnector(state)(_) + .apply + ) + .collectSignal[Page.UpravDukazKriteria]( + pages.detail.UpravDukaz.Connector(state)(_).apply + ) + .collectStatic(Page.Dashboard)( + connectors.DashboardPageConnector(state.actionBus).apply + ) + .collect[Page.NotFound](pg => + pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) + ) + .collect[Page.UnhandledError](pg => + pages.errors + .UnhandledErrorPage( + pages.errors.UnhandledErrorPage + .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), + state.actionBus + ) + ) + .collectStatic(Page.Directory)( + connectors + .DirectoryPageConnector(state.users, state.actionBus) + .apply + ) + div(cls := "h-full", child <-- pageSplitter.$view) + + // Pull in the stylesheet + val css: Css.type = Css diff --git a/app/src/main/scala/mdr/pdb/app/Routes.scala b/app/src/main/scala/mdr/pdb/app/Routes.scala new file mode 100644 index 0000000..2cc860c --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/Routes.scala @@ -0,0 +1,198 @@ +package mdr.pdb.app + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.* +import org.scalajs.dom +import zio.json.{*, given} +import mdr.pdb.OsobniCislo + +import scala.scalajs.js +import mdr.pdb.UserInfo +import mdr.pdb.Parameter +import mdr.pdb.ParameterCriteria +import mdr.pdb.app.Page.Titled + +// enum is not working with Waypoints' SplitRender collectStatic +sealed abstract class Page( + val id: String, + val title: String, + val parent: Option[Page] +) { + val path: Vector[Page] = + parent match + case None => Vector(this) + case Some(p) => p.path :+ this + + val isRoot: Boolean = parent.isEmpty +} + +object Page: + + case class Titled[V](value: V, title: Option[String] = None): + val show: String = title.getOrElse(value.toString) + + case object Directory extends Page("directory", "Adresář", None) + + case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) + + case class Detail(osobniCislo: Titled[OsobniCislo]) + extends Page("user", osobniCislo.show, Some(Directory)) + + object Detail { + def apply(o: UserInfo): Detail = Detail( + Titled(o.personalNumber, Some(o.name)) + ) + } + + case class DetailParametru( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String] + ) extends Page( + "parameter", + parametr.show, + Some(Detail(osobniCislo)) + ) + + object DetailParametru { + def apply(o: UserInfo, p: Parameter): DetailParametru = + DetailParametru( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)) + ) + } + + case class DetailKriteria( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String], + kriterium: Titled[String] + ) extends Page( + "criteria", + kriterium.show, + Some(DetailParametru(osobniCislo, parametr)) + ) + + object DetailKriteria { + def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = + DetailKriteria( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)), + Titled(k.id, Some(k.id)) + ) + } + + case class UpravDukazKriteria( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String], + kriterium: Titled[String] + ) extends Page( + "addProof", + "Důkaz", + Some(DetailKriteria(osobniCislo, parametr, kriterium)) + ) + + object UpravDukazKriteria { + def apply( + o: UserInfo, + p: Parameter, + k: ParameterCriteria + ): UpravDukazKriteria = + UpravDukazKriteria( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)), + Titled(k.id, Some(k.id)) + ) + } + + case class NotFound(url: String) extends Page("404", "404", Some(Directory)) + + case class UnhandledError( + errorName: Option[String], + errorMessage: Option[String] + ) extends Page("500", "Unexpected error", Some(Directory)) + +object Routes: + given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) + given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) + given [V: JsonEncoder]: JsonEncoder[Titled[V]] = + DeriveJsonEncoder.gen[Titled[V]] + given [V: JsonDecoder]: JsonDecoder[Titled[V]] = + DeriveJsonDecoder.gen[Titled[V]] + given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] + given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] + + val base = + js.`import`.meta.env.BASE_URL + .asInstanceOf[String] + "app" + + val homePage: Page = Page.Directory + + given router: Router[Page] = Router[Page]( + routes = List( + Route.static(homePage, root / endOfSegments, basePath = base), + Route.static( + Page.Dashboard, + root / "dashboard" / endOfSegments, + basePath = base + ), + Route[Page.Detail, String]( + encode = _.osobniCislo.value.toString, + decode = osc => Page.Detail(Titled(OsobniCislo(osc))), + root / "osoba" / segment[String] / endOfSegments, + basePath = base + ), + Route[Page.DetailParametru, (String, String)]( + encode = p => (p.osobniCislo.value.toString, p.parametr.value), + decode = + p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / endOfSegments, + basePath = base + ), + Route[Page.DetailKriteria, (String, String, String)]( + encode = p => + ( + p.osobniCislo.value.toString, + p.parametr.value, + p.kriterium.value.replaceAll("\\.", "--") + ), + decode = p => + Page.DetailKriteria( + Titled(OsobniCislo(p._1)), + Titled(p._2), + Titled(p._3.replaceAll("--", ".")) + ), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / "kriterium" / segment[String] / endOfSegments, + basePath = base + ), + Route[Page.UpravDukazKriteria, (String, String, String)]( + encode = p => + ( + p.osobniCislo.value.toString, + p.parametr.value, + p.kriterium.value.replaceAll("\\.", "--") + ), + decode = p => + Page.UpravDukazKriteria( + Titled(OsobniCislo(p._1)), + Titled(p._2), + Titled(p._3.replaceAll("--", ".")) + ), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / "kriterium" / segment[String] / "edit" / endOfSegments, + basePath = base + ) + ), + serializePage = _.toJson, + deserializePage = _.fromJson[Page] + .fold(s => throw IllegalStateException(s), identity), + getPageTitle = _.title, + routeFallback = url => Page.NotFound(url), + deserializeFallback = _ => Page.Dashboard + )( + $popStateEvent = windowEvents.onPopState, + owner = unsafeWindowOwner + ) diff --git a/app/src/main/scala/mdr/pdb/app/actions.scala b/app/src/main/scala/mdr/pdb/app/actions.scala new file mode 100644 index 0000000..d078b3a --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/actions.scala @@ -0,0 +1,21 @@ +package mdr.pdb.app + +import mdr.pdb.OsobniCislo +import mdr.pdb.UserInfo +import mdr.pdb.Parameter +import mdr.pdb.ParameterCriteria + +sealed trait Action + +case object FetchDirectory extends Action +case class FetchUserDetails(osc: OsobniCislo) extends Action +case class FetchParameters(osc: OsobniCislo) extends Action +case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action +case class FetchParameterCriteria( + osc: OsobniCislo, + paramId: String, + critId: String, + page: (UserInfo, Parameter, ParameterCriteria) => Page +) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action +case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/mdr/pdb/app/components/AppPage.scala new file mode 100644 index 0000000..130c817 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/AppPage.scala @@ -0,0 +1,82 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.Page +import mdr.pdb.{UserProfile, UserInfo, OsobniCislo} +import com.raquo.waypoint.Router +import mdr.pdb.app.Action +import mdr.pdb.app.NavigateTo +import mdr.pdb.UserFunction + +object AppPage: + // TODO: pages by logged in user + val pages: List[Page] = List(Page.Directory, Page.Dashboard) + + import NavigationBar.{Logo, MenuItem} + + val logo = Logo( + "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", + "Workflow" + ) + + // TODO: menu items by user profile + val userMenu = + List( + MenuItem("Your Profile"), + MenuItem("Settings"), + MenuItem("Sign out") + ) + + // TODO: load user profile + val $userProfile = Var( + UserProfile( + "tom", + UserInfo( + OsobniCislo("1031"), + "tom", + "Tom", + "Cook", + None, + None, + Some("tom@example.com"), + Some("+420 222 866 180"), + Some( + UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") + ), + Nil, + None + ) + ) + ) + + val $userInfo = $userProfile.signal.map(_.userInfo) + + type ViewModel = Option[HtmlElement] + def apply( + actionBus: Observer[Action] + )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using + router: Router[Page] + ): HtmlElement = + PageLayout(actionBus)( + $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => + PageLayout.ViewModel( + NavigationBar.ViewModel( + u, + pages.map(p => + NavigationBar.Link( + () => + PageLink( + p, + actionBus + ), + p == cp + ) + ), + userMenu, + logo + ), + c + ) + ), + mods + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala new file mode 100644 index 0000000..9375398 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala @@ -0,0 +1,143 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.tailwind.list.IconText.ViewModel +import fiftyforms.ui.components.tailwind.Icons +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.Page + +import com.raquo.laminar.api.L.{*, given} +import io.laminext.syntax.core.{*, given} +import mdr.pdb.app.Action + +object Breadcrumbs: + + private def slash = + import svg.{*, given} + svg( + cls := "flex-shrink-0 h-5 w-5 text-gray-300", + xmlns := "http://www.w3.org/2000/svg", + fill := "currentColor", + viewBox := "0 0 20 20", + ariaHidden := true, + path( + d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" + ) + ) + + def backIcon = + Icons.solid + .`arrow-narrow-left`() + .amend( + svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" + ) + + object Link: + case class ViewModel( + page: Page, + icon: Option[SvgElement], + extraClasses: String, + text: String, + textClasses: Option[String] = None + ) + + val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" + + def shortHome(p: Page) = ViewModel( + p, + Some(backIcon), + "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", + "Zpět na úvodní stránku", + None + ) + + def fullHome(p: Page) = ViewModel( + p, + Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), + "text-gray-400 hover:text-gray-500", + "Domů", + Some("sr-only") + ) + + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + PageLink + .container($m.map(_.page), actionBus) + .amend( + cls <-- $m.map(_.extraClasses), + child.maybe <-- $m.map(_.icon), + span( + cls <-- $m.map(_.textClasses.getOrElse("")), + child.text <-- $m.map(_.text) + ) + ) + + object FullBreadcrumbs: + type ViewModel = Page + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + ol( + role := "list", + cls := "flex items-center space-x-4", + children <-- $m.map(_.path).split(_.id)((_, _, $p) => + li( + div( + cls := "flex items-center", + child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), + Link( + $p.map(p => + if (p.isRoot) then Link.fullHome(p) + else + Link.ViewModel( + p, + None, + s"ml-4 max-w-xs truncate ${Link.baseClasses}", + p.title + ) + ), + actionBus + ) + ) + ) + ) + ) + + object ShortBreadcrumbs: + type ViewModel = Page + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + Link( + $m.map { p => + val target = p.parent.getOrElse(p) + if target.isRoot then Link.shortHome(target) + else + Link.ViewModel( + target, + Some(backIcon), + "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", + s"Zpět na ${target.title}" + ) + }, + actionBus + ) + + def apply(actionBus: Observer[Action])(using + router: Router[Page] + ): HtmlElement = + nav( + cls := "flex", + aria.label := "Breadcrumb", + div( + cls := "flex sm:hidden", + ShortBreadcrumbs(router.$currentPage, actionBus) + ), + div( + cls := "hidden sm:block", + FullBreadcrumbs(router.$currentPage, actionBus) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala new file mode 100644 index 0000000..4bb8b8d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala @@ -0,0 +1,242 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent +import fiftyforms.ui.components.tailwind.Icons +import fiftyforms.ui.components.tailwind.Avatar +import mdr.pdb.UserInfo +import io.laminext.syntax.core.* + +object NavigationBar: + + case class Logo(img: String, name: String) + case class Link(a: () => Anchor, active: Boolean) + case class MenuItem(title: String) + + case class ViewModel( + userInfo: UserInfo, + pages: List[Link], + userMenu: List[MenuItem], + logo: Logo + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + val $userInfo = $m.map(_.userInfo) + val mobileMenuOpen = Var(false) + + val desktopOnly = cls("hidden md:block") + val mobileOnly = cls("md:hidden") + + inline def avatarImage(size: Int = 8) = + Avatar($userInfo.map(_.img)).avatarImage(size) + + def notificationButton = button( + tpe := "button", + cls := List( + "bg-indigo-600", + "focus:outline-none", + "focus:ring-2", + "focus:ring-offset-2", + "focus:ring-offset-indigo-600", + "focus:ring-white", + "hover:text-white", + "p-1", + "rounded-full", + "text-indigo-200" + ), + span(cls := "sr-only", "View notifications"), + Icons.outline.bell() + ) + + def userProfile: HtmlElement = + val menuOpen = Var(false) + + def menuItem(item: MenuItem, idx: Int): HtmlElement = + a( + href := "#", + cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", + role := "menuitem", + tabIndex := -1, + idAttr := s"user-menu-item-$idx", + item.title + ) + + div( + cls := "ml-3 relative", + div( + button( + tpe := "button", + cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", + idAttr := "user-menu-button", + aria.expanded <-- menuOpen.signal, + aria.hasPopup := true, + span(cls := "sr-only", "Open user menu"), + child <-- avatarImage(), + onClick.preventDefault.mapTo( + !menuOpen.now() + ) --> menuOpen.writer + ) + ), + /* + * */ + div( + cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", + cls <-- menuOpen.signal.map { o => + if (o) "md:block" else "md:hidden" + }, + role := "menu", + aria.orientation := "vertical", + aria.labelledBy := "user-menu-button", + tabIndex := -1, + // : keyboard navigation + children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) + ) + ) + + def mobileProfile = + def menuItem(item: MenuItem): HtmlElement = + a( + href := "#", + cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", + item.title + ) + + div( + cls := "pt-4 pb-3 border-t border-indigo-700", + div( + cls := "flex items-center px-5", + div( + cls := "flex-shrink-0", + child <-- avatarImage(10) + ), + div( + cls := "ml-3", + div( + cls := "text-base font-medium text-white", + child.text <-- $userInfo.map(_.name) + ), + child.maybe <-- $userInfo.map( + _.email.map(e => + div( + cls := "text-sm font-medium text-indigo-300", + e + ) + ) + ) + ), + notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) + ), + div( + cls := "mt-3 px-2 space-y-1", + children <-- $m.map(_.userMenu.map(menuItem)) + ) + ) + + def pageLink(page: Link): Anchor = + page + .a() + .amend( + cls := "text-white px-3 py-2 rounded-md text-sm font-medium", + cls := Seq( + "bg-indigo-700" -> page.active, + "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active + ), + ariaCurrent := (if page.active then "page" else "false") + ) + + def logoImg: Image = + img( + cls := "h-8 w-8", + src <-- $m.map(_.logo.img), + alt <-- $m.map(_.logo.name) + ) + + def mobileMenuButton = button( + tpe := "button", + cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", + aria.controls := "mobile-menu", + aria.expanded <-- mobileMenuOpen.signal, + span(cls := "sr-only", "Open main menu"), + Icons.outline + .menu() + .amend( + svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") + ), + Icons.outline + .x() + .amend( + svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") + ), + onClick.preventDefault.mapTo( + !mobileMenuOpen.now() + ) --> mobileMenuOpen.writer + ) + + def navBarLeft = + div( + cls := "flex items-center", + div(cls := "flex-shrink-0", logoImg), + div( + desktopOnly, + div( + cls := "ml-10 flex items-baseline space-x-4", + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) + ) + ) + ) + + def navBarRight = + div( + desktopOnly, + div( + cls := "ml-4 flex items-center md:ml-6", + notificationButton, + userProfile + ) + ) + + def navBarMobile = + div( + cls := "-mr-2 flex", + mobileOnly, + mobileMenuButton + ) + + def navBar = + div( + cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", + div( + cls := "flex items-center justify-between h-16", + navBarLeft, + navBarRight, + navBarMobile + ) + ) + + def mobileMenu = + div( + mobileOnly, + cls <-- mobileMenuOpen.signal.map { o => + if (o) "block" else "hidden" + }, + idAttr := "mobile-menu", + div( + cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) + ), + mobileProfile + ) + + nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala new file mode 100644 index 0000000..c4cb10d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala @@ -0,0 +1,19 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.Page +import com.raquo.waypoint.Router +import mdr.pdb.app.Action + +object PageHeader: + def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = + header( + cls := "bg-white shadow-sm", + div( + cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", + h1( + cls := "text-lg leading-6 font-semibold text-gray-900", + Breadcrumbs(actionBus) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala new file mode 100644 index 0000000..aae4539 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala @@ -0,0 +1,30 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.Page +import mdr.pdb.app.Action +import fiftyforms.ui.components.tailwind.Loading +import io.laminext.syntax.core.* + +object PageLayout: + case class ViewModel( + navigation: NavigationBar.ViewModel, + content: Option[HtmlElement] + ) + def apply(actionBus: Observer[Action])( + $m: Signal[ViewModel], + mods: Modifier[HtmlElement]* + )(using router: Router[Page]): HtmlElement = + val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) + div( + cls := "h-full flex flex-col", + NavigationBar($m.map(_.navigation)), + child.maybe <-- router.$currentPage.map(_.isRoot) + .switch(None, Some(PageHeader(actionBus))), + main( + cls := "flex-grow-1 overflow-y-auto", + mods, + child <-- $maybeContent.map(_.getOrElse(Loading)) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/mdr/pdb/app/components/PageLink.scala new file mode 100644 index 0000000..e6e7052 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageLink.scala @@ -0,0 +1,40 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.LinkSupport.* +import mdr.pdb.app.Page +import mdr.pdb.app.Action +import com.raquo.waypoint.Router +import mdr.pdb.app.NavigateTo + +object PageLink: + type ViewModel = Page + def apply($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = + a(mods($m, actions), child.text <-- $m.map(_.title)) + + def apply(m: ViewModel, actions: Observer[Action])(using + router: Router[Page] + ): Anchor = apply(Val(m), actions) + + def container($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = a(mods($m, actions)) + + def container(m: ViewModel, actions: Observer[Action])(using + Router[Page] + ): Anchor = container(Val(m), actions) + + private def mods($m: Signal[Page], actions: Observer[Action])(using + router: Router[Page] + ): Modifier[Anchor] = + Seq( + href <-- $m.map(router.absoluteUrlForPage).recover { case _ => + Some("invalid url") + }, + composeEvents(onClick.noKeyMod.preventDefault)( + _.sample($m) + .map(NavigateTo.apply) + ) --> actions + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala new file mode 100644 index 0000000..7682772 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala @@ -0,0 +1,13 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.pages.dashboard.DashboardPage +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage + +class DashboardPageConnector(actionBus: Observer[Action])(using + router: Router[Page] +): + def apply: HtmlElement = + AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala new file mode 100644 index 0000000..76b9133 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala @@ -0,0 +1,76 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.Parameter +import mdr.pdb.UserInfo +import mdr.pdb.app.pages.detail.DetailParametruPage +import pages.detail.DetailKriteriaPage +import pages.detail.components.DukazyKriteria +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.ParameterCriteria + +object DetailKriteriaPageConnector { + trait AppState { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + } +} + +case class DetailKriteriaPageConnector( + state: DetailKriteriaPageConnector.AppState +)( + $page: Signal[Page.DetailKriteria] +)(using Router[Page]): + val $paramChangeSignal = + $page.splitOne(p => + (p.osobniCislo.value, p.parametr.value, p.kriterium.value) + )((x, _, _) => x) + val $pageChangeSignal = + $paramChangeSignal.map( + FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) + ) + + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + val $merged = + $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => + for { + da <- d + pa <- p + pb <- pa.find(_.id == pc._2) + ka <- pb.criteria.find(_.id == pc._3) + } yield (da, pb, ka) + ) + + def apply: HtmlElement = + AppPage(state.actionBus)( + $merged.map(_.map(buildModel)) + .split(_ => ())((_, s, $s) => + DetailKriteriaPage($s)(state.actionBus.contramap { + case DukazyKriteria.Add => + NavigateTo( + Page.UpravDukazKriteria( + Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), + Page.Titled(s.parametr.id, Some(s.parametr.nazev)), + Page.Titled(s.kriterium.id, Some(s.kriterium.id)) + ) + ) + }) + ), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: Parameter, + k: ParameterCriteria + ): DetailKriteriaPage.ViewModel = + DetailKriteriaPage.ViewModel( + o.toDetailOsoby, + p.toParametr(_ => a()), + k.toKriterium(_ => a()) + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DetailPageConnector.scala new file mode 100644 index 0000000..df3a6b7 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DetailPageConnector.scala @@ -0,0 +1,57 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.UserInfo +import pages.detail.DetailPage +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.pages.detail.components.DetailOsoby +import mdr.pdb.Parameter +import mdr.pdb.app.pages.detail.components.SeznamParametru +import fiftyforms.ui.components.tailwind.Color + +object DetailPageConnector { + trait AppState { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + } +} + +case class DetailPageConnector(state: DetailPageConnector.AppState)( + $page: Signal[Page.Detail] +)(using router: Router[Page]): + val $oscChangeSignal = $page.splitOne(_.osobniCislo.value)((osc, _, _) => osc) + val $pageChangeSignal = + $oscChangeSignal.flatMap(osc => + EventStream.fromSeq(Seq(FetchUserDetails(osc), FetchParameters(osc))) + ) + // TODO: filter the value based on the current osc + // OSC change will fetch new data, but still + // - we need to be sure that what we got is really what we ought to display + // - we want to display stale data accordingly (at least with loading indicator) + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + def apply: HtmlElement = + AppPage(state.actionBus)( + $data.combineWithFn($params)(_ zip _) + .map(_.map(buildModel)) + .split(_ => ())((_, _, s) => DetailPage(s)), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: List[Parameter] + ): DetailPage.ViewModel = + DetailPage.ViewModel( + o.toDetailOsoby, + p.map( + _.toParametr(param => + PageLink.container(Page.DetailParametru(o, param), state.actionBus) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DetailParametruPageConnector.scala new file mode 100644 index 0000000..fa7193d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DetailParametruPageConnector.scala @@ -0,0 +1,67 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.Parameter +import mdr.pdb.UserInfo +import mdr.pdb.app.pages.detail.DetailParametruPage +import pages.detail.DetailParametruPage +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.components.PageLink + +object DetailParametruPageConnector { + trait AppState { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + } +} + +case class DetailParametruPageConnector( + state: DetailParametruPageConnector.AppState +)( + $page: Signal[Page.DetailParametru] +)(using router: Router[Page]): + val $paramChangeSignal = + $page.splitOne(p => (p.osobniCislo.value, p.parametr.value))((x, _, _) => x) + val $pageChangeSignal = + $paramChangeSignal.map(FetchParameter(_, _)) + + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + val $merged = + $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => + for { + da <- d + pa <- p + pb <- pa.find(_.id == pc._2) + } yield (da, pb) + ) + + def apply: HtmlElement = + AppPage(state.actionBus)( + $merged.map(_.map(buildModel)) + .split(_ => ())((_, _, s) => DetailParametruPage(s)), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: Parameter + ): DetailParametruPage.ViewModel = + DetailParametruPage.ViewModel( + o.toDetailOsoby, + p.toParametr(p => + PageLink.container(Page.DetailParametru(o, p), state.actionBus) + ), + p.criteria.map( + _.toKriterium { c => + PageLink.container( + Page.DetailKriteria(o, p, c), + state.actionBus + ) + } + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DirectoryPageConnector.scala new file mode 100644 index 0000000..ab2c045 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DirectoryPageConnector.scala @@ -0,0 +1,34 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.UserInfo +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.components.AppPage + +case class DirectoryPageConnector( + $input: EventStream[List[UserInfo]], + actionBus: Observer[Action] +)(using router: Router[Page]): + val $data = $input.startWithNone + val $actionSignal = EventStream.fromValue(FetchDirectory) + + def apply: HtmlElement = + AppPage(actionBus)( + $data.split(_ => ())((_, _, s) => + pages.directory.DirectoryPage( + s.map( + _.map( + _.toUserRow(u => + PageLink.container( + Page.Detail(Page.Titled(u.personalNumber)), + actionBus + ) + ) + ) + ) + ) + ), + $actionSignal --> actionBus + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/mdr/pdb/app/connectors/conversions.scala new file mode 100644 index 0000000..f581796 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/conversions.scala @@ -0,0 +1,68 @@ +package mdr.pdb.app.connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.UserInfo +import mdr.pdb.app.pages.detail.components.DetailOsoby +import mdr.pdb.Parameter +import mdr.pdb.app.pages.detail.components.SeznamParametru +import mdr.pdb.ParameterCriteria +import mdr.pdb.app.pages.detail.components.SeznamKriterii +import mdr.pdb.app.pages.directory.components.UserRow +import mdr.pdb.app.pages.detail.components.DetailParametru +import mdr.pdb.app.pages.detail.components.DetailKriteria +import fiftyforms.ui.components.tailwind.Color + +extension (o: UserInfo) + def toDetailOsoby: DetailOsoby.ViewModel = + DetailOsoby.ViewModel( + o.personalNumber, + o.name, + o.email, + o.phone, + o.img, + o.mainFunction.map(f => + DetailOsoby.Funkce.ViewModel(f.name, f.dept, f.ou) + ), + o.userContracts.headOption.map(c => + DetailOsoby.PracovniPomer.ViewModel(c.rel, c.startDate, c.endDate) + ) + ) + +extension (param: Parameter) + def toParametr(container: Parameter => Anchor): DetailParametru.ViewModel = + DetailParametru.ViewModel( + id = param.id, + nazev = param.name, + popis = param.description, + status = "Nesplněno", + statusColor = Color.red, + a = container(param) + ) + +extension (crit: ParameterCriteria) + def toKriterium( + container: ParameterCriteria => Anchor + ): DetailKriteria.ViewModel = + DetailKriteria.ViewModel( + nazev = crit.criteriumText, + kapitola = crit.chapterId, + bod = crit.itemId, + status = "Nesplněno", + statusColor = Color.red, + splneno = false, + dukazy = Nil, + container = container(crit) + ) + +extension (user: UserInfo) + def toUserRow( + container: UserInfo => HtmlElement = (_: UserInfo) => div() + ): UserRow.ViewModel = + UserRow.ViewModel( + osobniCislo = user.personalNumber.toString, + celeJmeno = user.name, + prijmeni = user.surname, + hlavniFunkce = user.mainFunction.map(_.name), + img = user.img, + container = () => container(user) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/dashboard/DashboardPage.scala b/app/src/main/scala/mdr/pdb/app/pages/dashboard/DashboardPage.scala new file mode 100644 index 0000000..0eb8f7a --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/dashboard/DashboardPage.scala @@ -0,0 +1,11 @@ +package mdr.pdb.app.pages.dashboard + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.Page + +object DashboardPage: + + def render: HtmlElement = + div("Dashboard page") diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala new file mode 100644 index 0000000..ff3a25f --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala @@ -0,0 +1,31 @@ +package mdr.pdb.app.pages.detail + +import com.raquo.laminar.api.L.{*, given} + +import components._ + +object DetailKriteriaPage: + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametr: DetailParametru.ViewModel, + kriterium: DetailKriteria.ViewModel + ) + + def apply($m: Signal[ViewModel])( + action: Observer[DukazyKriteria.Action] + ): HtmlElement = + div( + cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + div( + DetailOsoby.header($m.map(_.osoba)), + DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") + ), + div( + DetailKriteria($m.map(_.kriterium)), + DukazyKriteria($m.map(_.kriterium.dukazy))(action) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/DetailPage.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailPage.scala new file mode 100644 index 0000000..b591cc9 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailPage.scala @@ -0,0 +1,25 @@ +package mdr.pdb.app.pages.detail + +import com.raquo.laminar.api.L.{*, given} + +import components._ +import mdr.pdb.app.Page +import com.raquo.waypoint.Router +import mdr.pdb.app.Action + +object DetailPage: + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametry: SeznamParametru.ViewModel + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + DetailOsoby($m.map(_.osoba)), + SeznamParametru($m.map(_.parametry)) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/DetailParametruPage.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailParametruPage.scala new file mode 100644 index 0000000..3322279 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailParametruPage.scala @@ -0,0 +1,24 @@ +package mdr.pdb.app.pages.detail + +import com.raquo.laminar.api.L.{*, given} + +import components._ + +object DetailParametruPage: + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametr: DetailParametru.ViewModel, + kriteria: SeznamKriterii.ViewModel + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + DetailOsoby.header($m.map(_.osoba)), + DetailParametru($m.map(_.parametr)), + SeznamKriterii($m.map(_.kriteria)) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala new file mode 100644 index 0000000..b58618a --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala @@ -0,0 +1,107 @@ +package mdr.pdb +package app +package pages.detail + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.pages.detail.components.UpravDukazForm +import fiftyforms.services.files.File + +object UpravDukaz: + + type ThisPage = Page.UpravDukazKriteria + type PageKey = (OsobniCislo, String, String) + + trait State { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def availableFiles: EventStream[List[File]] + def actionBus: Observer[Action] + } + + def keyOfPage(page: ThisPage): PageKey = + (page.osobniCislo.value, page.parametr.value, page.kriterium.value) + + def onChangeAction(key: PageKey): Action = + FetchParameterCriteria( + key._1, + key._2, + key._3, + Page.UpravDukazKriteria(_, _, _) + ) + + class Connector(state: State)($page: Signal[ThisPage])(using Router[Page]): + val $paramChangeSignal = $page.splitOne(keyOfPage)((x, _, _) => x) + val $pageChangeSignal = $paramChangeSignal.map(onChangeAction) + + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + val $merged = + $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => + for { + da <- d + pa <- p + pb <- pa.find(_.id == pc._2) + ka <- pb.criteria.find(_.id == pc._3) + } yield (da, pb, ka) + ) + + def apply: HtmlElement = + AppPage(state.actionBus)( + $merged.split(_ => ())((_, s, $s) => + PageComponent( + $s.map(buildModel), + state.availableFiles, + state.actionBus.contramap { + case UpravDukazForm.Cancelled => + NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) + case UpravDukazForm.AvailableFilesRequested => + FetchAvailableFiles(s._1.personalNumber) + } + ) + ), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: Parameter, + k: ParameterCriteria + ): PageComponent.ViewModel = + import connectors.* + PageComponent.ViewModel( + o.toDetailOsoby, + p.toParametr(_ => a()), + k.toKriterium(_ => a()) + ) + + object PageComponent: + import components.* + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametr: DetailParametru.ViewModel, + kriterium: DetailKriteria.ViewModel + ) + + def apply( + $m: Signal[ViewModel], + availableFilesStream: EventStream[List[File]], + events: Observer[UpravDukazForm.Event] + ): HtmlElement = + div( + cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + div( + DetailOsoby.header($m.map(_.osoba)), + DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") + ), + div( + DetailKriteria($m.map(_.kriterium)), + UpravDukazForm(availableFilesStream)(events) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailKriteria.scala new file mode 100644 index 0000000..a3ab634 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailKriteria.scala @@ -0,0 +1,37 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.CustomAttrs +import fiftyforms.ui.components.tailwind.Icons +import java.time.LocalDate +import fiftyforms.ui.components.tailwind.Color + +object DetailKriteria: + case class ViewModel( + nazev: String, + kapitola: String, + bod: String, + status: String, + statusColor: Color, + splneno: Boolean, + dukazy: List[DukazKriteria.ViewModel], + container: HtmlElement = div() + ) { + val id = s"${kapitola}${bod}" + } + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "pb-5 border-b border-gray-200", + h3( + cls := "text-xl leading-6 font-bold text-gray-900", + child.text <-- $m.map(_.nazev) + ), + h3( + cls := "mt-2 max-w-4xl text-sm text-gray-500", + "Kapitola ", + child.text <-- $m.map(_.kapitola), + " bod ", + child <-- $m.map(_.bod) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailOsoby.scala new file mode 100644 index 0000000..0fe2ba5 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailOsoby.scala @@ -0,0 +1,100 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.OsobniCislo + +import java.time.LocalDate +import fiftyforms.ui.components.tailwind.Avatar + +object DetailOsoby: + + object Funkce: + case class ViewModel( + nazev: String, + stredisko: String, + voj: String + ) + + def render($m: Signal[ViewModel]): HtmlElement = + p( + cls := "text-sm font-medium text-gray-500", + child.text <-- $m.map(_.nazev), + span( + cls := "hidden md:inline", + " @ ", + child.text <-- $m.map(_.stredisko), + ", ", + child.text <-- $m.map(_.voj) + ) + ) + + object PracovniPomer: + case class ViewModel( + druh: String, + pocatek: LocalDate, + konec: Option[LocalDate] + ) + + def render($m: Signal[ViewModel]): HtmlElement = + import fiftyforms.ui.components.tailwind.CustomAttrs.datetime + p( + cls := "text-sm font-medium text-gray-500", + child.text <-- $m.map(_.druh), + " od ", + time( + datetime <-- $m.map(_.pocatek.toString), + child.text <-- $m.map(_.pocatek.toString) + ) + ) + + case class ViewModel( + osobniCislo: OsobniCislo, + jmeno: String, + email: Option[String], + telefon: Option[String], + img: Option[String], + hlavniFunkce: Option[Funkce.ViewModel], + pracovniPomer: Option[PracovniPomer.ViewModel] + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "md:flex md:items-center md:justify-between md:space-x-4", + div( + cls := "flex items-start space-x-4", + div( + cls := "flex-shrink-0", + Avatar($m.map(_.img)).avatar(16) + ), + div( + h1( + cls := "text-2xl font-bold text-gray-900", + child.text <-- $m.map(_.jmeno) + ), + child.maybe <-- $m.map(_.hlavniFunkce).split(_ => ())((_, _, d) => + Funkce.render(d) + ), + child.maybe <-- $m.map(_.pracovniPomer).split(_ => ())((_, _, d) => + PracovniPomer.render(d) + ) + ) + ) + ) + + def header($m: Signal[ViewModel]): HtmlElement = + div( + cls := "md:flex md:items-center md:justify-between md:space-x-5", + div( + cls := "flex items-start space-x-4", + div( + cls := "flex-shrink-0", + Avatar($m.map(_.img)).avatar(8) + ), + div( + h1( + cls := "text-lg font-medium text-gray-800", + child.text <-- $m.map(_.jmeno) + ) + ) + ) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala deleted file mode 100644 index a3b00af..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala +++ /dev/null @@ -1,103 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import scala.scalajs.js.annotation.JSExportTopLevel -import scala.scalajs.js.annotation.JSExport -import scala.scalajs.js.annotation.JSImport -import scala.scalajs.js -import org.scalajs.dom -import com.raquo.laminar.api.L.{*, given} -import scala.scalajs.js.Date -import com.raquo.waypoint.Router -import com.raquo.waypoint.SplitRender -import cz.e_bs.cmi.mdr.pdb.app.services.DataFetcher -import scala.scalajs.js.JSON -import zio.json._ -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.state.AppState - -@js.native -@JSImport("stylesheets/main.css", JSImport.Namespace) -object Css extends js.Any - -@js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native -@JSImport("params/pdb-params.json", JSImport.Default) -object pdbParams extends js.Object - -@JSExportTopLevel("app") -object Main: - - @JSExport - def main(args: Array[String]): Unit = - import Routes.given - onLoad { - setupAirstream() - val appContainer = dom.document.querySelector("#app") - val _ = - render( - appContainer, - renderPage(state.MockAppState(using unsafeWindowOwner, router)) - ) - } - - private def onLoad(f: => Unit): Unit = - documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) - - private def setupAirstream()(using router: Router[Page]): Unit = - AirstreamError.registerUnhandledErrorCallback(err => - router.forcePage( - Page.UnhandledError( - Some(err.getClass.getName), // TODO: Fill only in dev mode - Some(err.getMessage) - ) - ) - ) - - def renderPage(state: AppState)(using - router: Router[Page] - ): HtmlElement = - val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) - .collectSignal[Page.Detail]( - connectors - .DetailPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailParametru]( - connectors - .DetailParametruPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailKriteria]( - connectors - .DetailKriteriaPageConnector(state)(_) - .apply - ) - .collectSignal[Page.UpravDukazKriteria]( - pages.detail.UpravDukaz.Connector(state)(_).apply - ) - .collectStatic(Page.Dashboard)( - connectors.DashboardPageConnector(state.actionBus).apply - ) - .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) - ) - .collect[Page.UnhandledError](pg => - pages.errors - .UnhandledErrorPage( - pages.errors.UnhandledErrorPage - .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), - state.actionBus - ) - ) - .collectStatic(Page.Directory)( - connectors - .DirectoryPageConnector(state.users, state.actionBus) - .apply - ) - div(cls := "h-full", child <-- pageSplitter.$view) - - // Pull in the stylesheet - val css: Css.type = Css diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala deleted file mode 100644 index 3bbc8c0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ /dev/null @@ -1,198 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.* -import org.scalajs.dom -import zio.json.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import scala.scalajs.js -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.Page.Titled - -// enum is not working with Waypoints' SplitRender collectStatic -sealed abstract class Page( - val id: String, - val title: String, - val parent: Option[Page] -) { - val path: Vector[Page] = - parent match - case None => Vector(this) - case Some(p) => p.path :+ this - - val isRoot: Boolean = parent.isEmpty -} - -object Page: - - case class Titled[V](value: V, title: Option[String] = None): - val show: String = title.getOrElse(value.toString) - - case object Directory extends Page("directory", "Adresář", None) - - case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) - - case class Detail(osobniCislo: Titled[OsobniCislo]) - extends Page("user", osobniCislo.show, Some(Directory)) - - object Detail { - def apply(o: UserInfo): Detail = Detail( - Titled(o.personalNumber, Some(o.name)) - ) - } - - case class DetailParametru( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String] - ) extends Page( - "parameter", - parametr.show, - Some(Detail(osobniCislo)) - ) - - object DetailParametru { - def apply(o: UserInfo, p: Parameter): DetailParametru = - DetailParametru( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)) - ) - } - - case class DetailKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "criteria", - kriterium.show, - Some(DetailParametru(osobniCislo, parametr)) - ) - - object DetailKriteria { - def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = - DetailKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class UpravDukazKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "addProof", - "Důkaz", - Some(DetailKriteria(osobniCislo, parametr, kriterium)) - ) - - object UpravDukazKriteria { - def apply( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): UpravDukazKriteria = - UpravDukazKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class NotFound(url: String) extends Page("404", "404", Some(Directory)) - - case class UnhandledError( - errorName: Option[String], - errorMessage: Option[String] - ) extends Page("500", "Unexpected error", Some(Directory)) - -object Routes: - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) - given [V: JsonEncoder]: JsonEncoder[Titled[V]] = - DeriveJsonEncoder.gen[Titled[V]] - given [V: JsonDecoder]: JsonDecoder[Titled[V]] = - DeriveJsonDecoder.gen[Titled[V]] - given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] - given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] - - val base = - js.`import`.meta.env.BASE_URL - .asInstanceOf[String] + "app" - - val homePage: Page = Page.Directory - - given router: Router[Page] = Router[Page]( - routes = List( - Route.static(homePage, root / endOfSegments, basePath = base), - Route.static( - Page.Dashboard, - root / "dashboard" / endOfSegments, - basePath = base - ), - Route[Page.Detail, String]( - encode = _.osobniCislo.value.toString, - decode = osc => Page.Detail(Titled(OsobniCislo(osc))), - root / "osoba" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.DetailParametru, (String, String)]( - encode = p => (p.osobniCislo.value.toString, p.parametr.value), - decode = - p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / endOfSegments, - basePath = base - ), - Route[Page.DetailKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.DetailKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.UpravDukazKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.UpravDukazKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / "edit" / endOfSegments, - basePath = base - ) - ), - serializePage = _.toJson, - deserializePage = _.fromJson[Page] - .fold(s => throw IllegalStateException(s), identity), - getPageTitle = _.title, - routeFallback = url => Page.NotFound(url), - deserializeFallback = _ => Page.Dashboard - )( - $popStateEvent = windowEvents.onPopState, - owner = unsafeWindowOwner - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala deleted file mode 100644 index b24b82b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -sealed trait Action - -case object FetchDirectory extends Action -case class FetchUserDetails(osc: OsobniCislo) extends Action -case class FetchParameters(osc: OsobniCislo) extends Action -case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action -case class FetchParameterCriteria( - osc: OsobniCislo, - paramId: String, - critId: String, - page: (UserInfo, Parameter, ParameterCriteria) => Page -) extends Action -case class FetchAvailableFiles(osc: OsobniCislo) extends Action -case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala deleted file mode 100644 index c637d35..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala +++ /dev/null @@ -1,82 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.{UserProfile, UserInfo, OsobniCislo} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo -import cz.e_bs.cmi.mdr.pdb.UserFunction - -object AppPage: - // TODO: pages by logged in user - val pages: List[Page] = List(Page.Directory, Page.Dashboard) - - import NavigationBar.{Logo, MenuItem} - - val logo = Logo( - "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", - "Workflow" - ) - - // TODO: menu items by user profile - val userMenu = - List( - MenuItem("Your Profile"), - MenuItem("Settings"), - MenuItem("Sign out") - ) - - // TODO: load user profile - val $userProfile = Var( - UserProfile( - "tom", - UserInfo( - OsobniCislo("1031"), - "tom", - "Tom", - "Cook", - None, - None, - Some("tom@example.com"), - Some("+420 222 866 180"), - Some( - UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") - ), - Nil, - None - ) - ) - ) - - val $userInfo = $userProfile.signal.map(_.userInfo) - - type ViewModel = Option[HtmlElement] - def apply( - actionBus: Observer[Action] - )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using - router: Router[Page] - ): HtmlElement = - PageLayout(actionBus)( - $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => - PageLayout.ViewModel( - NavigationBar.ViewModel( - u, - pages.map(p => - NavigationBar.Link( - () => - PageLink( - p, - actionBus - ), - p == cp - ) - ), - userMenu, - logo - ), - c - ) - ), - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala deleted file mode 100644 index bf473c6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ /dev/null @@ -1,143 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden -import fiftyforms.ui.components.tailwind.list.IconText.ViewModel -import fiftyforms.ui.components.tailwind.Icons -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Action - -object Breadcrumbs: - - private def slash = - import svg.{*, given} - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-300", - xmlns := "http://www.w3.org/2000/svg", - fill := "currentColor", - viewBox := "0 0 20 20", - ariaHidden := true, - path( - d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" - ) - ) - - def backIcon = - Icons.solid - .`arrow-narrow-left`() - .amend( - svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" - ) - - object Link: - case class ViewModel( - page: Page, - icon: Option[SvgElement], - extraClasses: String, - text: String, - textClasses: Option[String] = None - ) - - val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" - - def shortHome(p: Page) = ViewModel( - p, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - "Zpět na úvodní stránku", - None - ) - - def fullHome(p: Page) = ViewModel( - p, - Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), - "text-gray-400 hover:text-gray-500", - "Domů", - Some("sr-only") - ) - - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - PageLink - .container($m.map(_.page), actionBus) - .amend( - cls <-- $m.map(_.extraClasses), - child.maybe <-- $m.map(_.icon), - span( - cls <-- $m.map(_.textClasses.getOrElse("")), - child.text <-- $m.map(_.text) - ) - ) - - object FullBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - ol( - role := "list", - cls := "flex items-center space-x-4", - children <-- $m.map(_.path).split(_.id)((_, _, $p) => - li( - div( - cls := "flex items-center", - child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), - Link( - $p.map(p => - if (p.isRoot) then Link.fullHome(p) - else - Link.ViewModel( - p, - None, - s"ml-4 max-w-xs truncate ${Link.baseClasses}", - p.title - ) - ), - actionBus - ) - ) - ) - ) - ) - - object ShortBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - Link( - $m.map { p => - val target = p.parent.getOrElse(p) - if target.isRoot then Link.shortHome(target) - else - Link.ViewModel( - target, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - s"Zpět na ${target.title}" - ) - }, - actionBus - ) - - def apply(actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - nav( - cls := "flex", - aria.label := "Breadcrumb", - div( - cls := "flex sm:hidden", - ShortBreadcrumbs(router.$currentPage, actionBus) - ), - div( - cls := "hidden sm:block", - FullBreadcrumbs(router.$currentPage, actionBus) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala deleted file mode 100644 index 0db2047..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ /dev/null @@ -1,242 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent -import fiftyforms.ui.components.tailwind.Icons -import fiftyforms.ui.components.tailwind.Avatar -import cz.e_bs.cmi.mdr.pdb.UserInfo -import io.laminext.syntax.core.* - -object NavigationBar: - - case class Logo(img: String, name: String) - case class Link(a: () => Anchor, active: Boolean) - case class MenuItem(title: String) - - case class ViewModel( - userInfo: UserInfo, - pages: List[Link], - userMenu: List[MenuItem], - logo: Logo - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - val $userInfo = $m.map(_.userInfo) - val mobileMenuOpen = Var(false) - - val desktopOnly = cls("hidden md:block") - val mobileOnly = cls("md:hidden") - - inline def avatarImage(size: Int = 8) = - Avatar($userInfo.map(_.img)).avatarImage(size) - - def notificationButton = button( - tpe := "button", - cls := List( - "bg-indigo-600", - "focus:outline-none", - "focus:ring-2", - "focus:ring-offset-2", - "focus:ring-offset-indigo-600", - "focus:ring-white", - "hover:text-white", - "p-1", - "rounded-full", - "text-indigo-200" - ), - span(cls := "sr-only", "View notifications"), - Icons.outline.bell() - ) - - def userProfile: HtmlElement = - val menuOpen = Var(false) - - def menuItem(item: MenuItem, idx: Int): HtmlElement = - a( - href := "#", - cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", - role := "menuitem", - tabIndex := -1, - idAttr := s"user-menu-item-$idx", - item.title - ) - - div( - cls := "ml-3 relative", - div( - button( - tpe := "button", - cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - idAttr := "user-menu-button", - aria.expanded <-- menuOpen.signal, - aria.hasPopup := true, - span(cls := "sr-only", "Open user menu"), - child <-- avatarImage(), - onClick.preventDefault.mapTo( - !menuOpen.now() - ) --> menuOpen.writer - ) - ), - /* - * */ - div( - cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", - cls <-- menuOpen.signal.map { o => - if (o) "md:block" else "md:hidden" - }, - role := "menu", - aria.orientation := "vertical", - aria.labelledBy := "user-menu-button", - tabIndex := -1, - // : keyboard navigation - children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) - ) - ) - - def mobileProfile = - def menuItem(item: MenuItem): HtmlElement = - a( - href := "#", - cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", - item.title - ) - - div( - cls := "pt-4 pb-3 border-t border-indigo-700", - div( - cls := "flex items-center px-5", - div( - cls := "flex-shrink-0", - child <-- avatarImage(10) - ), - div( - cls := "ml-3", - div( - cls := "text-base font-medium text-white", - child.text <-- $userInfo.map(_.name) - ), - child.maybe <-- $userInfo.map( - _.email.map(e => - div( - cls := "text-sm font-medium text-indigo-300", - e - ) - ) - ) - ), - notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) - ), - div( - cls := "mt-3 px-2 space-y-1", - children <-- $m.map(_.userMenu.map(menuItem)) - ) - ) - - def pageLink(page: Link): Anchor = - page - .a() - .amend( - cls := "text-white px-3 py-2 rounded-md text-sm font-medium", - cls := Seq( - "bg-indigo-700" -> page.active, - "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active - ), - ariaCurrent := (if page.active then "page" else "false") - ) - - def logoImg: Image = - img( - cls := "h-8 w-8", - src <-- $m.map(_.logo.img), - alt <-- $m.map(_.logo.name) - ) - - def mobileMenuButton = button( - tpe := "button", - cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - aria.controls := "mobile-menu", - aria.expanded <-- mobileMenuOpen.signal, - span(cls := "sr-only", "Open main menu"), - Icons.outline - .menu() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") - ), - Icons.outline - .x() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") - ), - onClick.preventDefault.mapTo( - !mobileMenuOpen.now() - ) --> mobileMenuOpen.writer - ) - - def navBarLeft = - div( - cls := "flex items-center", - div(cls := "flex-shrink-0", logoImg), - div( - desktopOnly, - div( - cls := "ml-10 flex items-baseline space-x-4", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ) - ) - ) - - def navBarRight = - div( - desktopOnly, - div( - cls := "ml-4 flex items-center md:ml-6", - notificationButton, - userProfile - ) - ) - - def navBarMobile = - div( - cls := "-mr-2 flex", - mobileOnly, - mobileMenuButton - ) - - def navBar = - div( - cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex items-center justify-between h-16", - navBarLeft, - navBarRight, - navBarMobile - ) - ) - - def mobileMenu = - div( - mobileOnly, - cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }, - idAttr := "mobile-menu", - div( - cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ), - mobileProfile - ) - - nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala deleted file mode 100644 index 75339df..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object PageHeader: - def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = - header( - cls := "bg-white shadow-sm", - div( - cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", - h1( - cls := "text-lg leading-6 font-semibold text-gray-900", - Breadcrumbs(actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala deleted file mode 100644 index 5096463..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ /dev/null @@ -1,30 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import fiftyforms.ui.components.tailwind.Loading -import io.laminext.syntax.core.* - -object PageLayout: - case class ViewModel( - navigation: NavigationBar.ViewModel, - content: Option[HtmlElement] - ) - def apply(actionBus: Observer[Action])( - $m: Signal[ViewModel], - mods: Modifier[HtmlElement]* - )(using router: Router[Page]): HtmlElement = - val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) - div( - cls := "h-full flex flex-col", - NavigationBar($m.map(_.navigation)), - child.maybe <-- router.$currentPage.map(_.isRoot) - .switch(None, Some(PageHeader(actionBus))), - main( - cls := "flex-grow-1 overflow-y-auto", - mods, - child <-- $maybeContent.map(_.getOrElse(Loading)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala deleted file mode 100644 index 62d820f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.LinkSupport.* -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo - -object PageLink: - type ViewModel = Page - def apply($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = - a(mods($m, actions), child.text <-- $m.map(_.title)) - - def apply(m: ViewModel, actions: Observer[Action])(using - router: Router[Page] - ): Anchor = apply(Val(m), actions) - - def container($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = a(mods($m, actions)) - - def container(m: ViewModel, actions: Observer[Action])(using - Router[Page] - ): Anchor = container(Val(m), actions) - - private def mods($m: Signal[Page], actions: Observer[Action])(using - router: Router[Page] - ): Modifier[Anchor] = - Seq( - href <-- $m.map(router.absoluteUrlForPage).recover { case _ => - Some("invalid url") - }, - composeEvents(onClick.noKeyMod.preventDefault)( - _.sample($m) - .map(NavigateTo.apply) - ) --> actions - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala deleted file mode 100644 index 87f825a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.pages.dashboard.DashboardPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -class DashboardPageConnector(actionBus: Observer[Action])(using - router: Router[Page] -): - def apply: HtmlElement = - AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala deleted file mode 100644 index e9b0f71..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala +++ /dev/null @@ -1,76 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailKriteriaPage -import pages.detail.components.DukazyKriteria -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -object DetailKriteriaPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailKriteriaPageConnector( - state: DetailKriteriaPageConnector.AppState -)( - $page: Signal[Page.DetailKriteria] -)(using Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => - (p.osobniCislo.value, p.parametr.value, p.kriterium.value) - )((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map( - FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) - ) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, s, $s) => - DetailKriteriaPage($s)(state.actionBus.contramap { - case DukazyKriteria.Add => - NavigateTo( - Page.UpravDukazKriteria( - Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), - Page.Titled(s.parametr.id, Some(s.parametr.nazev)), - Page.Titled(s.kriterium.id, Some(s.kriterium.id)) - ) - ) - }) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): DetailKriteriaPage.ViewModel = - DetailKriteriaPage.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala deleted file mode 100644 index 4d8024f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ /dev/null @@ -1,57 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import pages.detail.DetailPage -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import fiftyforms.ui.components.tailwind.Color - -object DetailPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailPageConnector(state: DetailPageConnector.AppState)( - $page: Signal[Page.Detail] -)(using router: Router[Page]): - val $oscChangeSignal = $page.splitOne(_.osobniCislo.value)((osc, _, _) => osc) - val $pageChangeSignal = - $oscChangeSignal.flatMap(osc => - EventStream.fromSeq(Seq(FetchUserDetails(osc), FetchParameters(osc))) - ) - // TODO: filter the value based on the current osc - // OSC change will fetch new data, but still - // - we need to be sure that what we got is really what we ought to display - // - we want to display stale data accordingly (at least with loading indicator) - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - def apply: HtmlElement = - AppPage(state.actionBus)( - $data.combineWithFn($params)(_ zip _) - .map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: List[Parameter] - ): DetailPage.ViewModel = - DetailPage.ViewModel( - o.toDetailOsoby, - p.map( - _.toParametr(param => - PageLink.container(Page.DetailParametru(o, param), state.actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala deleted file mode 100644 index fdaa7f8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailParametruPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink - -object DetailParametruPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailParametruPageConnector( - state: DetailParametruPageConnector.AppState -)( - $page: Signal[Page.DetailParametru] -)(using router: Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => (p.osobniCislo.value, p.parametr.value))((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map(FetchParameter(_, _)) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - } yield (da, pb) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailParametruPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter - ): DetailParametruPage.ViewModel = - DetailParametruPage.ViewModel( - o.toDetailOsoby, - p.toParametr(p => - PageLink.container(Page.DetailParametru(o, p), state.actionBus) - ), - p.criteria.map( - _.toKriterium { c => - PageLink.container( - Page.DetailKriteria(o, p, c), - state.actionBus - ) - } - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala deleted file mode 100644 index 049aa22..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -case class DirectoryPageConnector( - $input: EventStream[List[UserInfo]], - actionBus: Observer[Action] -)(using router: Router[Page]): - val $data = $input.startWithNone - val $actionSignal = EventStream.fromValue(FetchDirectory) - - def apply: HtmlElement = - AppPage(actionBus)( - $data.split(_ => ())((_, _, s) => - pages.directory.DirectoryPage( - s.map( - _.map( - _.toUserRow(u => - PageLink.container( - Page.Detail(Page.Titled(u.personalNumber)), - actionBus - ) - ) - ) - ) - ) - ), - $actionSignal --> actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala deleted file mode 100644 index 88e7450..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ /dev/null @@ -1,68 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii -import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria -import fiftyforms.ui.components.tailwind.Color - -extension (o: UserInfo) - def toDetailOsoby: DetailOsoby.ViewModel = - DetailOsoby.ViewModel( - o.personalNumber, - o.name, - o.email, - o.phone, - o.img, - o.mainFunction.map(f => - DetailOsoby.Funkce.ViewModel(f.name, f.dept, f.ou) - ), - o.userContracts.headOption.map(c => - DetailOsoby.PracovniPomer.ViewModel(c.rel, c.startDate, c.endDate) - ) - ) - -extension (param: Parameter) - def toParametr(container: Parameter => Anchor): DetailParametru.ViewModel = - DetailParametru.ViewModel( - id = param.id, - nazev = param.name, - popis = param.description, - status = "Nesplněno", - statusColor = Color.red, - a = container(param) - ) - -extension (crit: ParameterCriteria) - def toKriterium( - container: ParameterCriteria => Anchor - ): DetailKriteria.ViewModel = - DetailKriteria.ViewModel( - nazev = crit.criteriumText, - kapitola = crit.chapterId, - bod = crit.itemId, - status = "Nesplněno", - statusColor = Color.red, - splneno = false, - dukazy = Nil, - container = container(crit) - ) - -extension (user: UserInfo) - def toUserRow( - container: UserInfo => HtmlElement = (_: UserInfo) => div() - ): UserRow.ViewModel = - UserRow.ViewModel( - osobniCislo = user.personalNumber.toString, - celeJmeno = user.name, - prijmeni = user.surname, - hlavniFunkce = user.mainFunction.map(_.name), - img = user.img, - container = () => container(user) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala deleted file mode 100644 index 7d5b2c1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.dashboard - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.Page - -object DashboardPage: - - def render: HtmlElement = - div("Dashboard page") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala deleted file mode 100644 index 9ef0698..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala +++ /dev/null @@ -1,31 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailKriteriaPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply($m: Signal[ViewModel])( - action: Observer[DukazyKriteria.Action] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - DukazyKriteria($m.map(_.kriterium.dukazy))(action) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala deleted file mode 100644 index 3b316e1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object DetailPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametry: SeznamParametru.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby($m.map(_.osoba)), - SeznamParametru($m.map(_.parametry)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala deleted file mode 100644 index 17224cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala +++ /dev/null @@ -1,24 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailParametruPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriteria: SeznamKriterii.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby.header($m.map(_.osoba)), - DetailParametru($m.map(_.parametr)), - SeznamKriterii($m.map(_.kriteria)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala deleted file mode 100644 index dc912fa..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ /dev/null @@ -1,107 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb -package app -package pages.detail - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.UpravDukazForm -import fiftyforms.services.files.File - -object UpravDukaz: - - type ThisPage = Page.UpravDukazKriteria - type PageKey = (OsobniCislo, String, String) - - trait State { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def availableFiles: EventStream[List[File]] - def actionBus: Observer[Action] - } - - def keyOfPage(page: ThisPage): PageKey = - (page.osobniCislo.value, page.parametr.value, page.kriterium.value) - - def onChangeAction(key: PageKey): Action = - FetchParameterCriteria( - key._1, - key._2, - key._3, - Page.UpravDukazKriteria(_, _, _) - ) - - class Connector(state: State)($page: Signal[ThisPage])(using Router[Page]): - val $paramChangeSignal = $page.splitOne(keyOfPage)((x, _, _) => x) - val $pageChangeSignal = $paramChangeSignal.map(onChangeAction) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.split(_ => ())((_, s, $s) => - PageComponent( - $s.map(buildModel), - state.availableFiles, - state.actionBus.contramap { - case UpravDukazForm.Cancelled => - NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) - case UpravDukazForm.AvailableFilesRequested => - FetchAvailableFiles(s._1.personalNumber) - } - ) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): PageComponent.ViewModel = - import connectors.* - PageComponent.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) - - object PageComponent: - import components.* - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply( - $m: Signal[ViewModel], - availableFilesStream: EventStream[List[File]], - events: Observer[UpravDukazForm.Event] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - UpravDukazForm(availableFilesStream)(events) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala deleted file mode 100644 index ca311b0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.Icons -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Color - -object DetailKriteria: - case class ViewModel( - nazev: String, - kapitola: String, - bod: String, - status: String, - statusColor: Color, - splneno: Boolean, - dukazy: List[DukazKriteria.ViewModel], - container: HtmlElement = div() - ) { - val id = s"${kapitola}${bod}" - } - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h3( - cls := "text-xl leading-6 font-bold text-gray-900", - child.text <-- $m.map(_.nazev) - ), - h3( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - "Kapitola ", - child.text <-- $m.map(_.kapitola), - " bod ", - child <-- $m.map(_.bod) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala deleted file mode 100644 index ae90290..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala +++ /dev/null @@ -1,100 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Avatar - -object DetailOsoby: - - object Funkce: - case class ViewModel( - nazev: String, - stredisko: String, - voj: String - ) - - def render($m: Signal[ViewModel]): HtmlElement = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.nazev), - span( - cls := "hidden md:inline", - " @ ", - child.text <-- $m.map(_.stredisko), - ", ", - child.text <-- $m.map(_.voj) - ) - ) - - object PracovniPomer: - case class ViewModel( - druh: String, - pocatek: LocalDate, - konec: Option[LocalDate] - ) - - def render($m: Signal[ViewModel]): HtmlElement = - import fiftyforms.ui.components.tailwind.CustomAttrs.datetime - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.druh), - " od ", - time( - datetime <-- $m.map(_.pocatek.toString), - child.text <-- $m.map(_.pocatek.toString) - ) - ) - - case class ViewModel( - osobniCislo: OsobniCislo, - jmeno: String, - email: Option[String], - telefon: Option[String], - img: Option[String], - hlavniFunkce: Option[Funkce.ViewModel], - pracovniPomer: Option[PracovniPomer.ViewModel] - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-4", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(16) - ), - div( - h1( - cls := "text-2xl font-bold text-gray-900", - child.text <-- $m.map(_.jmeno) - ), - child.maybe <-- $m.map(_.hlavniFunkce).split(_ => ())((_, _, d) => - Funkce.render(d) - ), - child.maybe <-- $m.map(_.pracovniPomer).split(_ => ())((_, _, d) => - PracovniPomer.render(d) - ) - ) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-5", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(8) - ), - div( - h1( - cls := "text-lg font-medium text-gray-800", - child.text <-- $m.map(_.jmeno) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala deleted file mode 100644 index e11cb62..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color - -object DetailParametru: - case class ViewModel( - id: String, - nazev: String, - popis: String, - status: String, - statusColor: Color, - a: Anchor - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h2( - cls := "text-2xl leading-6 font-medium text-gray-900", - child.text <-- $m.map(_.nazev) - ), - p( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - child.text <-- $m.map(_.popis) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - h2( - cls := "text-lg leading-6 font-medium text-gray-600", - child.text <-- $m.map(_.nazev) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala deleted file mode 100644 index a00ede8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala +++ /dev/null @@ -1,177 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.CustomAttrs - -object DukazKriteria: - case class Osoba(osobniCislo: String, jmeno: String) - case class Udalost(osoba: Osoba, datum: LocalDate) - case class Dokument( - url: String, - nazev: String, - pridal: Udalost, - odebral: Option[Udalost] - ) - case class ViewModel( - dokumenty: List[Dokument], - autorizoval: Option[Udalost], - platiDo: Option[LocalDate], - poznámka: Option[String] - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-lg", - div( - cls := "px-4 py-5 sm:px-6", - h3( - cls := "text-lg leading-6 font-medium text-gray-900", - """Applicant Information""" - ), - p( - cls := "mt-1 max-w-2xl text-sm text-gray-500", - """Personal details and application.""" - ) - ), - div( - cls := "border-t border-gray-200 px-4 py-5 sm:px-6", - dl( - cls := "grid grid-cols-1 gap-x-4 gap-y-8 sm:grid-cols-2", - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Full name""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Margot Foster""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Application for""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Backend Developer""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Email address""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """margotfoster@example.com""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Salary expectation""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """$120,000""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """About""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Fugiat ipsum ipsum deserunt culpa aute sint do nostrud anim incididunt cillum culpa consequat. Excepteur qui ipsum aliquip consequat sint. Sit id mollit nulla mollit nostrud in ea officia proident. Irure nostrud pariatur mollit ad adipisicing reprehenderit deserunt qui eu.""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """Attachments""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - ul( - role := "list", - cls := "border border-gray-200 rounded-md divide-y divide-gray-200", - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """resume_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ), - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """coverletter_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala deleted file mode 100644 index 22f134f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala +++ /dev/null @@ -1,35 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object DukazyKriteria: - sealed trait Action - case object Add extends Action - - type ViewModel = List[DukazKriteria.ViewModel] - - def apply($m: Signal[ViewModel])(actions: Observer[Action]): HtmlElement = - div( - child.maybe <-- $m.map(m => - if (m.isEmpty) then Some(prazdnyDukaz(actions)) - else None - ), - children <-- $m.map(_.zipWithIndex).split(_._2)((_, _, $s) => - DukazKriteria($s.map(_._1)) - ) - ) - - private def prazdnyDukaz(actions: Observer[Action]): HtmlElement = - button( - tpe := "button", - onClick.preventDefault.mapTo(Add) --> actions, - cls := "relative block w-full border-2 border-gray-300 border-dashed rounded-lg p-12 text-center hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - Icons.outline - .`document-add`(12) - .amend(svg.cls := "mx-auto text-gray-400"), - span( - cls := "mt-2 block text-sm font-medium text-gray-900", - "Přidat důkaz" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala deleted file mode 100644 index f628697..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.list.StackedList -import fiftyforms.ui.components.tailwind.list.ListRow -import fiftyforms.ui.components.tailwind.list.RowTag -import fiftyforms.ui.components.tailwind.list.RowNext -import java.time.LocalDate - -object SeznamKriterii: - type ViewModel = List[DetailKriteria.ViewModel] - - private val kritList = new StackedList[DetailKriteria.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - kritList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = - p(cls := "text-sm text-gray-500", s"${i.kapitola}${i.bod}"), - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.container - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala deleted file mode 100644 index 2bfe4bc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.list.{ - StackedList, - ListRow, - RowTag, - PropList, - IconText, - RowNext -} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.LinkSupport.* - -object SeznamParametru: - type ViewModel = List[DetailParametru.ViewModel] - - private val parametrList = new StackedList[DetailParametru.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - parametrList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = emptyNode, - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.a - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala deleted file mode 100644 index f048e9d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ /dev/null @@ -1,101 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.form.* -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement -import fiftyforms.services.files.components.tailwind.FilePicker -import cz.e_bs.cmi.mdr.pdb.frontend.AutorizujDukaz -import cz.e_bs.cmi.mdr.pdb.frontend.DocumentRef -import fiftyforms.services.files.File - -object UpravDukazForm: - sealed trait Event - case object Cancelled extends Event - case object AvailableFilesRequested extends Event - def apply(availableFilesStream: EventStream[List[File]])( - updates: Observer[Event] - ): HtmlElement = - val (filesStream, filesObserver) = - EventStream.withObserver[FilePicker.Event] - val files = Var[List[File]](Nil) - def submitButtons: HtmlElement = - div( - cls := "pt-5", - div( - cls := "flex justify-end", - button( - tpe := "button", - cls := "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zrušit", - onClick.mapTo(Cancelled) --> updates - ), - button( - tpe := "submit", - cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - disabled <-- files.signal.map(_.isEmpty), - "Autorizovat" - ) - ) - ) - - div( - filesStream.collect { case FilePicker.SelectionUpdated(files) => - files.to(List) - } --> files.writer, - filesStream.collect { case FilePicker.AvailableFilesRequested => - AvailableFilesRequested - } --> updates, - cls := "bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6", - Form( - Form.Body( - Form.Section( - FormHeader( - FormHeader.ViewModel( - "Doložení kritéria", - "Sestavte doklady poskytující důkaz kritéria a potvrďte odesláním formuláře. Případné limitace či jiné relevantní údaje vepište do pole Komentář." - ) - ), - FormFields( - FormRow( - "dokumenty", - "Dokumenty", - FilePicker(files.signal, availableFilesStream)(filesObserver) - .amend(idAttr := "dokumenty", cls("max-w-lg")) - ).toHtml, - FormRow( - "platnost", - "Platnost", - input( - idAttr := "platnost", - name := "platnost", - tpe := "date", - autoComplete := "date", - cls := "block max-w-lg w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" - ) - ).toHtml, - FormRow( - "komentar", - "Komentář", - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - textArea( - idAttr := "komentar", - name := "about", - rows := 3, - cls := "max-w-lg shadow-sm block w-full focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border border-gray-300 rounded-md" - ), - p( - cls := "mt-2 text-sm text-gray-500", - "Doplňte prosím potřebné informace související s doložením kritéria, včetně případných limitací." - ) - ) - ).toHtml - ) - ) - ), - submitButtons - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala deleted file mode 100644 index 41033f6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -import components._ - -object DirectoryPage: - - type ViewModel = List[UserRow.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - val (actionsStream, actionObserver) = - EventStream.withObserver[SearchForm.Action] - val $filter = actionsStream - .collect { - case SearchForm.ClearFilter => None - case SearchForm.SetFilter(t) => Some(t) - } - .startWith(None) - val byLetter = for { - d <- $m - f <- $filter - } yield for { - (letter, users) <- d - .filter { user => - f match - case None => true - case Some(t) => user.search.contains(t) - } - .groupBy(_.prijmeni.head) - .to(List) - .sortBy(_._1) - } yield (letter.toString, users.sortBy(_.prijmeni)) - - div( - cls := "h-full max-w-7xl mx-auto order-first flex flex-col flex-shrink-0", - SearchForm(actionObserver), - Directory(byLetter) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala deleted file mode 100644 index 73f915d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -object Directory: - - object Header: - type ViewModel = String - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500", - h3(child.text <-- $m) - ) - - object UserList: - type ViewModel = List[UserRow.ViewModel] - def apply($m: Signal[ViewModel]): HtmlElement = - ul( - role := "list", - cls := "relative z-0 divide-y divide-gray-200", - children <-- $m.split(_.osobniCislo)((_, _, s) => UserRow(s)) - ) - - type ViewModel = List[(String, List[UserRow.ViewModel])] - def apply($m: Signal[ViewModel]): HtmlElement = - nav( - cls := "flex-1 min-h-0 overflow-y-auto", - aria.label := "Directory", - children <-- $m.split(_._1)((_, _, s) => - div( - cls := "relative", - Header(s.map(_._1)), - UserList(s.map(_._2)) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala deleted file mode 100644 index e182910..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala +++ /dev/null @@ -1,54 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object SearchForm: - sealed trait Action - case object SubmitFilter extends Action - sealed trait FilterAction extends Action - case object ClearFilter extends FilterAction - case class SetFilter(value: String) extends FilterAction - - def apply(actions: Observer[Action]): HtmlElement = - div( - cls := "px-6 pt-4 pb-4", - form( - cls := "flex space-x-4", - onSubmit.mapTo(SubmitFilter) --> actions, - div( - cls := "flex-1 min-w-0", - label( - forId := "search", - cls := "sr-only", - "Hledat" - ), - div( - cls := "relative rounded-md shadow-sm", - div( - cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", - Icons.solid.search().amend(svg.cls := "text-gray-400") - ), - input( - tpe := "search", - name := "search", - idAttr := "search", - cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", - placeholder := "Hledat", - composeEvents(onInput.mapToValue.setAsValue.map(SetFilter(_)))( - _.throttle(500) - ) --> actions - ) - ) - ), - button( - tpe := "submit", - cls := "inline-flex justify-center px-3.5 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500", - Icons.solid.filter().amend(svg.cls := "text-gray-400"), - span( - cls := "sr-only", - "Hledat" - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala deleted file mode 100644 index 82f7723..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala +++ /dev/null @@ -1,52 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.airstream.core.Signal -import fiftyforms.ui.components.tailwind.Avatar - -object UserRow: - case class ViewModel( - osobniCislo: String, - celeJmeno: String, - prijmeni: String, - hlavniFunkce: Option[String], - img: Option[String], - container: () => HtmlElement = () => div() - ) { - val search = prijmeni.toLowerCase - } - - def apply($m: Signal[ViewModel]): HtmlElement = - inline def avatarImage = - Avatar($m.map(_.img)).avatarImage(10) - - li( - div( - cls := "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500", - div( - cls := "flex-shrink-0", - child <-- avatarImage - ), - div( - cls := "flex-1 min-w-0", - child <-- $m.map { o => - o.container() - .amend( - cls := "focus:outline-none", - span( - cls := "absolute inset-0", - aria.hidden := true - ), - p( - cls := "text-sm font-medium text-gray-900", - o.celeJmeno - ), - p( - cls := "text-sm text-gray-500 truncate", - o.hlavniFunkce - ) - ) - } - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala deleted file mode 100644 index 467c5f5..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala +++ /dev/null @@ -1,99 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router - -object ErrorPage: - case class ViewModel( - homePage: Page, - errorName: String, - title: String, - subTitle: String - ) - - def apply(m: ViewModel, actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - val ViewModel(homePage, errorName, title, subTitle) = m - div( - cls := "min-h-full pt-16 pb-12 flex flex-col bg-white", - main( - cls := "flex-grow flex flex-col justify-center max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex-shrink-0 flex justify-center", - a( - href := "/", - cls := "inline-flex", - span( - cls := "sr-only", - """Workflow""" - ), - img( - cls := "h-12 w-auto", - src := "https://tailwindui.com/img/logos/workflow-mark.svg?color=indigo&shade=600", - alt := "" - ) - ) - ), - div( - cls := "py-16", - div( - cls := "text-center", - p( - cls := "text-sm font-semibold text-indigo-600 uppercase tracking-wide", - errorName - ), - h1( - cls := "mt-2 text-4xl font-extrabold text-gray-900 tracking-tight sm:text-5xl", - title - ), - p( - cls := "mt-2 text-base text-gray-500", - subTitle - ), - div( - cls := "mt-6", - PageLink - .container(homePage, actionBus) - .amend( - cls := "text-base font-medium text-indigo-600 hover:text-indigo-500", - """Go back home""" - ) - ) - ) - ) - ), - footer( - cls := "flex-shrink-0 max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", - nav( - cls := "flex justify-center space-x-4", - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Contact Support""" - ), - span( - cls := "inline-block border-l border-gray-300", - aria.hidden := true - ), - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Status""" - ), - span( - cls := "inline-block border-l border-gray-300", - aria.hidden := true - ), - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Twitter""" - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala deleted file mode 100644 index bd9baec..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action - -object NotFoundPage: - def apply(homePage: Page, url: String, actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - ErrorPage( - ErrorPage.ViewModel( - homePage, - "404 error", - "Page not found.", - s"Sorry, but page $url doesn't exist." - ), - actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala deleted file mode 100644 index 476fe3c..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action - -object UnhandledErrorPage: - - case class ViewModel( - homePage: Page, - errorName: Option[String], - errorMessage: Option[String] - ) - - def apply(m: ViewModel, actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - ErrorPage( - ErrorPage.ViewModel( - m.homePage, - "Unexpected error occurred", - m.errorName.getOrElse( - "Uh oh!" - ), // TODO: translations, better text than uh oh - m.errorMessage.getOrElse( - "This wasn't supposed to happen! Please try again." - ) - ), - actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala deleted file mode 100644 index 25f059f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala +++ /dev/null @@ -1,6 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.services - -import com.raquo.airstream.core.Observer - -trait DataFetcher[K, A]: - def fetch(id: K, sink: Observer[A]): Unit diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala deleted file mode 100644 index a469830..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala +++ /dev/null @@ -1,120 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package state - -import com.raquo.airstream.core.EventStream -import cz.e_bs.cmi.mdr.pdb.{UserInfo, OsobniCislo} -import com.raquo.airstream.core.Observer -import scala.scalajs.js -import scala.scalajs.js.JSON -import zio.json._ -import com.raquo.airstream.eventbus.EventBus -import com.raquo.airstream.ownership.Owner -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.UserFunction -import cz.e_bs.cmi.mdr.pdb.UserContract -import fiftyforms.services.files.File - -trait AppState - extends connectors.DetailPageConnector.AppState - with connectors.DetailParametruPageConnector.AppState - with connectors.DetailKriteriaPageConnector.AppState - with pages.detail.UpravDukaz.State: - def users: EventStream[List[UserInfo]] - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - -class MockAppState(implicit owner: Owner, router: Router[Page]) - extends AppState: - - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonDecoder[UserFunction] = DeriveJsonDecoder.gen - given JsonDecoder[UserContract] = DeriveJsonDecoder.gen - given JsonDecoder[UserInfo] = DeriveJsonDecoder.gen - - given JsonDecoder[ParameterCriteria] = DeriveJsonDecoder.gen - given JsonDecoder[Parameter] = DeriveJsonDecoder.gen - - private val actions = EventBus[Action]() - private val (parametersStream, pushParameters) = - EventStream.withCallback[List[Parameter]] - private val (usersStream, pushUsers) = - EventStream.withCallback[List[UserInfo]] - private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] - private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] - - private val mockData: List[UserInfo] = - mockUsers - .asInstanceOf[js.Dictionary[js.Object]] - .values - // TODO: is there a more efficient way to parse from JS object directly? - .map { o => - val parsed = JSON.stringify(o).fromJson[UserInfo] - parsed.left.foreach(org.scalajs.dom.console.log(_)) - parsed - } - .collect { case Right(u) => - u - } - .toList - - private val mockParameters: List[Parameter] = - pdbParams - .asInstanceOf[js.Dictionary[js.Object]] - .values - .map(o => JSON.stringify(o).fromJson[Parameter]) - .collect { case Right(p) => p } - .toList - - // TODO: Extract to separate event handler - actions.events.foreach { - case FetchDirectory => pushUsers(mockData) - case FetchUserDetails(osc) => - mockData.find(_.personalNumber == osc).foreach { o => - pushDetails(o) - router.replaceState(Page.Detail(o)) - } - case FetchParameters(osc) => - pushParameters(mockParameters) - case FetchParameter(osc, paramId) => - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(Page.DetailParametru(o, p)) - case FetchParameterCriteria(osc, paramId, critId, page) => - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - c <- p.criteria.find(_.id == critId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(page(o, p, c)) - case NavigateTo(page) => router.pushState(page) - case FetchAvailableFiles(osc) => - pushFiles( - List( - File("https://tc163.cmi.cz/here", "Example file") - ) - ) - } - - override def users: EventStream[List[UserInfo]] = - usersStream.debugWithName("users") - - override def details: EventStream[UserInfo] = - detailsStream.debugWithName("details") - - override def parameters: EventStream[List[Parameter]] = - parametersStream.debugWithName("parameters") - - override def availableFiles: EventStream[List[File]] = - filesStream.debugWithName("available files") - - override def actionBus: Observer[Action] = - actions.writer.debugWithName("actions writer") diff --git a/app/src/main/scala/mdr/pdb/app/Main.scala b/app/src/main/scala/mdr/pdb/app/Main.scala new file mode 100644 index 0000000..081b4ee --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/Main.scala @@ -0,0 +1,103 @@ +package mdr.pdb.app + +import scala.scalajs.js.annotation.JSExportTopLevel +import scala.scalajs.js.annotation.JSExport +import scala.scalajs.js.annotation.JSImport +import scala.scalajs.js +import org.scalajs.dom +import com.raquo.laminar.api.L.{*, given} +import scala.scalajs.js.Date +import com.raquo.waypoint.Router +import com.raquo.waypoint.SplitRender +import mdr.pdb.app.services.DataFetcher +import scala.scalajs.js.JSON +import zio.json._ +import mdr.pdb.UserInfo +import mdr.pdb.app.state.AppState + +@js.native +@JSImport("stylesheets/main.css", JSImport.Namespace) +object Css extends js.Any + +@js.native +@JSImport("data/users.json", JSImport.Default) +object mockUsers extends js.Object + +@js.native +@JSImport("params/pdb-params.json", JSImport.Default) +object pdbParams extends js.Object + +@JSExportTopLevel("app") +object Main: + + @JSExport + def main(args: Array[String]): Unit = + import Routes.given + onLoad { + setupAirstream() + val appContainer = dom.document.querySelector("#app") + val _ = + render( + appContainer, + renderPage(state.MockAppState(using unsafeWindowOwner, router)) + ) + } + + private def onLoad(f: => Unit): Unit = + documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) + + private def setupAirstream()(using router: Router[Page]): Unit = + AirstreamError.registerUnhandledErrorCallback(err => + router.forcePage( + Page.UnhandledError( + Some(err.getClass.getName), // TODO: Fill only in dev mode + Some(err.getMessage) + ) + ) + ) + + def renderPage(state: AppState)(using + router: Router[Page] + ): HtmlElement = + val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) + .collectSignal[Page.Detail]( + connectors + .DetailPageConnector(state)(_) + .apply + ) + .collectSignal[Page.DetailParametru]( + connectors + .DetailParametruPageConnector(state)(_) + .apply + ) + .collectSignal[Page.DetailKriteria]( + connectors + .DetailKriteriaPageConnector(state)(_) + .apply + ) + .collectSignal[Page.UpravDukazKriteria]( + pages.detail.UpravDukaz.Connector(state)(_).apply + ) + .collectStatic(Page.Dashboard)( + connectors.DashboardPageConnector(state.actionBus).apply + ) + .collect[Page.NotFound](pg => + pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) + ) + .collect[Page.UnhandledError](pg => + pages.errors + .UnhandledErrorPage( + pages.errors.UnhandledErrorPage + .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), + state.actionBus + ) + ) + .collectStatic(Page.Directory)( + connectors + .DirectoryPageConnector(state.users, state.actionBus) + .apply + ) + div(cls := "h-full", child <-- pageSplitter.$view) + + // Pull in the stylesheet + val css: Css.type = Css diff --git a/app/src/main/scala/mdr/pdb/app/Routes.scala b/app/src/main/scala/mdr/pdb/app/Routes.scala new file mode 100644 index 0000000..2cc860c --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/Routes.scala @@ -0,0 +1,198 @@ +package mdr.pdb.app + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.* +import org.scalajs.dom +import zio.json.{*, given} +import mdr.pdb.OsobniCislo + +import scala.scalajs.js +import mdr.pdb.UserInfo +import mdr.pdb.Parameter +import mdr.pdb.ParameterCriteria +import mdr.pdb.app.Page.Titled + +// enum is not working with Waypoints' SplitRender collectStatic +sealed abstract class Page( + val id: String, + val title: String, + val parent: Option[Page] +) { + val path: Vector[Page] = + parent match + case None => Vector(this) + case Some(p) => p.path :+ this + + val isRoot: Boolean = parent.isEmpty +} + +object Page: + + case class Titled[V](value: V, title: Option[String] = None): + val show: String = title.getOrElse(value.toString) + + case object Directory extends Page("directory", "Adresář", None) + + case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) + + case class Detail(osobniCislo: Titled[OsobniCislo]) + extends Page("user", osobniCislo.show, Some(Directory)) + + object Detail { + def apply(o: UserInfo): Detail = Detail( + Titled(o.personalNumber, Some(o.name)) + ) + } + + case class DetailParametru( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String] + ) extends Page( + "parameter", + parametr.show, + Some(Detail(osobniCislo)) + ) + + object DetailParametru { + def apply(o: UserInfo, p: Parameter): DetailParametru = + DetailParametru( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)) + ) + } + + case class DetailKriteria( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String], + kriterium: Titled[String] + ) extends Page( + "criteria", + kriterium.show, + Some(DetailParametru(osobniCislo, parametr)) + ) + + object DetailKriteria { + def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = + DetailKriteria( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)), + Titled(k.id, Some(k.id)) + ) + } + + case class UpravDukazKriteria( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String], + kriterium: Titled[String] + ) extends Page( + "addProof", + "Důkaz", + Some(DetailKriteria(osobniCislo, parametr, kriterium)) + ) + + object UpravDukazKriteria { + def apply( + o: UserInfo, + p: Parameter, + k: ParameterCriteria + ): UpravDukazKriteria = + UpravDukazKriteria( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)), + Titled(k.id, Some(k.id)) + ) + } + + case class NotFound(url: String) extends Page("404", "404", Some(Directory)) + + case class UnhandledError( + errorName: Option[String], + errorMessage: Option[String] + ) extends Page("500", "Unexpected error", Some(Directory)) + +object Routes: + given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) + given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) + given [V: JsonEncoder]: JsonEncoder[Titled[V]] = + DeriveJsonEncoder.gen[Titled[V]] + given [V: JsonDecoder]: JsonDecoder[Titled[V]] = + DeriveJsonDecoder.gen[Titled[V]] + given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] + given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] + + val base = + js.`import`.meta.env.BASE_URL + .asInstanceOf[String] + "app" + + val homePage: Page = Page.Directory + + given router: Router[Page] = Router[Page]( + routes = List( + Route.static(homePage, root / endOfSegments, basePath = base), + Route.static( + Page.Dashboard, + root / "dashboard" / endOfSegments, + basePath = base + ), + Route[Page.Detail, String]( + encode = _.osobniCislo.value.toString, + decode = osc => Page.Detail(Titled(OsobniCislo(osc))), + root / "osoba" / segment[String] / endOfSegments, + basePath = base + ), + Route[Page.DetailParametru, (String, String)]( + encode = p => (p.osobniCislo.value.toString, p.parametr.value), + decode = + p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / endOfSegments, + basePath = base + ), + Route[Page.DetailKriteria, (String, String, String)]( + encode = p => + ( + p.osobniCislo.value.toString, + p.parametr.value, + p.kriterium.value.replaceAll("\\.", "--") + ), + decode = p => + Page.DetailKriteria( + Titled(OsobniCislo(p._1)), + Titled(p._2), + Titled(p._3.replaceAll("--", ".")) + ), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / "kriterium" / segment[String] / endOfSegments, + basePath = base + ), + Route[Page.UpravDukazKriteria, (String, String, String)]( + encode = p => + ( + p.osobniCislo.value.toString, + p.parametr.value, + p.kriterium.value.replaceAll("\\.", "--") + ), + decode = p => + Page.UpravDukazKriteria( + Titled(OsobniCislo(p._1)), + Titled(p._2), + Titled(p._3.replaceAll("--", ".")) + ), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / "kriterium" / segment[String] / "edit" / endOfSegments, + basePath = base + ) + ), + serializePage = _.toJson, + deserializePage = _.fromJson[Page] + .fold(s => throw IllegalStateException(s), identity), + getPageTitle = _.title, + routeFallback = url => Page.NotFound(url), + deserializeFallback = _ => Page.Dashboard + )( + $popStateEvent = windowEvents.onPopState, + owner = unsafeWindowOwner + ) diff --git a/app/src/main/scala/mdr/pdb/app/actions.scala b/app/src/main/scala/mdr/pdb/app/actions.scala new file mode 100644 index 0000000..d078b3a --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/actions.scala @@ -0,0 +1,21 @@ +package mdr.pdb.app + +import mdr.pdb.OsobniCislo +import mdr.pdb.UserInfo +import mdr.pdb.Parameter +import mdr.pdb.ParameterCriteria + +sealed trait Action + +case object FetchDirectory extends Action +case class FetchUserDetails(osc: OsobniCislo) extends Action +case class FetchParameters(osc: OsobniCislo) extends Action +case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action +case class FetchParameterCriteria( + osc: OsobniCislo, + paramId: String, + critId: String, + page: (UserInfo, Parameter, ParameterCriteria) => Page +) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action +case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/mdr/pdb/app/components/AppPage.scala new file mode 100644 index 0000000..130c817 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/AppPage.scala @@ -0,0 +1,82 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.Page +import mdr.pdb.{UserProfile, UserInfo, OsobniCislo} +import com.raquo.waypoint.Router +import mdr.pdb.app.Action +import mdr.pdb.app.NavigateTo +import mdr.pdb.UserFunction + +object AppPage: + // TODO: pages by logged in user + val pages: List[Page] = List(Page.Directory, Page.Dashboard) + + import NavigationBar.{Logo, MenuItem} + + val logo = Logo( + "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", + "Workflow" + ) + + // TODO: menu items by user profile + val userMenu = + List( + MenuItem("Your Profile"), + MenuItem("Settings"), + MenuItem("Sign out") + ) + + // TODO: load user profile + val $userProfile = Var( + UserProfile( + "tom", + UserInfo( + OsobniCislo("1031"), + "tom", + "Tom", + "Cook", + None, + None, + Some("tom@example.com"), + Some("+420 222 866 180"), + Some( + UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") + ), + Nil, + None + ) + ) + ) + + val $userInfo = $userProfile.signal.map(_.userInfo) + + type ViewModel = Option[HtmlElement] + def apply( + actionBus: Observer[Action] + )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using + router: Router[Page] + ): HtmlElement = + PageLayout(actionBus)( + $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => + PageLayout.ViewModel( + NavigationBar.ViewModel( + u, + pages.map(p => + NavigationBar.Link( + () => + PageLink( + p, + actionBus + ), + p == cp + ) + ), + userMenu, + logo + ), + c + ) + ), + mods + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala new file mode 100644 index 0000000..9375398 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala @@ -0,0 +1,143 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.tailwind.list.IconText.ViewModel +import fiftyforms.ui.components.tailwind.Icons +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.Page + +import com.raquo.laminar.api.L.{*, given} +import io.laminext.syntax.core.{*, given} +import mdr.pdb.app.Action + +object Breadcrumbs: + + private def slash = + import svg.{*, given} + svg( + cls := "flex-shrink-0 h-5 w-5 text-gray-300", + xmlns := "http://www.w3.org/2000/svg", + fill := "currentColor", + viewBox := "0 0 20 20", + ariaHidden := true, + path( + d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" + ) + ) + + def backIcon = + Icons.solid + .`arrow-narrow-left`() + .amend( + svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" + ) + + object Link: + case class ViewModel( + page: Page, + icon: Option[SvgElement], + extraClasses: String, + text: String, + textClasses: Option[String] = None + ) + + val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" + + def shortHome(p: Page) = ViewModel( + p, + Some(backIcon), + "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", + "Zpět na úvodní stránku", + None + ) + + def fullHome(p: Page) = ViewModel( + p, + Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), + "text-gray-400 hover:text-gray-500", + "Domů", + Some("sr-only") + ) + + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + PageLink + .container($m.map(_.page), actionBus) + .amend( + cls <-- $m.map(_.extraClasses), + child.maybe <-- $m.map(_.icon), + span( + cls <-- $m.map(_.textClasses.getOrElse("")), + child.text <-- $m.map(_.text) + ) + ) + + object FullBreadcrumbs: + type ViewModel = Page + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + ol( + role := "list", + cls := "flex items-center space-x-4", + children <-- $m.map(_.path).split(_.id)((_, _, $p) => + li( + div( + cls := "flex items-center", + child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), + Link( + $p.map(p => + if (p.isRoot) then Link.fullHome(p) + else + Link.ViewModel( + p, + None, + s"ml-4 max-w-xs truncate ${Link.baseClasses}", + p.title + ) + ), + actionBus + ) + ) + ) + ) + ) + + object ShortBreadcrumbs: + type ViewModel = Page + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + Link( + $m.map { p => + val target = p.parent.getOrElse(p) + if target.isRoot then Link.shortHome(target) + else + Link.ViewModel( + target, + Some(backIcon), + "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", + s"Zpět na ${target.title}" + ) + }, + actionBus + ) + + def apply(actionBus: Observer[Action])(using + router: Router[Page] + ): HtmlElement = + nav( + cls := "flex", + aria.label := "Breadcrumb", + div( + cls := "flex sm:hidden", + ShortBreadcrumbs(router.$currentPage, actionBus) + ), + div( + cls := "hidden sm:block", + FullBreadcrumbs(router.$currentPage, actionBus) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala new file mode 100644 index 0000000..4bb8b8d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala @@ -0,0 +1,242 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent +import fiftyforms.ui.components.tailwind.Icons +import fiftyforms.ui.components.tailwind.Avatar +import mdr.pdb.UserInfo +import io.laminext.syntax.core.* + +object NavigationBar: + + case class Logo(img: String, name: String) + case class Link(a: () => Anchor, active: Boolean) + case class MenuItem(title: String) + + case class ViewModel( + userInfo: UserInfo, + pages: List[Link], + userMenu: List[MenuItem], + logo: Logo + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + val $userInfo = $m.map(_.userInfo) + val mobileMenuOpen = Var(false) + + val desktopOnly = cls("hidden md:block") + val mobileOnly = cls("md:hidden") + + inline def avatarImage(size: Int = 8) = + Avatar($userInfo.map(_.img)).avatarImage(size) + + def notificationButton = button( + tpe := "button", + cls := List( + "bg-indigo-600", + "focus:outline-none", + "focus:ring-2", + "focus:ring-offset-2", + "focus:ring-offset-indigo-600", + "focus:ring-white", + "hover:text-white", + "p-1", + "rounded-full", + "text-indigo-200" + ), + span(cls := "sr-only", "View notifications"), + Icons.outline.bell() + ) + + def userProfile: HtmlElement = + val menuOpen = Var(false) + + def menuItem(item: MenuItem, idx: Int): HtmlElement = + a( + href := "#", + cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", + role := "menuitem", + tabIndex := -1, + idAttr := s"user-menu-item-$idx", + item.title + ) + + div( + cls := "ml-3 relative", + div( + button( + tpe := "button", + cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", + idAttr := "user-menu-button", + aria.expanded <-- menuOpen.signal, + aria.hasPopup := true, + span(cls := "sr-only", "Open user menu"), + child <-- avatarImage(), + onClick.preventDefault.mapTo( + !menuOpen.now() + ) --> menuOpen.writer + ) + ), + /* + * */ + div( + cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", + cls <-- menuOpen.signal.map { o => + if (o) "md:block" else "md:hidden" + }, + role := "menu", + aria.orientation := "vertical", + aria.labelledBy := "user-menu-button", + tabIndex := -1, + // : keyboard navigation + children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) + ) + ) + + def mobileProfile = + def menuItem(item: MenuItem): HtmlElement = + a( + href := "#", + cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", + item.title + ) + + div( + cls := "pt-4 pb-3 border-t border-indigo-700", + div( + cls := "flex items-center px-5", + div( + cls := "flex-shrink-0", + child <-- avatarImage(10) + ), + div( + cls := "ml-3", + div( + cls := "text-base font-medium text-white", + child.text <-- $userInfo.map(_.name) + ), + child.maybe <-- $userInfo.map( + _.email.map(e => + div( + cls := "text-sm font-medium text-indigo-300", + e + ) + ) + ) + ), + notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) + ), + div( + cls := "mt-3 px-2 space-y-1", + children <-- $m.map(_.userMenu.map(menuItem)) + ) + ) + + def pageLink(page: Link): Anchor = + page + .a() + .amend( + cls := "text-white px-3 py-2 rounded-md text-sm font-medium", + cls := Seq( + "bg-indigo-700" -> page.active, + "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active + ), + ariaCurrent := (if page.active then "page" else "false") + ) + + def logoImg: Image = + img( + cls := "h-8 w-8", + src <-- $m.map(_.logo.img), + alt <-- $m.map(_.logo.name) + ) + + def mobileMenuButton = button( + tpe := "button", + cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", + aria.controls := "mobile-menu", + aria.expanded <-- mobileMenuOpen.signal, + span(cls := "sr-only", "Open main menu"), + Icons.outline + .menu() + .amend( + svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") + ), + Icons.outline + .x() + .amend( + svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") + ), + onClick.preventDefault.mapTo( + !mobileMenuOpen.now() + ) --> mobileMenuOpen.writer + ) + + def navBarLeft = + div( + cls := "flex items-center", + div(cls := "flex-shrink-0", logoImg), + div( + desktopOnly, + div( + cls := "ml-10 flex items-baseline space-x-4", + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) + ) + ) + ) + + def navBarRight = + div( + desktopOnly, + div( + cls := "ml-4 flex items-center md:ml-6", + notificationButton, + userProfile + ) + ) + + def navBarMobile = + div( + cls := "-mr-2 flex", + mobileOnly, + mobileMenuButton + ) + + def navBar = + div( + cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", + div( + cls := "flex items-center justify-between h-16", + navBarLeft, + navBarRight, + navBarMobile + ) + ) + + def mobileMenu = + div( + mobileOnly, + cls <-- mobileMenuOpen.signal.map { o => + if (o) "block" else "hidden" + }, + idAttr := "mobile-menu", + div( + cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) + ), + mobileProfile + ) + + nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala new file mode 100644 index 0000000..c4cb10d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala @@ -0,0 +1,19 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.Page +import com.raquo.waypoint.Router +import mdr.pdb.app.Action + +object PageHeader: + def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = + header( + cls := "bg-white shadow-sm", + div( + cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", + h1( + cls := "text-lg leading-6 font-semibold text-gray-900", + Breadcrumbs(actionBus) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala new file mode 100644 index 0000000..aae4539 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala @@ -0,0 +1,30 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.Page +import mdr.pdb.app.Action +import fiftyforms.ui.components.tailwind.Loading +import io.laminext.syntax.core.* + +object PageLayout: + case class ViewModel( + navigation: NavigationBar.ViewModel, + content: Option[HtmlElement] + ) + def apply(actionBus: Observer[Action])( + $m: Signal[ViewModel], + mods: Modifier[HtmlElement]* + )(using router: Router[Page]): HtmlElement = + val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) + div( + cls := "h-full flex flex-col", + NavigationBar($m.map(_.navigation)), + child.maybe <-- router.$currentPage.map(_.isRoot) + .switch(None, Some(PageHeader(actionBus))), + main( + cls := "flex-grow-1 overflow-y-auto", + mods, + child <-- $maybeContent.map(_.getOrElse(Loading)) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/mdr/pdb/app/components/PageLink.scala new file mode 100644 index 0000000..e6e7052 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageLink.scala @@ -0,0 +1,40 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.LinkSupport.* +import mdr.pdb.app.Page +import mdr.pdb.app.Action +import com.raquo.waypoint.Router +import mdr.pdb.app.NavigateTo + +object PageLink: + type ViewModel = Page + def apply($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = + a(mods($m, actions), child.text <-- $m.map(_.title)) + + def apply(m: ViewModel, actions: Observer[Action])(using + router: Router[Page] + ): Anchor = apply(Val(m), actions) + + def container($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = a(mods($m, actions)) + + def container(m: ViewModel, actions: Observer[Action])(using + Router[Page] + ): Anchor = container(Val(m), actions) + + private def mods($m: Signal[Page], actions: Observer[Action])(using + router: Router[Page] + ): Modifier[Anchor] = + Seq( + href <-- $m.map(router.absoluteUrlForPage).recover { case _ => + Some("invalid url") + }, + composeEvents(onClick.noKeyMod.preventDefault)( + _.sample($m) + .map(NavigateTo.apply) + ) --> actions + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala new file mode 100644 index 0000000..7682772 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala @@ -0,0 +1,13 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.pages.dashboard.DashboardPage +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage + +class DashboardPageConnector(actionBus: Observer[Action])(using + router: Router[Page] +): + def apply: HtmlElement = + AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala new file mode 100644 index 0000000..76b9133 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala @@ -0,0 +1,76 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.Parameter +import mdr.pdb.UserInfo +import mdr.pdb.app.pages.detail.DetailParametruPage +import pages.detail.DetailKriteriaPage +import pages.detail.components.DukazyKriteria +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.ParameterCriteria + +object DetailKriteriaPageConnector { + trait AppState { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + } +} + +case class DetailKriteriaPageConnector( + state: DetailKriteriaPageConnector.AppState +)( + $page: Signal[Page.DetailKriteria] +)(using Router[Page]): + val $paramChangeSignal = + $page.splitOne(p => + (p.osobniCislo.value, p.parametr.value, p.kriterium.value) + )((x, _, _) => x) + val $pageChangeSignal = + $paramChangeSignal.map( + FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) + ) + + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + val $merged = + $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => + for { + da <- d + pa <- p + pb <- pa.find(_.id == pc._2) + ka <- pb.criteria.find(_.id == pc._3) + } yield (da, pb, ka) + ) + + def apply: HtmlElement = + AppPage(state.actionBus)( + $merged.map(_.map(buildModel)) + .split(_ => ())((_, s, $s) => + DetailKriteriaPage($s)(state.actionBus.contramap { + case DukazyKriteria.Add => + NavigateTo( + Page.UpravDukazKriteria( + Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), + Page.Titled(s.parametr.id, Some(s.parametr.nazev)), + Page.Titled(s.kriterium.id, Some(s.kriterium.id)) + ) + ) + }) + ), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: Parameter, + k: ParameterCriteria + ): DetailKriteriaPage.ViewModel = + DetailKriteriaPage.ViewModel( + o.toDetailOsoby, + p.toParametr(_ => a()), + k.toKriterium(_ => a()) + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DetailPageConnector.scala new file mode 100644 index 0000000..df3a6b7 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DetailPageConnector.scala @@ -0,0 +1,57 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.UserInfo +import pages.detail.DetailPage +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.pages.detail.components.DetailOsoby +import mdr.pdb.Parameter +import mdr.pdb.app.pages.detail.components.SeznamParametru +import fiftyforms.ui.components.tailwind.Color + +object DetailPageConnector { + trait AppState { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + } +} + +case class DetailPageConnector(state: DetailPageConnector.AppState)( + $page: Signal[Page.Detail] +)(using router: Router[Page]): + val $oscChangeSignal = $page.splitOne(_.osobniCislo.value)((osc, _, _) => osc) + val $pageChangeSignal = + $oscChangeSignal.flatMap(osc => + EventStream.fromSeq(Seq(FetchUserDetails(osc), FetchParameters(osc))) + ) + // TODO: filter the value based on the current osc + // OSC change will fetch new data, but still + // - we need to be sure that what we got is really what we ought to display + // - we want to display stale data accordingly (at least with loading indicator) + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + def apply: HtmlElement = + AppPage(state.actionBus)( + $data.combineWithFn($params)(_ zip _) + .map(_.map(buildModel)) + .split(_ => ())((_, _, s) => DetailPage(s)), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: List[Parameter] + ): DetailPage.ViewModel = + DetailPage.ViewModel( + o.toDetailOsoby, + p.map( + _.toParametr(param => + PageLink.container(Page.DetailParametru(o, param), state.actionBus) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DetailParametruPageConnector.scala new file mode 100644 index 0000000..fa7193d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DetailParametruPageConnector.scala @@ -0,0 +1,67 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.Parameter +import mdr.pdb.UserInfo +import mdr.pdb.app.pages.detail.DetailParametruPage +import pages.detail.DetailParametruPage +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.components.PageLink + +object DetailParametruPageConnector { + trait AppState { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + } +} + +case class DetailParametruPageConnector( + state: DetailParametruPageConnector.AppState +)( + $page: Signal[Page.DetailParametru] +)(using router: Router[Page]): + val $paramChangeSignal = + $page.splitOne(p => (p.osobniCislo.value, p.parametr.value))((x, _, _) => x) + val $pageChangeSignal = + $paramChangeSignal.map(FetchParameter(_, _)) + + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + val $merged = + $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => + for { + da <- d + pa <- p + pb <- pa.find(_.id == pc._2) + } yield (da, pb) + ) + + def apply: HtmlElement = + AppPage(state.actionBus)( + $merged.map(_.map(buildModel)) + .split(_ => ())((_, _, s) => DetailParametruPage(s)), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: Parameter + ): DetailParametruPage.ViewModel = + DetailParametruPage.ViewModel( + o.toDetailOsoby, + p.toParametr(p => + PageLink.container(Page.DetailParametru(o, p), state.actionBus) + ), + p.criteria.map( + _.toKriterium { c => + PageLink.container( + Page.DetailKriteria(o, p, c), + state.actionBus + ) + } + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DirectoryPageConnector.scala new file mode 100644 index 0000000..ab2c045 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DirectoryPageConnector.scala @@ -0,0 +1,34 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.UserInfo +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.components.AppPage + +case class DirectoryPageConnector( + $input: EventStream[List[UserInfo]], + actionBus: Observer[Action] +)(using router: Router[Page]): + val $data = $input.startWithNone + val $actionSignal = EventStream.fromValue(FetchDirectory) + + def apply: HtmlElement = + AppPage(actionBus)( + $data.split(_ => ())((_, _, s) => + pages.directory.DirectoryPage( + s.map( + _.map( + _.toUserRow(u => + PageLink.container( + Page.Detail(Page.Titled(u.personalNumber)), + actionBus + ) + ) + ) + ) + ) + ), + $actionSignal --> actionBus + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/mdr/pdb/app/connectors/conversions.scala new file mode 100644 index 0000000..f581796 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/conversions.scala @@ -0,0 +1,68 @@ +package mdr.pdb.app.connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.UserInfo +import mdr.pdb.app.pages.detail.components.DetailOsoby +import mdr.pdb.Parameter +import mdr.pdb.app.pages.detail.components.SeznamParametru +import mdr.pdb.ParameterCriteria +import mdr.pdb.app.pages.detail.components.SeznamKriterii +import mdr.pdb.app.pages.directory.components.UserRow +import mdr.pdb.app.pages.detail.components.DetailParametru +import mdr.pdb.app.pages.detail.components.DetailKriteria +import fiftyforms.ui.components.tailwind.Color + +extension (o: UserInfo) + def toDetailOsoby: DetailOsoby.ViewModel = + DetailOsoby.ViewModel( + o.personalNumber, + o.name, + o.email, + o.phone, + o.img, + o.mainFunction.map(f => + DetailOsoby.Funkce.ViewModel(f.name, f.dept, f.ou) + ), + o.userContracts.headOption.map(c => + DetailOsoby.PracovniPomer.ViewModel(c.rel, c.startDate, c.endDate) + ) + ) + +extension (param: Parameter) + def toParametr(container: Parameter => Anchor): DetailParametru.ViewModel = + DetailParametru.ViewModel( + id = param.id, + nazev = param.name, + popis = param.description, + status = "Nesplněno", + statusColor = Color.red, + a = container(param) + ) + +extension (crit: ParameterCriteria) + def toKriterium( + container: ParameterCriteria => Anchor + ): DetailKriteria.ViewModel = + DetailKriteria.ViewModel( + nazev = crit.criteriumText, + kapitola = crit.chapterId, + bod = crit.itemId, + status = "Nesplněno", + statusColor = Color.red, + splneno = false, + dukazy = Nil, + container = container(crit) + ) + +extension (user: UserInfo) + def toUserRow( + container: UserInfo => HtmlElement = (_: UserInfo) => div() + ): UserRow.ViewModel = + UserRow.ViewModel( + osobniCislo = user.personalNumber.toString, + celeJmeno = user.name, + prijmeni = user.surname, + hlavniFunkce = user.mainFunction.map(_.name), + img = user.img, + container = () => container(user) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/dashboard/DashboardPage.scala b/app/src/main/scala/mdr/pdb/app/pages/dashboard/DashboardPage.scala new file mode 100644 index 0000000..0eb8f7a --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/dashboard/DashboardPage.scala @@ -0,0 +1,11 @@ +package mdr.pdb.app.pages.dashboard + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.Page + +object DashboardPage: + + def render: HtmlElement = + div("Dashboard page") diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala new file mode 100644 index 0000000..ff3a25f --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala @@ -0,0 +1,31 @@ +package mdr.pdb.app.pages.detail + +import com.raquo.laminar.api.L.{*, given} + +import components._ + +object DetailKriteriaPage: + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametr: DetailParametru.ViewModel, + kriterium: DetailKriteria.ViewModel + ) + + def apply($m: Signal[ViewModel])( + action: Observer[DukazyKriteria.Action] + ): HtmlElement = + div( + cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + div( + DetailOsoby.header($m.map(_.osoba)), + DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") + ), + div( + DetailKriteria($m.map(_.kriterium)), + DukazyKriteria($m.map(_.kriterium.dukazy))(action) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/DetailPage.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailPage.scala new file mode 100644 index 0000000..b591cc9 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailPage.scala @@ -0,0 +1,25 @@ +package mdr.pdb.app.pages.detail + +import com.raquo.laminar.api.L.{*, given} + +import components._ +import mdr.pdb.app.Page +import com.raquo.waypoint.Router +import mdr.pdb.app.Action + +object DetailPage: + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametry: SeznamParametru.ViewModel + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + DetailOsoby($m.map(_.osoba)), + SeznamParametru($m.map(_.parametry)) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/DetailParametruPage.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailParametruPage.scala new file mode 100644 index 0000000..3322279 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailParametruPage.scala @@ -0,0 +1,24 @@ +package mdr.pdb.app.pages.detail + +import com.raquo.laminar.api.L.{*, given} + +import components._ + +object DetailParametruPage: + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametr: DetailParametru.ViewModel, + kriteria: SeznamKriterii.ViewModel + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + DetailOsoby.header($m.map(_.osoba)), + DetailParametru($m.map(_.parametr)), + SeznamKriterii($m.map(_.kriteria)) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala new file mode 100644 index 0000000..b58618a --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala @@ -0,0 +1,107 @@ +package mdr.pdb +package app +package pages.detail + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.pages.detail.components.UpravDukazForm +import fiftyforms.services.files.File + +object UpravDukaz: + + type ThisPage = Page.UpravDukazKriteria + type PageKey = (OsobniCislo, String, String) + + trait State { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def availableFiles: EventStream[List[File]] + def actionBus: Observer[Action] + } + + def keyOfPage(page: ThisPage): PageKey = + (page.osobniCislo.value, page.parametr.value, page.kriterium.value) + + def onChangeAction(key: PageKey): Action = + FetchParameterCriteria( + key._1, + key._2, + key._3, + Page.UpravDukazKriteria(_, _, _) + ) + + class Connector(state: State)($page: Signal[ThisPage])(using Router[Page]): + val $paramChangeSignal = $page.splitOne(keyOfPage)((x, _, _) => x) + val $pageChangeSignal = $paramChangeSignal.map(onChangeAction) + + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + val $merged = + $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => + for { + da <- d + pa <- p + pb <- pa.find(_.id == pc._2) + ka <- pb.criteria.find(_.id == pc._3) + } yield (da, pb, ka) + ) + + def apply: HtmlElement = + AppPage(state.actionBus)( + $merged.split(_ => ())((_, s, $s) => + PageComponent( + $s.map(buildModel), + state.availableFiles, + state.actionBus.contramap { + case UpravDukazForm.Cancelled => + NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) + case UpravDukazForm.AvailableFilesRequested => + FetchAvailableFiles(s._1.personalNumber) + } + ) + ), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: Parameter, + k: ParameterCriteria + ): PageComponent.ViewModel = + import connectors.* + PageComponent.ViewModel( + o.toDetailOsoby, + p.toParametr(_ => a()), + k.toKriterium(_ => a()) + ) + + object PageComponent: + import components.* + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametr: DetailParametru.ViewModel, + kriterium: DetailKriteria.ViewModel + ) + + def apply( + $m: Signal[ViewModel], + availableFilesStream: EventStream[List[File]], + events: Observer[UpravDukazForm.Event] + ): HtmlElement = + div( + cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + div( + DetailOsoby.header($m.map(_.osoba)), + DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") + ), + div( + DetailKriteria($m.map(_.kriterium)), + UpravDukazForm(availableFilesStream)(events) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailKriteria.scala new file mode 100644 index 0000000..a3ab634 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailKriteria.scala @@ -0,0 +1,37 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.CustomAttrs +import fiftyforms.ui.components.tailwind.Icons +import java.time.LocalDate +import fiftyforms.ui.components.tailwind.Color + +object DetailKriteria: + case class ViewModel( + nazev: String, + kapitola: String, + bod: String, + status: String, + statusColor: Color, + splneno: Boolean, + dukazy: List[DukazKriteria.ViewModel], + container: HtmlElement = div() + ) { + val id = s"${kapitola}${bod}" + } + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "pb-5 border-b border-gray-200", + h3( + cls := "text-xl leading-6 font-bold text-gray-900", + child.text <-- $m.map(_.nazev) + ), + h3( + cls := "mt-2 max-w-4xl text-sm text-gray-500", + "Kapitola ", + child.text <-- $m.map(_.kapitola), + " bod ", + child <-- $m.map(_.bod) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailOsoby.scala new file mode 100644 index 0000000..0fe2ba5 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailOsoby.scala @@ -0,0 +1,100 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.OsobniCislo + +import java.time.LocalDate +import fiftyforms.ui.components.tailwind.Avatar + +object DetailOsoby: + + object Funkce: + case class ViewModel( + nazev: String, + stredisko: String, + voj: String + ) + + def render($m: Signal[ViewModel]): HtmlElement = + p( + cls := "text-sm font-medium text-gray-500", + child.text <-- $m.map(_.nazev), + span( + cls := "hidden md:inline", + " @ ", + child.text <-- $m.map(_.stredisko), + ", ", + child.text <-- $m.map(_.voj) + ) + ) + + object PracovniPomer: + case class ViewModel( + druh: String, + pocatek: LocalDate, + konec: Option[LocalDate] + ) + + def render($m: Signal[ViewModel]): HtmlElement = + import fiftyforms.ui.components.tailwind.CustomAttrs.datetime + p( + cls := "text-sm font-medium text-gray-500", + child.text <-- $m.map(_.druh), + " od ", + time( + datetime <-- $m.map(_.pocatek.toString), + child.text <-- $m.map(_.pocatek.toString) + ) + ) + + case class ViewModel( + osobniCislo: OsobniCislo, + jmeno: String, + email: Option[String], + telefon: Option[String], + img: Option[String], + hlavniFunkce: Option[Funkce.ViewModel], + pracovniPomer: Option[PracovniPomer.ViewModel] + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "md:flex md:items-center md:justify-between md:space-x-4", + div( + cls := "flex items-start space-x-4", + div( + cls := "flex-shrink-0", + Avatar($m.map(_.img)).avatar(16) + ), + div( + h1( + cls := "text-2xl font-bold text-gray-900", + child.text <-- $m.map(_.jmeno) + ), + child.maybe <-- $m.map(_.hlavniFunkce).split(_ => ())((_, _, d) => + Funkce.render(d) + ), + child.maybe <-- $m.map(_.pracovniPomer).split(_ => ())((_, _, d) => + PracovniPomer.render(d) + ) + ) + ) + ) + + def header($m: Signal[ViewModel]): HtmlElement = + div( + cls := "md:flex md:items-center md:justify-between md:space-x-5", + div( + cls := "flex items-start space-x-4", + div( + cls := "flex-shrink-0", + Avatar($m.map(_.img)).avatar(8) + ), + div( + h1( + cls := "text-lg font-medium text-gray-800", + child.text <-- $m.map(_.jmeno) + ) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailParametru.scala new file mode 100644 index 0000000..6310dde --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailParametru.scala @@ -0,0 +1,34 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.Color + +object DetailParametru: + case class ViewModel( + id: String, + nazev: String, + popis: String, + status: String, + statusColor: Color, + a: Anchor + ) + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "pb-5 border-b border-gray-200", + h2( + cls := "text-2xl leading-6 font-medium text-gray-900", + child.text <-- $m.map(_.nazev) + ), + p( + cls := "mt-2 max-w-4xl text-sm text-gray-500", + child.text <-- $m.map(_.popis) + ) + ) + + def header($m: Signal[ViewModel]): HtmlElement = + div( + h2( + cls := "text-lg leading-6 font-medium text-gray-600", + child.text <-- $m.map(_.nazev) + ) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala deleted file mode 100644 index a3b00af..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala +++ /dev/null @@ -1,103 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import scala.scalajs.js.annotation.JSExportTopLevel -import scala.scalajs.js.annotation.JSExport -import scala.scalajs.js.annotation.JSImport -import scala.scalajs.js -import org.scalajs.dom -import com.raquo.laminar.api.L.{*, given} -import scala.scalajs.js.Date -import com.raquo.waypoint.Router -import com.raquo.waypoint.SplitRender -import cz.e_bs.cmi.mdr.pdb.app.services.DataFetcher -import scala.scalajs.js.JSON -import zio.json._ -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.state.AppState - -@js.native -@JSImport("stylesheets/main.css", JSImport.Namespace) -object Css extends js.Any - -@js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native -@JSImport("params/pdb-params.json", JSImport.Default) -object pdbParams extends js.Object - -@JSExportTopLevel("app") -object Main: - - @JSExport - def main(args: Array[String]): Unit = - import Routes.given - onLoad { - setupAirstream() - val appContainer = dom.document.querySelector("#app") - val _ = - render( - appContainer, - renderPage(state.MockAppState(using unsafeWindowOwner, router)) - ) - } - - private def onLoad(f: => Unit): Unit = - documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) - - private def setupAirstream()(using router: Router[Page]): Unit = - AirstreamError.registerUnhandledErrorCallback(err => - router.forcePage( - Page.UnhandledError( - Some(err.getClass.getName), // TODO: Fill only in dev mode - Some(err.getMessage) - ) - ) - ) - - def renderPage(state: AppState)(using - router: Router[Page] - ): HtmlElement = - val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) - .collectSignal[Page.Detail]( - connectors - .DetailPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailParametru]( - connectors - .DetailParametruPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailKriteria]( - connectors - .DetailKriteriaPageConnector(state)(_) - .apply - ) - .collectSignal[Page.UpravDukazKriteria]( - pages.detail.UpravDukaz.Connector(state)(_).apply - ) - .collectStatic(Page.Dashboard)( - connectors.DashboardPageConnector(state.actionBus).apply - ) - .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) - ) - .collect[Page.UnhandledError](pg => - pages.errors - .UnhandledErrorPage( - pages.errors.UnhandledErrorPage - .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), - state.actionBus - ) - ) - .collectStatic(Page.Directory)( - connectors - .DirectoryPageConnector(state.users, state.actionBus) - .apply - ) - div(cls := "h-full", child <-- pageSplitter.$view) - - // Pull in the stylesheet - val css: Css.type = Css diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala deleted file mode 100644 index 3bbc8c0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ /dev/null @@ -1,198 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.* -import org.scalajs.dom -import zio.json.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import scala.scalajs.js -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.Page.Titled - -// enum is not working with Waypoints' SplitRender collectStatic -sealed abstract class Page( - val id: String, - val title: String, - val parent: Option[Page] -) { - val path: Vector[Page] = - parent match - case None => Vector(this) - case Some(p) => p.path :+ this - - val isRoot: Boolean = parent.isEmpty -} - -object Page: - - case class Titled[V](value: V, title: Option[String] = None): - val show: String = title.getOrElse(value.toString) - - case object Directory extends Page("directory", "Adresář", None) - - case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) - - case class Detail(osobniCislo: Titled[OsobniCislo]) - extends Page("user", osobniCislo.show, Some(Directory)) - - object Detail { - def apply(o: UserInfo): Detail = Detail( - Titled(o.personalNumber, Some(o.name)) - ) - } - - case class DetailParametru( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String] - ) extends Page( - "parameter", - parametr.show, - Some(Detail(osobniCislo)) - ) - - object DetailParametru { - def apply(o: UserInfo, p: Parameter): DetailParametru = - DetailParametru( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)) - ) - } - - case class DetailKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "criteria", - kriterium.show, - Some(DetailParametru(osobniCislo, parametr)) - ) - - object DetailKriteria { - def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = - DetailKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class UpravDukazKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "addProof", - "Důkaz", - Some(DetailKriteria(osobniCislo, parametr, kriterium)) - ) - - object UpravDukazKriteria { - def apply( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): UpravDukazKriteria = - UpravDukazKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class NotFound(url: String) extends Page("404", "404", Some(Directory)) - - case class UnhandledError( - errorName: Option[String], - errorMessage: Option[String] - ) extends Page("500", "Unexpected error", Some(Directory)) - -object Routes: - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) - given [V: JsonEncoder]: JsonEncoder[Titled[V]] = - DeriveJsonEncoder.gen[Titled[V]] - given [V: JsonDecoder]: JsonDecoder[Titled[V]] = - DeriveJsonDecoder.gen[Titled[V]] - given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] - given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] - - val base = - js.`import`.meta.env.BASE_URL - .asInstanceOf[String] + "app" - - val homePage: Page = Page.Directory - - given router: Router[Page] = Router[Page]( - routes = List( - Route.static(homePage, root / endOfSegments, basePath = base), - Route.static( - Page.Dashboard, - root / "dashboard" / endOfSegments, - basePath = base - ), - Route[Page.Detail, String]( - encode = _.osobniCislo.value.toString, - decode = osc => Page.Detail(Titled(OsobniCislo(osc))), - root / "osoba" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.DetailParametru, (String, String)]( - encode = p => (p.osobniCislo.value.toString, p.parametr.value), - decode = - p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / endOfSegments, - basePath = base - ), - Route[Page.DetailKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.DetailKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.UpravDukazKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.UpravDukazKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / "edit" / endOfSegments, - basePath = base - ) - ), - serializePage = _.toJson, - deserializePage = _.fromJson[Page] - .fold(s => throw IllegalStateException(s), identity), - getPageTitle = _.title, - routeFallback = url => Page.NotFound(url), - deserializeFallback = _ => Page.Dashboard - )( - $popStateEvent = windowEvents.onPopState, - owner = unsafeWindowOwner - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala deleted file mode 100644 index b24b82b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -sealed trait Action - -case object FetchDirectory extends Action -case class FetchUserDetails(osc: OsobniCislo) extends Action -case class FetchParameters(osc: OsobniCislo) extends Action -case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action -case class FetchParameterCriteria( - osc: OsobniCislo, - paramId: String, - critId: String, - page: (UserInfo, Parameter, ParameterCriteria) => Page -) extends Action -case class FetchAvailableFiles(osc: OsobniCislo) extends Action -case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala deleted file mode 100644 index c637d35..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala +++ /dev/null @@ -1,82 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.{UserProfile, UserInfo, OsobniCislo} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo -import cz.e_bs.cmi.mdr.pdb.UserFunction - -object AppPage: - // TODO: pages by logged in user - val pages: List[Page] = List(Page.Directory, Page.Dashboard) - - import NavigationBar.{Logo, MenuItem} - - val logo = Logo( - "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", - "Workflow" - ) - - // TODO: menu items by user profile - val userMenu = - List( - MenuItem("Your Profile"), - MenuItem("Settings"), - MenuItem("Sign out") - ) - - // TODO: load user profile - val $userProfile = Var( - UserProfile( - "tom", - UserInfo( - OsobniCislo("1031"), - "tom", - "Tom", - "Cook", - None, - None, - Some("tom@example.com"), - Some("+420 222 866 180"), - Some( - UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") - ), - Nil, - None - ) - ) - ) - - val $userInfo = $userProfile.signal.map(_.userInfo) - - type ViewModel = Option[HtmlElement] - def apply( - actionBus: Observer[Action] - )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using - router: Router[Page] - ): HtmlElement = - PageLayout(actionBus)( - $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => - PageLayout.ViewModel( - NavigationBar.ViewModel( - u, - pages.map(p => - NavigationBar.Link( - () => - PageLink( - p, - actionBus - ), - p == cp - ) - ), - userMenu, - logo - ), - c - ) - ), - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala deleted file mode 100644 index bf473c6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ /dev/null @@ -1,143 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden -import fiftyforms.ui.components.tailwind.list.IconText.ViewModel -import fiftyforms.ui.components.tailwind.Icons -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Action - -object Breadcrumbs: - - private def slash = - import svg.{*, given} - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-300", - xmlns := "http://www.w3.org/2000/svg", - fill := "currentColor", - viewBox := "0 0 20 20", - ariaHidden := true, - path( - d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" - ) - ) - - def backIcon = - Icons.solid - .`arrow-narrow-left`() - .amend( - svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" - ) - - object Link: - case class ViewModel( - page: Page, - icon: Option[SvgElement], - extraClasses: String, - text: String, - textClasses: Option[String] = None - ) - - val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" - - def shortHome(p: Page) = ViewModel( - p, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - "Zpět na úvodní stránku", - None - ) - - def fullHome(p: Page) = ViewModel( - p, - Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), - "text-gray-400 hover:text-gray-500", - "Domů", - Some("sr-only") - ) - - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - PageLink - .container($m.map(_.page), actionBus) - .amend( - cls <-- $m.map(_.extraClasses), - child.maybe <-- $m.map(_.icon), - span( - cls <-- $m.map(_.textClasses.getOrElse("")), - child.text <-- $m.map(_.text) - ) - ) - - object FullBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - ol( - role := "list", - cls := "flex items-center space-x-4", - children <-- $m.map(_.path).split(_.id)((_, _, $p) => - li( - div( - cls := "flex items-center", - child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), - Link( - $p.map(p => - if (p.isRoot) then Link.fullHome(p) - else - Link.ViewModel( - p, - None, - s"ml-4 max-w-xs truncate ${Link.baseClasses}", - p.title - ) - ), - actionBus - ) - ) - ) - ) - ) - - object ShortBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - Link( - $m.map { p => - val target = p.parent.getOrElse(p) - if target.isRoot then Link.shortHome(target) - else - Link.ViewModel( - target, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - s"Zpět na ${target.title}" - ) - }, - actionBus - ) - - def apply(actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - nav( - cls := "flex", - aria.label := "Breadcrumb", - div( - cls := "flex sm:hidden", - ShortBreadcrumbs(router.$currentPage, actionBus) - ), - div( - cls := "hidden sm:block", - FullBreadcrumbs(router.$currentPage, actionBus) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala deleted file mode 100644 index 0db2047..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ /dev/null @@ -1,242 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent -import fiftyforms.ui.components.tailwind.Icons -import fiftyforms.ui.components.tailwind.Avatar -import cz.e_bs.cmi.mdr.pdb.UserInfo -import io.laminext.syntax.core.* - -object NavigationBar: - - case class Logo(img: String, name: String) - case class Link(a: () => Anchor, active: Boolean) - case class MenuItem(title: String) - - case class ViewModel( - userInfo: UserInfo, - pages: List[Link], - userMenu: List[MenuItem], - logo: Logo - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - val $userInfo = $m.map(_.userInfo) - val mobileMenuOpen = Var(false) - - val desktopOnly = cls("hidden md:block") - val mobileOnly = cls("md:hidden") - - inline def avatarImage(size: Int = 8) = - Avatar($userInfo.map(_.img)).avatarImage(size) - - def notificationButton = button( - tpe := "button", - cls := List( - "bg-indigo-600", - "focus:outline-none", - "focus:ring-2", - "focus:ring-offset-2", - "focus:ring-offset-indigo-600", - "focus:ring-white", - "hover:text-white", - "p-1", - "rounded-full", - "text-indigo-200" - ), - span(cls := "sr-only", "View notifications"), - Icons.outline.bell() - ) - - def userProfile: HtmlElement = - val menuOpen = Var(false) - - def menuItem(item: MenuItem, idx: Int): HtmlElement = - a( - href := "#", - cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", - role := "menuitem", - tabIndex := -1, - idAttr := s"user-menu-item-$idx", - item.title - ) - - div( - cls := "ml-3 relative", - div( - button( - tpe := "button", - cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - idAttr := "user-menu-button", - aria.expanded <-- menuOpen.signal, - aria.hasPopup := true, - span(cls := "sr-only", "Open user menu"), - child <-- avatarImage(), - onClick.preventDefault.mapTo( - !menuOpen.now() - ) --> menuOpen.writer - ) - ), - /* - * */ - div( - cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", - cls <-- menuOpen.signal.map { o => - if (o) "md:block" else "md:hidden" - }, - role := "menu", - aria.orientation := "vertical", - aria.labelledBy := "user-menu-button", - tabIndex := -1, - // : keyboard navigation - children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) - ) - ) - - def mobileProfile = - def menuItem(item: MenuItem): HtmlElement = - a( - href := "#", - cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", - item.title - ) - - div( - cls := "pt-4 pb-3 border-t border-indigo-700", - div( - cls := "flex items-center px-5", - div( - cls := "flex-shrink-0", - child <-- avatarImage(10) - ), - div( - cls := "ml-3", - div( - cls := "text-base font-medium text-white", - child.text <-- $userInfo.map(_.name) - ), - child.maybe <-- $userInfo.map( - _.email.map(e => - div( - cls := "text-sm font-medium text-indigo-300", - e - ) - ) - ) - ), - notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) - ), - div( - cls := "mt-3 px-2 space-y-1", - children <-- $m.map(_.userMenu.map(menuItem)) - ) - ) - - def pageLink(page: Link): Anchor = - page - .a() - .amend( - cls := "text-white px-3 py-2 rounded-md text-sm font-medium", - cls := Seq( - "bg-indigo-700" -> page.active, - "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active - ), - ariaCurrent := (if page.active then "page" else "false") - ) - - def logoImg: Image = - img( - cls := "h-8 w-8", - src <-- $m.map(_.logo.img), - alt <-- $m.map(_.logo.name) - ) - - def mobileMenuButton = button( - tpe := "button", - cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - aria.controls := "mobile-menu", - aria.expanded <-- mobileMenuOpen.signal, - span(cls := "sr-only", "Open main menu"), - Icons.outline - .menu() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") - ), - Icons.outline - .x() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") - ), - onClick.preventDefault.mapTo( - !mobileMenuOpen.now() - ) --> mobileMenuOpen.writer - ) - - def navBarLeft = - div( - cls := "flex items-center", - div(cls := "flex-shrink-0", logoImg), - div( - desktopOnly, - div( - cls := "ml-10 flex items-baseline space-x-4", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ) - ) - ) - - def navBarRight = - div( - desktopOnly, - div( - cls := "ml-4 flex items-center md:ml-6", - notificationButton, - userProfile - ) - ) - - def navBarMobile = - div( - cls := "-mr-2 flex", - mobileOnly, - mobileMenuButton - ) - - def navBar = - div( - cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex items-center justify-between h-16", - navBarLeft, - navBarRight, - navBarMobile - ) - ) - - def mobileMenu = - div( - mobileOnly, - cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }, - idAttr := "mobile-menu", - div( - cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ), - mobileProfile - ) - - nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala deleted file mode 100644 index 75339df..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object PageHeader: - def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = - header( - cls := "bg-white shadow-sm", - div( - cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", - h1( - cls := "text-lg leading-6 font-semibold text-gray-900", - Breadcrumbs(actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala deleted file mode 100644 index 5096463..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ /dev/null @@ -1,30 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import fiftyforms.ui.components.tailwind.Loading -import io.laminext.syntax.core.* - -object PageLayout: - case class ViewModel( - navigation: NavigationBar.ViewModel, - content: Option[HtmlElement] - ) - def apply(actionBus: Observer[Action])( - $m: Signal[ViewModel], - mods: Modifier[HtmlElement]* - )(using router: Router[Page]): HtmlElement = - val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) - div( - cls := "h-full flex flex-col", - NavigationBar($m.map(_.navigation)), - child.maybe <-- router.$currentPage.map(_.isRoot) - .switch(None, Some(PageHeader(actionBus))), - main( - cls := "flex-grow-1 overflow-y-auto", - mods, - child <-- $maybeContent.map(_.getOrElse(Loading)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala deleted file mode 100644 index 62d820f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.LinkSupport.* -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo - -object PageLink: - type ViewModel = Page - def apply($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = - a(mods($m, actions), child.text <-- $m.map(_.title)) - - def apply(m: ViewModel, actions: Observer[Action])(using - router: Router[Page] - ): Anchor = apply(Val(m), actions) - - def container($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = a(mods($m, actions)) - - def container(m: ViewModel, actions: Observer[Action])(using - Router[Page] - ): Anchor = container(Val(m), actions) - - private def mods($m: Signal[Page], actions: Observer[Action])(using - router: Router[Page] - ): Modifier[Anchor] = - Seq( - href <-- $m.map(router.absoluteUrlForPage).recover { case _ => - Some("invalid url") - }, - composeEvents(onClick.noKeyMod.preventDefault)( - _.sample($m) - .map(NavigateTo.apply) - ) --> actions - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala deleted file mode 100644 index 87f825a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.pages.dashboard.DashboardPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -class DashboardPageConnector(actionBus: Observer[Action])(using - router: Router[Page] -): - def apply: HtmlElement = - AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala deleted file mode 100644 index e9b0f71..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala +++ /dev/null @@ -1,76 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailKriteriaPage -import pages.detail.components.DukazyKriteria -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -object DetailKriteriaPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailKriteriaPageConnector( - state: DetailKriteriaPageConnector.AppState -)( - $page: Signal[Page.DetailKriteria] -)(using Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => - (p.osobniCislo.value, p.parametr.value, p.kriterium.value) - )((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map( - FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) - ) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, s, $s) => - DetailKriteriaPage($s)(state.actionBus.contramap { - case DukazyKriteria.Add => - NavigateTo( - Page.UpravDukazKriteria( - Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), - Page.Titled(s.parametr.id, Some(s.parametr.nazev)), - Page.Titled(s.kriterium.id, Some(s.kriterium.id)) - ) - ) - }) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): DetailKriteriaPage.ViewModel = - DetailKriteriaPage.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala deleted file mode 100644 index 4d8024f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ /dev/null @@ -1,57 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import pages.detail.DetailPage -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import fiftyforms.ui.components.tailwind.Color - -object DetailPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailPageConnector(state: DetailPageConnector.AppState)( - $page: Signal[Page.Detail] -)(using router: Router[Page]): - val $oscChangeSignal = $page.splitOne(_.osobniCislo.value)((osc, _, _) => osc) - val $pageChangeSignal = - $oscChangeSignal.flatMap(osc => - EventStream.fromSeq(Seq(FetchUserDetails(osc), FetchParameters(osc))) - ) - // TODO: filter the value based on the current osc - // OSC change will fetch new data, but still - // - we need to be sure that what we got is really what we ought to display - // - we want to display stale data accordingly (at least with loading indicator) - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - def apply: HtmlElement = - AppPage(state.actionBus)( - $data.combineWithFn($params)(_ zip _) - .map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: List[Parameter] - ): DetailPage.ViewModel = - DetailPage.ViewModel( - o.toDetailOsoby, - p.map( - _.toParametr(param => - PageLink.container(Page.DetailParametru(o, param), state.actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala deleted file mode 100644 index fdaa7f8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailParametruPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink - -object DetailParametruPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailParametruPageConnector( - state: DetailParametruPageConnector.AppState -)( - $page: Signal[Page.DetailParametru] -)(using router: Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => (p.osobniCislo.value, p.parametr.value))((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map(FetchParameter(_, _)) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - } yield (da, pb) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailParametruPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter - ): DetailParametruPage.ViewModel = - DetailParametruPage.ViewModel( - o.toDetailOsoby, - p.toParametr(p => - PageLink.container(Page.DetailParametru(o, p), state.actionBus) - ), - p.criteria.map( - _.toKriterium { c => - PageLink.container( - Page.DetailKriteria(o, p, c), - state.actionBus - ) - } - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala deleted file mode 100644 index 049aa22..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -case class DirectoryPageConnector( - $input: EventStream[List[UserInfo]], - actionBus: Observer[Action] -)(using router: Router[Page]): - val $data = $input.startWithNone - val $actionSignal = EventStream.fromValue(FetchDirectory) - - def apply: HtmlElement = - AppPage(actionBus)( - $data.split(_ => ())((_, _, s) => - pages.directory.DirectoryPage( - s.map( - _.map( - _.toUserRow(u => - PageLink.container( - Page.Detail(Page.Titled(u.personalNumber)), - actionBus - ) - ) - ) - ) - ) - ), - $actionSignal --> actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala deleted file mode 100644 index 88e7450..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ /dev/null @@ -1,68 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii -import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria -import fiftyforms.ui.components.tailwind.Color - -extension (o: UserInfo) - def toDetailOsoby: DetailOsoby.ViewModel = - DetailOsoby.ViewModel( - o.personalNumber, - o.name, - o.email, - o.phone, - o.img, - o.mainFunction.map(f => - DetailOsoby.Funkce.ViewModel(f.name, f.dept, f.ou) - ), - o.userContracts.headOption.map(c => - DetailOsoby.PracovniPomer.ViewModel(c.rel, c.startDate, c.endDate) - ) - ) - -extension (param: Parameter) - def toParametr(container: Parameter => Anchor): DetailParametru.ViewModel = - DetailParametru.ViewModel( - id = param.id, - nazev = param.name, - popis = param.description, - status = "Nesplněno", - statusColor = Color.red, - a = container(param) - ) - -extension (crit: ParameterCriteria) - def toKriterium( - container: ParameterCriteria => Anchor - ): DetailKriteria.ViewModel = - DetailKriteria.ViewModel( - nazev = crit.criteriumText, - kapitola = crit.chapterId, - bod = crit.itemId, - status = "Nesplněno", - statusColor = Color.red, - splneno = false, - dukazy = Nil, - container = container(crit) - ) - -extension (user: UserInfo) - def toUserRow( - container: UserInfo => HtmlElement = (_: UserInfo) => div() - ): UserRow.ViewModel = - UserRow.ViewModel( - osobniCislo = user.personalNumber.toString, - celeJmeno = user.name, - prijmeni = user.surname, - hlavniFunkce = user.mainFunction.map(_.name), - img = user.img, - container = () => container(user) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala deleted file mode 100644 index 7d5b2c1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.dashboard - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.Page - -object DashboardPage: - - def render: HtmlElement = - div("Dashboard page") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala deleted file mode 100644 index 9ef0698..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala +++ /dev/null @@ -1,31 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailKriteriaPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply($m: Signal[ViewModel])( - action: Observer[DukazyKriteria.Action] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - DukazyKriteria($m.map(_.kriterium.dukazy))(action) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala deleted file mode 100644 index 3b316e1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object DetailPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametry: SeznamParametru.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby($m.map(_.osoba)), - SeznamParametru($m.map(_.parametry)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala deleted file mode 100644 index 17224cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala +++ /dev/null @@ -1,24 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailParametruPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriteria: SeznamKriterii.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby.header($m.map(_.osoba)), - DetailParametru($m.map(_.parametr)), - SeznamKriterii($m.map(_.kriteria)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala deleted file mode 100644 index dc912fa..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ /dev/null @@ -1,107 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb -package app -package pages.detail - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.UpravDukazForm -import fiftyforms.services.files.File - -object UpravDukaz: - - type ThisPage = Page.UpravDukazKriteria - type PageKey = (OsobniCislo, String, String) - - trait State { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def availableFiles: EventStream[List[File]] - def actionBus: Observer[Action] - } - - def keyOfPage(page: ThisPage): PageKey = - (page.osobniCislo.value, page.parametr.value, page.kriterium.value) - - def onChangeAction(key: PageKey): Action = - FetchParameterCriteria( - key._1, - key._2, - key._3, - Page.UpravDukazKriteria(_, _, _) - ) - - class Connector(state: State)($page: Signal[ThisPage])(using Router[Page]): - val $paramChangeSignal = $page.splitOne(keyOfPage)((x, _, _) => x) - val $pageChangeSignal = $paramChangeSignal.map(onChangeAction) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.split(_ => ())((_, s, $s) => - PageComponent( - $s.map(buildModel), - state.availableFiles, - state.actionBus.contramap { - case UpravDukazForm.Cancelled => - NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) - case UpravDukazForm.AvailableFilesRequested => - FetchAvailableFiles(s._1.personalNumber) - } - ) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): PageComponent.ViewModel = - import connectors.* - PageComponent.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) - - object PageComponent: - import components.* - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply( - $m: Signal[ViewModel], - availableFilesStream: EventStream[List[File]], - events: Observer[UpravDukazForm.Event] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - UpravDukazForm(availableFilesStream)(events) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala deleted file mode 100644 index ca311b0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.Icons -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Color - -object DetailKriteria: - case class ViewModel( - nazev: String, - kapitola: String, - bod: String, - status: String, - statusColor: Color, - splneno: Boolean, - dukazy: List[DukazKriteria.ViewModel], - container: HtmlElement = div() - ) { - val id = s"${kapitola}${bod}" - } - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h3( - cls := "text-xl leading-6 font-bold text-gray-900", - child.text <-- $m.map(_.nazev) - ), - h3( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - "Kapitola ", - child.text <-- $m.map(_.kapitola), - " bod ", - child <-- $m.map(_.bod) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala deleted file mode 100644 index ae90290..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala +++ /dev/null @@ -1,100 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Avatar - -object DetailOsoby: - - object Funkce: - case class ViewModel( - nazev: String, - stredisko: String, - voj: String - ) - - def render($m: Signal[ViewModel]): HtmlElement = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.nazev), - span( - cls := "hidden md:inline", - " @ ", - child.text <-- $m.map(_.stredisko), - ", ", - child.text <-- $m.map(_.voj) - ) - ) - - object PracovniPomer: - case class ViewModel( - druh: String, - pocatek: LocalDate, - konec: Option[LocalDate] - ) - - def render($m: Signal[ViewModel]): HtmlElement = - import fiftyforms.ui.components.tailwind.CustomAttrs.datetime - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.druh), - " od ", - time( - datetime <-- $m.map(_.pocatek.toString), - child.text <-- $m.map(_.pocatek.toString) - ) - ) - - case class ViewModel( - osobniCislo: OsobniCislo, - jmeno: String, - email: Option[String], - telefon: Option[String], - img: Option[String], - hlavniFunkce: Option[Funkce.ViewModel], - pracovniPomer: Option[PracovniPomer.ViewModel] - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-4", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(16) - ), - div( - h1( - cls := "text-2xl font-bold text-gray-900", - child.text <-- $m.map(_.jmeno) - ), - child.maybe <-- $m.map(_.hlavniFunkce).split(_ => ())((_, _, d) => - Funkce.render(d) - ), - child.maybe <-- $m.map(_.pracovniPomer).split(_ => ())((_, _, d) => - PracovniPomer.render(d) - ) - ) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-5", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(8) - ), - div( - h1( - cls := "text-lg font-medium text-gray-800", - child.text <-- $m.map(_.jmeno) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala deleted file mode 100644 index e11cb62..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color - -object DetailParametru: - case class ViewModel( - id: String, - nazev: String, - popis: String, - status: String, - statusColor: Color, - a: Anchor - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h2( - cls := "text-2xl leading-6 font-medium text-gray-900", - child.text <-- $m.map(_.nazev) - ), - p( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - child.text <-- $m.map(_.popis) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - h2( - cls := "text-lg leading-6 font-medium text-gray-600", - child.text <-- $m.map(_.nazev) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala deleted file mode 100644 index a00ede8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala +++ /dev/null @@ -1,177 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.CustomAttrs - -object DukazKriteria: - case class Osoba(osobniCislo: String, jmeno: String) - case class Udalost(osoba: Osoba, datum: LocalDate) - case class Dokument( - url: String, - nazev: String, - pridal: Udalost, - odebral: Option[Udalost] - ) - case class ViewModel( - dokumenty: List[Dokument], - autorizoval: Option[Udalost], - platiDo: Option[LocalDate], - poznámka: Option[String] - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-lg", - div( - cls := "px-4 py-5 sm:px-6", - h3( - cls := "text-lg leading-6 font-medium text-gray-900", - """Applicant Information""" - ), - p( - cls := "mt-1 max-w-2xl text-sm text-gray-500", - """Personal details and application.""" - ) - ), - div( - cls := "border-t border-gray-200 px-4 py-5 sm:px-6", - dl( - cls := "grid grid-cols-1 gap-x-4 gap-y-8 sm:grid-cols-2", - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Full name""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Margot Foster""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Application for""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Backend Developer""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Email address""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """margotfoster@example.com""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Salary expectation""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """$120,000""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """About""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Fugiat ipsum ipsum deserunt culpa aute sint do nostrud anim incididunt cillum culpa consequat. Excepteur qui ipsum aliquip consequat sint. Sit id mollit nulla mollit nostrud in ea officia proident. Irure nostrud pariatur mollit ad adipisicing reprehenderit deserunt qui eu.""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """Attachments""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - ul( - role := "list", - cls := "border border-gray-200 rounded-md divide-y divide-gray-200", - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """resume_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ), - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """coverletter_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala deleted file mode 100644 index 22f134f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala +++ /dev/null @@ -1,35 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object DukazyKriteria: - sealed trait Action - case object Add extends Action - - type ViewModel = List[DukazKriteria.ViewModel] - - def apply($m: Signal[ViewModel])(actions: Observer[Action]): HtmlElement = - div( - child.maybe <-- $m.map(m => - if (m.isEmpty) then Some(prazdnyDukaz(actions)) - else None - ), - children <-- $m.map(_.zipWithIndex).split(_._2)((_, _, $s) => - DukazKriteria($s.map(_._1)) - ) - ) - - private def prazdnyDukaz(actions: Observer[Action]): HtmlElement = - button( - tpe := "button", - onClick.preventDefault.mapTo(Add) --> actions, - cls := "relative block w-full border-2 border-gray-300 border-dashed rounded-lg p-12 text-center hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - Icons.outline - .`document-add`(12) - .amend(svg.cls := "mx-auto text-gray-400"), - span( - cls := "mt-2 block text-sm font-medium text-gray-900", - "Přidat důkaz" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala deleted file mode 100644 index f628697..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.list.StackedList -import fiftyforms.ui.components.tailwind.list.ListRow -import fiftyforms.ui.components.tailwind.list.RowTag -import fiftyforms.ui.components.tailwind.list.RowNext -import java.time.LocalDate - -object SeznamKriterii: - type ViewModel = List[DetailKriteria.ViewModel] - - private val kritList = new StackedList[DetailKriteria.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - kritList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = - p(cls := "text-sm text-gray-500", s"${i.kapitola}${i.bod}"), - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.container - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala deleted file mode 100644 index 2bfe4bc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.list.{ - StackedList, - ListRow, - RowTag, - PropList, - IconText, - RowNext -} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.LinkSupport.* - -object SeznamParametru: - type ViewModel = List[DetailParametru.ViewModel] - - private val parametrList = new StackedList[DetailParametru.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - parametrList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = emptyNode, - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.a - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala deleted file mode 100644 index f048e9d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ /dev/null @@ -1,101 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.form.* -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement -import fiftyforms.services.files.components.tailwind.FilePicker -import cz.e_bs.cmi.mdr.pdb.frontend.AutorizujDukaz -import cz.e_bs.cmi.mdr.pdb.frontend.DocumentRef -import fiftyforms.services.files.File - -object UpravDukazForm: - sealed trait Event - case object Cancelled extends Event - case object AvailableFilesRequested extends Event - def apply(availableFilesStream: EventStream[List[File]])( - updates: Observer[Event] - ): HtmlElement = - val (filesStream, filesObserver) = - EventStream.withObserver[FilePicker.Event] - val files = Var[List[File]](Nil) - def submitButtons: HtmlElement = - div( - cls := "pt-5", - div( - cls := "flex justify-end", - button( - tpe := "button", - cls := "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zrušit", - onClick.mapTo(Cancelled) --> updates - ), - button( - tpe := "submit", - cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - disabled <-- files.signal.map(_.isEmpty), - "Autorizovat" - ) - ) - ) - - div( - filesStream.collect { case FilePicker.SelectionUpdated(files) => - files.to(List) - } --> files.writer, - filesStream.collect { case FilePicker.AvailableFilesRequested => - AvailableFilesRequested - } --> updates, - cls := "bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6", - Form( - Form.Body( - Form.Section( - FormHeader( - FormHeader.ViewModel( - "Doložení kritéria", - "Sestavte doklady poskytující důkaz kritéria a potvrďte odesláním formuláře. Případné limitace či jiné relevantní údaje vepište do pole Komentář." - ) - ), - FormFields( - FormRow( - "dokumenty", - "Dokumenty", - FilePicker(files.signal, availableFilesStream)(filesObserver) - .amend(idAttr := "dokumenty", cls("max-w-lg")) - ).toHtml, - FormRow( - "platnost", - "Platnost", - input( - idAttr := "platnost", - name := "platnost", - tpe := "date", - autoComplete := "date", - cls := "block max-w-lg w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" - ) - ).toHtml, - FormRow( - "komentar", - "Komentář", - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - textArea( - idAttr := "komentar", - name := "about", - rows := 3, - cls := "max-w-lg shadow-sm block w-full focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border border-gray-300 rounded-md" - ), - p( - cls := "mt-2 text-sm text-gray-500", - "Doplňte prosím potřebné informace související s doložením kritéria, včetně případných limitací." - ) - ) - ).toHtml - ) - ) - ), - submitButtons - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala deleted file mode 100644 index 41033f6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -import components._ - -object DirectoryPage: - - type ViewModel = List[UserRow.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - val (actionsStream, actionObserver) = - EventStream.withObserver[SearchForm.Action] - val $filter = actionsStream - .collect { - case SearchForm.ClearFilter => None - case SearchForm.SetFilter(t) => Some(t) - } - .startWith(None) - val byLetter = for { - d <- $m - f <- $filter - } yield for { - (letter, users) <- d - .filter { user => - f match - case None => true - case Some(t) => user.search.contains(t) - } - .groupBy(_.prijmeni.head) - .to(List) - .sortBy(_._1) - } yield (letter.toString, users.sortBy(_.prijmeni)) - - div( - cls := "h-full max-w-7xl mx-auto order-first flex flex-col flex-shrink-0", - SearchForm(actionObserver), - Directory(byLetter) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala deleted file mode 100644 index 73f915d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -object Directory: - - object Header: - type ViewModel = String - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500", - h3(child.text <-- $m) - ) - - object UserList: - type ViewModel = List[UserRow.ViewModel] - def apply($m: Signal[ViewModel]): HtmlElement = - ul( - role := "list", - cls := "relative z-0 divide-y divide-gray-200", - children <-- $m.split(_.osobniCislo)((_, _, s) => UserRow(s)) - ) - - type ViewModel = List[(String, List[UserRow.ViewModel])] - def apply($m: Signal[ViewModel]): HtmlElement = - nav( - cls := "flex-1 min-h-0 overflow-y-auto", - aria.label := "Directory", - children <-- $m.split(_._1)((_, _, s) => - div( - cls := "relative", - Header(s.map(_._1)), - UserList(s.map(_._2)) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala deleted file mode 100644 index e182910..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala +++ /dev/null @@ -1,54 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object SearchForm: - sealed trait Action - case object SubmitFilter extends Action - sealed trait FilterAction extends Action - case object ClearFilter extends FilterAction - case class SetFilter(value: String) extends FilterAction - - def apply(actions: Observer[Action]): HtmlElement = - div( - cls := "px-6 pt-4 pb-4", - form( - cls := "flex space-x-4", - onSubmit.mapTo(SubmitFilter) --> actions, - div( - cls := "flex-1 min-w-0", - label( - forId := "search", - cls := "sr-only", - "Hledat" - ), - div( - cls := "relative rounded-md shadow-sm", - div( - cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", - Icons.solid.search().amend(svg.cls := "text-gray-400") - ), - input( - tpe := "search", - name := "search", - idAttr := "search", - cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", - placeholder := "Hledat", - composeEvents(onInput.mapToValue.setAsValue.map(SetFilter(_)))( - _.throttle(500) - ) --> actions - ) - ) - ), - button( - tpe := "submit", - cls := "inline-flex justify-center px-3.5 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500", - Icons.solid.filter().amend(svg.cls := "text-gray-400"), - span( - cls := "sr-only", - "Hledat" - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala deleted file mode 100644 index 82f7723..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala +++ /dev/null @@ -1,52 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.airstream.core.Signal -import fiftyforms.ui.components.tailwind.Avatar - -object UserRow: - case class ViewModel( - osobniCislo: String, - celeJmeno: String, - prijmeni: String, - hlavniFunkce: Option[String], - img: Option[String], - container: () => HtmlElement = () => div() - ) { - val search = prijmeni.toLowerCase - } - - def apply($m: Signal[ViewModel]): HtmlElement = - inline def avatarImage = - Avatar($m.map(_.img)).avatarImage(10) - - li( - div( - cls := "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500", - div( - cls := "flex-shrink-0", - child <-- avatarImage - ), - div( - cls := "flex-1 min-w-0", - child <-- $m.map { o => - o.container() - .amend( - cls := "focus:outline-none", - span( - cls := "absolute inset-0", - aria.hidden := true - ), - p( - cls := "text-sm font-medium text-gray-900", - o.celeJmeno - ), - p( - cls := "text-sm text-gray-500 truncate", - o.hlavniFunkce - ) - ) - } - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala deleted file mode 100644 index 467c5f5..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala +++ /dev/null @@ -1,99 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router - -object ErrorPage: - case class ViewModel( - homePage: Page, - errorName: String, - title: String, - subTitle: String - ) - - def apply(m: ViewModel, actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - val ViewModel(homePage, errorName, title, subTitle) = m - div( - cls := "min-h-full pt-16 pb-12 flex flex-col bg-white", - main( - cls := "flex-grow flex flex-col justify-center max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex-shrink-0 flex justify-center", - a( - href := "/", - cls := "inline-flex", - span( - cls := "sr-only", - """Workflow""" - ), - img( - cls := "h-12 w-auto", - src := "https://tailwindui.com/img/logos/workflow-mark.svg?color=indigo&shade=600", - alt := "" - ) - ) - ), - div( - cls := "py-16", - div( - cls := "text-center", - p( - cls := "text-sm font-semibold text-indigo-600 uppercase tracking-wide", - errorName - ), - h1( - cls := "mt-2 text-4xl font-extrabold text-gray-900 tracking-tight sm:text-5xl", - title - ), - p( - cls := "mt-2 text-base text-gray-500", - subTitle - ), - div( - cls := "mt-6", - PageLink - .container(homePage, actionBus) - .amend( - cls := "text-base font-medium text-indigo-600 hover:text-indigo-500", - """Go back home""" - ) - ) - ) - ) - ), - footer( - cls := "flex-shrink-0 max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", - nav( - cls := "flex justify-center space-x-4", - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Contact Support""" - ), - span( - cls := "inline-block border-l border-gray-300", - aria.hidden := true - ), - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Status""" - ), - span( - cls := "inline-block border-l border-gray-300", - aria.hidden := true - ), - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Twitter""" - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala deleted file mode 100644 index bd9baec..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action - -object NotFoundPage: - def apply(homePage: Page, url: String, actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - ErrorPage( - ErrorPage.ViewModel( - homePage, - "404 error", - "Page not found.", - s"Sorry, but page $url doesn't exist." - ), - actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala deleted file mode 100644 index 476fe3c..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action - -object UnhandledErrorPage: - - case class ViewModel( - homePage: Page, - errorName: Option[String], - errorMessage: Option[String] - ) - - def apply(m: ViewModel, actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - ErrorPage( - ErrorPage.ViewModel( - m.homePage, - "Unexpected error occurred", - m.errorName.getOrElse( - "Uh oh!" - ), // TODO: translations, better text than uh oh - m.errorMessage.getOrElse( - "This wasn't supposed to happen! Please try again." - ) - ), - actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala deleted file mode 100644 index 25f059f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala +++ /dev/null @@ -1,6 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.services - -import com.raquo.airstream.core.Observer - -trait DataFetcher[K, A]: - def fetch(id: K, sink: Observer[A]): Unit diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala deleted file mode 100644 index a469830..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala +++ /dev/null @@ -1,120 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package state - -import com.raquo.airstream.core.EventStream -import cz.e_bs.cmi.mdr.pdb.{UserInfo, OsobniCislo} -import com.raquo.airstream.core.Observer -import scala.scalajs.js -import scala.scalajs.js.JSON -import zio.json._ -import com.raquo.airstream.eventbus.EventBus -import com.raquo.airstream.ownership.Owner -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.UserFunction -import cz.e_bs.cmi.mdr.pdb.UserContract -import fiftyforms.services.files.File - -trait AppState - extends connectors.DetailPageConnector.AppState - with connectors.DetailParametruPageConnector.AppState - with connectors.DetailKriteriaPageConnector.AppState - with pages.detail.UpravDukaz.State: - def users: EventStream[List[UserInfo]] - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - -class MockAppState(implicit owner: Owner, router: Router[Page]) - extends AppState: - - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonDecoder[UserFunction] = DeriveJsonDecoder.gen - given JsonDecoder[UserContract] = DeriveJsonDecoder.gen - given JsonDecoder[UserInfo] = DeriveJsonDecoder.gen - - given JsonDecoder[ParameterCriteria] = DeriveJsonDecoder.gen - given JsonDecoder[Parameter] = DeriveJsonDecoder.gen - - private val actions = EventBus[Action]() - private val (parametersStream, pushParameters) = - EventStream.withCallback[List[Parameter]] - private val (usersStream, pushUsers) = - EventStream.withCallback[List[UserInfo]] - private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] - private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] - - private val mockData: List[UserInfo] = - mockUsers - .asInstanceOf[js.Dictionary[js.Object]] - .values - // TODO: is there a more efficient way to parse from JS object directly? - .map { o => - val parsed = JSON.stringify(o).fromJson[UserInfo] - parsed.left.foreach(org.scalajs.dom.console.log(_)) - parsed - } - .collect { case Right(u) => - u - } - .toList - - private val mockParameters: List[Parameter] = - pdbParams - .asInstanceOf[js.Dictionary[js.Object]] - .values - .map(o => JSON.stringify(o).fromJson[Parameter]) - .collect { case Right(p) => p } - .toList - - // TODO: Extract to separate event handler - actions.events.foreach { - case FetchDirectory => pushUsers(mockData) - case FetchUserDetails(osc) => - mockData.find(_.personalNumber == osc).foreach { o => - pushDetails(o) - router.replaceState(Page.Detail(o)) - } - case FetchParameters(osc) => - pushParameters(mockParameters) - case FetchParameter(osc, paramId) => - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(Page.DetailParametru(o, p)) - case FetchParameterCriteria(osc, paramId, critId, page) => - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - c <- p.criteria.find(_.id == critId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(page(o, p, c)) - case NavigateTo(page) => router.pushState(page) - case FetchAvailableFiles(osc) => - pushFiles( - List( - File("https://tc163.cmi.cz/here", "Example file") - ) - ) - } - - override def users: EventStream[List[UserInfo]] = - usersStream.debugWithName("users") - - override def details: EventStream[UserInfo] = - detailsStream.debugWithName("details") - - override def parameters: EventStream[List[Parameter]] = - parametersStream.debugWithName("parameters") - - override def availableFiles: EventStream[List[File]] = - filesStream.debugWithName("available files") - - override def actionBus: Observer[Action] = - actions.writer.debugWithName("actions writer") diff --git a/app/src/main/scala/mdr/pdb/app/Main.scala b/app/src/main/scala/mdr/pdb/app/Main.scala new file mode 100644 index 0000000..081b4ee --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/Main.scala @@ -0,0 +1,103 @@ +package mdr.pdb.app + +import scala.scalajs.js.annotation.JSExportTopLevel +import scala.scalajs.js.annotation.JSExport +import scala.scalajs.js.annotation.JSImport +import scala.scalajs.js +import org.scalajs.dom +import com.raquo.laminar.api.L.{*, given} +import scala.scalajs.js.Date +import com.raquo.waypoint.Router +import com.raquo.waypoint.SplitRender +import mdr.pdb.app.services.DataFetcher +import scala.scalajs.js.JSON +import zio.json._ +import mdr.pdb.UserInfo +import mdr.pdb.app.state.AppState + +@js.native +@JSImport("stylesheets/main.css", JSImport.Namespace) +object Css extends js.Any + +@js.native +@JSImport("data/users.json", JSImport.Default) +object mockUsers extends js.Object + +@js.native +@JSImport("params/pdb-params.json", JSImport.Default) +object pdbParams extends js.Object + +@JSExportTopLevel("app") +object Main: + + @JSExport + def main(args: Array[String]): Unit = + import Routes.given + onLoad { + setupAirstream() + val appContainer = dom.document.querySelector("#app") + val _ = + render( + appContainer, + renderPage(state.MockAppState(using unsafeWindowOwner, router)) + ) + } + + private def onLoad(f: => Unit): Unit = + documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) + + private def setupAirstream()(using router: Router[Page]): Unit = + AirstreamError.registerUnhandledErrorCallback(err => + router.forcePage( + Page.UnhandledError( + Some(err.getClass.getName), // TODO: Fill only in dev mode + Some(err.getMessage) + ) + ) + ) + + def renderPage(state: AppState)(using + router: Router[Page] + ): HtmlElement = + val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) + .collectSignal[Page.Detail]( + connectors + .DetailPageConnector(state)(_) + .apply + ) + .collectSignal[Page.DetailParametru]( + connectors + .DetailParametruPageConnector(state)(_) + .apply + ) + .collectSignal[Page.DetailKriteria]( + connectors + .DetailKriteriaPageConnector(state)(_) + .apply + ) + .collectSignal[Page.UpravDukazKriteria]( + pages.detail.UpravDukaz.Connector(state)(_).apply + ) + .collectStatic(Page.Dashboard)( + connectors.DashboardPageConnector(state.actionBus).apply + ) + .collect[Page.NotFound](pg => + pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) + ) + .collect[Page.UnhandledError](pg => + pages.errors + .UnhandledErrorPage( + pages.errors.UnhandledErrorPage + .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), + state.actionBus + ) + ) + .collectStatic(Page.Directory)( + connectors + .DirectoryPageConnector(state.users, state.actionBus) + .apply + ) + div(cls := "h-full", child <-- pageSplitter.$view) + + // Pull in the stylesheet + val css: Css.type = Css diff --git a/app/src/main/scala/mdr/pdb/app/Routes.scala b/app/src/main/scala/mdr/pdb/app/Routes.scala new file mode 100644 index 0000000..2cc860c --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/Routes.scala @@ -0,0 +1,198 @@ +package mdr.pdb.app + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.* +import org.scalajs.dom +import zio.json.{*, given} +import mdr.pdb.OsobniCislo + +import scala.scalajs.js +import mdr.pdb.UserInfo +import mdr.pdb.Parameter +import mdr.pdb.ParameterCriteria +import mdr.pdb.app.Page.Titled + +// enum is not working with Waypoints' SplitRender collectStatic +sealed abstract class Page( + val id: String, + val title: String, + val parent: Option[Page] +) { + val path: Vector[Page] = + parent match + case None => Vector(this) + case Some(p) => p.path :+ this + + val isRoot: Boolean = parent.isEmpty +} + +object Page: + + case class Titled[V](value: V, title: Option[String] = None): + val show: String = title.getOrElse(value.toString) + + case object Directory extends Page("directory", "Adresář", None) + + case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) + + case class Detail(osobniCislo: Titled[OsobniCislo]) + extends Page("user", osobniCislo.show, Some(Directory)) + + object Detail { + def apply(o: UserInfo): Detail = Detail( + Titled(o.personalNumber, Some(o.name)) + ) + } + + case class DetailParametru( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String] + ) extends Page( + "parameter", + parametr.show, + Some(Detail(osobniCislo)) + ) + + object DetailParametru { + def apply(o: UserInfo, p: Parameter): DetailParametru = + DetailParametru( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)) + ) + } + + case class DetailKriteria( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String], + kriterium: Titled[String] + ) extends Page( + "criteria", + kriterium.show, + Some(DetailParametru(osobniCislo, parametr)) + ) + + object DetailKriteria { + def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = + DetailKriteria( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)), + Titled(k.id, Some(k.id)) + ) + } + + case class UpravDukazKriteria( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String], + kriterium: Titled[String] + ) extends Page( + "addProof", + "Důkaz", + Some(DetailKriteria(osobniCislo, parametr, kriterium)) + ) + + object UpravDukazKriteria { + def apply( + o: UserInfo, + p: Parameter, + k: ParameterCriteria + ): UpravDukazKriteria = + UpravDukazKriteria( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)), + Titled(k.id, Some(k.id)) + ) + } + + case class NotFound(url: String) extends Page("404", "404", Some(Directory)) + + case class UnhandledError( + errorName: Option[String], + errorMessage: Option[String] + ) extends Page("500", "Unexpected error", Some(Directory)) + +object Routes: + given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) + given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) + given [V: JsonEncoder]: JsonEncoder[Titled[V]] = + DeriveJsonEncoder.gen[Titled[V]] + given [V: JsonDecoder]: JsonDecoder[Titled[V]] = + DeriveJsonDecoder.gen[Titled[V]] + given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] + given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] + + val base = + js.`import`.meta.env.BASE_URL + .asInstanceOf[String] + "app" + + val homePage: Page = Page.Directory + + given router: Router[Page] = Router[Page]( + routes = List( + Route.static(homePage, root / endOfSegments, basePath = base), + Route.static( + Page.Dashboard, + root / "dashboard" / endOfSegments, + basePath = base + ), + Route[Page.Detail, String]( + encode = _.osobniCislo.value.toString, + decode = osc => Page.Detail(Titled(OsobniCislo(osc))), + root / "osoba" / segment[String] / endOfSegments, + basePath = base + ), + Route[Page.DetailParametru, (String, String)]( + encode = p => (p.osobniCislo.value.toString, p.parametr.value), + decode = + p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / endOfSegments, + basePath = base + ), + Route[Page.DetailKriteria, (String, String, String)]( + encode = p => + ( + p.osobniCislo.value.toString, + p.parametr.value, + p.kriterium.value.replaceAll("\\.", "--") + ), + decode = p => + Page.DetailKriteria( + Titled(OsobniCislo(p._1)), + Titled(p._2), + Titled(p._3.replaceAll("--", ".")) + ), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / "kriterium" / segment[String] / endOfSegments, + basePath = base + ), + Route[Page.UpravDukazKriteria, (String, String, String)]( + encode = p => + ( + p.osobniCislo.value.toString, + p.parametr.value, + p.kriterium.value.replaceAll("\\.", "--") + ), + decode = p => + Page.UpravDukazKriteria( + Titled(OsobniCislo(p._1)), + Titled(p._2), + Titled(p._3.replaceAll("--", ".")) + ), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / "kriterium" / segment[String] / "edit" / endOfSegments, + basePath = base + ) + ), + serializePage = _.toJson, + deserializePage = _.fromJson[Page] + .fold(s => throw IllegalStateException(s), identity), + getPageTitle = _.title, + routeFallback = url => Page.NotFound(url), + deserializeFallback = _ => Page.Dashboard + )( + $popStateEvent = windowEvents.onPopState, + owner = unsafeWindowOwner + ) diff --git a/app/src/main/scala/mdr/pdb/app/actions.scala b/app/src/main/scala/mdr/pdb/app/actions.scala new file mode 100644 index 0000000..d078b3a --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/actions.scala @@ -0,0 +1,21 @@ +package mdr.pdb.app + +import mdr.pdb.OsobniCislo +import mdr.pdb.UserInfo +import mdr.pdb.Parameter +import mdr.pdb.ParameterCriteria + +sealed trait Action + +case object FetchDirectory extends Action +case class FetchUserDetails(osc: OsobniCislo) extends Action +case class FetchParameters(osc: OsobniCislo) extends Action +case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action +case class FetchParameterCriteria( + osc: OsobniCislo, + paramId: String, + critId: String, + page: (UserInfo, Parameter, ParameterCriteria) => Page +) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action +case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/mdr/pdb/app/components/AppPage.scala new file mode 100644 index 0000000..130c817 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/AppPage.scala @@ -0,0 +1,82 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.Page +import mdr.pdb.{UserProfile, UserInfo, OsobniCislo} +import com.raquo.waypoint.Router +import mdr.pdb.app.Action +import mdr.pdb.app.NavigateTo +import mdr.pdb.UserFunction + +object AppPage: + // TODO: pages by logged in user + val pages: List[Page] = List(Page.Directory, Page.Dashboard) + + import NavigationBar.{Logo, MenuItem} + + val logo = Logo( + "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", + "Workflow" + ) + + // TODO: menu items by user profile + val userMenu = + List( + MenuItem("Your Profile"), + MenuItem("Settings"), + MenuItem("Sign out") + ) + + // TODO: load user profile + val $userProfile = Var( + UserProfile( + "tom", + UserInfo( + OsobniCislo("1031"), + "tom", + "Tom", + "Cook", + None, + None, + Some("tom@example.com"), + Some("+420 222 866 180"), + Some( + UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") + ), + Nil, + None + ) + ) + ) + + val $userInfo = $userProfile.signal.map(_.userInfo) + + type ViewModel = Option[HtmlElement] + def apply( + actionBus: Observer[Action] + )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using + router: Router[Page] + ): HtmlElement = + PageLayout(actionBus)( + $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => + PageLayout.ViewModel( + NavigationBar.ViewModel( + u, + pages.map(p => + NavigationBar.Link( + () => + PageLink( + p, + actionBus + ), + p == cp + ) + ), + userMenu, + logo + ), + c + ) + ), + mods + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala new file mode 100644 index 0000000..9375398 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala @@ -0,0 +1,143 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.tailwind.list.IconText.ViewModel +import fiftyforms.ui.components.tailwind.Icons +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.Page + +import com.raquo.laminar.api.L.{*, given} +import io.laminext.syntax.core.{*, given} +import mdr.pdb.app.Action + +object Breadcrumbs: + + private def slash = + import svg.{*, given} + svg( + cls := "flex-shrink-0 h-5 w-5 text-gray-300", + xmlns := "http://www.w3.org/2000/svg", + fill := "currentColor", + viewBox := "0 0 20 20", + ariaHidden := true, + path( + d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" + ) + ) + + def backIcon = + Icons.solid + .`arrow-narrow-left`() + .amend( + svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" + ) + + object Link: + case class ViewModel( + page: Page, + icon: Option[SvgElement], + extraClasses: String, + text: String, + textClasses: Option[String] = None + ) + + val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" + + def shortHome(p: Page) = ViewModel( + p, + Some(backIcon), + "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", + "Zpět na úvodní stránku", + None + ) + + def fullHome(p: Page) = ViewModel( + p, + Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), + "text-gray-400 hover:text-gray-500", + "Domů", + Some("sr-only") + ) + + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + PageLink + .container($m.map(_.page), actionBus) + .amend( + cls <-- $m.map(_.extraClasses), + child.maybe <-- $m.map(_.icon), + span( + cls <-- $m.map(_.textClasses.getOrElse("")), + child.text <-- $m.map(_.text) + ) + ) + + object FullBreadcrumbs: + type ViewModel = Page + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + ol( + role := "list", + cls := "flex items-center space-x-4", + children <-- $m.map(_.path).split(_.id)((_, _, $p) => + li( + div( + cls := "flex items-center", + child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), + Link( + $p.map(p => + if (p.isRoot) then Link.fullHome(p) + else + Link.ViewModel( + p, + None, + s"ml-4 max-w-xs truncate ${Link.baseClasses}", + p.title + ) + ), + actionBus + ) + ) + ) + ) + ) + + object ShortBreadcrumbs: + type ViewModel = Page + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + Link( + $m.map { p => + val target = p.parent.getOrElse(p) + if target.isRoot then Link.shortHome(target) + else + Link.ViewModel( + target, + Some(backIcon), + "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", + s"Zpět na ${target.title}" + ) + }, + actionBus + ) + + def apply(actionBus: Observer[Action])(using + router: Router[Page] + ): HtmlElement = + nav( + cls := "flex", + aria.label := "Breadcrumb", + div( + cls := "flex sm:hidden", + ShortBreadcrumbs(router.$currentPage, actionBus) + ), + div( + cls := "hidden sm:block", + FullBreadcrumbs(router.$currentPage, actionBus) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala new file mode 100644 index 0000000..4bb8b8d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala @@ -0,0 +1,242 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent +import fiftyforms.ui.components.tailwind.Icons +import fiftyforms.ui.components.tailwind.Avatar +import mdr.pdb.UserInfo +import io.laminext.syntax.core.* + +object NavigationBar: + + case class Logo(img: String, name: String) + case class Link(a: () => Anchor, active: Boolean) + case class MenuItem(title: String) + + case class ViewModel( + userInfo: UserInfo, + pages: List[Link], + userMenu: List[MenuItem], + logo: Logo + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + val $userInfo = $m.map(_.userInfo) + val mobileMenuOpen = Var(false) + + val desktopOnly = cls("hidden md:block") + val mobileOnly = cls("md:hidden") + + inline def avatarImage(size: Int = 8) = + Avatar($userInfo.map(_.img)).avatarImage(size) + + def notificationButton = button( + tpe := "button", + cls := List( + "bg-indigo-600", + "focus:outline-none", + "focus:ring-2", + "focus:ring-offset-2", + "focus:ring-offset-indigo-600", + "focus:ring-white", + "hover:text-white", + "p-1", + "rounded-full", + "text-indigo-200" + ), + span(cls := "sr-only", "View notifications"), + Icons.outline.bell() + ) + + def userProfile: HtmlElement = + val menuOpen = Var(false) + + def menuItem(item: MenuItem, idx: Int): HtmlElement = + a( + href := "#", + cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", + role := "menuitem", + tabIndex := -1, + idAttr := s"user-menu-item-$idx", + item.title + ) + + div( + cls := "ml-3 relative", + div( + button( + tpe := "button", + cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", + idAttr := "user-menu-button", + aria.expanded <-- menuOpen.signal, + aria.hasPopup := true, + span(cls := "sr-only", "Open user menu"), + child <-- avatarImage(), + onClick.preventDefault.mapTo( + !menuOpen.now() + ) --> menuOpen.writer + ) + ), + /* + * */ + div( + cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", + cls <-- menuOpen.signal.map { o => + if (o) "md:block" else "md:hidden" + }, + role := "menu", + aria.orientation := "vertical", + aria.labelledBy := "user-menu-button", + tabIndex := -1, + // : keyboard navigation + children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) + ) + ) + + def mobileProfile = + def menuItem(item: MenuItem): HtmlElement = + a( + href := "#", + cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", + item.title + ) + + div( + cls := "pt-4 pb-3 border-t border-indigo-700", + div( + cls := "flex items-center px-5", + div( + cls := "flex-shrink-0", + child <-- avatarImage(10) + ), + div( + cls := "ml-3", + div( + cls := "text-base font-medium text-white", + child.text <-- $userInfo.map(_.name) + ), + child.maybe <-- $userInfo.map( + _.email.map(e => + div( + cls := "text-sm font-medium text-indigo-300", + e + ) + ) + ) + ), + notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) + ), + div( + cls := "mt-3 px-2 space-y-1", + children <-- $m.map(_.userMenu.map(menuItem)) + ) + ) + + def pageLink(page: Link): Anchor = + page + .a() + .amend( + cls := "text-white px-3 py-2 rounded-md text-sm font-medium", + cls := Seq( + "bg-indigo-700" -> page.active, + "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active + ), + ariaCurrent := (if page.active then "page" else "false") + ) + + def logoImg: Image = + img( + cls := "h-8 w-8", + src <-- $m.map(_.logo.img), + alt <-- $m.map(_.logo.name) + ) + + def mobileMenuButton = button( + tpe := "button", + cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", + aria.controls := "mobile-menu", + aria.expanded <-- mobileMenuOpen.signal, + span(cls := "sr-only", "Open main menu"), + Icons.outline + .menu() + .amend( + svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") + ), + Icons.outline + .x() + .amend( + svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") + ), + onClick.preventDefault.mapTo( + !mobileMenuOpen.now() + ) --> mobileMenuOpen.writer + ) + + def navBarLeft = + div( + cls := "flex items-center", + div(cls := "flex-shrink-0", logoImg), + div( + desktopOnly, + div( + cls := "ml-10 flex items-baseline space-x-4", + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) + ) + ) + ) + + def navBarRight = + div( + desktopOnly, + div( + cls := "ml-4 flex items-center md:ml-6", + notificationButton, + userProfile + ) + ) + + def navBarMobile = + div( + cls := "-mr-2 flex", + mobileOnly, + mobileMenuButton + ) + + def navBar = + div( + cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", + div( + cls := "flex items-center justify-between h-16", + navBarLeft, + navBarRight, + navBarMobile + ) + ) + + def mobileMenu = + div( + mobileOnly, + cls <-- mobileMenuOpen.signal.map { o => + if (o) "block" else "hidden" + }, + idAttr := "mobile-menu", + div( + cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) + ), + mobileProfile + ) + + nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala new file mode 100644 index 0000000..c4cb10d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala @@ -0,0 +1,19 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.Page +import com.raquo.waypoint.Router +import mdr.pdb.app.Action + +object PageHeader: + def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = + header( + cls := "bg-white shadow-sm", + div( + cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", + h1( + cls := "text-lg leading-6 font-semibold text-gray-900", + Breadcrumbs(actionBus) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala new file mode 100644 index 0000000..aae4539 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala @@ -0,0 +1,30 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.Page +import mdr.pdb.app.Action +import fiftyforms.ui.components.tailwind.Loading +import io.laminext.syntax.core.* + +object PageLayout: + case class ViewModel( + navigation: NavigationBar.ViewModel, + content: Option[HtmlElement] + ) + def apply(actionBus: Observer[Action])( + $m: Signal[ViewModel], + mods: Modifier[HtmlElement]* + )(using router: Router[Page]): HtmlElement = + val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) + div( + cls := "h-full flex flex-col", + NavigationBar($m.map(_.navigation)), + child.maybe <-- router.$currentPage.map(_.isRoot) + .switch(None, Some(PageHeader(actionBus))), + main( + cls := "flex-grow-1 overflow-y-auto", + mods, + child <-- $maybeContent.map(_.getOrElse(Loading)) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/mdr/pdb/app/components/PageLink.scala new file mode 100644 index 0000000..e6e7052 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageLink.scala @@ -0,0 +1,40 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.LinkSupport.* +import mdr.pdb.app.Page +import mdr.pdb.app.Action +import com.raquo.waypoint.Router +import mdr.pdb.app.NavigateTo + +object PageLink: + type ViewModel = Page + def apply($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = + a(mods($m, actions), child.text <-- $m.map(_.title)) + + def apply(m: ViewModel, actions: Observer[Action])(using + router: Router[Page] + ): Anchor = apply(Val(m), actions) + + def container($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = a(mods($m, actions)) + + def container(m: ViewModel, actions: Observer[Action])(using + Router[Page] + ): Anchor = container(Val(m), actions) + + private def mods($m: Signal[Page], actions: Observer[Action])(using + router: Router[Page] + ): Modifier[Anchor] = + Seq( + href <-- $m.map(router.absoluteUrlForPage).recover { case _ => + Some("invalid url") + }, + composeEvents(onClick.noKeyMod.preventDefault)( + _.sample($m) + .map(NavigateTo.apply) + ) --> actions + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala new file mode 100644 index 0000000..7682772 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala @@ -0,0 +1,13 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.pages.dashboard.DashboardPage +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage + +class DashboardPageConnector(actionBus: Observer[Action])(using + router: Router[Page] +): + def apply: HtmlElement = + AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala new file mode 100644 index 0000000..76b9133 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala @@ -0,0 +1,76 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.Parameter +import mdr.pdb.UserInfo +import mdr.pdb.app.pages.detail.DetailParametruPage +import pages.detail.DetailKriteriaPage +import pages.detail.components.DukazyKriteria +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.ParameterCriteria + +object DetailKriteriaPageConnector { + trait AppState { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + } +} + +case class DetailKriteriaPageConnector( + state: DetailKriteriaPageConnector.AppState +)( + $page: Signal[Page.DetailKriteria] +)(using Router[Page]): + val $paramChangeSignal = + $page.splitOne(p => + (p.osobniCislo.value, p.parametr.value, p.kriterium.value) + )((x, _, _) => x) + val $pageChangeSignal = + $paramChangeSignal.map( + FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) + ) + + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + val $merged = + $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => + for { + da <- d + pa <- p + pb <- pa.find(_.id == pc._2) + ka <- pb.criteria.find(_.id == pc._3) + } yield (da, pb, ka) + ) + + def apply: HtmlElement = + AppPage(state.actionBus)( + $merged.map(_.map(buildModel)) + .split(_ => ())((_, s, $s) => + DetailKriteriaPage($s)(state.actionBus.contramap { + case DukazyKriteria.Add => + NavigateTo( + Page.UpravDukazKriteria( + Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), + Page.Titled(s.parametr.id, Some(s.parametr.nazev)), + Page.Titled(s.kriterium.id, Some(s.kriterium.id)) + ) + ) + }) + ), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: Parameter, + k: ParameterCriteria + ): DetailKriteriaPage.ViewModel = + DetailKriteriaPage.ViewModel( + o.toDetailOsoby, + p.toParametr(_ => a()), + k.toKriterium(_ => a()) + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DetailPageConnector.scala new file mode 100644 index 0000000..df3a6b7 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DetailPageConnector.scala @@ -0,0 +1,57 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.UserInfo +import pages.detail.DetailPage +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.pages.detail.components.DetailOsoby +import mdr.pdb.Parameter +import mdr.pdb.app.pages.detail.components.SeznamParametru +import fiftyforms.ui.components.tailwind.Color + +object DetailPageConnector { + trait AppState { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + } +} + +case class DetailPageConnector(state: DetailPageConnector.AppState)( + $page: Signal[Page.Detail] +)(using router: Router[Page]): + val $oscChangeSignal = $page.splitOne(_.osobniCislo.value)((osc, _, _) => osc) + val $pageChangeSignal = + $oscChangeSignal.flatMap(osc => + EventStream.fromSeq(Seq(FetchUserDetails(osc), FetchParameters(osc))) + ) + // TODO: filter the value based on the current osc + // OSC change will fetch new data, but still + // - we need to be sure that what we got is really what we ought to display + // - we want to display stale data accordingly (at least with loading indicator) + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + def apply: HtmlElement = + AppPage(state.actionBus)( + $data.combineWithFn($params)(_ zip _) + .map(_.map(buildModel)) + .split(_ => ())((_, _, s) => DetailPage(s)), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: List[Parameter] + ): DetailPage.ViewModel = + DetailPage.ViewModel( + o.toDetailOsoby, + p.map( + _.toParametr(param => + PageLink.container(Page.DetailParametru(o, param), state.actionBus) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DetailParametruPageConnector.scala new file mode 100644 index 0000000..fa7193d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DetailParametruPageConnector.scala @@ -0,0 +1,67 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.Parameter +import mdr.pdb.UserInfo +import mdr.pdb.app.pages.detail.DetailParametruPage +import pages.detail.DetailParametruPage +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.components.PageLink + +object DetailParametruPageConnector { + trait AppState { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + } +} + +case class DetailParametruPageConnector( + state: DetailParametruPageConnector.AppState +)( + $page: Signal[Page.DetailParametru] +)(using router: Router[Page]): + val $paramChangeSignal = + $page.splitOne(p => (p.osobniCislo.value, p.parametr.value))((x, _, _) => x) + val $pageChangeSignal = + $paramChangeSignal.map(FetchParameter(_, _)) + + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + val $merged = + $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => + for { + da <- d + pa <- p + pb <- pa.find(_.id == pc._2) + } yield (da, pb) + ) + + def apply: HtmlElement = + AppPage(state.actionBus)( + $merged.map(_.map(buildModel)) + .split(_ => ())((_, _, s) => DetailParametruPage(s)), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: Parameter + ): DetailParametruPage.ViewModel = + DetailParametruPage.ViewModel( + o.toDetailOsoby, + p.toParametr(p => + PageLink.container(Page.DetailParametru(o, p), state.actionBus) + ), + p.criteria.map( + _.toKriterium { c => + PageLink.container( + Page.DetailKriteria(o, p, c), + state.actionBus + ) + } + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DirectoryPageConnector.scala new file mode 100644 index 0000000..ab2c045 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DirectoryPageConnector.scala @@ -0,0 +1,34 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.UserInfo +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.components.AppPage + +case class DirectoryPageConnector( + $input: EventStream[List[UserInfo]], + actionBus: Observer[Action] +)(using router: Router[Page]): + val $data = $input.startWithNone + val $actionSignal = EventStream.fromValue(FetchDirectory) + + def apply: HtmlElement = + AppPage(actionBus)( + $data.split(_ => ())((_, _, s) => + pages.directory.DirectoryPage( + s.map( + _.map( + _.toUserRow(u => + PageLink.container( + Page.Detail(Page.Titled(u.personalNumber)), + actionBus + ) + ) + ) + ) + ) + ), + $actionSignal --> actionBus + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/mdr/pdb/app/connectors/conversions.scala new file mode 100644 index 0000000..f581796 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/conversions.scala @@ -0,0 +1,68 @@ +package mdr.pdb.app.connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.UserInfo +import mdr.pdb.app.pages.detail.components.DetailOsoby +import mdr.pdb.Parameter +import mdr.pdb.app.pages.detail.components.SeznamParametru +import mdr.pdb.ParameterCriteria +import mdr.pdb.app.pages.detail.components.SeznamKriterii +import mdr.pdb.app.pages.directory.components.UserRow +import mdr.pdb.app.pages.detail.components.DetailParametru +import mdr.pdb.app.pages.detail.components.DetailKriteria +import fiftyforms.ui.components.tailwind.Color + +extension (o: UserInfo) + def toDetailOsoby: DetailOsoby.ViewModel = + DetailOsoby.ViewModel( + o.personalNumber, + o.name, + o.email, + o.phone, + o.img, + o.mainFunction.map(f => + DetailOsoby.Funkce.ViewModel(f.name, f.dept, f.ou) + ), + o.userContracts.headOption.map(c => + DetailOsoby.PracovniPomer.ViewModel(c.rel, c.startDate, c.endDate) + ) + ) + +extension (param: Parameter) + def toParametr(container: Parameter => Anchor): DetailParametru.ViewModel = + DetailParametru.ViewModel( + id = param.id, + nazev = param.name, + popis = param.description, + status = "Nesplněno", + statusColor = Color.red, + a = container(param) + ) + +extension (crit: ParameterCriteria) + def toKriterium( + container: ParameterCriteria => Anchor + ): DetailKriteria.ViewModel = + DetailKriteria.ViewModel( + nazev = crit.criteriumText, + kapitola = crit.chapterId, + bod = crit.itemId, + status = "Nesplněno", + statusColor = Color.red, + splneno = false, + dukazy = Nil, + container = container(crit) + ) + +extension (user: UserInfo) + def toUserRow( + container: UserInfo => HtmlElement = (_: UserInfo) => div() + ): UserRow.ViewModel = + UserRow.ViewModel( + osobniCislo = user.personalNumber.toString, + celeJmeno = user.name, + prijmeni = user.surname, + hlavniFunkce = user.mainFunction.map(_.name), + img = user.img, + container = () => container(user) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/dashboard/DashboardPage.scala b/app/src/main/scala/mdr/pdb/app/pages/dashboard/DashboardPage.scala new file mode 100644 index 0000000..0eb8f7a --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/dashboard/DashboardPage.scala @@ -0,0 +1,11 @@ +package mdr.pdb.app.pages.dashboard + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.Page + +object DashboardPage: + + def render: HtmlElement = + div("Dashboard page") diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala new file mode 100644 index 0000000..ff3a25f --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala @@ -0,0 +1,31 @@ +package mdr.pdb.app.pages.detail + +import com.raquo.laminar.api.L.{*, given} + +import components._ + +object DetailKriteriaPage: + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametr: DetailParametru.ViewModel, + kriterium: DetailKriteria.ViewModel + ) + + def apply($m: Signal[ViewModel])( + action: Observer[DukazyKriteria.Action] + ): HtmlElement = + div( + cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + div( + DetailOsoby.header($m.map(_.osoba)), + DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") + ), + div( + DetailKriteria($m.map(_.kriterium)), + DukazyKriteria($m.map(_.kriterium.dukazy))(action) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/DetailPage.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailPage.scala new file mode 100644 index 0000000..b591cc9 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailPage.scala @@ -0,0 +1,25 @@ +package mdr.pdb.app.pages.detail + +import com.raquo.laminar.api.L.{*, given} + +import components._ +import mdr.pdb.app.Page +import com.raquo.waypoint.Router +import mdr.pdb.app.Action + +object DetailPage: + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametry: SeznamParametru.ViewModel + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + DetailOsoby($m.map(_.osoba)), + SeznamParametru($m.map(_.parametry)) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/DetailParametruPage.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailParametruPage.scala new file mode 100644 index 0000000..3322279 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailParametruPage.scala @@ -0,0 +1,24 @@ +package mdr.pdb.app.pages.detail + +import com.raquo.laminar.api.L.{*, given} + +import components._ + +object DetailParametruPage: + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametr: DetailParametru.ViewModel, + kriteria: SeznamKriterii.ViewModel + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + DetailOsoby.header($m.map(_.osoba)), + DetailParametru($m.map(_.parametr)), + SeznamKriterii($m.map(_.kriteria)) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala new file mode 100644 index 0000000..b58618a --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala @@ -0,0 +1,107 @@ +package mdr.pdb +package app +package pages.detail + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.pages.detail.components.UpravDukazForm +import fiftyforms.services.files.File + +object UpravDukaz: + + type ThisPage = Page.UpravDukazKriteria + type PageKey = (OsobniCislo, String, String) + + trait State { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def availableFiles: EventStream[List[File]] + def actionBus: Observer[Action] + } + + def keyOfPage(page: ThisPage): PageKey = + (page.osobniCislo.value, page.parametr.value, page.kriterium.value) + + def onChangeAction(key: PageKey): Action = + FetchParameterCriteria( + key._1, + key._2, + key._3, + Page.UpravDukazKriteria(_, _, _) + ) + + class Connector(state: State)($page: Signal[ThisPage])(using Router[Page]): + val $paramChangeSignal = $page.splitOne(keyOfPage)((x, _, _) => x) + val $pageChangeSignal = $paramChangeSignal.map(onChangeAction) + + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + val $merged = + $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => + for { + da <- d + pa <- p + pb <- pa.find(_.id == pc._2) + ka <- pb.criteria.find(_.id == pc._3) + } yield (da, pb, ka) + ) + + def apply: HtmlElement = + AppPage(state.actionBus)( + $merged.split(_ => ())((_, s, $s) => + PageComponent( + $s.map(buildModel), + state.availableFiles, + state.actionBus.contramap { + case UpravDukazForm.Cancelled => + NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) + case UpravDukazForm.AvailableFilesRequested => + FetchAvailableFiles(s._1.personalNumber) + } + ) + ), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: Parameter, + k: ParameterCriteria + ): PageComponent.ViewModel = + import connectors.* + PageComponent.ViewModel( + o.toDetailOsoby, + p.toParametr(_ => a()), + k.toKriterium(_ => a()) + ) + + object PageComponent: + import components.* + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametr: DetailParametru.ViewModel, + kriterium: DetailKriteria.ViewModel + ) + + def apply( + $m: Signal[ViewModel], + availableFilesStream: EventStream[List[File]], + events: Observer[UpravDukazForm.Event] + ): HtmlElement = + div( + cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + div( + DetailOsoby.header($m.map(_.osoba)), + DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") + ), + div( + DetailKriteria($m.map(_.kriterium)), + UpravDukazForm(availableFilesStream)(events) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailKriteria.scala new file mode 100644 index 0000000..a3ab634 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailKriteria.scala @@ -0,0 +1,37 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.CustomAttrs +import fiftyforms.ui.components.tailwind.Icons +import java.time.LocalDate +import fiftyforms.ui.components.tailwind.Color + +object DetailKriteria: + case class ViewModel( + nazev: String, + kapitola: String, + bod: String, + status: String, + statusColor: Color, + splneno: Boolean, + dukazy: List[DukazKriteria.ViewModel], + container: HtmlElement = div() + ) { + val id = s"${kapitola}${bod}" + } + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "pb-5 border-b border-gray-200", + h3( + cls := "text-xl leading-6 font-bold text-gray-900", + child.text <-- $m.map(_.nazev) + ), + h3( + cls := "mt-2 max-w-4xl text-sm text-gray-500", + "Kapitola ", + child.text <-- $m.map(_.kapitola), + " bod ", + child <-- $m.map(_.bod) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailOsoby.scala new file mode 100644 index 0000000..0fe2ba5 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailOsoby.scala @@ -0,0 +1,100 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.OsobniCislo + +import java.time.LocalDate +import fiftyforms.ui.components.tailwind.Avatar + +object DetailOsoby: + + object Funkce: + case class ViewModel( + nazev: String, + stredisko: String, + voj: String + ) + + def render($m: Signal[ViewModel]): HtmlElement = + p( + cls := "text-sm font-medium text-gray-500", + child.text <-- $m.map(_.nazev), + span( + cls := "hidden md:inline", + " @ ", + child.text <-- $m.map(_.stredisko), + ", ", + child.text <-- $m.map(_.voj) + ) + ) + + object PracovniPomer: + case class ViewModel( + druh: String, + pocatek: LocalDate, + konec: Option[LocalDate] + ) + + def render($m: Signal[ViewModel]): HtmlElement = + import fiftyforms.ui.components.tailwind.CustomAttrs.datetime + p( + cls := "text-sm font-medium text-gray-500", + child.text <-- $m.map(_.druh), + " od ", + time( + datetime <-- $m.map(_.pocatek.toString), + child.text <-- $m.map(_.pocatek.toString) + ) + ) + + case class ViewModel( + osobniCislo: OsobniCislo, + jmeno: String, + email: Option[String], + telefon: Option[String], + img: Option[String], + hlavniFunkce: Option[Funkce.ViewModel], + pracovniPomer: Option[PracovniPomer.ViewModel] + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "md:flex md:items-center md:justify-between md:space-x-4", + div( + cls := "flex items-start space-x-4", + div( + cls := "flex-shrink-0", + Avatar($m.map(_.img)).avatar(16) + ), + div( + h1( + cls := "text-2xl font-bold text-gray-900", + child.text <-- $m.map(_.jmeno) + ), + child.maybe <-- $m.map(_.hlavniFunkce).split(_ => ())((_, _, d) => + Funkce.render(d) + ), + child.maybe <-- $m.map(_.pracovniPomer).split(_ => ())((_, _, d) => + PracovniPomer.render(d) + ) + ) + ) + ) + + def header($m: Signal[ViewModel]): HtmlElement = + div( + cls := "md:flex md:items-center md:justify-between md:space-x-5", + div( + cls := "flex items-start space-x-4", + div( + cls := "flex-shrink-0", + Avatar($m.map(_.img)).avatar(8) + ), + div( + h1( + cls := "text-lg font-medium text-gray-800", + child.text <-- $m.map(_.jmeno) + ) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailParametru.scala new file mode 100644 index 0000000..6310dde --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailParametru.scala @@ -0,0 +1,34 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.Color + +object DetailParametru: + case class ViewModel( + id: String, + nazev: String, + popis: String, + status: String, + statusColor: Color, + a: Anchor + ) + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "pb-5 border-b border-gray-200", + h2( + cls := "text-2xl leading-6 font-medium text-gray-900", + child.text <-- $m.map(_.nazev) + ), + p( + cls := "mt-2 max-w-4xl text-sm text-gray-500", + child.text <-- $m.map(_.popis) + ) + ) + + def header($m: Signal[ViewModel]): HtmlElement = + div( + h2( + cls := "text-lg leading-6 font-medium text-gray-600", + child.text <-- $m.map(_.nazev) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazKriteria.scala new file mode 100644 index 0000000..c292919 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazKriteria.scala @@ -0,0 +1,177 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import java.time.LocalDate +import fiftyforms.ui.components.tailwind.CustomAttrs + +object DukazKriteria: + case class Osoba(osobniCislo: String, jmeno: String) + case class Udalost(osoba: Osoba, datum: LocalDate) + case class Dokument( + url: String, + nazev: String, + pridal: Udalost, + odebral: Option[Udalost] + ) + case class ViewModel( + dokumenty: List[Dokument], + autorizoval: Option[Udalost], + platiDo: Option[LocalDate], + poznámka: Option[String] + ) + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "bg-white shadow overflow-hidden sm:rounded-lg", + div( + cls := "px-4 py-5 sm:px-6", + h3( + cls := "text-lg leading-6 font-medium text-gray-900", + """Applicant Information""" + ), + p( + cls := "mt-1 max-w-2xl text-sm text-gray-500", + """Personal details and application.""" + ) + ), + div( + cls := "border-t border-gray-200 px-4 py-5 sm:px-6", + dl( + cls := "grid grid-cols-1 gap-x-4 gap-y-8 sm:grid-cols-2", + div( + cls := "sm:col-span-1", + dt( + cls := "text-sm font-medium text-gray-500", + """Full name""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """Margot Foster""" + ) + ), + div( + cls := "sm:col-span-1", + dt( + cls := "text-sm font-medium text-gray-500", + """Application for""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """Backend Developer""" + ) + ), + div( + cls := "sm:col-span-1", + dt( + cls := "text-sm font-medium text-gray-500", + """Email address""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """margotfoster@example.com""" + ) + ), + div( + cls := "sm:col-span-1", + dt( + cls := "text-sm font-medium text-gray-500", + """Salary expectation""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """$120,000""" + ) + ), + div( + cls := "sm:col-span-2", + dt( + cls := "text-sm font-medium text-gray-500", + """About""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """Fugiat ipsum ipsum deserunt culpa aute sint do nostrud anim incididunt cillum culpa consequat. Excepteur qui ipsum aliquip consequat sint. Sit id mollit nulla mollit nostrud in ea officia proident. Irure nostrud pariatur mollit ad adipisicing reprehenderit deserunt qui eu.""" + ) + ), + div( + cls := "sm:col-span-2", + dt( + cls := "text-sm font-medium text-gray-500", + """Attachments""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + ul( + role := "list", + cls := "border border-gray-200 rounded-md divide-y divide-gray-200", + li( + cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", + div( + cls := "w-0 flex-1 flex items-center", { + import svg.* + import CustomAttrs.svg.ariaHidden + svg( + cls := "flex-shrink-0 h-5 w-5 text-gray-400", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + ariaHidden := true, + path( + fillRule := "evenodd", + d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", + clipRule := "evenodd" + ) + ) + }, + span( + cls := "ml-2 flex-1 w-0 truncate", + """resume_back_end_developer.pdf""" + ) + ), + div( + cls := "ml-4 flex-shrink-0", + a( + href := "#", + cls := "font-medium text-indigo-600 hover:text-indigo-500", + """Download""" + ) + ) + ), + li( + cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", + div( + cls := "w-0 flex-1 flex items-center", { + import svg.* + import CustomAttrs.svg.ariaHidden + svg( + cls := "flex-shrink-0 h-5 w-5 text-gray-400", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + ariaHidden := true, + path( + fillRule := "evenodd", + d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", + clipRule := "evenodd" + ) + ) + }, + span( + cls := "ml-2 flex-1 w-0 truncate", + """coverletter_back_end_developer.pdf""" + ) + ), + div( + cls := "ml-4 flex-shrink-0", + a( + href := "#", + cls := "font-medium text-indigo-600 hover:text-indigo-500", + """Download""" + ) + ) + ) + ) + ) + ) + ) + ) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala deleted file mode 100644 index a3b00af..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala +++ /dev/null @@ -1,103 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import scala.scalajs.js.annotation.JSExportTopLevel -import scala.scalajs.js.annotation.JSExport -import scala.scalajs.js.annotation.JSImport -import scala.scalajs.js -import org.scalajs.dom -import com.raquo.laminar.api.L.{*, given} -import scala.scalajs.js.Date -import com.raquo.waypoint.Router -import com.raquo.waypoint.SplitRender -import cz.e_bs.cmi.mdr.pdb.app.services.DataFetcher -import scala.scalajs.js.JSON -import zio.json._ -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.state.AppState - -@js.native -@JSImport("stylesheets/main.css", JSImport.Namespace) -object Css extends js.Any - -@js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native -@JSImport("params/pdb-params.json", JSImport.Default) -object pdbParams extends js.Object - -@JSExportTopLevel("app") -object Main: - - @JSExport - def main(args: Array[String]): Unit = - import Routes.given - onLoad { - setupAirstream() - val appContainer = dom.document.querySelector("#app") - val _ = - render( - appContainer, - renderPage(state.MockAppState(using unsafeWindowOwner, router)) - ) - } - - private def onLoad(f: => Unit): Unit = - documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) - - private def setupAirstream()(using router: Router[Page]): Unit = - AirstreamError.registerUnhandledErrorCallback(err => - router.forcePage( - Page.UnhandledError( - Some(err.getClass.getName), // TODO: Fill only in dev mode - Some(err.getMessage) - ) - ) - ) - - def renderPage(state: AppState)(using - router: Router[Page] - ): HtmlElement = - val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) - .collectSignal[Page.Detail]( - connectors - .DetailPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailParametru]( - connectors - .DetailParametruPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailKriteria]( - connectors - .DetailKriteriaPageConnector(state)(_) - .apply - ) - .collectSignal[Page.UpravDukazKriteria]( - pages.detail.UpravDukaz.Connector(state)(_).apply - ) - .collectStatic(Page.Dashboard)( - connectors.DashboardPageConnector(state.actionBus).apply - ) - .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) - ) - .collect[Page.UnhandledError](pg => - pages.errors - .UnhandledErrorPage( - pages.errors.UnhandledErrorPage - .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), - state.actionBus - ) - ) - .collectStatic(Page.Directory)( - connectors - .DirectoryPageConnector(state.users, state.actionBus) - .apply - ) - div(cls := "h-full", child <-- pageSplitter.$view) - - // Pull in the stylesheet - val css: Css.type = Css diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala deleted file mode 100644 index 3bbc8c0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ /dev/null @@ -1,198 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.* -import org.scalajs.dom -import zio.json.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import scala.scalajs.js -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.Page.Titled - -// enum is not working with Waypoints' SplitRender collectStatic -sealed abstract class Page( - val id: String, - val title: String, - val parent: Option[Page] -) { - val path: Vector[Page] = - parent match - case None => Vector(this) - case Some(p) => p.path :+ this - - val isRoot: Boolean = parent.isEmpty -} - -object Page: - - case class Titled[V](value: V, title: Option[String] = None): - val show: String = title.getOrElse(value.toString) - - case object Directory extends Page("directory", "Adresář", None) - - case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) - - case class Detail(osobniCislo: Titled[OsobniCislo]) - extends Page("user", osobniCislo.show, Some(Directory)) - - object Detail { - def apply(o: UserInfo): Detail = Detail( - Titled(o.personalNumber, Some(o.name)) - ) - } - - case class DetailParametru( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String] - ) extends Page( - "parameter", - parametr.show, - Some(Detail(osobniCislo)) - ) - - object DetailParametru { - def apply(o: UserInfo, p: Parameter): DetailParametru = - DetailParametru( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)) - ) - } - - case class DetailKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "criteria", - kriterium.show, - Some(DetailParametru(osobniCislo, parametr)) - ) - - object DetailKriteria { - def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = - DetailKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class UpravDukazKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "addProof", - "Důkaz", - Some(DetailKriteria(osobniCislo, parametr, kriterium)) - ) - - object UpravDukazKriteria { - def apply( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): UpravDukazKriteria = - UpravDukazKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class NotFound(url: String) extends Page("404", "404", Some(Directory)) - - case class UnhandledError( - errorName: Option[String], - errorMessage: Option[String] - ) extends Page("500", "Unexpected error", Some(Directory)) - -object Routes: - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) - given [V: JsonEncoder]: JsonEncoder[Titled[V]] = - DeriveJsonEncoder.gen[Titled[V]] - given [V: JsonDecoder]: JsonDecoder[Titled[V]] = - DeriveJsonDecoder.gen[Titled[V]] - given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] - given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] - - val base = - js.`import`.meta.env.BASE_URL - .asInstanceOf[String] + "app" - - val homePage: Page = Page.Directory - - given router: Router[Page] = Router[Page]( - routes = List( - Route.static(homePage, root / endOfSegments, basePath = base), - Route.static( - Page.Dashboard, - root / "dashboard" / endOfSegments, - basePath = base - ), - Route[Page.Detail, String]( - encode = _.osobniCislo.value.toString, - decode = osc => Page.Detail(Titled(OsobniCislo(osc))), - root / "osoba" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.DetailParametru, (String, String)]( - encode = p => (p.osobniCislo.value.toString, p.parametr.value), - decode = - p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / endOfSegments, - basePath = base - ), - Route[Page.DetailKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.DetailKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.UpravDukazKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.UpravDukazKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / "edit" / endOfSegments, - basePath = base - ) - ), - serializePage = _.toJson, - deserializePage = _.fromJson[Page] - .fold(s => throw IllegalStateException(s), identity), - getPageTitle = _.title, - routeFallback = url => Page.NotFound(url), - deserializeFallback = _ => Page.Dashboard - )( - $popStateEvent = windowEvents.onPopState, - owner = unsafeWindowOwner - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala deleted file mode 100644 index b24b82b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -sealed trait Action - -case object FetchDirectory extends Action -case class FetchUserDetails(osc: OsobniCislo) extends Action -case class FetchParameters(osc: OsobniCislo) extends Action -case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action -case class FetchParameterCriteria( - osc: OsobniCislo, - paramId: String, - critId: String, - page: (UserInfo, Parameter, ParameterCriteria) => Page -) extends Action -case class FetchAvailableFiles(osc: OsobniCislo) extends Action -case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala deleted file mode 100644 index c637d35..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala +++ /dev/null @@ -1,82 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.{UserProfile, UserInfo, OsobniCislo} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo -import cz.e_bs.cmi.mdr.pdb.UserFunction - -object AppPage: - // TODO: pages by logged in user - val pages: List[Page] = List(Page.Directory, Page.Dashboard) - - import NavigationBar.{Logo, MenuItem} - - val logo = Logo( - "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", - "Workflow" - ) - - // TODO: menu items by user profile - val userMenu = - List( - MenuItem("Your Profile"), - MenuItem("Settings"), - MenuItem("Sign out") - ) - - // TODO: load user profile - val $userProfile = Var( - UserProfile( - "tom", - UserInfo( - OsobniCislo("1031"), - "tom", - "Tom", - "Cook", - None, - None, - Some("tom@example.com"), - Some("+420 222 866 180"), - Some( - UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") - ), - Nil, - None - ) - ) - ) - - val $userInfo = $userProfile.signal.map(_.userInfo) - - type ViewModel = Option[HtmlElement] - def apply( - actionBus: Observer[Action] - )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using - router: Router[Page] - ): HtmlElement = - PageLayout(actionBus)( - $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => - PageLayout.ViewModel( - NavigationBar.ViewModel( - u, - pages.map(p => - NavigationBar.Link( - () => - PageLink( - p, - actionBus - ), - p == cp - ) - ), - userMenu, - logo - ), - c - ) - ), - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala deleted file mode 100644 index bf473c6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ /dev/null @@ -1,143 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden -import fiftyforms.ui.components.tailwind.list.IconText.ViewModel -import fiftyforms.ui.components.tailwind.Icons -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Action - -object Breadcrumbs: - - private def slash = - import svg.{*, given} - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-300", - xmlns := "http://www.w3.org/2000/svg", - fill := "currentColor", - viewBox := "0 0 20 20", - ariaHidden := true, - path( - d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" - ) - ) - - def backIcon = - Icons.solid - .`arrow-narrow-left`() - .amend( - svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" - ) - - object Link: - case class ViewModel( - page: Page, - icon: Option[SvgElement], - extraClasses: String, - text: String, - textClasses: Option[String] = None - ) - - val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" - - def shortHome(p: Page) = ViewModel( - p, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - "Zpět na úvodní stránku", - None - ) - - def fullHome(p: Page) = ViewModel( - p, - Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), - "text-gray-400 hover:text-gray-500", - "Domů", - Some("sr-only") - ) - - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - PageLink - .container($m.map(_.page), actionBus) - .amend( - cls <-- $m.map(_.extraClasses), - child.maybe <-- $m.map(_.icon), - span( - cls <-- $m.map(_.textClasses.getOrElse("")), - child.text <-- $m.map(_.text) - ) - ) - - object FullBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - ol( - role := "list", - cls := "flex items-center space-x-4", - children <-- $m.map(_.path).split(_.id)((_, _, $p) => - li( - div( - cls := "flex items-center", - child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), - Link( - $p.map(p => - if (p.isRoot) then Link.fullHome(p) - else - Link.ViewModel( - p, - None, - s"ml-4 max-w-xs truncate ${Link.baseClasses}", - p.title - ) - ), - actionBus - ) - ) - ) - ) - ) - - object ShortBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - Link( - $m.map { p => - val target = p.parent.getOrElse(p) - if target.isRoot then Link.shortHome(target) - else - Link.ViewModel( - target, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - s"Zpět na ${target.title}" - ) - }, - actionBus - ) - - def apply(actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - nav( - cls := "flex", - aria.label := "Breadcrumb", - div( - cls := "flex sm:hidden", - ShortBreadcrumbs(router.$currentPage, actionBus) - ), - div( - cls := "hidden sm:block", - FullBreadcrumbs(router.$currentPage, actionBus) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala deleted file mode 100644 index 0db2047..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ /dev/null @@ -1,242 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent -import fiftyforms.ui.components.tailwind.Icons -import fiftyforms.ui.components.tailwind.Avatar -import cz.e_bs.cmi.mdr.pdb.UserInfo -import io.laminext.syntax.core.* - -object NavigationBar: - - case class Logo(img: String, name: String) - case class Link(a: () => Anchor, active: Boolean) - case class MenuItem(title: String) - - case class ViewModel( - userInfo: UserInfo, - pages: List[Link], - userMenu: List[MenuItem], - logo: Logo - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - val $userInfo = $m.map(_.userInfo) - val mobileMenuOpen = Var(false) - - val desktopOnly = cls("hidden md:block") - val mobileOnly = cls("md:hidden") - - inline def avatarImage(size: Int = 8) = - Avatar($userInfo.map(_.img)).avatarImage(size) - - def notificationButton = button( - tpe := "button", - cls := List( - "bg-indigo-600", - "focus:outline-none", - "focus:ring-2", - "focus:ring-offset-2", - "focus:ring-offset-indigo-600", - "focus:ring-white", - "hover:text-white", - "p-1", - "rounded-full", - "text-indigo-200" - ), - span(cls := "sr-only", "View notifications"), - Icons.outline.bell() - ) - - def userProfile: HtmlElement = - val menuOpen = Var(false) - - def menuItem(item: MenuItem, idx: Int): HtmlElement = - a( - href := "#", - cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", - role := "menuitem", - tabIndex := -1, - idAttr := s"user-menu-item-$idx", - item.title - ) - - div( - cls := "ml-3 relative", - div( - button( - tpe := "button", - cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - idAttr := "user-menu-button", - aria.expanded <-- menuOpen.signal, - aria.hasPopup := true, - span(cls := "sr-only", "Open user menu"), - child <-- avatarImage(), - onClick.preventDefault.mapTo( - !menuOpen.now() - ) --> menuOpen.writer - ) - ), - /* - * */ - div( - cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", - cls <-- menuOpen.signal.map { o => - if (o) "md:block" else "md:hidden" - }, - role := "menu", - aria.orientation := "vertical", - aria.labelledBy := "user-menu-button", - tabIndex := -1, - // : keyboard navigation - children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) - ) - ) - - def mobileProfile = - def menuItem(item: MenuItem): HtmlElement = - a( - href := "#", - cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", - item.title - ) - - div( - cls := "pt-4 pb-3 border-t border-indigo-700", - div( - cls := "flex items-center px-5", - div( - cls := "flex-shrink-0", - child <-- avatarImage(10) - ), - div( - cls := "ml-3", - div( - cls := "text-base font-medium text-white", - child.text <-- $userInfo.map(_.name) - ), - child.maybe <-- $userInfo.map( - _.email.map(e => - div( - cls := "text-sm font-medium text-indigo-300", - e - ) - ) - ) - ), - notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) - ), - div( - cls := "mt-3 px-2 space-y-1", - children <-- $m.map(_.userMenu.map(menuItem)) - ) - ) - - def pageLink(page: Link): Anchor = - page - .a() - .amend( - cls := "text-white px-3 py-2 rounded-md text-sm font-medium", - cls := Seq( - "bg-indigo-700" -> page.active, - "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active - ), - ariaCurrent := (if page.active then "page" else "false") - ) - - def logoImg: Image = - img( - cls := "h-8 w-8", - src <-- $m.map(_.logo.img), - alt <-- $m.map(_.logo.name) - ) - - def mobileMenuButton = button( - tpe := "button", - cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - aria.controls := "mobile-menu", - aria.expanded <-- mobileMenuOpen.signal, - span(cls := "sr-only", "Open main menu"), - Icons.outline - .menu() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") - ), - Icons.outline - .x() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") - ), - onClick.preventDefault.mapTo( - !mobileMenuOpen.now() - ) --> mobileMenuOpen.writer - ) - - def navBarLeft = - div( - cls := "flex items-center", - div(cls := "flex-shrink-0", logoImg), - div( - desktopOnly, - div( - cls := "ml-10 flex items-baseline space-x-4", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ) - ) - ) - - def navBarRight = - div( - desktopOnly, - div( - cls := "ml-4 flex items-center md:ml-6", - notificationButton, - userProfile - ) - ) - - def navBarMobile = - div( - cls := "-mr-2 flex", - mobileOnly, - mobileMenuButton - ) - - def navBar = - div( - cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex items-center justify-between h-16", - navBarLeft, - navBarRight, - navBarMobile - ) - ) - - def mobileMenu = - div( - mobileOnly, - cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }, - idAttr := "mobile-menu", - div( - cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ), - mobileProfile - ) - - nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala deleted file mode 100644 index 75339df..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object PageHeader: - def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = - header( - cls := "bg-white shadow-sm", - div( - cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", - h1( - cls := "text-lg leading-6 font-semibold text-gray-900", - Breadcrumbs(actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala deleted file mode 100644 index 5096463..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ /dev/null @@ -1,30 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import fiftyforms.ui.components.tailwind.Loading -import io.laminext.syntax.core.* - -object PageLayout: - case class ViewModel( - navigation: NavigationBar.ViewModel, - content: Option[HtmlElement] - ) - def apply(actionBus: Observer[Action])( - $m: Signal[ViewModel], - mods: Modifier[HtmlElement]* - )(using router: Router[Page]): HtmlElement = - val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) - div( - cls := "h-full flex flex-col", - NavigationBar($m.map(_.navigation)), - child.maybe <-- router.$currentPage.map(_.isRoot) - .switch(None, Some(PageHeader(actionBus))), - main( - cls := "flex-grow-1 overflow-y-auto", - mods, - child <-- $maybeContent.map(_.getOrElse(Loading)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala deleted file mode 100644 index 62d820f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.LinkSupport.* -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo - -object PageLink: - type ViewModel = Page - def apply($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = - a(mods($m, actions), child.text <-- $m.map(_.title)) - - def apply(m: ViewModel, actions: Observer[Action])(using - router: Router[Page] - ): Anchor = apply(Val(m), actions) - - def container($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = a(mods($m, actions)) - - def container(m: ViewModel, actions: Observer[Action])(using - Router[Page] - ): Anchor = container(Val(m), actions) - - private def mods($m: Signal[Page], actions: Observer[Action])(using - router: Router[Page] - ): Modifier[Anchor] = - Seq( - href <-- $m.map(router.absoluteUrlForPage).recover { case _ => - Some("invalid url") - }, - composeEvents(onClick.noKeyMod.preventDefault)( - _.sample($m) - .map(NavigateTo.apply) - ) --> actions - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala deleted file mode 100644 index 87f825a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.pages.dashboard.DashboardPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -class DashboardPageConnector(actionBus: Observer[Action])(using - router: Router[Page] -): - def apply: HtmlElement = - AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala deleted file mode 100644 index e9b0f71..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala +++ /dev/null @@ -1,76 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailKriteriaPage -import pages.detail.components.DukazyKriteria -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -object DetailKriteriaPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailKriteriaPageConnector( - state: DetailKriteriaPageConnector.AppState -)( - $page: Signal[Page.DetailKriteria] -)(using Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => - (p.osobniCislo.value, p.parametr.value, p.kriterium.value) - )((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map( - FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) - ) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, s, $s) => - DetailKriteriaPage($s)(state.actionBus.contramap { - case DukazyKriteria.Add => - NavigateTo( - Page.UpravDukazKriteria( - Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), - Page.Titled(s.parametr.id, Some(s.parametr.nazev)), - Page.Titled(s.kriterium.id, Some(s.kriterium.id)) - ) - ) - }) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): DetailKriteriaPage.ViewModel = - DetailKriteriaPage.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala deleted file mode 100644 index 4d8024f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ /dev/null @@ -1,57 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import pages.detail.DetailPage -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import fiftyforms.ui.components.tailwind.Color - -object DetailPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailPageConnector(state: DetailPageConnector.AppState)( - $page: Signal[Page.Detail] -)(using router: Router[Page]): - val $oscChangeSignal = $page.splitOne(_.osobniCislo.value)((osc, _, _) => osc) - val $pageChangeSignal = - $oscChangeSignal.flatMap(osc => - EventStream.fromSeq(Seq(FetchUserDetails(osc), FetchParameters(osc))) - ) - // TODO: filter the value based on the current osc - // OSC change will fetch new data, but still - // - we need to be sure that what we got is really what we ought to display - // - we want to display stale data accordingly (at least with loading indicator) - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - def apply: HtmlElement = - AppPage(state.actionBus)( - $data.combineWithFn($params)(_ zip _) - .map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: List[Parameter] - ): DetailPage.ViewModel = - DetailPage.ViewModel( - o.toDetailOsoby, - p.map( - _.toParametr(param => - PageLink.container(Page.DetailParametru(o, param), state.actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala deleted file mode 100644 index fdaa7f8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailParametruPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink - -object DetailParametruPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailParametruPageConnector( - state: DetailParametruPageConnector.AppState -)( - $page: Signal[Page.DetailParametru] -)(using router: Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => (p.osobniCislo.value, p.parametr.value))((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map(FetchParameter(_, _)) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - } yield (da, pb) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailParametruPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter - ): DetailParametruPage.ViewModel = - DetailParametruPage.ViewModel( - o.toDetailOsoby, - p.toParametr(p => - PageLink.container(Page.DetailParametru(o, p), state.actionBus) - ), - p.criteria.map( - _.toKriterium { c => - PageLink.container( - Page.DetailKriteria(o, p, c), - state.actionBus - ) - } - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala deleted file mode 100644 index 049aa22..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -case class DirectoryPageConnector( - $input: EventStream[List[UserInfo]], - actionBus: Observer[Action] -)(using router: Router[Page]): - val $data = $input.startWithNone - val $actionSignal = EventStream.fromValue(FetchDirectory) - - def apply: HtmlElement = - AppPage(actionBus)( - $data.split(_ => ())((_, _, s) => - pages.directory.DirectoryPage( - s.map( - _.map( - _.toUserRow(u => - PageLink.container( - Page.Detail(Page.Titled(u.personalNumber)), - actionBus - ) - ) - ) - ) - ) - ), - $actionSignal --> actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala deleted file mode 100644 index 88e7450..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ /dev/null @@ -1,68 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii -import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria -import fiftyforms.ui.components.tailwind.Color - -extension (o: UserInfo) - def toDetailOsoby: DetailOsoby.ViewModel = - DetailOsoby.ViewModel( - o.personalNumber, - o.name, - o.email, - o.phone, - o.img, - o.mainFunction.map(f => - DetailOsoby.Funkce.ViewModel(f.name, f.dept, f.ou) - ), - o.userContracts.headOption.map(c => - DetailOsoby.PracovniPomer.ViewModel(c.rel, c.startDate, c.endDate) - ) - ) - -extension (param: Parameter) - def toParametr(container: Parameter => Anchor): DetailParametru.ViewModel = - DetailParametru.ViewModel( - id = param.id, - nazev = param.name, - popis = param.description, - status = "Nesplněno", - statusColor = Color.red, - a = container(param) - ) - -extension (crit: ParameterCriteria) - def toKriterium( - container: ParameterCriteria => Anchor - ): DetailKriteria.ViewModel = - DetailKriteria.ViewModel( - nazev = crit.criteriumText, - kapitola = crit.chapterId, - bod = crit.itemId, - status = "Nesplněno", - statusColor = Color.red, - splneno = false, - dukazy = Nil, - container = container(crit) - ) - -extension (user: UserInfo) - def toUserRow( - container: UserInfo => HtmlElement = (_: UserInfo) => div() - ): UserRow.ViewModel = - UserRow.ViewModel( - osobniCislo = user.personalNumber.toString, - celeJmeno = user.name, - prijmeni = user.surname, - hlavniFunkce = user.mainFunction.map(_.name), - img = user.img, - container = () => container(user) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala deleted file mode 100644 index 7d5b2c1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.dashboard - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.Page - -object DashboardPage: - - def render: HtmlElement = - div("Dashboard page") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala deleted file mode 100644 index 9ef0698..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala +++ /dev/null @@ -1,31 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailKriteriaPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply($m: Signal[ViewModel])( - action: Observer[DukazyKriteria.Action] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - DukazyKriteria($m.map(_.kriterium.dukazy))(action) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala deleted file mode 100644 index 3b316e1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object DetailPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametry: SeznamParametru.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby($m.map(_.osoba)), - SeznamParametru($m.map(_.parametry)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala deleted file mode 100644 index 17224cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala +++ /dev/null @@ -1,24 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailParametruPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriteria: SeznamKriterii.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby.header($m.map(_.osoba)), - DetailParametru($m.map(_.parametr)), - SeznamKriterii($m.map(_.kriteria)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala deleted file mode 100644 index dc912fa..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ /dev/null @@ -1,107 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb -package app -package pages.detail - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.UpravDukazForm -import fiftyforms.services.files.File - -object UpravDukaz: - - type ThisPage = Page.UpravDukazKriteria - type PageKey = (OsobniCislo, String, String) - - trait State { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def availableFiles: EventStream[List[File]] - def actionBus: Observer[Action] - } - - def keyOfPage(page: ThisPage): PageKey = - (page.osobniCislo.value, page.parametr.value, page.kriterium.value) - - def onChangeAction(key: PageKey): Action = - FetchParameterCriteria( - key._1, - key._2, - key._3, - Page.UpravDukazKriteria(_, _, _) - ) - - class Connector(state: State)($page: Signal[ThisPage])(using Router[Page]): - val $paramChangeSignal = $page.splitOne(keyOfPage)((x, _, _) => x) - val $pageChangeSignal = $paramChangeSignal.map(onChangeAction) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.split(_ => ())((_, s, $s) => - PageComponent( - $s.map(buildModel), - state.availableFiles, - state.actionBus.contramap { - case UpravDukazForm.Cancelled => - NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) - case UpravDukazForm.AvailableFilesRequested => - FetchAvailableFiles(s._1.personalNumber) - } - ) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): PageComponent.ViewModel = - import connectors.* - PageComponent.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) - - object PageComponent: - import components.* - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply( - $m: Signal[ViewModel], - availableFilesStream: EventStream[List[File]], - events: Observer[UpravDukazForm.Event] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - UpravDukazForm(availableFilesStream)(events) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala deleted file mode 100644 index ca311b0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.Icons -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Color - -object DetailKriteria: - case class ViewModel( - nazev: String, - kapitola: String, - bod: String, - status: String, - statusColor: Color, - splneno: Boolean, - dukazy: List[DukazKriteria.ViewModel], - container: HtmlElement = div() - ) { - val id = s"${kapitola}${bod}" - } - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h3( - cls := "text-xl leading-6 font-bold text-gray-900", - child.text <-- $m.map(_.nazev) - ), - h3( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - "Kapitola ", - child.text <-- $m.map(_.kapitola), - " bod ", - child <-- $m.map(_.bod) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala deleted file mode 100644 index ae90290..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala +++ /dev/null @@ -1,100 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Avatar - -object DetailOsoby: - - object Funkce: - case class ViewModel( - nazev: String, - stredisko: String, - voj: String - ) - - def render($m: Signal[ViewModel]): HtmlElement = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.nazev), - span( - cls := "hidden md:inline", - " @ ", - child.text <-- $m.map(_.stredisko), - ", ", - child.text <-- $m.map(_.voj) - ) - ) - - object PracovniPomer: - case class ViewModel( - druh: String, - pocatek: LocalDate, - konec: Option[LocalDate] - ) - - def render($m: Signal[ViewModel]): HtmlElement = - import fiftyforms.ui.components.tailwind.CustomAttrs.datetime - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.druh), - " od ", - time( - datetime <-- $m.map(_.pocatek.toString), - child.text <-- $m.map(_.pocatek.toString) - ) - ) - - case class ViewModel( - osobniCislo: OsobniCislo, - jmeno: String, - email: Option[String], - telefon: Option[String], - img: Option[String], - hlavniFunkce: Option[Funkce.ViewModel], - pracovniPomer: Option[PracovniPomer.ViewModel] - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-4", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(16) - ), - div( - h1( - cls := "text-2xl font-bold text-gray-900", - child.text <-- $m.map(_.jmeno) - ), - child.maybe <-- $m.map(_.hlavniFunkce).split(_ => ())((_, _, d) => - Funkce.render(d) - ), - child.maybe <-- $m.map(_.pracovniPomer).split(_ => ())((_, _, d) => - PracovniPomer.render(d) - ) - ) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-5", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(8) - ), - div( - h1( - cls := "text-lg font-medium text-gray-800", - child.text <-- $m.map(_.jmeno) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala deleted file mode 100644 index e11cb62..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color - -object DetailParametru: - case class ViewModel( - id: String, - nazev: String, - popis: String, - status: String, - statusColor: Color, - a: Anchor - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h2( - cls := "text-2xl leading-6 font-medium text-gray-900", - child.text <-- $m.map(_.nazev) - ), - p( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - child.text <-- $m.map(_.popis) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - h2( - cls := "text-lg leading-6 font-medium text-gray-600", - child.text <-- $m.map(_.nazev) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala deleted file mode 100644 index a00ede8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala +++ /dev/null @@ -1,177 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.CustomAttrs - -object DukazKriteria: - case class Osoba(osobniCislo: String, jmeno: String) - case class Udalost(osoba: Osoba, datum: LocalDate) - case class Dokument( - url: String, - nazev: String, - pridal: Udalost, - odebral: Option[Udalost] - ) - case class ViewModel( - dokumenty: List[Dokument], - autorizoval: Option[Udalost], - platiDo: Option[LocalDate], - poznámka: Option[String] - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-lg", - div( - cls := "px-4 py-5 sm:px-6", - h3( - cls := "text-lg leading-6 font-medium text-gray-900", - """Applicant Information""" - ), - p( - cls := "mt-1 max-w-2xl text-sm text-gray-500", - """Personal details and application.""" - ) - ), - div( - cls := "border-t border-gray-200 px-4 py-5 sm:px-6", - dl( - cls := "grid grid-cols-1 gap-x-4 gap-y-8 sm:grid-cols-2", - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Full name""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Margot Foster""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Application for""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Backend Developer""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Email address""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """margotfoster@example.com""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Salary expectation""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """$120,000""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """About""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Fugiat ipsum ipsum deserunt culpa aute sint do nostrud anim incididunt cillum culpa consequat. Excepteur qui ipsum aliquip consequat sint. Sit id mollit nulla mollit nostrud in ea officia proident. Irure nostrud pariatur mollit ad adipisicing reprehenderit deserunt qui eu.""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """Attachments""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - ul( - role := "list", - cls := "border border-gray-200 rounded-md divide-y divide-gray-200", - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """resume_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ), - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """coverletter_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala deleted file mode 100644 index 22f134f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala +++ /dev/null @@ -1,35 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object DukazyKriteria: - sealed trait Action - case object Add extends Action - - type ViewModel = List[DukazKriteria.ViewModel] - - def apply($m: Signal[ViewModel])(actions: Observer[Action]): HtmlElement = - div( - child.maybe <-- $m.map(m => - if (m.isEmpty) then Some(prazdnyDukaz(actions)) - else None - ), - children <-- $m.map(_.zipWithIndex).split(_._2)((_, _, $s) => - DukazKriteria($s.map(_._1)) - ) - ) - - private def prazdnyDukaz(actions: Observer[Action]): HtmlElement = - button( - tpe := "button", - onClick.preventDefault.mapTo(Add) --> actions, - cls := "relative block w-full border-2 border-gray-300 border-dashed rounded-lg p-12 text-center hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - Icons.outline - .`document-add`(12) - .amend(svg.cls := "mx-auto text-gray-400"), - span( - cls := "mt-2 block text-sm font-medium text-gray-900", - "Přidat důkaz" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala deleted file mode 100644 index f628697..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.list.StackedList -import fiftyforms.ui.components.tailwind.list.ListRow -import fiftyforms.ui.components.tailwind.list.RowTag -import fiftyforms.ui.components.tailwind.list.RowNext -import java.time.LocalDate - -object SeznamKriterii: - type ViewModel = List[DetailKriteria.ViewModel] - - private val kritList = new StackedList[DetailKriteria.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - kritList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = - p(cls := "text-sm text-gray-500", s"${i.kapitola}${i.bod}"), - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.container - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala deleted file mode 100644 index 2bfe4bc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.list.{ - StackedList, - ListRow, - RowTag, - PropList, - IconText, - RowNext -} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.LinkSupport.* - -object SeznamParametru: - type ViewModel = List[DetailParametru.ViewModel] - - private val parametrList = new StackedList[DetailParametru.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - parametrList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = emptyNode, - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.a - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala deleted file mode 100644 index f048e9d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ /dev/null @@ -1,101 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.form.* -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement -import fiftyforms.services.files.components.tailwind.FilePicker -import cz.e_bs.cmi.mdr.pdb.frontend.AutorizujDukaz -import cz.e_bs.cmi.mdr.pdb.frontend.DocumentRef -import fiftyforms.services.files.File - -object UpravDukazForm: - sealed trait Event - case object Cancelled extends Event - case object AvailableFilesRequested extends Event - def apply(availableFilesStream: EventStream[List[File]])( - updates: Observer[Event] - ): HtmlElement = - val (filesStream, filesObserver) = - EventStream.withObserver[FilePicker.Event] - val files = Var[List[File]](Nil) - def submitButtons: HtmlElement = - div( - cls := "pt-5", - div( - cls := "flex justify-end", - button( - tpe := "button", - cls := "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zrušit", - onClick.mapTo(Cancelled) --> updates - ), - button( - tpe := "submit", - cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - disabled <-- files.signal.map(_.isEmpty), - "Autorizovat" - ) - ) - ) - - div( - filesStream.collect { case FilePicker.SelectionUpdated(files) => - files.to(List) - } --> files.writer, - filesStream.collect { case FilePicker.AvailableFilesRequested => - AvailableFilesRequested - } --> updates, - cls := "bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6", - Form( - Form.Body( - Form.Section( - FormHeader( - FormHeader.ViewModel( - "Doložení kritéria", - "Sestavte doklady poskytující důkaz kritéria a potvrďte odesláním formuláře. Případné limitace či jiné relevantní údaje vepište do pole Komentář." - ) - ), - FormFields( - FormRow( - "dokumenty", - "Dokumenty", - FilePicker(files.signal, availableFilesStream)(filesObserver) - .amend(idAttr := "dokumenty", cls("max-w-lg")) - ).toHtml, - FormRow( - "platnost", - "Platnost", - input( - idAttr := "platnost", - name := "platnost", - tpe := "date", - autoComplete := "date", - cls := "block max-w-lg w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" - ) - ).toHtml, - FormRow( - "komentar", - "Komentář", - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - textArea( - idAttr := "komentar", - name := "about", - rows := 3, - cls := "max-w-lg shadow-sm block w-full focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border border-gray-300 rounded-md" - ), - p( - cls := "mt-2 text-sm text-gray-500", - "Doplňte prosím potřebné informace související s doložením kritéria, včetně případných limitací." - ) - ) - ).toHtml - ) - ) - ), - submitButtons - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala deleted file mode 100644 index 41033f6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -import components._ - -object DirectoryPage: - - type ViewModel = List[UserRow.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - val (actionsStream, actionObserver) = - EventStream.withObserver[SearchForm.Action] - val $filter = actionsStream - .collect { - case SearchForm.ClearFilter => None - case SearchForm.SetFilter(t) => Some(t) - } - .startWith(None) - val byLetter = for { - d <- $m - f <- $filter - } yield for { - (letter, users) <- d - .filter { user => - f match - case None => true - case Some(t) => user.search.contains(t) - } - .groupBy(_.prijmeni.head) - .to(List) - .sortBy(_._1) - } yield (letter.toString, users.sortBy(_.prijmeni)) - - div( - cls := "h-full max-w-7xl mx-auto order-first flex flex-col flex-shrink-0", - SearchForm(actionObserver), - Directory(byLetter) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala deleted file mode 100644 index 73f915d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -object Directory: - - object Header: - type ViewModel = String - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500", - h3(child.text <-- $m) - ) - - object UserList: - type ViewModel = List[UserRow.ViewModel] - def apply($m: Signal[ViewModel]): HtmlElement = - ul( - role := "list", - cls := "relative z-0 divide-y divide-gray-200", - children <-- $m.split(_.osobniCislo)((_, _, s) => UserRow(s)) - ) - - type ViewModel = List[(String, List[UserRow.ViewModel])] - def apply($m: Signal[ViewModel]): HtmlElement = - nav( - cls := "flex-1 min-h-0 overflow-y-auto", - aria.label := "Directory", - children <-- $m.split(_._1)((_, _, s) => - div( - cls := "relative", - Header(s.map(_._1)), - UserList(s.map(_._2)) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala deleted file mode 100644 index e182910..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala +++ /dev/null @@ -1,54 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object SearchForm: - sealed trait Action - case object SubmitFilter extends Action - sealed trait FilterAction extends Action - case object ClearFilter extends FilterAction - case class SetFilter(value: String) extends FilterAction - - def apply(actions: Observer[Action]): HtmlElement = - div( - cls := "px-6 pt-4 pb-4", - form( - cls := "flex space-x-4", - onSubmit.mapTo(SubmitFilter) --> actions, - div( - cls := "flex-1 min-w-0", - label( - forId := "search", - cls := "sr-only", - "Hledat" - ), - div( - cls := "relative rounded-md shadow-sm", - div( - cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", - Icons.solid.search().amend(svg.cls := "text-gray-400") - ), - input( - tpe := "search", - name := "search", - idAttr := "search", - cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", - placeholder := "Hledat", - composeEvents(onInput.mapToValue.setAsValue.map(SetFilter(_)))( - _.throttle(500) - ) --> actions - ) - ) - ), - button( - tpe := "submit", - cls := "inline-flex justify-center px-3.5 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500", - Icons.solid.filter().amend(svg.cls := "text-gray-400"), - span( - cls := "sr-only", - "Hledat" - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala deleted file mode 100644 index 82f7723..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala +++ /dev/null @@ -1,52 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.airstream.core.Signal -import fiftyforms.ui.components.tailwind.Avatar - -object UserRow: - case class ViewModel( - osobniCislo: String, - celeJmeno: String, - prijmeni: String, - hlavniFunkce: Option[String], - img: Option[String], - container: () => HtmlElement = () => div() - ) { - val search = prijmeni.toLowerCase - } - - def apply($m: Signal[ViewModel]): HtmlElement = - inline def avatarImage = - Avatar($m.map(_.img)).avatarImage(10) - - li( - div( - cls := "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500", - div( - cls := "flex-shrink-0", - child <-- avatarImage - ), - div( - cls := "flex-1 min-w-0", - child <-- $m.map { o => - o.container() - .amend( - cls := "focus:outline-none", - span( - cls := "absolute inset-0", - aria.hidden := true - ), - p( - cls := "text-sm font-medium text-gray-900", - o.celeJmeno - ), - p( - cls := "text-sm text-gray-500 truncate", - o.hlavniFunkce - ) - ) - } - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala deleted file mode 100644 index 467c5f5..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala +++ /dev/null @@ -1,99 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router - -object ErrorPage: - case class ViewModel( - homePage: Page, - errorName: String, - title: String, - subTitle: String - ) - - def apply(m: ViewModel, actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - val ViewModel(homePage, errorName, title, subTitle) = m - div( - cls := "min-h-full pt-16 pb-12 flex flex-col bg-white", - main( - cls := "flex-grow flex flex-col justify-center max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex-shrink-0 flex justify-center", - a( - href := "/", - cls := "inline-flex", - span( - cls := "sr-only", - """Workflow""" - ), - img( - cls := "h-12 w-auto", - src := "https://tailwindui.com/img/logos/workflow-mark.svg?color=indigo&shade=600", - alt := "" - ) - ) - ), - div( - cls := "py-16", - div( - cls := "text-center", - p( - cls := "text-sm font-semibold text-indigo-600 uppercase tracking-wide", - errorName - ), - h1( - cls := "mt-2 text-4xl font-extrabold text-gray-900 tracking-tight sm:text-5xl", - title - ), - p( - cls := "mt-2 text-base text-gray-500", - subTitle - ), - div( - cls := "mt-6", - PageLink - .container(homePage, actionBus) - .amend( - cls := "text-base font-medium text-indigo-600 hover:text-indigo-500", - """Go back home""" - ) - ) - ) - ) - ), - footer( - cls := "flex-shrink-0 max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", - nav( - cls := "flex justify-center space-x-4", - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Contact Support""" - ), - span( - cls := "inline-block border-l border-gray-300", - aria.hidden := true - ), - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Status""" - ), - span( - cls := "inline-block border-l border-gray-300", - aria.hidden := true - ), - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Twitter""" - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala deleted file mode 100644 index bd9baec..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action - -object NotFoundPage: - def apply(homePage: Page, url: String, actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - ErrorPage( - ErrorPage.ViewModel( - homePage, - "404 error", - "Page not found.", - s"Sorry, but page $url doesn't exist." - ), - actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala deleted file mode 100644 index 476fe3c..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action - -object UnhandledErrorPage: - - case class ViewModel( - homePage: Page, - errorName: Option[String], - errorMessage: Option[String] - ) - - def apply(m: ViewModel, actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - ErrorPage( - ErrorPage.ViewModel( - m.homePage, - "Unexpected error occurred", - m.errorName.getOrElse( - "Uh oh!" - ), // TODO: translations, better text than uh oh - m.errorMessage.getOrElse( - "This wasn't supposed to happen! Please try again." - ) - ), - actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala deleted file mode 100644 index 25f059f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala +++ /dev/null @@ -1,6 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.services - -import com.raquo.airstream.core.Observer - -trait DataFetcher[K, A]: - def fetch(id: K, sink: Observer[A]): Unit diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala deleted file mode 100644 index a469830..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala +++ /dev/null @@ -1,120 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package state - -import com.raquo.airstream.core.EventStream -import cz.e_bs.cmi.mdr.pdb.{UserInfo, OsobniCislo} -import com.raquo.airstream.core.Observer -import scala.scalajs.js -import scala.scalajs.js.JSON -import zio.json._ -import com.raquo.airstream.eventbus.EventBus -import com.raquo.airstream.ownership.Owner -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.UserFunction -import cz.e_bs.cmi.mdr.pdb.UserContract -import fiftyforms.services.files.File - -trait AppState - extends connectors.DetailPageConnector.AppState - with connectors.DetailParametruPageConnector.AppState - with connectors.DetailKriteriaPageConnector.AppState - with pages.detail.UpravDukaz.State: - def users: EventStream[List[UserInfo]] - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - -class MockAppState(implicit owner: Owner, router: Router[Page]) - extends AppState: - - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonDecoder[UserFunction] = DeriveJsonDecoder.gen - given JsonDecoder[UserContract] = DeriveJsonDecoder.gen - given JsonDecoder[UserInfo] = DeriveJsonDecoder.gen - - given JsonDecoder[ParameterCriteria] = DeriveJsonDecoder.gen - given JsonDecoder[Parameter] = DeriveJsonDecoder.gen - - private val actions = EventBus[Action]() - private val (parametersStream, pushParameters) = - EventStream.withCallback[List[Parameter]] - private val (usersStream, pushUsers) = - EventStream.withCallback[List[UserInfo]] - private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] - private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] - - private val mockData: List[UserInfo] = - mockUsers - .asInstanceOf[js.Dictionary[js.Object]] - .values - // TODO: is there a more efficient way to parse from JS object directly? - .map { o => - val parsed = JSON.stringify(o).fromJson[UserInfo] - parsed.left.foreach(org.scalajs.dom.console.log(_)) - parsed - } - .collect { case Right(u) => - u - } - .toList - - private val mockParameters: List[Parameter] = - pdbParams - .asInstanceOf[js.Dictionary[js.Object]] - .values - .map(o => JSON.stringify(o).fromJson[Parameter]) - .collect { case Right(p) => p } - .toList - - // TODO: Extract to separate event handler - actions.events.foreach { - case FetchDirectory => pushUsers(mockData) - case FetchUserDetails(osc) => - mockData.find(_.personalNumber == osc).foreach { o => - pushDetails(o) - router.replaceState(Page.Detail(o)) - } - case FetchParameters(osc) => - pushParameters(mockParameters) - case FetchParameter(osc, paramId) => - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(Page.DetailParametru(o, p)) - case FetchParameterCriteria(osc, paramId, critId, page) => - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - c <- p.criteria.find(_.id == critId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(page(o, p, c)) - case NavigateTo(page) => router.pushState(page) - case FetchAvailableFiles(osc) => - pushFiles( - List( - File("https://tc163.cmi.cz/here", "Example file") - ) - ) - } - - override def users: EventStream[List[UserInfo]] = - usersStream.debugWithName("users") - - override def details: EventStream[UserInfo] = - detailsStream.debugWithName("details") - - override def parameters: EventStream[List[Parameter]] = - parametersStream.debugWithName("parameters") - - override def availableFiles: EventStream[List[File]] = - filesStream.debugWithName("available files") - - override def actionBus: Observer[Action] = - actions.writer.debugWithName("actions writer") diff --git a/app/src/main/scala/mdr/pdb/app/Main.scala b/app/src/main/scala/mdr/pdb/app/Main.scala new file mode 100644 index 0000000..081b4ee --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/Main.scala @@ -0,0 +1,103 @@ +package mdr.pdb.app + +import scala.scalajs.js.annotation.JSExportTopLevel +import scala.scalajs.js.annotation.JSExport +import scala.scalajs.js.annotation.JSImport +import scala.scalajs.js +import org.scalajs.dom +import com.raquo.laminar.api.L.{*, given} +import scala.scalajs.js.Date +import com.raquo.waypoint.Router +import com.raquo.waypoint.SplitRender +import mdr.pdb.app.services.DataFetcher +import scala.scalajs.js.JSON +import zio.json._ +import mdr.pdb.UserInfo +import mdr.pdb.app.state.AppState + +@js.native +@JSImport("stylesheets/main.css", JSImport.Namespace) +object Css extends js.Any + +@js.native +@JSImport("data/users.json", JSImport.Default) +object mockUsers extends js.Object + +@js.native +@JSImport("params/pdb-params.json", JSImport.Default) +object pdbParams extends js.Object + +@JSExportTopLevel("app") +object Main: + + @JSExport + def main(args: Array[String]): Unit = + import Routes.given + onLoad { + setupAirstream() + val appContainer = dom.document.querySelector("#app") + val _ = + render( + appContainer, + renderPage(state.MockAppState(using unsafeWindowOwner, router)) + ) + } + + private def onLoad(f: => Unit): Unit = + documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) + + private def setupAirstream()(using router: Router[Page]): Unit = + AirstreamError.registerUnhandledErrorCallback(err => + router.forcePage( + Page.UnhandledError( + Some(err.getClass.getName), // TODO: Fill only in dev mode + Some(err.getMessage) + ) + ) + ) + + def renderPage(state: AppState)(using + router: Router[Page] + ): HtmlElement = + val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) + .collectSignal[Page.Detail]( + connectors + .DetailPageConnector(state)(_) + .apply + ) + .collectSignal[Page.DetailParametru]( + connectors + .DetailParametruPageConnector(state)(_) + .apply + ) + .collectSignal[Page.DetailKriteria]( + connectors + .DetailKriteriaPageConnector(state)(_) + .apply + ) + .collectSignal[Page.UpravDukazKriteria]( + pages.detail.UpravDukaz.Connector(state)(_).apply + ) + .collectStatic(Page.Dashboard)( + connectors.DashboardPageConnector(state.actionBus).apply + ) + .collect[Page.NotFound](pg => + pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) + ) + .collect[Page.UnhandledError](pg => + pages.errors + .UnhandledErrorPage( + pages.errors.UnhandledErrorPage + .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), + state.actionBus + ) + ) + .collectStatic(Page.Directory)( + connectors + .DirectoryPageConnector(state.users, state.actionBus) + .apply + ) + div(cls := "h-full", child <-- pageSplitter.$view) + + // Pull in the stylesheet + val css: Css.type = Css diff --git a/app/src/main/scala/mdr/pdb/app/Routes.scala b/app/src/main/scala/mdr/pdb/app/Routes.scala new file mode 100644 index 0000000..2cc860c --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/Routes.scala @@ -0,0 +1,198 @@ +package mdr.pdb.app + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.* +import org.scalajs.dom +import zio.json.{*, given} +import mdr.pdb.OsobniCislo + +import scala.scalajs.js +import mdr.pdb.UserInfo +import mdr.pdb.Parameter +import mdr.pdb.ParameterCriteria +import mdr.pdb.app.Page.Titled + +// enum is not working with Waypoints' SplitRender collectStatic +sealed abstract class Page( + val id: String, + val title: String, + val parent: Option[Page] +) { + val path: Vector[Page] = + parent match + case None => Vector(this) + case Some(p) => p.path :+ this + + val isRoot: Boolean = parent.isEmpty +} + +object Page: + + case class Titled[V](value: V, title: Option[String] = None): + val show: String = title.getOrElse(value.toString) + + case object Directory extends Page("directory", "Adresář", None) + + case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) + + case class Detail(osobniCislo: Titled[OsobniCislo]) + extends Page("user", osobniCislo.show, Some(Directory)) + + object Detail { + def apply(o: UserInfo): Detail = Detail( + Titled(o.personalNumber, Some(o.name)) + ) + } + + case class DetailParametru( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String] + ) extends Page( + "parameter", + parametr.show, + Some(Detail(osobniCislo)) + ) + + object DetailParametru { + def apply(o: UserInfo, p: Parameter): DetailParametru = + DetailParametru( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)) + ) + } + + case class DetailKriteria( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String], + kriterium: Titled[String] + ) extends Page( + "criteria", + kriterium.show, + Some(DetailParametru(osobniCislo, parametr)) + ) + + object DetailKriteria { + def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = + DetailKriteria( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)), + Titled(k.id, Some(k.id)) + ) + } + + case class UpravDukazKriteria( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String], + kriterium: Titled[String] + ) extends Page( + "addProof", + "Důkaz", + Some(DetailKriteria(osobniCislo, parametr, kriterium)) + ) + + object UpravDukazKriteria { + def apply( + o: UserInfo, + p: Parameter, + k: ParameterCriteria + ): UpravDukazKriteria = + UpravDukazKriteria( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)), + Titled(k.id, Some(k.id)) + ) + } + + case class NotFound(url: String) extends Page("404", "404", Some(Directory)) + + case class UnhandledError( + errorName: Option[String], + errorMessage: Option[String] + ) extends Page("500", "Unexpected error", Some(Directory)) + +object Routes: + given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) + given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) + given [V: JsonEncoder]: JsonEncoder[Titled[V]] = + DeriveJsonEncoder.gen[Titled[V]] + given [V: JsonDecoder]: JsonDecoder[Titled[V]] = + DeriveJsonDecoder.gen[Titled[V]] + given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] + given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] + + val base = + js.`import`.meta.env.BASE_URL + .asInstanceOf[String] + "app" + + val homePage: Page = Page.Directory + + given router: Router[Page] = Router[Page]( + routes = List( + Route.static(homePage, root / endOfSegments, basePath = base), + Route.static( + Page.Dashboard, + root / "dashboard" / endOfSegments, + basePath = base + ), + Route[Page.Detail, String]( + encode = _.osobniCislo.value.toString, + decode = osc => Page.Detail(Titled(OsobniCislo(osc))), + root / "osoba" / segment[String] / endOfSegments, + basePath = base + ), + Route[Page.DetailParametru, (String, String)]( + encode = p => (p.osobniCislo.value.toString, p.parametr.value), + decode = + p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / endOfSegments, + basePath = base + ), + Route[Page.DetailKriteria, (String, String, String)]( + encode = p => + ( + p.osobniCislo.value.toString, + p.parametr.value, + p.kriterium.value.replaceAll("\\.", "--") + ), + decode = p => + Page.DetailKriteria( + Titled(OsobniCislo(p._1)), + Titled(p._2), + Titled(p._3.replaceAll("--", ".")) + ), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / "kriterium" / segment[String] / endOfSegments, + basePath = base + ), + Route[Page.UpravDukazKriteria, (String, String, String)]( + encode = p => + ( + p.osobniCislo.value.toString, + p.parametr.value, + p.kriterium.value.replaceAll("\\.", "--") + ), + decode = p => + Page.UpravDukazKriteria( + Titled(OsobniCislo(p._1)), + Titled(p._2), + Titled(p._3.replaceAll("--", ".")) + ), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / "kriterium" / segment[String] / "edit" / endOfSegments, + basePath = base + ) + ), + serializePage = _.toJson, + deserializePage = _.fromJson[Page] + .fold(s => throw IllegalStateException(s), identity), + getPageTitle = _.title, + routeFallback = url => Page.NotFound(url), + deserializeFallback = _ => Page.Dashboard + )( + $popStateEvent = windowEvents.onPopState, + owner = unsafeWindowOwner + ) diff --git a/app/src/main/scala/mdr/pdb/app/actions.scala b/app/src/main/scala/mdr/pdb/app/actions.scala new file mode 100644 index 0000000..d078b3a --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/actions.scala @@ -0,0 +1,21 @@ +package mdr.pdb.app + +import mdr.pdb.OsobniCislo +import mdr.pdb.UserInfo +import mdr.pdb.Parameter +import mdr.pdb.ParameterCriteria + +sealed trait Action + +case object FetchDirectory extends Action +case class FetchUserDetails(osc: OsobniCislo) extends Action +case class FetchParameters(osc: OsobniCislo) extends Action +case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action +case class FetchParameterCriteria( + osc: OsobniCislo, + paramId: String, + critId: String, + page: (UserInfo, Parameter, ParameterCriteria) => Page +) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action +case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/mdr/pdb/app/components/AppPage.scala new file mode 100644 index 0000000..130c817 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/AppPage.scala @@ -0,0 +1,82 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.Page +import mdr.pdb.{UserProfile, UserInfo, OsobniCislo} +import com.raquo.waypoint.Router +import mdr.pdb.app.Action +import mdr.pdb.app.NavigateTo +import mdr.pdb.UserFunction + +object AppPage: + // TODO: pages by logged in user + val pages: List[Page] = List(Page.Directory, Page.Dashboard) + + import NavigationBar.{Logo, MenuItem} + + val logo = Logo( + "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", + "Workflow" + ) + + // TODO: menu items by user profile + val userMenu = + List( + MenuItem("Your Profile"), + MenuItem("Settings"), + MenuItem("Sign out") + ) + + // TODO: load user profile + val $userProfile = Var( + UserProfile( + "tom", + UserInfo( + OsobniCislo("1031"), + "tom", + "Tom", + "Cook", + None, + None, + Some("tom@example.com"), + Some("+420 222 866 180"), + Some( + UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") + ), + Nil, + None + ) + ) + ) + + val $userInfo = $userProfile.signal.map(_.userInfo) + + type ViewModel = Option[HtmlElement] + def apply( + actionBus: Observer[Action] + )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using + router: Router[Page] + ): HtmlElement = + PageLayout(actionBus)( + $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => + PageLayout.ViewModel( + NavigationBar.ViewModel( + u, + pages.map(p => + NavigationBar.Link( + () => + PageLink( + p, + actionBus + ), + p == cp + ) + ), + userMenu, + logo + ), + c + ) + ), + mods + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala new file mode 100644 index 0000000..9375398 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala @@ -0,0 +1,143 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.tailwind.list.IconText.ViewModel +import fiftyforms.ui.components.tailwind.Icons +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.Page + +import com.raquo.laminar.api.L.{*, given} +import io.laminext.syntax.core.{*, given} +import mdr.pdb.app.Action + +object Breadcrumbs: + + private def slash = + import svg.{*, given} + svg( + cls := "flex-shrink-0 h-5 w-5 text-gray-300", + xmlns := "http://www.w3.org/2000/svg", + fill := "currentColor", + viewBox := "0 0 20 20", + ariaHidden := true, + path( + d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" + ) + ) + + def backIcon = + Icons.solid + .`arrow-narrow-left`() + .amend( + svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" + ) + + object Link: + case class ViewModel( + page: Page, + icon: Option[SvgElement], + extraClasses: String, + text: String, + textClasses: Option[String] = None + ) + + val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" + + def shortHome(p: Page) = ViewModel( + p, + Some(backIcon), + "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", + "Zpět na úvodní stránku", + None + ) + + def fullHome(p: Page) = ViewModel( + p, + Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), + "text-gray-400 hover:text-gray-500", + "Domů", + Some("sr-only") + ) + + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + PageLink + .container($m.map(_.page), actionBus) + .amend( + cls <-- $m.map(_.extraClasses), + child.maybe <-- $m.map(_.icon), + span( + cls <-- $m.map(_.textClasses.getOrElse("")), + child.text <-- $m.map(_.text) + ) + ) + + object FullBreadcrumbs: + type ViewModel = Page + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + ol( + role := "list", + cls := "flex items-center space-x-4", + children <-- $m.map(_.path).split(_.id)((_, _, $p) => + li( + div( + cls := "flex items-center", + child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), + Link( + $p.map(p => + if (p.isRoot) then Link.fullHome(p) + else + Link.ViewModel( + p, + None, + s"ml-4 max-w-xs truncate ${Link.baseClasses}", + p.title + ) + ), + actionBus + ) + ) + ) + ) + ) + + object ShortBreadcrumbs: + type ViewModel = Page + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + Link( + $m.map { p => + val target = p.parent.getOrElse(p) + if target.isRoot then Link.shortHome(target) + else + Link.ViewModel( + target, + Some(backIcon), + "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", + s"Zpět na ${target.title}" + ) + }, + actionBus + ) + + def apply(actionBus: Observer[Action])(using + router: Router[Page] + ): HtmlElement = + nav( + cls := "flex", + aria.label := "Breadcrumb", + div( + cls := "flex sm:hidden", + ShortBreadcrumbs(router.$currentPage, actionBus) + ), + div( + cls := "hidden sm:block", + FullBreadcrumbs(router.$currentPage, actionBus) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala new file mode 100644 index 0000000..4bb8b8d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala @@ -0,0 +1,242 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent +import fiftyforms.ui.components.tailwind.Icons +import fiftyforms.ui.components.tailwind.Avatar +import mdr.pdb.UserInfo +import io.laminext.syntax.core.* + +object NavigationBar: + + case class Logo(img: String, name: String) + case class Link(a: () => Anchor, active: Boolean) + case class MenuItem(title: String) + + case class ViewModel( + userInfo: UserInfo, + pages: List[Link], + userMenu: List[MenuItem], + logo: Logo + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + val $userInfo = $m.map(_.userInfo) + val mobileMenuOpen = Var(false) + + val desktopOnly = cls("hidden md:block") + val mobileOnly = cls("md:hidden") + + inline def avatarImage(size: Int = 8) = + Avatar($userInfo.map(_.img)).avatarImage(size) + + def notificationButton = button( + tpe := "button", + cls := List( + "bg-indigo-600", + "focus:outline-none", + "focus:ring-2", + "focus:ring-offset-2", + "focus:ring-offset-indigo-600", + "focus:ring-white", + "hover:text-white", + "p-1", + "rounded-full", + "text-indigo-200" + ), + span(cls := "sr-only", "View notifications"), + Icons.outline.bell() + ) + + def userProfile: HtmlElement = + val menuOpen = Var(false) + + def menuItem(item: MenuItem, idx: Int): HtmlElement = + a( + href := "#", + cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", + role := "menuitem", + tabIndex := -1, + idAttr := s"user-menu-item-$idx", + item.title + ) + + div( + cls := "ml-3 relative", + div( + button( + tpe := "button", + cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", + idAttr := "user-menu-button", + aria.expanded <-- menuOpen.signal, + aria.hasPopup := true, + span(cls := "sr-only", "Open user menu"), + child <-- avatarImage(), + onClick.preventDefault.mapTo( + !menuOpen.now() + ) --> menuOpen.writer + ) + ), + /* + * */ + div( + cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", + cls <-- menuOpen.signal.map { o => + if (o) "md:block" else "md:hidden" + }, + role := "menu", + aria.orientation := "vertical", + aria.labelledBy := "user-menu-button", + tabIndex := -1, + // : keyboard navigation + children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) + ) + ) + + def mobileProfile = + def menuItem(item: MenuItem): HtmlElement = + a( + href := "#", + cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", + item.title + ) + + div( + cls := "pt-4 pb-3 border-t border-indigo-700", + div( + cls := "flex items-center px-5", + div( + cls := "flex-shrink-0", + child <-- avatarImage(10) + ), + div( + cls := "ml-3", + div( + cls := "text-base font-medium text-white", + child.text <-- $userInfo.map(_.name) + ), + child.maybe <-- $userInfo.map( + _.email.map(e => + div( + cls := "text-sm font-medium text-indigo-300", + e + ) + ) + ) + ), + notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) + ), + div( + cls := "mt-3 px-2 space-y-1", + children <-- $m.map(_.userMenu.map(menuItem)) + ) + ) + + def pageLink(page: Link): Anchor = + page + .a() + .amend( + cls := "text-white px-3 py-2 rounded-md text-sm font-medium", + cls := Seq( + "bg-indigo-700" -> page.active, + "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active + ), + ariaCurrent := (if page.active then "page" else "false") + ) + + def logoImg: Image = + img( + cls := "h-8 w-8", + src <-- $m.map(_.logo.img), + alt <-- $m.map(_.logo.name) + ) + + def mobileMenuButton = button( + tpe := "button", + cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", + aria.controls := "mobile-menu", + aria.expanded <-- mobileMenuOpen.signal, + span(cls := "sr-only", "Open main menu"), + Icons.outline + .menu() + .amend( + svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") + ), + Icons.outline + .x() + .amend( + svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") + ), + onClick.preventDefault.mapTo( + !mobileMenuOpen.now() + ) --> mobileMenuOpen.writer + ) + + def navBarLeft = + div( + cls := "flex items-center", + div(cls := "flex-shrink-0", logoImg), + div( + desktopOnly, + div( + cls := "ml-10 flex items-baseline space-x-4", + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) + ) + ) + ) + + def navBarRight = + div( + desktopOnly, + div( + cls := "ml-4 flex items-center md:ml-6", + notificationButton, + userProfile + ) + ) + + def navBarMobile = + div( + cls := "-mr-2 flex", + mobileOnly, + mobileMenuButton + ) + + def navBar = + div( + cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", + div( + cls := "flex items-center justify-between h-16", + navBarLeft, + navBarRight, + navBarMobile + ) + ) + + def mobileMenu = + div( + mobileOnly, + cls <-- mobileMenuOpen.signal.map { o => + if (o) "block" else "hidden" + }, + idAttr := "mobile-menu", + div( + cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) + ), + mobileProfile + ) + + nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala new file mode 100644 index 0000000..c4cb10d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala @@ -0,0 +1,19 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.Page +import com.raquo.waypoint.Router +import mdr.pdb.app.Action + +object PageHeader: + def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = + header( + cls := "bg-white shadow-sm", + div( + cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", + h1( + cls := "text-lg leading-6 font-semibold text-gray-900", + Breadcrumbs(actionBus) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala new file mode 100644 index 0000000..aae4539 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala @@ -0,0 +1,30 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.Page +import mdr.pdb.app.Action +import fiftyforms.ui.components.tailwind.Loading +import io.laminext.syntax.core.* + +object PageLayout: + case class ViewModel( + navigation: NavigationBar.ViewModel, + content: Option[HtmlElement] + ) + def apply(actionBus: Observer[Action])( + $m: Signal[ViewModel], + mods: Modifier[HtmlElement]* + )(using router: Router[Page]): HtmlElement = + val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) + div( + cls := "h-full flex flex-col", + NavigationBar($m.map(_.navigation)), + child.maybe <-- router.$currentPage.map(_.isRoot) + .switch(None, Some(PageHeader(actionBus))), + main( + cls := "flex-grow-1 overflow-y-auto", + mods, + child <-- $maybeContent.map(_.getOrElse(Loading)) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/mdr/pdb/app/components/PageLink.scala new file mode 100644 index 0000000..e6e7052 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageLink.scala @@ -0,0 +1,40 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.LinkSupport.* +import mdr.pdb.app.Page +import mdr.pdb.app.Action +import com.raquo.waypoint.Router +import mdr.pdb.app.NavigateTo + +object PageLink: + type ViewModel = Page + def apply($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = + a(mods($m, actions), child.text <-- $m.map(_.title)) + + def apply(m: ViewModel, actions: Observer[Action])(using + router: Router[Page] + ): Anchor = apply(Val(m), actions) + + def container($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = a(mods($m, actions)) + + def container(m: ViewModel, actions: Observer[Action])(using + Router[Page] + ): Anchor = container(Val(m), actions) + + private def mods($m: Signal[Page], actions: Observer[Action])(using + router: Router[Page] + ): Modifier[Anchor] = + Seq( + href <-- $m.map(router.absoluteUrlForPage).recover { case _ => + Some("invalid url") + }, + composeEvents(onClick.noKeyMod.preventDefault)( + _.sample($m) + .map(NavigateTo.apply) + ) --> actions + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala new file mode 100644 index 0000000..7682772 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala @@ -0,0 +1,13 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.pages.dashboard.DashboardPage +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage + +class DashboardPageConnector(actionBus: Observer[Action])(using + router: Router[Page] +): + def apply: HtmlElement = + AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala new file mode 100644 index 0000000..76b9133 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala @@ -0,0 +1,76 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.Parameter +import mdr.pdb.UserInfo +import mdr.pdb.app.pages.detail.DetailParametruPage +import pages.detail.DetailKriteriaPage +import pages.detail.components.DukazyKriteria +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.ParameterCriteria + +object DetailKriteriaPageConnector { + trait AppState { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + } +} + +case class DetailKriteriaPageConnector( + state: DetailKriteriaPageConnector.AppState +)( + $page: Signal[Page.DetailKriteria] +)(using Router[Page]): + val $paramChangeSignal = + $page.splitOne(p => + (p.osobniCislo.value, p.parametr.value, p.kriterium.value) + )((x, _, _) => x) + val $pageChangeSignal = + $paramChangeSignal.map( + FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) + ) + + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + val $merged = + $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => + for { + da <- d + pa <- p + pb <- pa.find(_.id == pc._2) + ka <- pb.criteria.find(_.id == pc._3) + } yield (da, pb, ka) + ) + + def apply: HtmlElement = + AppPage(state.actionBus)( + $merged.map(_.map(buildModel)) + .split(_ => ())((_, s, $s) => + DetailKriteriaPage($s)(state.actionBus.contramap { + case DukazyKriteria.Add => + NavigateTo( + Page.UpravDukazKriteria( + Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), + Page.Titled(s.parametr.id, Some(s.parametr.nazev)), + Page.Titled(s.kriterium.id, Some(s.kriterium.id)) + ) + ) + }) + ), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: Parameter, + k: ParameterCriteria + ): DetailKriteriaPage.ViewModel = + DetailKriteriaPage.ViewModel( + o.toDetailOsoby, + p.toParametr(_ => a()), + k.toKriterium(_ => a()) + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DetailPageConnector.scala new file mode 100644 index 0000000..df3a6b7 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DetailPageConnector.scala @@ -0,0 +1,57 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.UserInfo +import pages.detail.DetailPage +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.pages.detail.components.DetailOsoby +import mdr.pdb.Parameter +import mdr.pdb.app.pages.detail.components.SeznamParametru +import fiftyforms.ui.components.tailwind.Color + +object DetailPageConnector { + trait AppState { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + } +} + +case class DetailPageConnector(state: DetailPageConnector.AppState)( + $page: Signal[Page.Detail] +)(using router: Router[Page]): + val $oscChangeSignal = $page.splitOne(_.osobniCislo.value)((osc, _, _) => osc) + val $pageChangeSignal = + $oscChangeSignal.flatMap(osc => + EventStream.fromSeq(Seq(FetchUserDetails(osc), FetchParameters(osc))) + ) + // TODO: filter the value based on the current osc + // OSC change will fetch new data, but still + // - we need to be sure that what we got is really what we ought to display + // - we want to display stale data accordingly (at least with loading indicator) + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + def apply: HtmlElement = + AppPage(state.actionBus)( + $data.combineWithFn($params)(_ zip _) + .map(_.map(buildModel)) + .split(_ => ())((_, _, s) => DetailPage(s)), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: List[Parameter] + ): DetailPage.ViewModel = + DetailPage.ViewModel( + o.toDetailOsoby, + p.map( + _.toParametr(param => + PageLink.container(Page.DetailParametru(o, param), state.actionBus) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DetailParametruPageConnector.scala new file mode 100644 index 0000000..fa7193d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DetailParametruPageConnector.scala @@ -0,0 +1,67 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.Parameter +import mdr.pdb.UserInfo +import mdr.pdb.app.pages.detail.DetailParametruPage +import pages.detail.DetailParametruPage +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.components.PageLink + +object DetailParametruPageConnector { + trait AppState { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + } +} + +case class DetailParametruPageConnector( + state: DetailParametruPageConnector.AppState +)( + $page: Signal[Page.DetailParametru] +)(using router: Router[Page]): + val $paramChangeSignal = + $page.splitOne(p => (p.osobniCislo.value, p.parametr.value))((x, _, _) => x) + val $pageChangeSignal = + $paramChangeSignal.map(FetchParameter(_, _)) + + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + val $merged = + $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => + for { + da <- d + pa <- p + pb <- pa.find(_.id == pc._2) + } yield (da, pb) + ) + + def apply: HtmlElement = + AppPage(state.actionBus)( + $merged.map(_.map(buildModel)) + .split(_ => ())((_, _, s) => DetailParametruPage(s)), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: Parameter + ): DetailParametruPage.ViewModel = + DetailParametruPage.ViewModel( + o.toDetailOsoby, + p.toParametr(p => + PageLink.container(Page.DetailParametru(o, p), state.actionBus) + ), + p.criteria.map( + _.toKriterium { c => + PageLink.container( + Page.DetailKriteria(o, p, c), + state.actionBus + ) + } + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DirectoryPageConnector.scala new file mode 100644 index 0000000..ab2c045 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DirectoryPageConnector.scala @@ -0,0 +1,34 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.UserInfo +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.components.AppPage + +case class DirectoryPageConnector( + $input: EventStream[List[UserInfo]], + actionBus: Observer[Action] +)(using router: Router[Page]): + val $data = $input.startWithNone + val $actionSignal = EventStream.fromValue(FetchDirectory) + + def apply: HtmlElement = + AppPage(actionBus)( + $data.split(_ => ())((_, _, s) => + pages.directory.DirectoryPage( + s.map( + _.map( + _.toUserRow(u => + PageLink.container( + Page.Detail(Page.Titled(u.personalNumber)), + actionBus + ) + ) + ) + ) + ) + ), + $actionSignal --> actionBus + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/mdr/pdb/app/connectors/conversions.scala new file mode 100644 index 0000000..f581796 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/conversions.scala @@ -0,0 +1,68 @@ +package mdr.pdb.app.connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.UserInfo +import mdr.pdb.app.pages.detail.components.DetailOsoby +import mdr.pdb.Parameter +import mdr.pdb.app.pages.detail.components.SeznamParametru +import mdr.pdb.ParameterCriteria +import mdr.pdb.app.pages.detail.components.SeznamKriterii +import mdr.pdb.app.pages.directory.components.UserRow +import mdr.pdb.app.pages.detail.components.DetailParametru +import mdr.pdb.app.pages.detail.components.DetailKriteria +import fiftyforms.ui.components.tailwind.Color + +extension (o: UserInfo) + def toDetailOsoby: DetailOsoby.ViewModel = + DetailOsoby.ViewModel( + o.personalNumber, + o.name, + o.email, + o.phone, + o.img, + o.mainFunction.map(f => + DetailOsoby.Funkce.ViewModel(f.name, f.dept, f.ou) + ), + o.userContracts.headOption.map(c => + DetailOsoby.PracovniPomer.ViewModel(c.rel, c.startDate, c.endDate) + ) + ) + +extension (param: Parameter) + def toParametr(container: Parameter => Anchor): DetailParametru.ViewModel = + DetailParametru.ViewModel( + id = param.id, + nazev = param.name, + popis = param.description, + status = "Nesplněno", + statusColor = Color.red, + a = container(param) + ) + +extension (crit: ParameterCriteria) + def toKriterium( + container: ParameterCriteria => Anchor + ): DetailKriteria.ViewModel = + DetailKriteria.ViewModel( + nazev = crit.criteriumText, + kapitola = crit.chapterId, + bod = crit.itemId, + status = "Nesplněno", + statusColor = Color.red, + splneno = false, + dukazy = Nil, + container = container(crit) + ) + +extension (user: UserInfo) + def toUserRow( + container: UserInfo => HtmlElement = (_: UserInfo) => div() + ): UserRow.ViewModel = + UserRow.ViewModel( + osobniCislo = user.personalNumber.toString, + celeJmeno = user.name, + prijmeni = user.surname, + hlavniFunkce = user.mainFunction.map(_.name), + img = user.img, + container = () => container(user) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/dashboard/DashboardPage.scala b/app/src/main/scala/mdr/pdb/app/pages/dashboard/DashboardPage.scala new file mode 100644 index 0000000..0eb8f7a --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/dashboard/DashboardPage.scala @@ -0,0 +1,11 @@ +package mdr.pdb.app.pages.dashboard + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.Page + +object DashboardPage: + + def render: HtmlElement = + div("Dashboard page") diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala new file mode 100644 index 0000000..ff3a25f --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala @@ -0,0 +1,31 @@ +package mdr.pdb.app.pages.detail + +import com.raquo.laminar.api.L.{*, given} + +import components._ + +object DetailKriteriaPage: + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametr: DetailParametru.ViewModel, + kriterium: DetailKriteria.ViewModel + ) + + def apply($m: Signal[ViewModel])( + action: Observer[DukazyKriteria.Action] + ): HtmlElement = + div( + cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + div( + DetailOsoby.header($m.map(_.osoba)), + DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") + ), + div( + DetailKriteria($m.map(_.kriterium)), + DukazyKriteria($m.map(_.kriterium.dukazy))(action) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/DetailPage.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailPage.scala new file mode 100644 index 0000000..b591cc9 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailPage.scala @@ -0,0 +1,25 @@ +package mdr.pdb.app.pages.detail + +import com.raquo.laminar.api.L.{*, given} + +import components._ +import mdr.pdb.app.Page +import com.raquo.waypoint.Router +import mdr.pdb.app.Action + +object DetailPage: + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametry: SeznamParametru.ViewModel + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + DetailOsoby($m.map(_.osoba)), + SeznamParametru($m.map(_.parametry)) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/DetailParametruPage.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailParametruPage.scala new file mode 100644 index 0000000..3322279 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailParametruPage.scala @@ -0,0 +1,24 @@ +package mdr.pdb.app.pages.detail + +import com.raquo.laminar.api.L.{*, given} + +import components._ + +object DetailParametruPage: + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametr: DetailParametru.ViewModel, + kriteria: SeznamKriterii.ViewModel + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + DetailOsoby.header($m.map(_.osoba)), + DetailParametru($m.map(_.parametr)), + SeznamKriterii($m.map(_.kriteria)) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala new file mode 100644 index 0000000..b58618a --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala @@ -0,0 +1,107 @@ +package mdr.pdb +package app +package pages.detail + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.pages.detail.components.UpravDukazForm +import fiftyforms.services.files.File + +object UpravDukaz: + + type ThisPage = Page.UpravDukazKriteria + type PageKey = (OsobniCislo, String, String) + + trait State { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def availableFiles: EventStream[List[File]] + def actionBus: Observer[Action] + } + + def keyOfPage(page: ThisPage): PageKey = + (page.osobniCislo.value, page.parametr.value, page.kriterium.value) + + def onChangeAction(key: PageKey): Action = + FetchParameterCriteria( + key._1, + key._2, + key._3, + Page.UpravDukazKriteria(_, _, _) + ) + + class Connector(state: State)($page: Signal[ThisPage])(using Router[Page]): + val $paramChangeSignal = $page.splitOne(keyOfPage)((x, _, _) => x) + val $pageChangeSignal = $paramChangeSignal.map(onChangeAction) + + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + val $merged = + $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => + for { + da <- d + pa <- p + pb <- pa.find(_.id == pc._2) + ka <- pb.criteria.find(_.id == pc._3) + } yield (da, pb, ka) + ) + + def apply: HtmlElement = + AppPage(state.actionBus)( + $merged.split(_ => ())((_, s, $s) => + PageComponent( + $s.map(buildModel), + state.availableFiles, + state.actionBus.contramap { + case UpravDukazForm.Cancelled => + NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) + case UpravDukazForm.AvailableFilesRequested => + FetchAvailableFiles(s._1.personalNumber) + } + ) + ), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: Parameter, + k: ParameterCriteria + ): PageComponent.ViewModel = + import connectors.* + PageComponent.ViewModel( + o.toDetailOsoby, + p.toParametr(_ => a()), + k.toKriterium(_ => a()) + ) + + object PageComponent: + import components.* + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametr: DetailParametru.ViewModel, + kriterium: DetailKriteria.ViewModel + ) + + def apply( + $m: Signal[ViewModel], + availableFilesStream: EventStream[List[File]], + events: Observer[UpravDukazForm.Event] + ): HtmlElement = + div( + cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + div( + DetailOsoby.header($m.map(_.osoba)), + DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") + ), + div( + DetailKriteria($m.map(_.kriterium)), + UpravDukazForm(availableFilesStream)(events) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailKriteria.scala new file mode 100644 index 0000000..a3ab634 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailKriteria.scala @@ -0,0 +1,37 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.CustomAttrs +import fiftyforms.ui.components.tailwind.Icons +import java.time.LocalDate +import fiftyforms.ui.components.tailwind.Color + +object DetailKriteria: + case class ViewModel( + nazev: String, + kapitola: String, + bod: String, + status: String, + statusColor: Color, + splneno: Boolean, + dukazy: List[DukazKriteria.ViewModel], + container: HtmlElement = div() + ) { + val id = s"${kapitola}${bod}" + } + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "pb-5 border-b border-gray-200", + h3( + cls := "text-xl leading-6 font-bold text-gray-900", + child.text <-- $m.map(_.nazev) + ), + h3( + cls := "mt-2 max-w-4xl text-sm text-gray-500", + "Kapitola ", + child.text <-- $m.map(_.kapitola), + " bod ", + child <-- $m.map(_.bod) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailOsoby.scala new file mode 100644 index 0000000..0fe2ba5 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailOsoby.scala @@ -0,0 +1,100 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.OsobniCislo + +import java.time.LocalDate +import fiftyforms.ui.components.tailwind.Avatar + +object DetailOsoby: + + object Funkce: + case class ViewModel( + nazev: String, + stredisko: String, + voj: String + ) + + def render($m: Signal[ViewModel]): HtmlElement = + p( + cls := "text-sm font-medium text-gray-500", + child.text <-- $m.map(_.nazev), + span( + cls := "hidden md:inline", + " @ ", + child.text <-- $m.map(_.stredisko), + ", ", + child.text <-- $m.map(_.voj) + ) + ) + + object PracovniPomer: + case class ViewModel( + druh: String, + pocatek: LocalDate, + konec: Option[LocalDate] + ) + + def render($m: Signal[ViewModel]): HtmlElement = + import fiftyforms.ui.components.tailwind.CustomAttrs.datetime + p( + cls := "text-sm font-medium text-gray-500", + child.text <-- $m.map(_.druh), + " od ", + time( + datetime <-- $m.map(_.pocatek.toString), + child.text <-- $m.map(_.pocatek.toString) + ) + ) + + case class ViewModel( + osobniCislo: OsobniCislo, + jmeno: String, + email: Option[String], + telefon: Option[String], + img: Option[String], + hlavniFunkce: Option[Funkce.ViewModel], + pracovniPomer: Option[PracovniPomer.ViewModel] + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "md:flex md:items-center md:justify-between md:space-x-4", + div( + cls := "flex items-start space-x-4", + div( + cls := "flex-shrink-0", + Avatar($m.map(_.img)).avatar(16) + ), + div( + h1( + cls := "text-2xl font-bold text-gray-900", + child.text <-- $m.map(_.jmeno) + ), + child.maybe <-- $m.map(_.hlavniFunkce).split(_ => ())((_, _, d) => + Funkce.render(d) + ), + child.maybe <-- $m.map(_.pracovniPomer).split(_ => ())((_, _, d) => + PracovniPomer.render(d) + ) + ) + ) + ) + + def header($m: Signal[ViewModel]): HtmlElement = + div( + cls := "md:flex md:items-center md:justify-between md:space-x-5", + div( + cls := "flex items-start space-x-4", + div( + cls := "flex-shrink-0", + Avatar($m.map(_.img)).avatar(8) + ), + div( + h1( + cls := "text-lg font-medium text-gray-800", + child.text <-- $m.map(_.jmeno) + ) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailParametru.scala new file mode 100644 index 0000000..6310dde --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailParametru.scala @@ -0,0 +1,34 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.Color + +object DetailParametru: + case class ViewModel( + id: String, + nazev: String, + popis: String, + status: String, + statusColor: Color, + a: Anchor + ) + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "pb-5 border-b border-gray-200", + h2( + cls := "text-2xl leading-6 font-medium text-gray-900", + child.text <-- $m.map(_.nazev) + ), + p( + cls := "mt-2 max-w-4xl text-sm text-gray-500", + child.text <-- $m.map(_.popis) + ) + ) + + def header($m: Signal[ViewModel]): HtmlElement = + div( + h2( + cls := "text-lg leading-6 font-medium text-gray-600", + child.text <-- $m.map(_.nazev) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazKriteria.scala new file mode 100644 index 0000000..c292919 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazKriteria.scala @@ -0,0 +1,177 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import java.time.LocalDate +import fiftyforms.ui.components.tailwind.CustomAttrs + +object DukazKriteria: + case class Osoba(osobniCislo: String, jmeno: String) + case class Udalost(osoba: Osoba, datum: LocalDate) + case class Dokument( + url: String, + nazev: String, + pridal: Udalost, + odebral: Option[Udalost] + ) + case class ViewModel( + dokumenty: List[Dokument], + autorizoval: Option[Udalost], + platiDo: Option[LocalDate], + poznámka: Option[String] + ) + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "bg-white shadow overflow-hidden sm:rounded-lg", + div( + cls := "px-4 py-5 sm:px-6", + h3( + cls := "text-lg leading-6 font-medium text-gray-900", + """Applicant Information""" + ), + p( + cls := "mt-1 max-w-2xl text-sm text-gray-500", + """Personal details and application.""" + ) + ), + div( + cls := "border-t border-gray-200 px-4 py-5 sm:px-6", + dl( + cls := "grid grid-cols-1 gap-x-4 gap-y-8 sm:grid-cols-2", + div( + cls := "sm:col-span-1", + dt( + cls := "text-sm font-medium text-gray-500", + """Full name""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """Margot Foster""" + ) + ), + div( + cls := "sm:col-span-1", + dt( + cls := "text-sm font-medium text-gray-500", + """Application for""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """Backend Developer""" + ) + ), + div( + cls := "sm:col-span-1", + dt( + cls := "text-sm font-medium text-gray-500", + """Email address""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """margotfoster@example.com""" + ) + ), + div( + cls := "sm:col-span-1", + dt( + cls := "text-sm font-medium text-gray-500", + """Salary expectation""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """$120,000""" + ) + ), + div( + cls := "sm:col-span-2", + dt( + cls := "text-sm font-medium text-gray-500", + """About""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """Fugiat ipsum ipsum deserunt culpa aute sint do nostrud anim incididunt cillum culpa consequat. Excepteur qui ipsum aliquip consequat sint. Sit id mollit nulla mollit nostrud in ea officia proident. Irure nostrud pariatur mollit ad adipisicing reprehenderit deserunt qui eu.""" + ) + ), + div( + cls := "sm:col-span-2", + dt( + cls := "text-sm font-medium text-gray-500", + """Attachments""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + ul( + role := "list", + cls := "border border-gray-200 rounded-md divide-y divide-gray-200", + li( + cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", + div( + cls := "w-0 flex-1 flex items-center", { + import svg.* + import CustomAttrs.svg.ariaHidden + svg( + cls := "flex-shrink-0 h-5 w-5 text-gray-400", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + ariaHidden := true, + path( + fillRule := "evenodd", + d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", + clipRule := "evenodd" + ) + ) + }, + span( + cls := "ml-2 flex-1 w-0 truncate", + """resume_back_end_developer.pdf""" + ) + ), + div( + cls := "ml-4 flex-shrink-0", + a( + href := "#", + cls := "font-medium text-indigo-600 hover:text-indigo-500", + """Download""" + ) + ) + ), + li( + cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", + div( + cls := "w-0 flex-1 flex items-center", { + import svg.* + import CustomAttrs.svg.ariaHidden + svg( + cls := "flex-shrink-0 h-5 w-5 text-gray-400", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + ariaHidden := true, + path( + fillRule := "evenodd", + d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", + clipRule := "evenodd" + ) + ) + }, + span( + cls := "ml-2 flex-1 w-0 truncate", + """coverletter_back_end_developer.pdf""" + ) + ), + div( + cls := "ml-4 flex-shrink-0", + a( + href := "#", + cls := "font-medium text-indigo-600 hover:text-indigo-500", + """Download""" + ) + ) + ) + ) + ) + ) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala new file mode 100644 index 0000000..d604860 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala @@ -0,0 +1,35 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.Icons + +object DukazyKriteria: + sealed trait Action + case object Add extends Action + + type ViewModel = List[DukazKriteria.ViewModel] + + def apply($m: Signal[ViewModel])(actions: Observer[Action]): HtmlElement = + div( + child.maybe <-- $m.map(m => + if (m.isEmpty) then Some(prazdnyDukaz(actions)) + else None + ), + children <-- $m.map(_.zipWithIndex).split(_._2)((_, _, $s) => + DukazKriteria($s.map(_._1)) + ) + ) + + private def prazdnyDukaz(actions: Observer[Action]): HtmlElement = + button( + tpe := "button", + onClick.preventDefault.mapTo(Add) --> actions, + cls := "relative block w-full border-2 border-gray-300 border-dashed rounded-lg p-12 text-center hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + Icons.outline + .`document-add`(12) + .amend(svg.cls := "mx-auto text-gray-400"), + span( + cls := "mt-2 block text-sm font-medium text-gray-900", + "Přidat důkaz" + ) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala deleted file mode 100644 index a3b00af..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala +++ /dev/null @@ -1,103 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import scala.scalajs.js.annotation.JSExportTopLevel -import scala.scalajs.js.annotation.JSExport -import scala.scalajs.js.annotation.JSImport -import scala.scalajs.js -import org.scalajs.dom -import com.raquo.laminar.api.L.{*, given} -import scala.scalajs.js.Date -import com.raquo.waypoint.Router -import com.raquo.waypoint.SplitRender -import cz.e_bs.cmi.mdr.pdb.app.services.DataFetcher -import scala.scalajs.js.JSON -import zio.json._ -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.state.AppState - -@js.native -@JSImport("stylesheets/main.css", JSImport.Namespace) -object Css extends js.Any - -@js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native -@JSImport("params/pdb-params.json", JSImport.Default) -object pdbParams extends js.Object - -@JSExportTopLevel("app") -object Main: - - @JSExport - def main(args: Array[String]): Unit = - import Routes.given - onLoad { - setupAirstream() - val appContainer = dom.document.querySelector("#app") - val _ = - render( - appContainer, - renderPage(state.MockAppState(using unsafeWindowOwner, router)) - ) - } - - private def onLoad(f: => Unit): Unit = - documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) - - private def setupAirstream()(using router: Router[Page]): Unit = - AirstreamError.registerUnhandledErrorCallback(err => - router.forcePage( - Page.UnhandledError( - Some(err.getClass.getName), // TODO: Fill only in dev mode - Some(err.getMessage) - ) - ) - ) - - def renderPage(state: AppState)(using - router: Router[Page] - ): HtmlElement = - val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) - .collectSignal[Page.Detail]( - connectors - .DetailPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailParametru]( - connectors - .DetailParametruPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailKriteria]( - connectors - .DetailKriteriaPageConnector(state)(_) - .apply - ) - .collectSignal[Page.UpravDukazKriteria]( - pages.detail.UpravDukaz.Connector(state)(_).apply - ) - .collectStatic(Page.Dashboard)( - connectors.DashboardPageConnector(state.actionBus).apply - ) - .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) - ) - .collect[Page.UnhandledError](pg => - pages.errors - .UnhandledErrorPage( - pages.errors.UnhandledErrorPage - .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), - state.actionBus - ) - ) - .collectStatic(Page.Directory)( - connectors - .DirectoryPageConnector(state.users, state.actionBus) - .apply - ) - div(cls := "h-full", child <-- pageSplitter.$view) - - // Pull in the stylesheet - val css: Css.type = Css diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala deleted file mode 100644 index 3bbc8c0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ /dev/null @@ -1,198 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.* -import org.scalajs.dom -import zio.json.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import scala.scalajs.js -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.Page.Titled - -// enum is not working with Waypoints' SplitRender collectStatic -sealed abstract class Page( - val id: String, - val title: String, - val parent: Option[Page] -) { - val path: Vector[Page] = - parent match - case None => Vector(this) - case Some(p) => p.path :+ this - - val isRoot: Boolean = parent.isEmpty -} - -object Page: - - case class Titled[V](value: V, title: Option[String] = None): - val show: String = title.getOrElse(value.toString) - - case object Directory extends Page("directory", "Adresář", None) - - case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) - - case class Detail(osobniCislo: Titled[OsobniCislo]) - extends Page("user", osobniCislo.show, Some(Directory)) - - object Detail { - def apply(o: UserInfo): Detail = Detail( - Titled(o.personalNumber, Some(o.name)) - ) - } - - case class DetailParametru( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String] - ) extends Page( - "parameter", - parametr.show, - Some(Detail(osobniCislo)) - ) - - object DetailParametru { - def apply(o: UserInfo, p: Parameter): DetailParametru = - DetailParametru( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)) - ) - } - - case class DetailKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "criteria", - kriterium.show, - Some(DetailParametru(osobniCislo, parametr)) - ) - - object DetailKriteria { - def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = - DetailKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class UpravDukazKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "addProof", - "Důkaz", - Some(DetailKriteria(osobniCislo, parametr, kriterium)) - ) - - object UpravDukazKriteria { - def apply( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): UpravDukazKriteria = - UpravDukazKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class NotFound(url: String) extends Page("404", "404", Some(Directory)) - - case class UnhandledError( - errorName: Option[String], - errorMessage: Option[String] - ) extends Page("500", "Unexpected error", Some(Directory)) - -object Routes: - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) - given [V: JsonEncoder]: JsonEncoder[Titled[V]] = - DeriveJsonEncoder.gen[Titled[V]] - given [V: JsonDecoder]: JsonDecoder[Titled[V]] = - DeriveJsonDecoder.gen[Titled[V]] - given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] - given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] - - val base = - js.`import`.meta.env.BASE_URL - .asInstanceOf[String] + "app" - - val homePage: Page = Page.Directory - - given router: Router[Page] = Router[Page]( - routes = List( - Route.static(homePage, root / endOfSegments, basePath = base), - Route.static( - Page.Dashboard, - root / "dashboard" / endOfSegments, - basePath = base - ), - Route[Page.Detail, String]( - encode = _.osobniCislo.value.toString, - decode = osc => Page.Detail(Titled(OsobniCislo(osc))), - root / "osoba" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.DetailParametru, (String, String)]( - encode = p => (p.osobniCislo.value.toString, p.parametr.value), - decode = - p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / endOfSegments, - basePath = base - ), - Route[Page.DetailKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.DetailKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.UpravDukazKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.UpravDukazKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / "edit" / endOfSegments, - basePath = base - ) - ), - serializePage = _.toJson, - deserializePage = _.fromJson[Page] - .fold(s => throw IllegalStateException(s), identity), - getPageTitle = _.title, - routeFallback = url => Page.NotFound(url), - deserializeFallback = _ => Page.Dashboard - )( - $popStateEvent = windowEvents.onPopState, - owner = unsafeWindowOwner - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala deleted file mode 100644 index b24b82b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -sealed trait Action - -case object FetchDirectory extends Action -case class FetchUserDetails(osc: OsobniCislo) extends Action -case class FetchParameters(osc: OsobniCislo) extends Action -case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action -case class FetchParameterCriteria( - osc: OsobniCislo, - paramId: String, - critId: String, - page: (UserInfo, Parameter, ParameterCriteria) => Page -) extends Action -case class FetchAvailableFiles(osc: OsobniCislo) extends Action -case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala deleted file mode 100644 index c637d35..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala +++ /dev/null @@ -1,82 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.{UserProfile, UserInfo, OsobniCislo} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo -import cz.e_bs.cmi.mdr.pdb.UserFunction - -object AppPage: - // TODO: pages by logged in user - val pages: List[Page] = List(Page.Directory, Page.Dashboard) - - import NavigationBar.{Logo, MenuItem} - - val logo = Logo( - "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", - "Workflow" - ) - - // TODO: menu items by user profile - val userMenu = - List( - MenuItem("Your Profile"), - MenuItem("Settings"), - MenuItem("Sign out") - ) - - // TODO: load user profile - val $userProfile = Var( - UserProfile( - "tom", - UserInfo( - OsobniCislo("1031"), - "tom", - "Tom", - "Cook", - None, - None, - Some("tom@example.com"), - Some("+420 222 866 180"), - Some( - UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") - ), - Nil, - None - ) - ) - ) - - val $userInfo = $userProfile.signal.map(_.userInfo) - - type ViewModel = Option[HtmlElement] - def apply( - actionBus: Observer[Action] - )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using - router: Router[Page] - ): HtmlElement = - PageLayout(actionBus)( - $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => - PageLayout.ViewModel( - NavigationBar.ViewModel( - u, - pages.map(p => - NavigationBar.Link( - () => - PageLink( - p, - actionBus - ), - p == cp - ) - ), - userMenu, - logo - ), - c - ) - ), - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala deleted file mode 100644 index bf473c6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ /dev/null @@ -1,143 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden -import fiftyforms.ui.components.tailwind.list.IconText.ViewModel -import fiftyforms.ui.components.tailwind.Icons -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Action - -object Breadcrumbs: - - private def slash = - import svg.{*, given} - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-300", - xmlns := "http://www.w3.org/2000/svg", - fill := "currentColor", - viewBox := "0 0 20 20", - ariaHidden := true, - path( - d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" - ) - ) - - def backIcon = - Icons.solid - .`arrow-narrow-left`() - .amend( - svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" - ) - - object Link: - case class ViewModel( - page: Page, - icon: Option[SvgElement], - extraClasses: String, - text: String, - textClasses: Option[String] = None - ) - - val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" - - def shortHome(p: Page) = ViewModel( - p, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - "Zpět na úvodní stránku", - None - ) - - def fullHome(p: Page) = ViewModel( - p, - Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), - "text-gray-400 hover:text-gray-500", - "Domů", - Some("sr-only") - ) - - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - PageLink - .container($m.map(_.page), actionBus) - .amend( - cls <-- $m.map(_.extraClasses), - child.maybe <-- $m.map(_.icon), - span( - cls <-- $m.map(_.textClasses.getOrElse("")), - child.text <-- $m.map(_.text) - ) - ) - - object FullBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - ol( - role := "list", - cls := "flex items-center space-x-4", - children <-- $m.map(_.path).split(_.id)((_, _, $p) => - li( - div( - cls := "flex items-center", - child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), - Link( - $p.map(p => - if (p.isRoot) then Link.fullHome(p) - else - Link.ViewModel( - p, - None, - s"ml-4 max-w-xs truncate ${Link.baseClasses}", - p.title - ) - ), - actionBus - ) - ) - ) - ) - ) - - object ShortBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - Link( - $m.map { p => - val target = p.parent.getOrElse(p) - if target.isRoot then Link.shortHome(target) - else - Link.ViewModel( - target, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - s"Zpět na ${target.title}" - ) - }, - actionBus - ) - - def apply(actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - nav( - cls := "flex", - aria.label := "Breadcrumb", - div( - cls := "flex sm:hidden", - ShortBreadcrumbs(router.$currentPage, actionBus) - ), - div( - cls := "hidden sm:block", - FullBreadcrumbs(router.$currentPage, actionBus) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala deleted file mode 100644 index 0db2047..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ /dev/null @@ -1,242 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent -import fiftyforms.ui.components.tailwind.Icons -import fiftyforms.ui.components.tailwind.Avatar -import cz.e_bs.cmi.mdr.pdb.UserInfo -import io.laminext.syntax.core.* - -object NavigationBar: - - case class Logo(img: String, name: String) - case class Link(a: () => Anchor, active: Boolean) - case class MenuItem(title: String) - - case class ViewModel( - userInfo: UserInfo, - pages: List[Link], - userMenu: List[MenuItem], - logo: Logo - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - val $userInfo = $m.map(_.userInfo) - val mobileMenuOpen = Var(false) - - val desktopOnly = cls("hidden md:block") - val mobileOnly = cls("md:hidden") - - inline def avatarImage(size: Int = 8) = - Avatar($userInfo.map(_.img)).avatarImage(size) - - def notificationButton = button( - tpe := "button", - cls := List( - "bg-indigo-600", - "focus:outline-none", - "focus:ring-2", - "focus:ring-offset-2", - "focus:ring-offset-indigo-600", - "focus:ring-white", - "hover:text-white", - "p-1", - "rounded-full", - "text-indigo-200" - ), - span(cls := "sr-only", "View notifications"), - Icons.outline.bell() - ) - - def userProfile: HtmlElement = - val menuOpen = Var(false) - - def menuItem(item: MenuItem, idx: Int): HtmlElement = - a( - href := "#", - cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", - role := "menuitem", - tabIndex := -1, - idAttr := s"user-menu-item-$idx", - item.title - ) - - div( - cls := "ml-3 relative", - div( - button( - tpe := "button", - cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - idAttr := "user-menu-button", - aria.expanded <-- menuOpen.signal, - aria.hasPopup := true, - span(cls := "sr-only", "Open user menu"), - child <-- avatarImage(), - onClick.preventDefault.mapTo( - !menuOpen.now() - ) --> menuOpen.writer - ) - ), - /* - * */ - div( - cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", - cls <-- menuOpen.signal.map { o => - if (o) "md:block" else "md:hidden" - }, - role := "menu", - aria.orientation := "vertical", - aria.labelledBy := "user-menu-button", - tabIndex := -1, - // : keyboard navigation - children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) - ) - ) - - def mobileProfile = - def menuItem(item: MenuItem): HtmlElement = - a( - href := "#", - cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", - item.title - ) - - div( - cls := "pt-4 pb-3 border-t border-indigo-700", - div( - cls := "flex items-center px-5", - div( - cls := "flex-shrink-0", - child <-- avatarImage(10) - ), - div( - cls := "ml-3", - div( - cls := "text-base font-medium text-white", - child.text <-- $userInfo.map(_.name) - ), - child.maybe <-- $userInfo.map( - _.email.map(e => - div( - cls := "text-sm font-medium text-indigo-300", - e - ) - ) - ) - ), - notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) - ), - div( - cls := "mt-3 px-2 space-y-1", - children <-- $m.map(_.userMenu.map(menuItem)) - ) - ) - - def pageLink(page: Link): Anchor = - page - .a() - .amend( - cls := "text-white px-3 py-2 rounded-md text-sm font-medium", - cls := Seq( - "bg-indigo-700" -> page.active, - "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active - ), - ariaCurrent := (if page.active then "page" else "false") - ) - - def logoImg: Image = - img( - cls := "h-8 w-8", - src <-- $m.map(_.logo.img), - alt <-- $m.map(_.logo.name) - ) - - def mobileMenuButton = button( - tpe := "button", - cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - aria.controls := "mobile-menu", - aria.expanded <-- mobileMenuOpen.signal, - span(cls := "sr-only", "Open main menu"), - Icons.outline - .menu() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") - ), - Icons.outline - .x() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") - ), - onClick.preventDefault.mapTo( - !mobileMenuOpen.now() - ) --> mobileMenuOpen.writer - ) - - def navBarLeft = - div( - cls := "flex items-center", - div(cls := "flex-shrink-0", logoImg), - div( - desktopOnly, - div( - cls := "ml-10 flex items-baseline space-x-4", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ) - ) - ) - - def navBarRight = - div( - desktopOnly, - div( - cls := "ml-4 flex items-center md:ml-6", - notificationButton, - userProfile - ) - ) - - def navBarMobile = - div( - cls := "-mr-2 flex", - mobileOnly, - mobileMenuButton - ) - - def navBar = - div( - cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex items-center justify-between h-16", - navBarLeft, - navBarRight, - navBarMobile - ) - ) - - def mobileMenu = - div( - mobileOnly, - cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }, - idAttr := "mobile-menu", - div( - cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ), - mobileProfile - ) - - nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala deleted file mode 100644 index 75339df..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object PageHeader: - def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = - header( - cls := "bg-white shadow-sm", - div( - cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", - h1( - cls := "text-lg leading-6 font-semibold text-gray-900", - Breadcrumbs(actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala deleted file mode 100644 index 5096463..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ /dev/null @@ -1,30 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import fiftyforms.ui.components.tailwind.Loading -import io.laminext.syntax.core.* - -object PageLayout: - case class ViewModel( - navigation: NavigationBar.ViewModel, - content: Option[HtmlElement] - ) - def apply(actionBus: Observer[Action])( - $m: Signal[ViewModel], - mods: Modifier[HtmlElement]* - )(using router: Router[Page]): HtmlElement = - val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) - div( - cls := "h-full flex flex-col", - NavigationBar($m.map(_.navigation)), - child.maybe <-- router.$currentPage.map(_.isRoot) - .switch(None, Some(PageHeader(actionBus))), - main( - cls := "flex-grow-1 overflow-y-auto", - mods, - child <-- $maybeContent.map(_.getOrElse(Loading)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala deleted file mode 100644 index 62d820f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.LinkSupport.* -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo - -object PageLink: - type ViewModel = Page - def apply($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = - a(mods($m, actions), child.text <-- $m.map(_.title)) - - def apply(m: ViewModel, actions: Observer[Action])(using - router: Router[Page] - ): Anchor = apply(Val(m), actions) - - def container($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = a(mods($m, actions)) - - def container(m: ViewModel, actions: Observer[Action])(using - Router[Page] - ): Anchor = container(Val(m), actions) - - private def mods($m: Signal[Page], actions: Observer[Action])(using - router: Router[Page] - ): Modifier[Anchor] = - Seq( - href <-- $m.map(router.absoluteUrlForPage).recover { case _ => - Some("invalid url") - }, - composeEvents(onClick.noKeyMod.preventDefault)( - _.sample($m) - .map(NavigateTo.apply) - ) --> actions - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala deleted file mode 100644 index 87f825a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.pages.dashboard.DashboardPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -class DashboardPageConnector(actionBus: Observer[Action])(using - router: Router[Page] -): - def apply: HtmlElement = - AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala deleted file mode 100644 index e9b0f71..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala +++ /dev/null @@ -1,76 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailKriteriaPage -import pages.detail.components.DukazyKriteria -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -object DetailKriteriaPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailKriteriaPageConnector( - state: DetailKriteriaPageConnector.AppState -)( - $page: Signal[Page.DetailKriteria] -)(using Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => - (p.osobniCislo.value, p.parametr.value, p.kriterium.value) - )((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map( - FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) - ) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, s, $s) => - DetailKriteriaPage($s)(state.actionBus.contramap { - case DukazyKriteria.Add => - NavigateTo( - Page.UpravDukazKriteria( - Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), - Page.Titled(s.parametr.id, Some(s.parametr.nazev)), - Page.Titled(s.kriterium.id, Some(s.kriterium.id)) - ) - ) - }) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): DetailKriteriaPage.ViewModel = - DetailKriteriaPage.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala deleted file mode 100644 index 4d8024f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ /dev/null @@ -1,57 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import pages.detail.DetailPage -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import fiftyforms.ui.components.tailwind.Color - -object DetailPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailPageConnector(state: DetailPageConnector.AppState)( - $page: Signal[Page.Detail] -)(using router: Router[Page]): - val $oscChangeSignal = $page.splitOne(_.osobniCislo.value)((osc, _, _) => osc) - val $pageChangeSignal = - $oscChangeSignal.flatMap(osc => - EventStream.fromSeq(Seq(FetchUserDetails(osc), FetchParameters(osc))) - ) - // TODO: filter the value based on the current osc - // OSC change will fetch new data, but still - // - we need to be sure that what we got is really what we ought to display - // - we want to display stale data accordingly (at least with loading indicator) - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - def apply: HtmlElement = - AppPage(state.actionBus)( - $data.combineWithFn($params)(_ zip _) - .map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: List[Parameter] - ): DetailPage.ViewModel = - DetailPage.ViewModel( - o.toDetailOsoby, - p.map( - _.toParametr(param => - PageLink.container(Page.DetailParametru(o, param), state.actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala deleted file mode 100644 index fdaa7f8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailParametruPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink - -object DetailParametruPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailParametruPageConnector( - state: DetailParametruPageConnector.AppState -)( - $page: Signal[Page.DetailParametru] -)(using router: Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => (p.osobniCislo.value, p.parametr.value))((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map(FetchParameter(_, _)) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - } yield (da, pb) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailParametruPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter - ): DetailParametruPage.ViewModel = - DetailParametruPage.ViewModel( - o.toDetailOsoby, - p.toParametr(p => - PageLink.container(Page.DetailParametru(o, p), state.actionBus) - ), - p.criteria.map( - _.toKriterium { c => - PageLink.container( - Page.DetailKriteria(o, p, c), - state.actionBus - ) - } - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala deleted file mode 100644 index 049aa22..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -case class DirectoryPageConnector( - $input: EventStream[List[UserInfo]], - actionBus: Observer[Action] -)(using router: Router[Page]): - val $data = $input.startWithNone - val $actionSignal = EventStream.fromValue(FetchDirectory) - - def apply: HtmlElement = - AppPage(actionBus)( - $data.split(_ => ())((_, _, s) => - pages.directory.DirectoryPage( - s.map( - _.map( - _.toUserRow(u => - PageLink.container( - Page.Detail(Page.Titled(u.personalNumber)), - actionBus - ) - ) - ) - ) - ) - ), - $actionSignal --> actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala deleted file mode 100644 index 88e7450..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ /dev/null @@ -1,68 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii -import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria -import fiftyforms.ui.components.tailwind.Color - -extension (o: UserInfo) - def toDetailOsoby: DetailOsoby.ViewModel = - DetailOsoby.ViewModel( - o.personalNumber, - o.name, - o.email, - o.phone, - o.img, - o.mainFunction.map(f => - DetailOsoby.Funkce.ViewModel(f.name, f.dept, f.ou) - ), - o.userContracts.headOption.map(c => - DetailOsoby.PracovniPomer.ViewModel(c.rel, c.startDate, c.endDate) - ) - ) - -extension (param: Parameter) - def toParametr(container: Parameter => Anchor): DetailParametru.ViewModel = - DetailParametru.ViewModel( - id = param.id, - nazev = param.name, - popis = param.description, - status = "Nesplněno", - statusColor = Color.red, - a = container(param) - ) - -extension (crit: ParameterCriteria) - def toKriterium( - container: ParameterCriteria => Anchor - ): DetailKriteria.ViewModel = - DetailKriteria.ViewModel( - nazev = crit.criteriumText, - kapitola = crit.chapterId, - bod = crit.itemId, - status = "Nesplněno", - statusColor = Color.red, - splneno = false, - dukazy = Nil, - container = container(crit) - ) - -extension (user: UserInfo) - def toUserRow( - container: UserInfo => HtmlElement = (_: UserInfo) => div() - ): UserRow.ViewModel = - UserRow.ViewModel( - osobniCislo = user.personalNumber.toString, - celeJmeno = user.name, - prijmeni = user.surname, - hlavniFunkce = user.mainFunction.map(_.name), - img = user.img, - container = () => container(user) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala deleted file mode 100644 index 7d5b2c1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.dashboard - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.Page - -object DashboardPage: - - def render: HtmlElement = - div("Dashboard page") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala deleted file mode 100644 index 9ef0698..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala +++ /dev/null @@ -1,31 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailKriteriaPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply($m: Signal[ViewModel])( - action: Observer[DukazyKriteria.Action] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - DukazyKriteria($m.map(_.kriterium.dukazy))(action) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala deleted file mode 100644 index 3b316e1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object DetailPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametry: SeznamParametru.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby($m.map(_.osoba)), - SeznamParametru($m.map(_.parametry)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala deleted file mode 100644 index 17224cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala +++ /dev/null @@ -1,24 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailParametruPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriteria: SeznamKriterii.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby.header($m.map(_.osoba)), - DetailParametru($m.map(_.parametr)), - SeznamKriterii($m.map(_.kriteria)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala deleted file mode 100644 index dc912fa..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ /dev/null @@ -1,107 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb -package app -package pages.detail - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.UpravDukazForm -import fiftyforms.services.files.File - -object UpravDukaz: - - type ThisPage = Page.UpravDukazKriteria - type PageKey = (OsobniCislo, String, String) - - trait State { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def availableFiles: EventStream[List[File]] - def actionBus: Observer[Action] - } - - def keyOfPage(page: ThisPage): PageKey = - (page.osobniCislo.value, page.parametr.value, page.kriterium.value) - - def onChangeAction(key: PageKey): Action = - FetchParameterCriteria( - key._1, - key._2, - key._3, - Page.UpravDukazKriteria(_, _, _) - ) - - class Connector(state: State)($page: Signal[ThisPage])(using Router[Page]): - val $paramChangeSignal = $page.splitOne(keyOfPage)((x, _, _) => x) - val $pageChangeSignal = $paramChangeSignal.map(onChangeAction) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.split(_ => ())((_, s, $s) => - PageComponent( - $s.map(buildModel), - state.availableFiles, - state.actionBus.contramap { - case UpravDukazForm.Cancelled => - NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) - case UpravDukazForm.AvailableFilesRequested => - FetchAvailableFiles(s._1.personalNumber) - } - ) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): PageComponent.ViewModel = - import connectors.* - PageComponent.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) - - object PageComponent: - import components.* - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply( - $m: Signal[ViewModel], - availableFilesStream: EventStream[List[File]], - events: Observer[UpravDukazForm.Event] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - UpravDukazForm(availableFilesStream)(events) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala deleted file mode 100644 index ca311b0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.Icons -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Color - -object DetailKriteria: - case class ViewModel( - nazev: String, - kapitola: String, - bod: String, - status: String, - statusColor: Color, - splneno: Boolean, - dukazy: List[DukazKriteria.ViewModel], - container: HtmlElement = div() - ) { - val id = s"${kapitola}${bod}" - } - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h3( - cls := "text-xl leading-6 font-bold text-gray-900", - child.text <-- $m.map(_.nazev) - ), - h3( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - "Kapitola ", - child.text <-- $m.map(_.kapitola), - " bod ", - child <-- $m.map(_.bod) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala deleted file mode 100644 index ae90290..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala +++ /dev/null @@ -1,100 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Avatar - -object DetailOsoby: - - object Funkce: - case class ViewModel( - nazev: String, - stredisko: String, - voj: String - ) - - def render($m: Signal[ViewModel]): HtmlElement = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.nazev), - span( - cls := "hidden md:inline", - " @ ", - child.text <-- $m.map(_.stredisko), - ", ", - child.text <-- $m.map(_.voj) - ) - ) - - object PracovniPomer: - case class ViewModel( - druh: String, - pocatek: LocalDate, - konec: Option[LocalDate] - ) - - def render($m: Signal[ViewModel]): HtmlElement = - import fiftyforms.ui.components.tailwind.CustomAttrs.datetime - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.druh), - " od ", - time( - datetime <-- $m.map(_.pocatek.toString), - child.text <-- $m.map(_.pocatek.toString) - ) - ) - - case class ViewModel( - osobniCislo: OsobniCislo, - jmeno: String, - email: Option[String], - telefon: Option[String], - img: Option[String], - hlavniFunkce: Option[Funkce.ViewModel], - pracovniPomer: Option[PracovniPomer.ViewModel] - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-4", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(16) - ), - div( - h1( - cls := "text-2xl font-bold text-gray-900", - child.text <-- $m.map(_.jmeno) - ), - child.maybe <-- $m.map(_.hlavniFunkce).split(_ => ())((_, _, d) => - Funkce.render(d) - ), - child.maybe <-- $m.map(_.pracovniPomer).split(_ => ())((_, _, d) => - PracovniPomer.render(d) - ) - ) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-5", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(8) - ), - div( - h1( - cls := "text-lg font-medium text-gray-800", - child.text <-- $m.map(_.jmeno) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala deleted file mode 100644 index e11cb62..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color - -object DetailParametru: - case class ViewModel( - id: String, - nazev: String, - popis: String, - status: String, - statusColor: Color, - a: Anchor - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h2( - cls := "text-2xl leading-6 font-medium text-gray-900", - child.text <-- $m.map(_.nazev) - ), - p( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - child.text <-- $m.map(_.popis) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - h2( - cls := "text-lg leading-6 font-medium text-gray-600", - child.text <-- $m.map(_.nazev) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala deleted file mode 100644 index a00ede8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala +++ /dev/null @@ -1,177 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.CustomAttrs - -object DukazKriteria: - case class Osoba(osobniCislo: String, jmeno: String) - case class Udalost(osoba: Osoba, datum: LocalDate) - case class Dokument( - url: String, - nazev: String, - pridal: Udalost, - odebral: Option[Udalost] - ) - case class ViewModel( - dokumenty: List[Dokument], - autorizoval: Option[Udalost], - platiDo: Option[LocalDate], - poznámka: Option[String] - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-lg", - div( - cls := "px-4 py-5 sm:px-6", - h3( - cls := "text-lg leading-6 font-medium text-gray-900", - """Applicant Information""" - ), - p( - cls := "mt-1 max-w-2xl text-sm text-gray-500", - """Personal details and application.""" - ) - ), - div( - cls := "border-t border-gray-200 px-4 py-5 sm:px-6", - dl( - cls := "grid grid-cols-1 gap-x-4 gap-y-8 sm:grid-cols-2", - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Full name""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Margot Foster""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Application for""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Backend Developer""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Email address""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """margotfoster@example.com""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Salary expectation""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """$120,000""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """About""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Fugiat ipsum ipsum deserunt culpa aute sint do nostrud anim incididunt cillum culpa consequat. Excepteur qui ipsum aliquip consequat sint. Sit id mollit nulla mollit nostrud in ea officia proident. Irure nostrud pariatur mollit ad adipisicing reprehenderit deserunt qui eu.""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """Attachments""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - ul( - role := "list", - cls := "border border-gray-200 rounded-md divide-y divide-gray-200", - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """resume_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ), - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """coverletter_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala deleted file mode 100644 index 22f134f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala +++ /dev/null @@ -1,35 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object DukazyKriteria: - sealed trait Action - case object Add extends Action - - type ViewModel = List[DukazKriteria.ViewModel] - - def apply($m: Signal[ViewModel])(actions: Observer[Action]): HtmlElement = - div( - child.maybe <-- $m.map(m => - if (m.isEmpty) then Some(prazdnyDukaz(actions)) - else None - ), - children <-- $m.map(_.zipWithIndex).split(_._2)((_, _, $s) => - DukazKriteria($s.map(_._1)) - ) - ) - - private def prazdnyDukaz(actions: Observer[Action]): HtmlElement = - button( - tpe := "button", - onClick.preventDefault.mapTo(Add) --> actions, - cls := "relative block w-full border-2 border-gray-300 border-dashed rounded-lg p-12 text-center hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - Icons.outline - .`document-add`(12) - .amend(svg.cls := "mx-auto text-gray-400"), - span( - cls := "mt-2 block text-sm font-medium text-gray-900", - "Přidat důkaz" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala deleted file mode 100644 index f628697..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.list.StackedList -import fiftyforms.ui.components.tailwind.list.ListRow -import fiftyforms.ui.components.tailwind.list.RowTag -import fiftyforms.ui.components.tailwind.list.RowNext -import java.time.LocalDate - -object SeznamKriterii: - type ViewModel = List[DetailKriteria.ViewModel] - - private val kritList = new StackedList[DetailKriteria.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - kritList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = - p(cls := "text-sm text-gray-500", s"${i.kapitola}${i.bod}"), - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.container - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala deleted file mode 100644 index 2bfe4bc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.list.{ - StackedList, - ListRow, - RowTag, - PropList, - IconText, - RowNext -} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.LinkSupport.* - -object SeznamParametru: - type ViewModel = List[DetailParametru.ViewModel] - - private val parametrList = new StackedList[DetailParametru.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - parametrList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = emptyNode, - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.a - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala deleted file mode 100644 index f048e9d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ /dev/null @@ -1,101 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.form.* -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement -import fiftyforms.services.files.components.tailwind.FilePicker -import cz.e_bs.cmi.mdr.pdb.frontend.AutorizujDukaz -import cz.e_bs.cmi.mdr.pdb.frontend.DocumentRef -import fiftyforms.services.files.File - -object UpravDukazForm: - sealed trait Event - case object Cancelled extends Event - case object AvailableFilesRequested extends Event - def apply(availableFilesStream: EventStream[List[File]])( - updates: Observer[Event] - ): HtmlElement = - val (filesStream, filesObserver) = - EventStream.withObserver[FilePicker.Event] - val files = Var[List[File]](Nil) - def submitButtons: HtmlElement = - div( - cls := "pt-5", - div( - cls := "flex justify-end", - button( - tpe := "button", - cls := "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zrušit", - onClick.mapTo(Cancelled) --> updates - ), - button( - tpe := "submit", - cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - disabled <-- files.signal.map(_.isEmpty), - "Autorizovat" - ) - ) - ) - - div( - filesStream.collect { case FilePicker.SelectionUpdated(files) => - files.to(List) - } --> files.writer, - filesStream.collect { case FilePicker.AvailableFilesRequested => - AvailableFilesRequested - } --> updates, - cls := "bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6", - Form( - Form.Body( - Form.Section( - FormHeader( - FormHeader.ViewModel( - "Doložení kritéria", - "Sestavte doklady poskytující důkaz kritéria a potvrďte odesláním formuláře. Případné limitace či jiné relevantní údaje vepište do pole Komentář." - ) - ), - FormFields( - FormRow( - "dokumenty", - "Dokumenty", - FilePicker(files.signal, availableFilesStream)(filesObserver) - .amend(idAttr := "dokumenty", cls("max-w-lg")) - ).toHtml, - FormRow( - "platnost", - "Platnost", - input( - idAttr := "platnost", - name := "platnost", - tpe := "date", - autoComplete := "date", - cls := "block max-w-lg w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" - ) - ).toHtml, - FormRow( - "komentar", - "Komentář", - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - textArea( - idAttr := "komentar", - name := "about", - rows := 3, - cls := "max-w-lg shadow-sm block w-full focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border border-gray-300 rounded-md" - ), - p( - cls := "mt-2 text-sm text-gray-500", - "Doplňte prosím potřebné informace související s doložením kritéria, včetně případných limitací." - ) - ) - ).toHtml - ) - ) - ), - submitButtons - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala deleted file mode 100644 index 41033f6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -import components._ - -object DirectoryPage: - - type ViewModel = List[UserRow.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - val (actionsStream, actionObserver) = - EventStream.withObserver[SearchForm.Action] - val $filter = actionsStream - .collect { - case SearchForm.ClearFilter => None - case SearchForm.SetFilter(t) => Some(t) - } - .startWith(None) - val byLetter = for { - d <- $m - f <- $filter - } yield for { - (letter, users) <- d - .filter { user => - f match - case None => true - case Some(t) => user.search.contains(t) - } - .groupBy(_.prijmeni.head) - .to(List) - .sortBy(_._1) - } yield (letter.toString, users.sortBy(_.prijmeni)) - - div( - cls := "h-full max-w-7xl mx-auto order-first flex flex-col flex-shrink-0", - SearchForm(actionObserver), - Directory(byLetter) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala deleted file mode 100644 index 73f915d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -object Directory: - - object Header: - type ViewModel = String - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500", - h3(child.text <-- $m) - ) - - object UserList: - type ViewModel = List[UserRow.ViewModel] - def apply($m: Signal[ViewModel]): HtmlElement = - ul( - role := "list", - cls := "relative z-0 divide-y divide-gray-200", - children <-- $m.split(_.osobniCislo)((_, _, s) => UserRow(s)) - ) - - type ViewModel = List[(String, List[UserRow.ViewModel])] - def apply($m: Signal[ViewModel]): HtmlElement = - nav( - cls := "flex-1 min-h-0 overflow-y-auto", - aria.label := "Directory", - children <-- $m.split(_._1)((_, _, s) => - div( - cls := "relative", - Header(s.map(_._1)), - UserList(s.map(_._2)) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala deleted file mode 100644 index e182910..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala +++ /dev/null @@ -1,54 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object SearchForm: - sealed trait Action - case object SubmitFilter extends Action - sealed trait FilterAction extends Action - case object ClearFilter extends FilterAction - case class SetFilter(value: String) extends FilterAction - - def apply(actions: Observer[Action]): HtmlElement = - div( - cls := "px-6 pt-4 pb-4", - form( - cls := "flex space-x-4", - onSubmit.mapTo(SubmitFilter) --> actions, - div( - cls := "flex-1 min-w-0", - label( - forId := "search", - cls := "sr-only", - "Hledat" - ), - div( - cls := "relative rounded-md shadow-sm", - div( - cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", - Icons.solid.search().amend(svg.cls := "text-gray-400") - ), - input( - tpe := "search", - name := "search", - idAttr := "search", - cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", - placeholder := "Hledat", - composeEvents(onInput.mapToValue.setAsValue.map(SetFilter(_)))( - _.throttle(500) - ) --> actions - ) - ) - ), - button( - tpe := "submit", - cls := "inline-flex justify-center px-3.5 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500", - Icons.solid.filter().amend(svg.cls := "text-gray-400"), - span( - cls := "sr-only", - "Hledat" - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala deleted file mode 100644 index 82f7723..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala +++ /dev/null @@ -1,52 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.airstream.core.Signal -import fiftyforms.ui.components.tailwind.Avatar - -object UserRow: - case class ViewModel( - osobniCislo: String, - celeJmeno: String, - prijmeni: String, - hlavniFunkce: Option[String], - img: Option[String], - container: () => HtmlElement = () => div() - ) { - val search = prijmeni.toLowerCase - } - - def apply($m: Signal[ViewModel]): HtmlElement = - inline def avatarImage = - Avatar($m.map(_.img)).avatarImage(10) - - li( - div( - cls := "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500", - div( - cls := "flex-shrink-0", - child <-- avatarImage - ), - div( - cls := "flex-1 min-w-0", - child <-- $m.map { o => - o.container() - .amend( - cls := "focus:outline-none", - span( - cls := "absolute inset-0", - aria.hidden := true - ), - p( - cls := "text-sm font-medium text-gray-900", - o.celeJmeno - ), - p( - cls := "text-sm text-gray-500 truncate", - o.hlavniFunkce - ) - ) - } - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala deleted file mode 100644 index 467c5f5..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala +++ /dev/null @@ -1,99 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router - -object ErrorPage: - case class ViewModel( - homePage: Page, - errorName: String, - title: String, - subTitle: String - ) - - def apply(m: ViewModel, actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - val ViewModel(homePage, errorName, title, subTitle) = m - div( - cls := "min-h-full pt-16 pb-12 flex flex-col bg-white", - main( - cls := "flex-grow flex flex-col justify-center max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex-shrink-0 flex justify-center", - a( - href := "/", - cls := "inline-flex", - span( - cls := "sr-only", - """Workflow""" - ), - img( - cls := "h-12 w-auto", - src := "https://tailwindui.com/img/logos/workflow-mark.svg?color=indigo&shade=600", - alt := "" - ) - ) - ), - div( - cls := "py-16", - div( - cls := "text-center", - p( - cls := "text-sm font-semibold text-indigo-600 uppercase tracking-wide", - errorName - ), - h1( - cls := "mt-2 text-4xl font-extrabold text-gray-900 tracking-tight sm:text-5xl", - title - ), - p( - cls := "mt-2 text-base text-gray-500", - subTitle - ), - div( - cls := "mt-6", - PageLink - .container(homePage, actionBus) - .amend( - cls := "text-base font-medium text-indigo-600 hover:text-indigo-500", - """Go back home""" - ) - ) - ) - ) - ), - footer( - cls := "flex-shrink-0 max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", - nav( - cls := "flex justify-center space-x-4", - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Contact Support""" - ), - span( - cls := "inline-block border-l border-gray-300", - aria.hidden := true - ), - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Status""" - ), - span( - cls := "inline-block border-l border-gray-300", - aria.hidden := true - ), - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Twitter""" - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala deleted file mode 100644 index bd9baec..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action - -object NotFoundPage: - def apply(homePage: Page, url: String, actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - ErrorPage( - ErrorPage.ViewModel( - homePage, - "404 error", - "Page not found.", - s"Sorry, but page $url doesn't exist." - ), - actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala deleted file mode 100644 index 476fe3c..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action - -object UnhandledErrorPage: - - case class ViewModel( - homePage: Page, - errorName: Option[String], - errorMessage: Option[String] - ) - - def apply(m: ViewModel, actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - ErrorPage( - ErrorPage.ViewModel( - m.homePage, - "Unexpected error occurred", - m.errorName.getOrElse( - "Uh oh!" - ), // TODO: translations, better text than uh oh - m.errorMessage.getOrElse( - "This wasn't supposed to happen! Please try again." - ) - ), - actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala deleted file mode 100644 index 25f059f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala +++ /dev/null @@ -1,6 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.services - -import com.raquo.airstream.core.Observer - -trait DataFetcher[K, A]: - def fetch(id: K, sink: Observer[A]): Unit diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala deleted file mode 100644 index a469830..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala +++ /dev/null @@ -1,120 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package state - -import com.raquo.airstream.core.EventStream -import cz.e_bs.cmi.mdr.pdb.{UserInfo, OsobniCislo} -import com.raquo.airstream.core.Observer -import scala.scalajs.js -import scala.scalajs.js.JSON -import zio.json._ -import com.raquo.airstream.eventbus.EventBus -import com.raquo.airstream.ownership.Owner -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.UserFunction -import cz.e_bs.cmi.mdr.pdb.UserContract -import fiftyforms.services.files.File - -trait AppState - extends connectors.DetailPageConnector.AppState - with connectors.DetailParametruPageConnector.AppState - with connectors.DetailKriteriaPageConnector.AppState - with pages.detail.UpravDukaz.State: - def users: EventStream[List[UserInfo]] - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - -class MockAppState(implicit owner: Owner, router: Router[Page]) - extends AppState: - - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonDecoder[UserFunction] = DeriveJsonDecoder.gen - given JsonDecoder[UserContract] = DeriveJsonDecoder.gen - given JsonDecoder[UserInfo] = DeriveJsonDecoder.gen - - given JsonDecoder[ParameterCriteria] = DeriveJsonDecoder.gen - given JsonDecoder[Parameter] = DeriveJsonDecoder.gen - - private val actions = EventBus[Action]() - private val (parametersStream, pushParameters) = - EventStream.withCallback[List[Parameter]] - private val (usersStream, pushUsers) = - EventStream.withCallback[List[UserInfo]] - private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] - private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] - - private val mockData: List[UserInfo] = - mockUsers - .asInstanceOf[js.Dictionary[js.Object]] - .values - // TODO: is there a more efficient way to parse from JS object directly? - .map { o => - val parsed = JSON.stringify(o).fromJson[UserInfo] - parsed.left.foreach(org.scalajs.dom.console.log(_)) - parsed - } - .collect { case Right(u) => - u - } - .toList - - private val mockParameters: List[Parameter] = - pdbParams - .asInstanceOf[js.Dictionary[js.Object]] - .values - .map(o => JSON.stringify(o).fromJson[Parameter]) - .collect { case Right(p) => p } - .toList - - // TODO: Extract to separate event handler - actions.events.foreach { - case FetchDirectory => pushUsers(mockData) - case FetchUserDetails(osc) => - mockData.find(_.personalNumber == osc).foreach { o => - pushDetails(o) - router.replaceState(Page.Detail(o)) - } - case FetchParameters(osc) => - pushParameters(mockParameters) - case FetchParameter(osc, paramId) => - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(Page.DetailParametru(o, p)) - case FetchParameterCriteria(osc, paramId, critId, page) => - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - c <- p.criteria.find(_.id == critId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(page(o, p, c)) - case NavigateTo(page) => router.pushState(page) - case FetchAvailableFiles(osc) => - pushFiles( - List( - File("https://tc163.cmi.cz/here", "Example file") - ) - ) - } - - override def users: EventStream[List[UserInfo]] = - usersStream.debugWithName("users") - - override def details: EventStream[UserInfo] = - detailsStream.debugWithName("details") - - override def parameters: EventStream[List[Parameter]] = - parametersStream.debugWithName("parameters") - - override def availableFiles: EventStream[List[File]] = - filesStream.debugWithName("available files") - - override def actionBus: Observer[Action] = - actions.writer.debugWithName("actions writer") diff --git a/app/src/main/scala/mdr/pdb/app/Main.scala b/app/src/main/scala/mdr/pdb/app/Main.scala new file mode 100644 index 0000000..081b4ee --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/Main.scala @@ -0,0 +1,103 @@ +package mdr.pdb.app + +import scala.scalajs.js.annotation.JSExportTopLevel +import scala.scalajs.js.annotation.JSExport +import scala.scalajs.js.annotation.JSImport +import scala.scalajs.js +import org.scalajs.dom +import com.raquo.laminar.api.L.{*, given} +import scala.scalajs.js.Date +import com.raquo.waypoint.Router +import com.raquo.waypoint.SplitRender +import mdr.pdb.app.services.DataFetcher +import scala.scalajs.js.JSON +import zio.json._ +import mdr.pdb.UserInfo +import mdr.pdb.app.state.AppState + +@js.native +@JSImport("stylesheets/main.css", JSImport.Namespace) +object Css extends js.Any + +@js.native +@JSImport("data/users.json", JSImport.Default) +object mockUsers extends js.Object + +@js.native +@JSImport("params/pdb-params.json", JSImport.Default) +object pdbParams extends js.Object + +@JSExportTopLevel("app") +object Main: + + @JSExport + def main(args: Array[String]): Unit = + import Routes.given + onLoad { + setupAirstream() + val appContainer = dom.document.querySelector("#app") + val _ = + render( + appContainer, + renderPage(state.MockAppState(using unsafeWindowOwner, router)) + ) + } + + private def onLoad(f: => Unit): Unit = + documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) + + private def setupAirstream()(using router: Router[Page]): Unit = + AirstreamError.registerUnhandledErrorCallback(err => + router.forcePage( + Page.UnhandledError( + Some(err.getClass.getName), // TODO: Fill only in dev mode + Some(err.getMessage) + ) + ) + ) + + def renderPage(state: AppState)(using + router: Router[Page] + ): HtmlElement = + val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) + .collectSignal[Page.Detail]( + connectors + .DetailPageConnector(state)(_) + .apply + ) + .collectSignal[Page.DetailParametru]( + connectors + .DetailParametruPageConnector(state)(_) + .apply + ) + .collectSignal[Page.DetailKriteria]( + connectors + .DetailKriteriaPageConnector(state)(_) + .apply + ) + .collectSignal[Page.UpravDukazKriteria]( + pages.detail.UpravDukaz.Connector(state)(_).apply + ) + .collectStatic(Page.Dashboard)( + connectors.DashboardPageConnector(state.actionBus).apply + ) + .collect[Page.NotFound](pg => + pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) + ) + .collect[Page.UnhandledError](pg => + pages.errors + .UnhandledErrorPage( + pages.errors.UnhandledErrorPage + .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), + state.actionBus + ) + ) + .collectStatic(Page.Directory)( + connectors + .DirectoryPageConnector(state.users, state.actionBus) + .apply + ) + div(cls := "h-full", child <-- pageSplitter.$view) + + // Pull in the stylesheet + val css: Css.type = Css diff --git a/app/src/main/scala/mdr/pdb/app/Routes.scala b/app/src/main/scala/mdr/pdb/app/Routes.scala new file mode 100644 index 0000000..2cc860c --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/Routes.scala @@ -0,0 +1,198 @@ +package mdr.pdb.app + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.* +import org.scalajs.dom +import zio.json.{*, given} +import mdr.pdb.OsobniCislo + +import scala.scalajs.js +import mdr.pdb.UserInfo +import mdr.pdb.Parameter +import mdr.pdb.ParameterCriteria +import mdr.pdb.app.Page.Titled + +// enum is not working with Waypoints' SplitRender collectStatic +sealed abstract class Page( + val id: String, + val title: String, + val parent: Option[Page] +) { + val path: Vector[Page] = + parent match + case None => Vector(this) + case Some(p) => p.path :+ this + + val isRoot: Boolean = parent.isEmpty +} + +object Page: + + case class Titled[V](value: V, title: Option[String] = None): + val show: String = title.getOrElse(value.toString) + + case object Directory extends Page("directory", "Adresář", None) + + case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) + + case class Detail(osobniCislo: Titled[OsobniCislo]) + extends Page("user", osobniCislo.show, Some(Directory)) + + object Detail { + def apply(o: UserInfo): Detail = Detail( + Titled(o.personalNumber, Some(o.name)) + ) + } + + case class DetailParametru( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String] + ) extends Page( + "parameter", + parametr.show, + Some(Detail(osobniCislo)) + ) + + object DetailParametru { + def apply(o: UserInfo, p: Parameter): DetailParametru = + DetailParametru( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)) + ) + } + + case class DetailKriteria( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String], + kriterium: Titled[String] + ) extends Page( + "criteria", + kriterium.show, + Some(DetailParametru(osobniCislo, parametr)) + ) + + object DetailKriteria { + def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = + DetailKriteria( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)), + Titled(k.id, Some(k.id)) + ) + } + + case class UpravDukazKriteria( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String], + kriterium: Titled[String] + ) extends Page( + "addProof", + "Důkaz", + Some(DetailKriteria(osobniCislo, parametr, kriterium)) + ) + + object UpravDukazKriteria { + def apply( + o: UserInfo, + p: Parameter, + k: ParameterCriteria + ): UpravDukazKriteria = + UpravDukazKriteria( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)), + Titled(k.id, Some(k.id)) + ) + } + + case class NotFound(url: String) extends Page("404", "404", Some(Directory)) + + case class UnhandledError( + errorName: Option[String], + errorMessage: Option[String] + ) extends Page("500", "Unexpected error", Some(Directory)) + +object Routes: + given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) + given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) + given [V: JsonEncoder]: JsonEncoder[Titled[V]] = + DeriveJsonEncoder.gen[Titled[V]] + given [V: JsonDecoder]: JsonDecoder[Titled[V]] = + DeriveJsonDecoder.gen[Titled[V]] + given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] + given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] + + val base = + js.`import`.meta.env.BASE_URL + .asInstanceOf[String] + "app" + + val homePage: Page = Page.Directory + + given router: Router[Page] = Router[Page]( + routes = List( + Route.static(homePage, root / endOfSegments, basePath = base), + Route.static( + Page.Dashboard, + root / "dashboard" / endOfSegments, + basePath = base + ), + Route[Page.Detail, String]( + encode = _.osobniCislo.value.toString, + decode = osc => Page.Detail(Titled(OsobniCislo(osc))), + root / "osoba" / segment[String] / endOfSegments, + basePath = base + ), + Route[Page.DetailParametru, (String, String)]( + encode = p => (p.osobniCislo.value.toString, p.parametr.value), + decode = + p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / endOfSegments, + basePath = base + ), + Route[Page.DetailKriteria, (String, String, String)]( + encode = p => + ( + p.osobniCislo.value.toString, + p.parametr.value, + p.kriterium.value.replaceAll("\\.", "--") + ), + decode = p => + Page.DetailKriteria( + Titled(OsobniCislo(p._1)), + Titled(p._2), + Titled(p._3.replaceAll("--", ".")) + ), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / "kriterium" / segment[String] / endOfSegments, + basePath = base + ), + Route[Page.UpravDukazKriteria, (String, String, String)]( + encode = p => + ( + p.osobniCislo.value.toString, + p.parametr.value, + p.kriterium.value.replaceAll("\\.", "--") + ), + decode = p => + Page.UpravDukazKriteria( + Titled(OsobniCislo(p._1)), + Titled(p._2), + Titled(p._3.replaceAll("--", ".")) + ), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / "kriterium" / segment[String] / "edit" / endOfSegments, + basePath = base + ) + ), + serializePage = _.toJson, + deserializePage = _.fromJson[Page] + .fold(s => throw IllegalStateException(s), identity), + getPageTitle = _.title, + routeFallback = url => Page.NotFound(url), + deserializeFallback = _ => Page.Dashboard + )( + $popStateEvent = windowEvents.onPopState, + owner = unsafeWindowOwner + ) diff --git a/app/src/main/scala/mdr/pdb/app/actions.scala b/app/src/main/scala/mdr/pdb/app/actions.scala new file mode 100644 index 0000000..d078b3a --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/actions.scala @@ -0,0 +1,21 @@ +package mdr.pdb.app + +import mdr.pdb.OsobniCislo +import mdr.pdb.UserInfo +import mdr.pdb.Parameter +import mdr.pdb.ParameterCriteria + +sealed trait Action + +case object FetchDirectory extends Action +case class FetchUserDetails(osc: OsobniCislo) extends Action +case class FetchParameters(osc: OsobniCislo) extends Action +case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action +case class FetchParameterCriteria( + osc: OsobniCislo, + paramId: String, + critId: String, + page: (UserInfo, Parameter, ParameterCriteria) => Page +) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action +case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/mdr/pdb/app/components/AppPage.scala new file mode 100644 index 0000000..130c817 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/AppPage.scala @@ -0,0 +1,82 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.Page +import mdr.pdb.{UserProfile, UserInfo, OsobniCislo} +import com.raquo.waypoint.Router +import mdr.pdb.app.Action +import mdr.pdb.app.NavigateTo +import mdr.pdb.UserFunction + +object AppPage: + // TODO: pages by logged in user + val pages: List[Page] = List(Page.Directory, Page.Dashboard) + + import NavigationBar.{Logo, MenuItem} + + val logo = Logo( + "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", + "Workflow" + ) + + // TODO: menu items by user profile + val userMenu = + List( + MenuItem("Your Profile"), + MenuItem("Settings"), + MenuItem("Sign out") + ) + + // TODO: load user profile + val $userProfile = Var( + UserProfile( + "tom", + UserInfo( + OsobniCislo("1031"), + "tom", + "Tom", + "Cook", + None, + None, + Some("tom@example.com"), + Some("+420 222 866 180"), + Some( + UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") + ), + Nil, + None + ) + ) + ) + + val $userInfo = $userProfile.signal.map(_.userInfo) + + type ViewModel = Option[HtmlElement] + def apply( + actionBus: Observer[Action] + )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using + router: Router[Page] + ): HtmlElement = + PageLayout(actionBus)( + $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => + PageLayout.ViewModel( + NavigationBar.ViewModel( + u, + pages.map(p => + NavigationBar.Link( + () => + PageLink( + p, + actionBus + ), + p == cp + ) + ), + userMenu, + logo + ), + c + ) + ), + mods + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala new file mode 100644 index 0000000..9375398 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala @@ -0,0 +1,143 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.tailwind.list.IconText.ViewModel +import fiftyforms.ui.components.tailwind.Icons +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.Page + +import com.raquo.laminar.api.L.{*, given} +import io.laminext.syntax.core.{*, given} +import mdr.pdb.app.Action + +object Breadcrumbs: + + private def slash = + import svg.{*, given} + svg( + cls := "flex-shrink-0 h-5 w-5 text-gray-300", + xmlns := "http://www.w3.org/2000/svg", + fill := "currentColor", + viewBox := "0 0 20 20", + ariaHidden := true, + path( + d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" + ) + ) + + def backIcon = + Icons.solid + .`arrow-narrow-left`() + .amend( + svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" + ) + + object Link: + case class ViewModel( + page: Page, + icon: Option[SvgElement], + extraClasses: String, + text: String, + textClasses: Option[String] = None + ) + + val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" + + def shortHome(p: Page) = ViewModel( + p, + Some(backIcon), + "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", + "Zpět na úvodní stránku", + None + ) + + def fullHome(p: Page) = ViewModel( + p, + Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), + "text-gray-400 hover:text-gray-500", + "Domů", + Some("sr-only") + ) + + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + PageLink + .container($m.map(_.page), actionBus) + .amend( + cls <-- $m.map(_.extraClasses), + child.maybe <-- $m.map(_.icon), + span( + cls <-- $m.map(_.textClasses.getOrElse("")), + child.text <-- $m.map(_.text) + ) + ) + + object FullBreadcrumbs: + type ViewModel = Page + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + ol( + role := "list", + cls := "flex items-center space-x-4", + children <-- $m.map(_.path).split(_.id)((_, _, $p) => + li( + div( + cls := "flex items-center", + child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), + Link( + $p.map(p => + if (p.isRoot) then Link.fullHome(p) + else + Link.ViewModel( + p, + None, + s"ml-4 max-w-xs truncate ${Link.baseClasses}", + p.title + ) + ), + actionBus + ) + ) + ) + ) + ) + + object ShortBreadcrumbs: + type ViewModel = Page + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + Link( + $m.map { p => + val target = p.parent.getOrElse(p) + if target.isRoot then Link.shortHome(target) + else + Link.ViewModel( + target, + Some(backIcon), + "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", + s"Zpět na ${target.title}" + ) + }, + actionBus + ) + + def apply(actionBus: Observer[Action])(using + router: Router[Page] + ): HtmlElement = + nav( + cls := "flex", + aria.label := "Breadcrumb", + div( + cls := "flex sm:hidden", + ShortBreadcrumbs(router.$currentPage, actionBus) + ), + div( + cls := "hidden sm:block", + FullBreadcrumbs(router.$currentPage, actionBus) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala new file mode 100644 index 0000000..4bb8b8d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala @@ -0,0 +1,242 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent +import fiftyforms.ui.components.tailwind.Icons +import fiftyforms.ui.components.tailwind.Avatar +import mdr.pdb.UserInfo +import io.laminext.syntax.core.* + +object NavigationBar: + + case class Logo(img: String, name: String) + case class Link(a: () => Anchor, active: Boolean) + case class MenuItem(title: String) + + case class ViewModel( + userInfo: UserInfo, + pages: List[Link], + userMenu: List[MenuItem], + logo: Logo + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + val $userInfo = $m.map(_.userInfo) + val mobileMenuOpen = Var(false) + + val desktopOnly = cls("hidden md:block") + val mobileOnly = cls("md:hidden") + + inline def avatarImage(size: Int = 8) = + Avatar($userInfo.map(_.img)).avatarImage(size) + + def notificationButton = button( + tpe := "button", + cls := List( + "bg-indigo-600", + "focus:outline-none", + "focus:ring-2", + "focus:ring-offset-2", + "focus:ring-offset-indigo-600", + "focus:ring-white", + "hover:text-white", + "p-1", + "rounded-full", + "text-indigo-200" + ), + span(cls := "sr-only", "View notifications"), + Icons.outline.bell() + ) + + def userProfile: HtmlElement = + val menuOpen = Var(false) + + def menuItem(item: MenuItem, idx: Int): HtmlElement = + a( + href := "#", + cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", + role := "menuitem", + tabIndex := -1, + idAttr := s"user-menu-item-$idx", + item.title + ) + + div( + cls := "ml-3 relative", + div( + button( + tpe := "button", + cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", + idAttr := "user-menu-button", + aria.expanded <-- menuOpen.signal, + aria.hasPopup := true, + span(cls := "sr-only", "Open user menu"), + child <-- avatarImage(), + onClick.preventDefault.mapTo( + !menuOpen.now() + ) --> menuOpen.writer + ) + ), + /* + * */ + div( + cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", + cls <-- menuOpen.signal.map { o => + if (o) "md:block" else "md:hidden" + }, + role := "menu", + aria.orientation := "vertical", + aria.labelledBy := "user-menu-button", + tabIndex := -1, + // : keyboard navigation + children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) + ) + ) + + def mobileProfile = + def menuItem(item: MenuItem): HtmlElement = + a( + href := "#", + cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", + item.title + ) + + div( + cls := "pt-4 pb-3 border-t border-indigo-700", + div( + cls := "flex items-center px-5", + div( + cls := "flex-shrink-0", + child <-- avatarImage(10) + ), + div( + cls := "ml-3", + div( + cls := "text-base font-medium text-white", + child.text <-- $userInfo.map(_.name) + ), + child.maybe <-- $userInfo.map( + _.email.map(e => + div( + cls := "text-sm font-medium text-indigo-300", + e + ) + ) + ) + ), + notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) + ), + div( + cls := "mt-3 px-2 space-y-1", + children <-- $m.map(_.userMenu.map(menuItem)) + ) + ) + + def pageLink(page: Link): Anchor = + page + .a() + .amend( + cls := "text-white px-3 py-2 rounded-md text-sm font-medium", + cls := Seq( + "bg-indigo-700" -> page.active, + "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active + ), + ariaCurrent := (if page.active then "page" else "false") + ) + + def logoImg: Image = + img( + cls := "h-8 w-8", + src <-- $m.map(_.logo.img), + alt <-- $m.map(_.logo.name) + ) + + def mobileMenuButton = button( + tpe := "button", + cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", + aria.controls := "mobile-menu", + aria.expanded <-- mobileMenuOpen.signal, + span(cls := "sr-only", "Open main menu"), + Icons.outline + .menu() + .amend( + svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") + ), + Icons.outline + .x() + .amend( + svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") + ), + onClick.preventDefault.mapTo( + !mobileMenuOpen.now() + ) --> mobileMenuOpen.writer + ) + + def navBarLeft = + div( + cls := "flex items-center", + div(cls := "flex-shrink-0", logoImg), + div( + desktopOnly, + div( + cls := "ml-10 flex items-baseline space-x-4", + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) + ) + ) + ) + + def navBarRight = + div( + desktopOnly, + div( + cls := "ml-4 flex items-center md:ml-6", + notificationButton, + userProfile + ) + ) + + def navBarMobile = + div( + cls := "-mr-2 flex", + mobileOnly, + mobileMenuButton + ) + + def navBar = + div( + cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", + div( + cls := "flex items-center justify-between h-16", + navBarLeft, + navBarRight, + navBarMobile + ) + ) + + def mobileMenu = + div( + mobileOnly, + cls <-- mobileMenuOpen.signal.map { o => + if (o) "block" else "hidden" + }, + idAttr := "mobile-menu", + div( + cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) + ), + mobileProfile + ) + + nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala new file mode 100644 index 0000000..c4cb10d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala @@ -0,0 +1,19 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.Page +import com.raquo.waypoint.Router +import mdr.pdb.app.Action + +object PageHeader: + def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = + header( + cls := "bg-white shadow-sm", + div( + cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", + h1( + cls := "text-lg leading-6 font-semibold text-gray-900", + Breadcrumbs(actionBus) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala new file mode 100644 index 0000000..aae4539 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala @@ -0,0 +1,30 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.Page +import mdr.pdb.app.Action +import fiftyforms.ui.components.tailwind.Loading +import io.laminext.syntax.core.* + +object PageLayout: + case class ViewModel( + navigation: NavigationBar.ViewModel, + content: Option[HtmlElement] + ) + def apply(actionBus: Observer[Action])( + $m: Signal[ViewModel], + mods: Modifier[HtmlElement]* + )(using router: Router[Page]): HtmlElement = + val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) + div( + cls := "h-full flex flex-col", + NavigationBar($m.map(_.navigation)), + child.maybe <-- router.$currentPage.map(_.isRoot) + .switch(None, Some(PageHeader(actionBus))), + main( + cls := "flex-grow-1 overflow-y-auto", + mods, + child <-- $maybeContent.map(_.getOrElse(Loading)) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/mdr/pdb/app/components/PageLink.scala new file mode 100644 index 0000000..e6e7052 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageLink.scala @@ -0,0 +1,40 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.LinkSupport.* +import mdr.pdb.app.Page +import mdr.pdb.app.Action +import com.raquo.waypoint.Router +import mdr.pdb.app.NavigateTo + +object PageLink: + type ViewModel = Page + def apply($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = + a(mods($m, actions), child.text <-- $m.map(_.title)) + + def apply(m: ViewModel, actions: Observer[Action])(using + router: Router[Page] + ): Anchor = apply(Val(m), actions) + + def container($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = a(mods($m, actions)) + + def container(m: ViewModel, actions: Observer[Action])(using + Router[Page] + ): Anchor = container(Val(m), actions) + + private def mods($m: Signal[Page], actions: Observer[Action])(using + router: Router[Page] + ): Modifier[Anchor] = + Seq( + href <-- $m.map(router.absoluteUrlForPage).recover { case _ => + Some("invalid url") + }, + composeEvents(onClick.noKeyMod.preventDefault)( + _.sample($m) + .map(NavigateTo.apply) + ) --> actions + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala new file mode 100644 index 0000000..7682772 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala @@ -0,0 +1,13 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.pages.dashboard.DashboardPage +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage + +class DashboardPageConnector(actionBus: Observer[Action])(using + router: Router[Page] +): + def apply: HtmlElement = + AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala new file mode 100644 index 0000000..76b9133 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala @@ -0,0 +1,76 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.Parameter +import mdr.pdb.UserInfo +import mdr.pdb.app.pages.detail.DetailParametruPage +import pages.detail.DetailKriteriaPage +import pages.detail.components.DukazyKriteria +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.ParameterCriteria + +object DetailKriteriaPageConnector { + trait AppState { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + } +} + +case class DetailKriteriaPageConnector( + state: DetailKriteriaPageConnector.AppState +)( + $page: Signal[Page.DetailKriteria] +)(using Router[Page]): + val $paramChangeSignal = + $page.splitOne(p => + (p.osobniCislo.value, p.parametr.value, p.kriterium.value) + )((x, _, _) => x) + val $pageChangeSignal = + $paramChangeSignal.map( + FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) + ) + + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + val $merged = + $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => + for { + da <- d + pa <- p + pb <- pa.find(_.id == pc._2) + ka <- pb.criteria.find(_.id == pc._3) + } yield (da, pb, ka) + ) + + def apply: HtmlElement = + AppPage(state.actionBus)( + $merged.map(_.map(buildModel)) + .split(_ => ())((_, s, $s) => + DetailKriteriaPage($s)(state.actionBus.contramap { + case DukazyKriteria.Add => + NavigateTo( + Page.UpravDukazKriteria( + Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), + Page.Titled(s.parametr.id, Some(s.parametr.nazev)), + Page.Titled(s.kriterium.id, Some(s.kriterium.id)) + ) + ) + }) + ), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: Parameter, + k: ParameterCriteria + ): DetailKriteriaPage.ViewModel = + DetailKriteriaPage.ViewModel( + o.toDetailOsoby, + p.toParametr(_ => a()), + k.toKriterium(_ => a()) + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DetailPageConnector.scala new file mode 100644 index 0000000..df3a6b7 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DetailPageConnector.scala @@ -0,0 +1,57 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.UserInfo +import pages.detail.DetailPage +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.pages.detail.components.DetailOsoby +import mdr.pdb.Parameter +import mdr.pdb.app.pages.detail.components.SeznamParametru +import fiftyforms.ui.components.tailwind.Color + +object DetailPageConnector { + trait AppState { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + } +} + +case class DetailPageConnector(state: DetailPageConnector.AppState)( + $page: Signal[Page.Detail] +)(using router: Router[Page]): + val $oscChangeSignal = $page.splitOne(_.osobniCislo.value)((osc, _, _) => osc) + val $pageChangeSignal = + $oscChangeSignal.flatMap(osc => + EventStream.fromSeq(Seq(FetchUserDetails(osc), FetchParameters(osc))) + ) + // TODO: filter the value based on the current osc + // OSC change will fetch new data, but still + // - we need to be sure that what we got is really what we ought to display + // - we want to display stale data accordingly (at least with loading indicator) + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + def apply: HtmlElement = + AppPage(state.actionBus)( + $data.combineWithFn($params)(_ zip _) + .map(_.map(buildModel)) + .split(_ => ())((_, _, s) => DetailPage(s)), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: List[Parameter] + ): DetailPage.ViewModel = + DetailPage.ViewModel( + o.toDetailOsoby, + p.map( + _.toParametr(param => + PageLink.container(Page.DetailParametru(o, param), state.actionBus) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DetailParametruPageConnector.scala new file mode 100644 index 0000000..fa7193d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DetailParametruPageConnector.scala @@ -0,0 +1,67 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.Parameter +import mdr.pdb.UserInfo +import mdr.pdb.app.pages.detail.DetailParametruPage +import pages.detail.DetailParametruPage +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.components.PageLink + +object DetailParametruPageConnector { + trait AppState { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + } +} + +case class DetailParametruPageConnector( + state: DetailParametruPageConnector.AppState +)( + $page: Signal[Page.DetailParametru] +)(using router: Router[Page]): + val $paramChangeSignal = + $page.splitOne(p => (p.osobniCislo.value, p.parametr.value))((x, _, _) => x) + val $pageChangeSignal = + $paramChangeSignal.map(FetchParameter(_, _)) + + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + val $merged = + $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => + for { + da <- d + pa <- p + pb <- pa.find(_.id == pc._2) + } yield (da, pb) + ) + + def apply: HtmlElement = + AppPage(state.actionBus)( + $merged.map(_.map(buildModel)) + .split(_ => ())((_, _, s) => DetailParametruPage(s)), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: Parameter + ): DetailParametruPage.ViewModel = + DetailParametruPage.ViewModel( + o.toDetailOsoby, + p.toParametr(p => + PageLink.container(Page.DetailParametru(o, p), state.actionBus) + ), + p.criteria.map( + _.toKriterium { c => + PageLink.container( + Page.DetailKriteria(o, p, c), + state.actionBus + ) + } + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DirectoryPageConnector.scala new file mode 100644 index 0000000..ab2c045 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DirectoryPageConnector.scala @@ -0,0 +1,34 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.UserInfo +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.components.AppPage + +case class DirectoryPageConnector( + $input: EventStream[List[UserInfo]], + actionBus: Observer[Action] +)(using router: Router[Page]): + val $data = $input.startWithNone + val $actionSignal = EventStream.fromValue(FetchDirectory) + + def apply: HtmlElement = + AppPage(actionBus)( + $data.split(_ => ())((_, _, s) => + pages.directory.DirectoryPage( + s.map( + _.map( + _.toUserRow(u => + PageLink.container( + Page.Detail(Page.Titled(u.personalNumber)), + actionBus + ) + ) + ) + ) + ) + ), + $actionSignal --> actionBus + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/mdr/pdb/app/connectors/conversions.scala new file mode 100644 index 0000000..f581796 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/conversions.scala @@ -0,0 +1,68 @@ +package mdr.pdb.app.connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.UserInfo +import mdr.pdb.app.pages.detail.components.DetailOsoby +import mdr.pdb.Parameter +import mdr.pdb.app.pages.detail.components.SeznamParametru +import mdr.pdb.ParameterCriteria +import mdr.pdb.app.pages.detail.components.SeznamKriterii +import mdr.pdb.app.pages.directory.components.UserRow +import mdr.pdb.app.pages.detail.components.DetailParametru +import mdr.pdb.app.pages.detail.components.DetailKriteria +import fiftyforms.ui.components.tailwind.Color + +extension (o: UserInfo) + def toDetailOsoby: DetailOsoby.ViewModel = + DetailOsoby.ViewModel( + o.personalNumber, + o.name, + o.email, + o.phone, + o.img, + o.mainFunction.map(f => + DetailOsoby.Funkce.ViewModel(f.name, f.dept, f.ou) + ), + o.userContracts.headOption.map(c => + DetailOsoby.PracovniPomer.ViewModel(c.rel, c.startDate, c.endDate) + ) + ) + +extension (param: Parameter) + def toParametr(container: Parameter => Anchor): DetailParametru.ViewModel = + DetailParametru.ViewModel( + id = param.id, + nazev = param.name, + popis = param.description, + status = "Nesplněno", + statusColor = Color.red, + a = container(param) + ) + +extension (crit: ParameterCriteria) + def toKriterium( + container: ParameterCriteria => Anchor + ): DetailKriteria.ViewModel = + DetailKriteria.ViewModel( + nazev = crit.criteriumText, + kapitola = crit.chapterId, + bod = crit.itemId, + status = "Nesplněno", + statusColor = Color.red, + splneno = false, + dukazy = Nil, + container = container(crit) + ) + +extension (user: UserInfo) + def toUserRow( + container: UserInfo => HtmlElement = (_: UserInfo) => div() + ): UserRow.ViewModel = + UserRow.ViewModel( + osobniCislo = user.personalNumber.toString, + celeJmeno = user.name, + prijmeni = user.surname, + hlavniFunkce = user.mainFunction.map(_.name), + img = user.img, + container = () => container(user) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/dashboard/DashboardPage.scala b/app/src/main/scala/mdr/pdb/app/pages/dashboard/DashboardPage.scala new file mode 100644 index 0000000..0eb8f7a --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/dashboard/DashboardPage.scala @@ -0,0 +1,11 @@ +package mdr.pdb.app.pages.dashboard + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.Page + +object DashboardPage: + + def render: HtmlElement = + div("Dashboard page") diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala new file mode 100644 index 0000000..ff3a25f --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala @@ -0,0 +1,31 @@ +package mdr.pdb.app.pages.detail + +import com.raquo.laminar.api.L.{*, given} + +import components._ + +object DetailKriteriaPage: + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametr: DetailParametru.ViewModel, + kriterium: DetailKriteria.ViewModel + ) + + def apply($m: Signal[ViewModel])( + action: Observer[DukazyKriteria.Action] + ): HtmlElement = + div( + cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + div( + DetailOsoby.header($m.map(_.osoba)), + DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") + ), + div( + DetailKriteria($m.map(_.kriterium)), + DukazyKriteria($m.map(_.kriterium.dukazy))(action) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/DetailPage.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailPage.scala new file mode 100644 index 0000000..b591cc9 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailPage.scala @@ -0,0 +1,25 @@ +package mdr.pdb.app.pages.detail + +import com.raquo.laminar.api.L.{*, given} + +import components._ +import mdr.pdb.app.Page +import com.raquo.waypoint.Router +import mdr.pdb.app.Action + +object DetailPage: + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametry: SeznamParametru.ViewModel + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + DetailOsoby($m.map(_.osoba)), + SeznamParametru($m.map(_.parametry)) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/DetailParametruPage.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailParametruPage.scala new file mode 100644 index 0000000..3322279 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailParametruPage.scala @@ -0,0 +1,24 @@ +package mdr.pdb.app.pages.detail + +import com.raquo.laminar.api.L.{*, given} + +import components._ + +object DetailParametruPage: + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametr: DetailParametru.ViewModel, + kriteria: SeznamKriterii.ViewModel + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + DetailOsoby.header($m.map(_.osoba)), + DetailParametru($m.map(_.parametr)), + SeznamKriterii($m.map(_.kriteria)) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala new file mode 100644 index 0000000..b58618a --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala @@ -0,0 +1,107 @@ +package mdr.pdb +package app +package pages.detail + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.pages.detail.components.UpravDukazForm +import fiftyforms.services.files.File + +object UpravDukaz: + + type ThisPage = Page.UpravDukazKriteria + type PageKey = (OsobniCislo, String, String) + + trait State { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def availableFiles: EventStream[List[File]] + def actionBus: Observer[Action] + } + + def keyOfPage(page: ThisPage): PageKey = + (page.osobniCislo.value, page.parametr.value, page.kriterium.value) + + def onChangeAction(key: PageKey): Action = + FetchParameterCriteria( + key._1, + key._2, + key._3, + Page.UpravDukazKriteria(_, _, _) + ) + + class Connector(state: State)($page: Signal[ThisPage])(using Router[Page]): + val $paramChangeSignal = $page.splitOne(keyOfPage)((x, _, _) => x) + val $pageChangeSignal = $paramChangeSignal.map(onChangeAction) + + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + val $merged = + $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => + for { + da <- d + pa <- p + pb <- pa.find(_.id == pc._2) + ka <- pb.criteria.find(_.id == pc._3) + } yield (da, pb, ka) + ) + + def apply: HtmlElement = + AppPage(state.actionBus)( + $merged.split(_ => ())((_, s, $s) => + PageComponent( + $s.map(buildModel), + state.availableFiles, + state.actionBus.contramap { + case UpravDukazForm.Cancelled => + NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) + case UpravDukazForm.AvailableFilesRequested => + FetchAvailableFiles(s._1.personalNumber) + } + ) + ), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: Parameter, + k: ParameterCriteria + ): PageComponent.ViewModel = + import connectors.* + PageComponent.ViewModel( + o.toDetailOsoby, + p.toParametr(_ => a()), + k.toKriterium(_ => a()) + ) + + object PageComponent: + import components.* + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametr: DetailParametru.ViewModel, + kriterium: DetailKriteria.ViewModel + ) + + def apply( + $m: Signal[ViewModel], + availableFilesStream: EventStream[List[File]], + events: Observer[UpravDukazForm.Event] + ): HtmlElement = + div( + cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + div( + DetailOsoby.header($m.map(_.osoba)), + DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") + ), + div( + DetailKriteria($m.map(_.kriterium)), + UpravDukazForm(availableFilesStream)(events) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailKriteria.scala new file mode 100644 index 0000000..a3ab634 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailKriteria.scala @@ -0,0 +1,37 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.CustomAttrs +import fiftyforms.ui.components.tailwind.Icons +import java.time.LocalDate +import fiftyforms.ui.components.tailwind.Color + +object DetailKriteria: + case class ViewModel( + nazev: String, + kapitola: String, + bod: String, + status: String, + statusColor: Color, + splneno: Boolean, + dukazy: List[DukazKriteria.ViewModel], + container: HtmlElement = div() + ) { + val id = s"${kapitola}${bod}" + } + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "pb-5 border-b border-gray-200", + h3( + cls := "text-xl leading-6 font-bold text-gray-900", + child.text <-- $m.map(_.nazev) + ), + h3( + cls := "mt-2 max-w-4xl text-sm text-gray-500", + "Kapitola ", + child.text <-- $m.map(_.kapitola), + " bod ", + child <-- $m.map(_.bod) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailOsoby.scala new file mode 100644 index 0000000..0fe2ba5 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailOsoby.scala @@ -0,0 +1,100 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.OsobniCislo + +import java.time.LocalDate +import fiftyforms.ui.components.tailwind.Avatar + +object DetailOsoby: + + object Funkce: + case class ViewModel( + nazev: String, + stredisko: String, + voj: String + ) + + def render($m: Signal[ViewModel]): HtmlElement = + p( + cls := "text-sm font-medium text-gray-500", + child.text <-- $m.map(_.nazev), + span( + cls := "hidden md:inline", + " @ ", + child.text <-- $m.map(_.stredisko), + ", ", + child.text <-- $m.map(_.voj) + ) + ) + + object PracovniPomer: + case class ViewModel( + druh: String, + pocatek: LocalDate, + konec: Option[LocalDate] + ) + + def render($m: Signal[ViewModel]): HtmlElement = + import fiftyforms.ui.components.tailwind.CustomAttrs.datetime + p( + cls := "text-sm font-medium text-gray-500", + child.text <-- $m.map(_.druh), + " od ", + time( + datetime <-- $m.map(_.pocatek.toString), + child.text <-- $m.map(_.pocatek.toString) + ) + ) + + case class ViewModel( + osobniCislo: OsobniCislo, + jmeno: String, + email: Option[String], + telefon: Option[String], + img: Option[String], + hlavniFunkce: Option[Funkce.ViewModel], + pracovniPomer: Option[PracovniPomer.ViewModel] + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "md:flex md:items-center md:justify-between md:space-x-4", + div( + cls := "flex items-start space-x-4", + div( + cls := "flex-shrink-0", + Avatar($m.map(_.img)).avatar(16) + ), + div( + h1( + cls := "text-2xl font-bold text-gray-900", + child.text <-- $m.map(_.jmeno) + ), + child.maybe <-- $m.map(_.hlavniFunkce).split(_ => ())((_, _, d) => + Funkce.render(d) + ), + child.maybe <-- $m.map(_.pracovniPomer).split(_ => ())((_, _, d) => + PracovniPomer.render(d) + ) + ) + ) + ) + + def header($m: Signal[ViewModel]): HtmlElement = + div( + cls := "md:flex md:items-center md:justify-between md:space-x-5", + div( + cls := "flex items-start space-x-4", + div( + cls := "flex-shrink-0", + Avatar($m.map(_.img)).avatar(8) + ), + div( + h1( + cls := "text-lg font-medium text-gray-800", + child.text <-- $m.map(_.jmeno) + ) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailParametru.scala new file mode 100644 index 0000000..6310dde --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailParametru.scala @@ -0,0 +1,34 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.Color + +object DetailParametru: + case class ViewModel( + id: String, + nazev: String, + popis: String, + status: String, + statusColor: Color, + a: Anchor + ) + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "pb-5 border-b border-gray-200", + h2( + cls := "text-2xl leading-6 font-medium text-gray-900", + child.text <-- $m.map(_.nazev) + ), + p( + cls := "mt-2 max-w-4xl text-sm text-gray-500", + child.text <-- $m.map(_.popis) + ) + ) + + def header($m: Signal[ViewModel]): HtmlElement = + div( + h2( + cls := "text-lg leading-6 font-medium text-gray-600", + child.text <-- $m.map(_.nazev) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazKriteria.scala new file mode 100644 index 0000000..c292919 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazKriteria.scala @@ -0,0 +1,177 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import java.time.LocalDate +import fiftyforms.ui.components.tailwind.CustomAttrs + +object DukazKriteria: + case class Osoba(osobniCislo: String, jmeno: String) + case class Udalost(osoba: Osoba, datum: LocalDate) + case class Dokument( + url: String, + nazev: String, + pridal: Udalost, + odebral: Option[Udalost] + ) + case class ViewModel( + dokumenty: List[Dokument], + autorizoval: Option[Udalost], + platiDo: Option[LocalDate], + poznámka: Option[String] + ) + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "bg-white shadow overflow-hidden sm:rounded-lg", + div( + cls := "px-4 py-5 sm:px-6", + h3( + cls := "text-lg leading-6 font-medium text-gray-900", + """Applicant Information""" + ), + p( + cls := "mt-1 max-w-2xl text-sm text-gray-500", + """Personal details and application.""" + ) + ), + div( + cls := "border-t border-gray-200 px-4 py-5 sm:px-6", + dl( + cls := "grid grid-cols-1 gap-x-4 gap-y-8 sm:grid-cols-2", + div( + cls := "sm:col-span-1", + dt( + cls := "text-sm font-medium text-gray-500", + """Full name""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """Margot Foster""" + ) + ), + div( + cls := "sm:col-span-1", + dt( + cls := "text-sm font-medium text-gray-500", + """Application for""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """Backend Developer""" + ) + ), + div( + cls := "sm:col-span-1", + dt( + cls := "text-sm font-medium text-gray-500", + """Email address""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """margotfoster@example.com""" + ) + ), + div( + cls := "sm:col-span-1", + dt( + cls := "text-sm font-medium text-gray-500", + """Salary expectation""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """$120,000""" + ) + ), + div( + cls := "sm:col-span-2", + dt( + cls := "text-sm font-medium text-gray-500", + """About""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """Fugiat ipsum ipsum deserunt culpa aute sint do nostrud anim incididunt cillum culpa consequat. Excepteur qui ipsum aliquip consequat sint. Sit id mollit nulla mollit nostrud in ea officia proident. Irure nostrud pariatur mollit ad adipisicing reprehenderit deserunt qui eu.""" + ) + ), + div( + cls := "sm:col-span-2", + dt( + cls := "text-sm font-medium text-gray-500", + """Attachments""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + ul( + role := "list", + cls := "border border-gray-200 rounded-md divide-y divide-gray-200", + li( + cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", + div( + cls := "w-0 flex-1 flex items-center", { + import svg.* + import CustomAttrs.svg.ariaHidden + svg( + cls := "flex-shrink-0 h-5 w-5 text-gray-400", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + ariaHidden := true, + path( + fillRule := "evenodd", + d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", + clipRule := "evenodd" + ) + ) + }, + span( + cls := "ml-2 flex-1 w-0 truncate", + """resume_back_end_developer.pdf""" + ) + ), + div( + cls := "ml-4 flex-shrink-0", + a( + href := "#", + cls := "font-medium text-indigo-600 hover:text-indigo-500", + """Download""" + ) + ) + ), + li( + cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", + div( + cls := "w-0 flex-1 flex items-center", { + import svg.* + import CustomAttrs.svg.ariaHidden + svg( + cls := "flex-shrink-0 h-5 w-5 text-gray-400", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + ariaHidden := true, + path( + fillRule := "evenodd", + d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", + clipRule := "evenodd" + ) + ) + }, + span( + cls := "ml-2 flex-1 w-0 truncate", + """coverletter_back_end_developer.pdf""" + ) + ), + div( + cls := "ml-4 flex-shrink-0", + a( + href := "#", + cls := "font-medium text-indigo-600 hover:text-indigo-500", + """Download""" + ) + ) + ) + ) + ) + ) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala new file mode 100644 index 0000000..d604860 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala @@ -0,0 +1,35 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.Icons + +object DukazyKriteria: + sealed trait Action + case object Add extends Action + + type ViewModel = List[DukazKriteria.ViewModel] + + def apply($m: Signal[ViewModel])(actions: Observer[Action]): HtmlElement = + div( + child.maybe <-- $m.map(m => + if (m.isEmpty) then Some(prazdnyDukaz(actions)) + else None + ), + children <-- $m.map(_.zipWithIndex).split(_._2)((_, _, $s) => + DukazKriteria($s.map(_._1)) + ) + ) + + private def prazdnyDukaz(actions: Observer[Action]): HtmlElement = + button( + tpe := "button", + onClick.preventDefault.mapTo(Add) --> actions, + cls := "relative block w-full border-2 border-gray-300 border-dashed rounded-lg p-12 text-center hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + Icons.outline + .`document-add`(12) + .amend(svg.cls := "mx-auto text-gray-400"), + span( + cls := "mt-2 block text-sm font-medium text-gray-900", + "Přidat důkaz" + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala new file mode 100644 index 0000000..7e77a7b --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala @@ -0,0 +1,34 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.Color +import fiftyforms.ui.components.tailwind.list.StackedList +import fiftyforms.ui.components.tailwind.list.ListRow +import fiftyforms.ui.components.tailwind.list.RowTag +import fiftyforms.ui.components.tailwind.list.RowNext +import java.time.LocalDate + +object SeznamKriterii: + type ViewModel = List[DetailKriteria.ViewModel] + + private val kritList = new StackedList[DetailKriteria.ViewModel] + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "bg-white shadow overflow-hidden sm:rounded-md", + kritList($m, _.id) { $i => + $i.map { i => + ListRow.ViewModel( + title = i.nazev, + topRight = RowTag.render( + $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) + ), + bottomLeft = + p(cls := "text-sm text-gray-500", s"${i.kapitola}${i.bod}"), + bottomRight = emptyNode, + farRight = RowNext.render, + containerElement = i.container + ) + } + } + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala deleted file mode 100644 index a3b00af..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala +++ /dev/null @@ -1,103 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import scala.scalajs.js.annotation.JSExportTopLevel -import scala.scalajs.js.annotation.JSExport -import scala.scalajs.js.annotation.JSImport -import scala.scalajs.js -import org.scalajs.dom -import com.raquo.laminar.api.L.{*, given} -import scala.scalajs.js.Date -import com.raquo.waypoint.Router -import com.raquo.waypoint.SplitRender -import cz.e_bs.cmi.mdr.pdb.app.services.DataFetcher -import scala.scalajs.js.JSON -import zio.json._ -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.state.AppState - -@js.native -@JSImport("stylesheets/main.css", JSImport.Namespace) -object Css extends js.Any - -@js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native -@JSImport("params/pdb-params.json", JSImport.Default) -object pdbParams extends js.Object - -@JSExportTopLevel("app") -object Main: - - @JSExport - def main(args: Array[String]): Unit = - import Routes.given - onLoad { - setupAirstream() - val appContainer = dom.document.querySelector("#app") - val _ = - render( - appContainer, - renderPage(state.MockAppState(using unsafeWindowOwner, router)) - ) - } - - private def onLoad(f: => Unit): Unit = - documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) - - private def setupAirstream()(using router: Router[Page]): Unit = - AirstreamError.registerUnhandledErrorCallback(err => - router.forcePage( - Page.UnhandledError( - Some(err.getClass.getName), // TODO: Fill only in dev mode - Some(err.getMessage) - ) - ) - ) - - def renderPage(state: AppState)(using - router: Router[Page] - ): HtmlElement = - val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) - .collectSignal[Page.Detail]( - connectors - .DetailPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailParametru]( - connectors - .DetailParametruPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailKriteria]( - connectors - .DetailKriteriaPageConnector(state)(_) - .apply - ) - .collectSignal[Page.UpravDukazKriteria]( - pages.detail.UpravDukaz.Connector(state)(_).apply - ) - .collectStatic(Page.Dashboard)( - connectors.DashboardPageConnector(state.actionBus).apply - ) - .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) - ) - .collect[Page.UnhandledError](pg => - pages.errors - .UnhandledErrorPage( - pages.errors.UnhandledErrorPage - .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), - state.actionBus - ) - ) - .collectStatic(Page.Directory)( - connectors - .DirectoryPageConnector(state.users, state.actionBus) - .apply - ) - div(cls := "h-full", child <-- pageSplitter.$view) - - // Pull in the stylesheet - val css: Css.type = Css diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala deleted file mode 100644 index 3bbc8c0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ /dev/null @@ -1,198 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.* -import org.scalajs.dom -import zio.json.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import scala.scalajs.js -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.Page.Titled - -// enum is not working with Waypoints' SplitRender collectStatic -sealed abstract class Page( - val id: String, - val title: String, - val parent: Option[Page] -) { - val path: Vector[Page] = - parent match - case None => Vector(this) - case Some(p) => p.path :+ this - - val isRoot: Boolean = parent.isEmpty -} - -object Page: - - case class Titled[V](value: V, title: Option[String] = None): - val show: String = title.getOrElse(value.toString) - - case object Directory extends Page("directory", "Adresář", None) - - case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) - - case class Detail(osobniCislo: Titled[OsobniCislo]) - extends Page("user", osobniCislo.show, Some(Directory)) - - object Detail { - def apply(o: UserInfo): Detail = Detail( - Titled(o.personalNumber, Some(o.name)) - ) - } - - case class DetailParametru( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String] - ) extends Page( - "parameter", - parametr.show, - Some(Detail(osobniCislo)) - ) - - object DetailParametru { - def apply(o: UserInfo, p: Parameter): DetailParametru = - DetailParametru( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)) - ) - } - - case class DetailKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "criteria", - kriterium.show, - Some(DetailParametru(osobniCislo, parametr)) - ) - - object DetailKriteria { - def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = - DetailKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class UpravDukazKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "addProof", - "Důkaz", - Some(DetailKriteria(osobniCislo, parametr, kriterium)) - ) - - object UpravDukazKriteria { - def apply( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): UpravDukazKriteria = - UpravDukazKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class NotFound(url: String) extends Page("404", "404", Some(Directory)) - - case class UnhandledError( - errorName: Option[String], - errorMessage: Option[String] - ) extends Page("500", "Unexpected error", Some(Directory)) - -object Routes: - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) - given [V: JsonEncoder]: JsonEncoder[Titled[V]] = - DeriveJsonEncoder.gen[Titled[V]] - given [V: JsonDecoder]: JsonDecoder[Titled[V]] = - DeriveJsonDecoder.gen[Titled[V]] - given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] - given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] - - val base = - js.`import`.meta.env.BASE_URL - .asInstanceOf[String] + "app" - - val homePage: Page = Page.Directory - - given router: Router[Page] = Router[Page]( - routes = List( - Route.static(homePage, root / endOfSegments, basePath = base), - Route.static( - Page.Dashboard, - root / "dashboard" / endOfSegments, - basePath = base - ), - Route[Page.Detail, String]( - encode = _.osobniCislo.value.toString, - decode = osc => Page.Detail(Titled(OsobniCislo(osc))), - root / "osoba" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.DetailParametru, (String, String)]( - encode = p => (p.osobniCislo.value.toString, p.parametr.value), - decode = - p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / endOfSegments, - basePath = base - ), - Route[Page.DetailKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.DetailKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.UpravDukazKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.UpravDukazKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / "edit" / endOfSegments, - basePath = base - ) - ), - serializePage = _.toJson, - deserializePage = _.fromJson[Page] - .fold(s => throw IllegalStateException(s), identity), - getPageTitle = _.title, - routeFallback = url => Page.NotFound(url), - deserializeFallback = _ => Page.Dashboard - )( - $popStateEvent = windowEvents.onPopState, - owner = unsafeWindowOwner - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala deleted file mode 100644 index b24b82b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -sealed trait Action - -case object FetchDirectory extends Action -case class FetchUserDetails(osc: OsobniCislo) extends Action -case class FetchParameters(osc: OsobniCislo) extends Action -case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action -case class FetchParameterCriteria( - osc: OsobniCislo, - paramId: String, - critId: String, - page: (UserInfo, Parameter, ParameterCriteria) => Page -) extends Action -case class FetchAvailableFiles(osc: OsobniCislo) extends Action -case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala deleted file mode 100644 index c637d35..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala +++ /dev/null @@ -1,82 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.{UserProfile, UserInfo, OsobniCislo} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo -import cz.e_bs.cmi.mdr.pdb.UserFunction - -object AppPage: - // TODO: pages by logged in user - val pages: List[Page] = List(Page.Directory, Page.Dashboard) - - import NavigationBar.{Logo, MenuItem} - - val logo = Logo( - "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", - "Workflow" - ) - - // TODO: menu items by user profile - val userMenu = - List( - MenuItem("Your Profile"), - MenuItem("Settings"), - MenuItem("Sign out") - ) - - // TODO: load user profile - val $userProfile = Var( - UserProfile( - "tom", - UserInfo( - OsobniCislo("1031"), - "tom", - "Tom", - "Cook", - None, - None, - Some("tom@example.com"), - Some("+420 222 866 180"), - Some( - UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") - ), - Nil, - None - ) - ) - ) - - val $userInfo = $userProfile.signal.map(_.userInfo) - - type ViewModel = Option[HtmlElement] - def apply( - actionBus: Observer[Action] - )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using - router: Router[Page] - ): HtmlElement = - PageLayout(actionBus)( - $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => - PageLayout.ViewModel( - NavigationBar.ViewModel( - u, - pages.map(p => - NavigationBar.Link( - () => - PageLink( - p, - actionBus - ), - p == cp - ) - ), - userMenu, - logo - ), - c - ) - ), - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala deleted file mode 100644 index bf473c6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ /dev/null @@ -1,143 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden -import fiftyforms.ui.components.tailwind.list.IconText.ViewModel -import fiftyforms.ui.components.tailwind.Icons -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Action - -object Breadcrumbs: - - private def slash = - import svg.{*, given} - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-300", - xmlns := "http://www.w3.org/2000/svg", - fill := "currentColor", - viewBox := "0 0 20 20", - ariaHidden := true, - path( - d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" - ) - ) - - def backIcon = - Icons.solid - .`arrow-narrow-left`() - .amend( - svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" - ) - - object Link: - case class ViewModel( - page: Page, - icon: Option[SvgElement], - extraClasses: String, - text: String, - textClasses: Option[String] = None - ) - - val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" - - def shortHome(p: Page) = ViewModel( - p, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - "Zpět na úvodní stránku", - None - ) - - def fullHome(p: Page) = ViewModel( - p, - Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), - "text-gray-400 hover:text-gray-500", - "Domů", - Some("sr-only") - ) - - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - PageLink - .container($m.map(_.page), actionBus) - .amend( - cls <-- $m.map(_.extraClasses), - child.maybe <-- $m.map(_.icon), - span( - cls <-- $m.map(_.textClasses.getOrElse("")), - child.text <-- $m.map(_.text) - ) - ) - - object FullBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - ol( - role := "list", - cls := "flex items-center space-x-4", - children <-- $m.map(_.path).split(_.id)((_, _, $p) => - li( - div( - cls := "flex items-center", - child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), - Link( - $p.map(p => - if (p.isRoot) then Link.fullHome(p) - else - Link.ViewModel( - p, - None, - s"ml-4 max-w-xs truncate ${Link.baseClasses}", - p.title - ) - ), - actionBus - ) - ) - ) - ) - ) - - object ShortBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - Link( - $m.map { p => - val target = p.parent.getOrElse(p) - if target.isRoot then Link.shortHome(target) - else - Link.ViewModel( - target, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - s"Zpět na ${target.title}" - ) - }, - actionBus - ) - - def apply(actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - nav( - cls := "flex", - aria.label := "Breadcrumb", - div( - cls := "flex sm:hidden", - ShortBreadcrumbs(router.$currentPage, actionBus) - ), - div( - cls := "hidden sm:block", - FullBreadcrumbs(router.$currentPage, actionBus) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala deleted file mode 100644 index 0db2047..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ /dev/null @@ -1,242 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent -import fiftyforms.ui.components.tailwind.Icons -import fiftyforms.ui.components.tailwind.Avatar -import cz.e_bs.cmi.mdr.pdb.UserInfo -import io.laminext.syntax.core.* - -object NavigationBar: - - case class Logo(img: String, name: String) - case class Link(a: () => Anchor, active: Boolean) - case class MenuItem(title: String) - - case class ViewModel( - userInfo: UserInfo, - pages: List[Link], - userMenu: List[MenuItem], - logo: Logo - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - val $userInfo = $m.map(_.userInfo) - val mobileMenuOpen = Var(false) - - val desktopOnly = cls("hidden md:block") - val mobileOnly = cls("md:hidden") - - inline def avatarImage(size: Int = 8) = - Avatar($userInfo.map(_.img)).avatarImage(size) - - def notificationButton = button( - tpe := "button", - cls := List( - "bg-indigo-600", - "focus:outline-none", - "focus:ring-2", - "focus:ring-offset-2", - "focus:ring-offset-indigo-600", - "focus:ring-white", - "hover:text-white", - "p-1", - "rounded-full", - "text-indigo-200" - ), - span(cls := "sr-only", "View notifications"), - Icons.outline.bell() - ) - - def userProfile: HtmlElement = - val menuOpen = Var(false) - - def menuItem(item: MenuItem, idx: Int): HtmlElement = - a( - href := "#", - cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", - role := "menuitem", - tabIndex := -1, - idAttr := s"user-menu-item-$idx", - item.title - ) - - div( - cls := "ml-3 relative", - div( - button( - tpe := "button", - cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - idAttr := "user-menu-button", - aria.expanded <-- menuOpen.signal, - aria.hasPopup := true, - span(cls := "sr-only", "Open user menu"), - child <-- avatarImage(), - onClick.preventDefault.mapTo( - !menuOpen.now() - ) --> menuOpen.writer - ) - ), - /* - * */ - div( - cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", - cls <-- menuOpen.signal.map { o => - if (o) "md:block" else "md:hidden" - }, - role := "menu", - aria.orientation := "vertical", - aria.labelledBy := "user-menu-button", - tabIndex := -1, - // : keyboard navigation - children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) - ) - ) - - def mobileProfile = - def menuItem(item: MenuItem): HtmlElement = - a( - href := "#", - cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", - item.title - ) - - div( - cls := "pt-4 pb-3 border-t border-indigo-700", - div( - cls := "flex items-center px-5", - div( - cls := "flex-shrink-0", - child <-- avatarImage(10) - ), - div( - cls := "ml-3", - div( - cls := "text-base font-medium text-white", - child.text <-- $userInfo.map(_.name) - ), - child.maybe <-- $userInfo.map( - _.email.map(e => - div( - cls := "text-sm font-medium text-indigo-300", - e - ) - ) - ) - ), - notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) - ), - div( - cls := "mt-3 px-2 space-y-1", - children <-- $m.map(_.userMenu.map(menuItem)) - ) - ) - - def pageLink(page: Link): Anchor = - page - .a() - .amend( - cls := "text-white px-3 py-2 rounded-md text-sm font-medium", - cls := Seq( - "bg-indigo-700" -> page.active, - "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active - ), - ariaCurrent := (if page.active then "page" else "false") - ) - - def logoImg: Image = - img( - cls := "h-8 w-8", - src <-- $m.map(_.logo.img), - alt <-- $m.map(_.logo.name) - ) - - def mobileMenuButton = button( - tpe := "button", - cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - aria.controls := "mobile-menu", - aria.expanded <-- mobileMenuOpen.signal, - span(cls := "sr-only", "Open main menu"), - Icons.outline - .menu() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") - ), - Icons.outline - .x() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") - ), - onClick.preventDefault.mapTo( - !mobileMenuOpen.now() - ) --> mobileMenuOpen.writer - ) - - def navBarLeft = - div( - cls := "flex items-center", - div(cls := "flex-shrink-0", logoImg), - div( - desktopOnly, - div( - cls := "ml-10 flex items-baseline space-x-4", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ) - ) - ) - - def navBarRight = - div( - desktopOnly, - div( - cls := "ml-4 flex items-center md:ml-6", - notificationButton, - userProfile - ) - ) - - def navBarMobile = - div( - cls := "-mr-2 flex", - mobileOnly, - mobileMenuButton - ) - - def navBar = - div( - cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex items-center justify-between h-16", - navBarLeft, - navBarRight, - navBarMobile - ) - ) - - def mobileMenu = - div( - mobileOnly, - cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }, - idAttr := "mobile-menu", - div( - cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ), - mobileProfile - ) - - nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala deleted file mode 100644 index 75339df..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object PageHeader: - def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = - header( - cls := "bg-white shadow-sm", - div( - cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", - h1( - cls := "text-lg leading-6 font-semibold text-gray-900", - Breadcrumbs(actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala deleted file mode 100644 index 5096463..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ /dev/null @@ -1,30 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import fiftyforms.ui.components.tailwind.Loading -import io.laminext.syntax.core.* - -object PageLayout: - case class ViewModel( - navigation: NavigationBar.ViewModel, - content: Option[HtmlElement] - ) - def apply(actionBus: Observer[Action])( - $m: Signal[ViewModel], - mods: Modifier[HtmlElement]* - )(using router: Router[Page]): HtmlElement = - val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) - div( - cls := "h-full flex flex-col", - NavigationBar($m.map(_.navigation)), - child.maybe <-- router.$currentPage.map(_.isRoot) - .switch(None, Some(PageHeader(actionBus))), - main( - cls := "flex-grow-1 overflow-y-auto", - mods, - child <-- $maybeContent.map(_.getOrElse(Loading)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala deleted file mode 100644 index 62d820f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.LinkSupport.* -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo - -object PageLink: - type ViewModel = Page - def apply($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = - a(mods($m, actions), child.text <-- $m.map(_.title)) - - def apply(m: ViewModel, actions: Observer[Action])(using - router: Router[Page] - ): Anchor = apply(Val(m), actions) - - def container($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = a(mods($m, actions)) - - def container(m: ViewModel, actions: Observer[Action])(using - Router[Page] - ): Anchor = container(Val(m), actions) - - private def mods($m: Signal[Page], actions: Observer[Action])(using - router: Router[Page] - ): Modifier[Anchor] = - Seq( - href <-- $m.map(router.absoluteUrlForPage).recover { case _ => - Some("invalid url") - }, - composeEvents(onClick.noKeyMod.preventDefault)( - _.sample($m) - .map(NavigateTo.apply) - ) --> actions - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala deleted file mode 100644 index 87f825a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.pages.dashboard.DashboardPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -class DashboardPageConnector(actionBus: Observer[Action])(using - router: Router[Page] -): - def apply: HtmlElement = - AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala deleted file mode 100644 index e9b0f71..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala +++ /dev/null @@ -1,76 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailKriteriaPage -import pages.detail.components.DukazyKriteria -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -object DetailKriteriaPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailKriteriaPageConnector( - state: DetailKriteriaPageConnector.AppState -)( - $page: Signal[Page.DetailKriteria] -)(using Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => - (p.osobniCislo.value, p.parametr.value, p.kriterium.value) - )((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map( - FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) - ) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, s, $s) => - DetailKriteriaPage($s)(state.actionBus.contramap { - case DukazyKriteria.Add => - NavigateTo( - Page.UpravDukazKriteria( - Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), - Page.Titled(s.parametr.id, Some(s.parametr.nazev)), - Page.Titled(s.kriterium.id, Some(s.kriterium.id)) - ) - ) - }) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): DetailKriteriaPage.ViewModel = - DetailKriteriaPage.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala deleted file mode 100644 index 4d8024f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ /dev/null @@ -1,57 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import pages.detail.DetailPage -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import fiftyforms.ui.components.tailwind.Color - -object DetailPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailPageConnector(state: DetailPageConnector.AppState)( - $page: Signal[Page.Detail] -)(using router: Router[Page]): - val $oscChangeSignal = $page.splitOne(_.osobniCislo.value)((osc, _, _) => osc) - val $pageChangeSignal = - $oscChangeSignal.flatMap(osc => - EventStream.fromSeq(Seq(FetchUserDetails(osc), FetchParameters(osc))) - ) - // TODO: filter the value based on the current osc - // OSC change will fetch new data, but still - // - we need to be sure that what we got is really what we ought to display - // - we want to display stale data accordingly (at least with loading indicator) - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - def apply: HtmlElement = - AppPage(state.actionBus)( - $data.combineWithFn($params)(_ zip _) - .map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: List[Parameter] - ): DetailPage.ViewModel = - DetailPage.ViewModel( - o.toDetailOsoby, - p.map( - _.toParametr(param => - PageLink.container(Page.DetailParametru(o, param), state.actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala deleted file mode 100644 index fdaa7f8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailParametruPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink - -object DetailParametruPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailParametruPageConnector( - state: DetailParametruPageConnector.AppState -)( - $page: Signal[Page.DetailParametru] -)(using router: Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => (p.osobniCislo.value, p.parametr.value))((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map(FetchParameter(_, _)) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - } yield (da, pb) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailParametruPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter - ): DetailParametruPage.ViewModel = - DetailParametruPage.ViewModel( - o.toDetailOsoby, - p.toParametr(p => - PageLink.container(Page.DetailParametru(o, p), state.actionBus) - ), - p.criteria.map( - _.toKriterium { c => - PageLink.container( - Page.DetailKriteria(o, p, c), - state.actionBus - ) - } - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala deleted file mode 100644 index 049aa22..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -case class DirectoryPageConnector( - $input: EventStream[List[UserInfo]], - actionBus: Observer[Action] -)(using router: Router[Page]): - val $data = $input.startWithNone - val $actionSignal = EventStream.fromValue(FetchDirectory) - - def apply: HtmlElement = - AppPage(actionBus)( - $data.split(_ => ())((_, _, s) => - pages.directory.DirectoryPage( - s.map( - _.map( - _.toUserRow(u => - PageLink.container( - Page.Detail(Page.Titled(u.personalNumber)), - actionBus - ) - ) - ) - ) - ) - ), - $actionSignal --> actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala deleted file mode 100644 index 88e7450..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ /dev/null @@ -1,68 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii -import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria -import fiftyforms.ui.components.tailwind.Color - -extension (o: UserInfo) - def toDetailOsoby: DetailOsoby.ViewModel = - DetailOsoby.ViewModel( - o.personalNumber, - o.name, - o.email, - o.phone, - o.img, - o.mainFunction.map(f => - DetailOsoby.Funkce.ViewModel(f.name, f.dept, f.ou) - ), - o.userContracts.headOption.map(c => - DetailOsoby.PracovniPomer.ViewModel(c.rel, c.startDate, c.endDate) - ) - ) - -extension (param: Parameter) - def toParametr(container: Parameter => Anchor): DetailParametru.ViewModel = - DetailParametru.ViewModel( - id = param.id, - nazev = param.name, - popis = param.description, - status = "Nesplněno", - statusColor = Color.red, - a = container(param) - ) - -extension (crit: ParameterCriteria) - def toKriterium( - container: ParameterCriteria => Anchor - ): DetailKriteria.ViewModel = - DetailKriteria.ViewModel( - nazev = crit.criteriumText, - kapitola = crit.chapterId, - bod = crit.itemId, - status = "Nesplněno", - statusColor = Color.red, - splneno = false, - dukazy = Nil, - container = container(crit) - ) - -extension (user: UserInfo) - def toUserRow( - container: UserInfo => HtmlElement = (_: UserInfo) => div() - ): UserRow.ViewModel = - UserRow.ViewModel( - osobniCislo = user.personalNumber.toString, - celeJmeno = user.name, - prijmeni = user.surname, - hlavniFunkce = user.mainFunction.map(_.name), - img = user.img, - container = () => container(user) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala deleted file mode 100644 index 7d5b2c1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.dashboard - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.Page - -object DashboardPage: - - def render: HtmlElement = - div("Dashboard page") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala deleted file mode 100644 index 9ef0698..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala +++ /dev/null @@ -1,31 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailKriteriaPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply($m: Signal[ViewModel])( - action: Observer[DukazyKriteria.Action] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - DukazyKriteria($m.map(_.kriterium.dukazy))(action) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala deleted file mode 100644 index 3b316e1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object DetailPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametry: SeznamParametru.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby($m.map(_.osoba)), - SeznamParametru($m.map(_.parametry)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala deleted file mode 100644 index 17224cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala +++ /dev/null @@ -1,24 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailParametruPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriteria: SeznamKriterii.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby.header($m.map(_.osoba)), - DetailParametru($m.map(_.parametr)), - SeznamKriterii($m.map(_.kriteria)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala deleted file mode 100644 index dc912fa..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ /dev/null @@ -1,107 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb -package app -package pages.detail - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.UpravDukazForm -import fiftyforms.services.files.File - -object UpravDukaz: - - type ThisPage = Page.UpravDukazKriteria - type PageKey = (OsobniCislo, String, String) - - trait State { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def availableFiles: EventStream[List[File]] - def actionBus: Observer[Action] - } - - def keyOfPage(page: ThisPage): PageKey = - (page.osobniCislo.value, page.parametr.value, page.kriterium.value) - - def onChangeAction(key: PageKey): Action = - FetchParameterCriteria( - key._1, - key._2, - key._3, - Page.UpravDukazKriteria(_, _, _) - ) - - class Connector(state: State)($page: Signal[ThisPage])(using Router[Page]): - val $paramChangeSignal = $page.splitOne(keyOfPage)((x, _, _) => x) - val $pageChangeSignal = $paramChangeSignal.map(onChangeAction) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.split(_ => ())((_, s, $s) => - PageComponent( - $s.map(buildModel), - state.availableFiles, - state.actionBus.contramap { - case UpravDukazForm.Cancelled => - NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) - case UpravDukazForm.AvailableFilesRequested => - FetchAvailableFiles(s._1.personalNumber) - } - ) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): PageComponent.ViewModel = - import connectors.* - PageComponent.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) - - object PageComponent: - import components.* - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply( - $m: Signal[ViewModel], - availableFilesStream: EventStream[List[File]], - events: Observer[UpravDukazForm.Event] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - UpravDukazForm(availableFilesStream)(events) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala deleted file mode 100644 index ca311b0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.Icons -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Color - -object DetailKriteria: - case class ViewModel( - nazev: String, - kapitola: String, - bod: String, - status: String, - statusColor: Color, - splneno: Boolean, - dukazy: List[DukazKriteria.ViewModel], - container: HtmlElement = div() - ) { - val id = s"${kapitola}${bod}" - } - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h3( - cls := "text-xl leading-6 font-bold text-gray-900", - child.text <-- $m.map(_.nazev) - ), - h3( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - "Kapitola ", - child.text <-- $m.map(_.kapitola), - " bod ", - child <-- $m.map(_.bod) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala deleted file mode 100644 index ae90290..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala +++ /dev/null @@ -1,100 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Avatar - -object DetailOsoby: - - object Funkce: - case class ViewModel( - nazev: String, - stredisko: String, - voj: String - ) - - def render($m: Signal[ViewModel]): HtmlElement = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.nazev), - span( - cls := "hidden md:inline", - " @ ", - child.text <-- $m.map(_.stredisko), - ", ", - child.text <-- $m.map(_.voj) - ) - ) - - object PracovniPomer: - case class ViewModel( - druh: String, - pocatek: LocalDate, - konec: Option[LocalDate] - ) - - def render($m: Signal[ViewModel]): HtmlElement = - import fiftyforms.ui.components.tailwind.CustomAttrs.datetime - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.druh), - " od ", - time( - datetime <-- $m.map(_.pocatek.toString), - child.text <-- $m.map(_.pocatek.toString) - ) - ) - - case class ViewModel( - osobniCislo: OsobniCislo, - jmeno: String, - email: Option[String], - telefon: Option[String], - img: Option[String], - hlavniFunkce: Option[Funkce.ViewModel], - pracovniPomer: Option[PracovniPomer.ViewModel] - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-4", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(16) - ), - div( - h1( - cls := "text-2xl font-bold text-gray-900", - child.text <-- $m.map(_.jmeno) - ), - child.maybe <-- $m.map(_.hlavniFunkce).split(_ => ())((_, _, d) => - Funkce.render(d) - ), - child.maybe <-- $m.map(_.pracovniPomer).split(_ => ())((_, _, d) => - PracovniPomer.render(d) - ) - ) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-5", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(8) - ), - div( - h1( - cls := "text-lg font-medium text-gray-800", - child.text <-- $m.map(_.jmeno) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala deleted file mode 100644 index e11cb62..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color - -object DetailParametru: - case class ViewModel( - id: String, - nazev: String, - popis: String, - status: String, - statusColor: Color, - a: Anchor - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h2( - cls := "text-2xl leading-6 font-medium text-gray-900", - child.text <-- $m.map(_.nazev) - ), - p( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - child.text <-- $m.map(_.popis) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - h2( - cls := "text-lg leading-6 font-medium text-gray-600", - child.text <-- $m.map(_.nazev) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala deleted file mode 100644 index a00ede8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala +++ /dev/null @@ -1,177 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.CustomAttrs - -object DukazKriteria: - case class Osoba(osobniCislo: String, jmeno: String) - case class Udalost(osoba: Osoba, datum: LocalDate) - case class Dokument( - url: String, - nazev: String, - pridal: Udalost, - odebral: Option[Udalost] - ) - case class ViewModel( - dokumenty: List[Dokument], - autorizoval: Option[Udalost], - platiDo: Option[LocalDate], - poznámka: Option[String] - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-lg", - div( - cls := "px-4 py-5 sm:px-6", - h3( - cls := "text-lg leading-6 font-medium text-gray-900", - """Applicant Information""" - ), - p( - cls := "mt-1 max-w-2xl text-sm text-gray-500", - """Personal details and application.""" - ) - ), - div( - cls := "border-t border-gray-200 px-4 py-5 sm:px-6", - dl( - cls := "grid grid-cols-1 gap-x-4 gap-y-8 sm:grid-cols-2", - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Full name""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Margot Foster""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Application for""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Backend Developer""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Email address""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """margotfoster@example.com""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Salary expectation""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """$120,000""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """About""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Fugiat ipsum ipsum deserunt culpa aute sint do nostrud anim incididunt cillum culpa consequat. Excepteur qui ipsum aliquip consequat sint. Sit id mollit nulla mollit nostrud in ea officia proident. Irure nostrud pariatur mollit ad adipisicing reprehenderit deserunt qui eu.""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """Attachments""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - ul( - role := "list", - cls := "border border-gray-200 rounded-md divide-y divide-gray-200", - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """resume_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ), - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """coverletter_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala deleted file mode 100644 index 22f134f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala +++ /dev/null @@ -1,35 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object DukazyKriteria: - sealed trait Action - case object Add extends Action - - type ViewModel = List[DukazKriteria.ViewModel] - - def apply($m: Signal[ViewModel])(actions: Observer[Action]): HtmlElement = - div( - child.maybe <-- $m.map(m => - if (m.isEmpty) then Some(prazdnyDukaz(actions)) - else None - ), - children <-- $m.map(_.zipWithIndex).split(_._2)((_, _, $s) => - DukazKriteria($s.map(_._1)) - ) - ) - - private def prazdnyDukaz(actions: Observer[Action]): HtmlElement = - button( - tpe := "button", - onClick.preventDefault.mapTo(Add) --> actions, - cls := "relative block w-full border-2 border-gray-300 border-dashed rounded-lg p-12 text-center hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - Icons.outline - .`document-add`(12) - .amend(svg.cls := "mx-auto text-gray-400"), - span( - cls := "mt-2 block text-sm font-medium text-gray-900", - "Přidat důkaz" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala deleted file mode 100644 index f628697..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.list.StackedList -import fiftyforms.ui.components.tailwind.list.ListRow -import fiftyforms.ui.components.tailwind.list.RowTag -import fiftyforms.ui.components.tailwind.list.RowNext -import java.time.LocalDate - -object SeznamKriterii: - type ViewModel = List[DetailKriteria.ViewModel] - - private val kritList = new StackedList[DetailKriteria.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - kritList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = - p(cls := "text-sm text-gray-500", s"${i.kapitola}${i.bod}"), - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.container - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala deleted file mode 100644 index 2bfe4bc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.list.{ - StackedList, - ListRow, - RowTag, - PropList, - IconText, - RowNext -} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.LinkSupport.* - -object SeznamParametru: - type ViewModel = List[DetailParametru.ViewModel] - - private val parametrList = new StackedList[DetailParametru.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - parametrList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = emptyNode, - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.a - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala deleted file mode 100644 index f048e9d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ /dev/null @@ -1,101 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.form.* -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement -import fiftyforms.services.files.components.tailwind.FilePicker -import cz.e_bs.cmi.mdr.pdb.frontend.AutorizujDukaz -import cz.e_bs.cmi.mdr.pdb.frontend.DocumentRef -import fiftyforms.services.files.File - -object UpravDukazForm: - sealed trait Event - case object Cancelled extends Event - case object AvailableFilesRequested extends Event - def apply(availableFilesStream: EventStream[List[File]])( - updates: Observer[Event] - ): HtmlElement = - val (filesStream, filesObserver) = - EventStream.withObserver[FilePicker.Event] - val files = Var[List[File]](Nil) - def submitButtons: HtmlElement = - div( - cls := "pt-5", - div( - cls := "flex justify-end", - button( - tpe := "button", - cls := "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zrušit", - onClick.mapTo(Cancelled) --> updates - ), - button( - tpe := "submit", - cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - disabled <-- files.signal.map(_.isEmpty), - "Autorizovat" - ) - ) - ) - - div( - filesStream.collect { case FilePicker.SelectionUpdated(files) => - files.to(List) - } --> files.writer, - filesStream.collect { case FilePicker.AvailableFilesRequested => - AvailableFilesRequested - } --> updates, - cls := "bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6", - Form( - Form.Body( - Form.Section( - FormHeader( - FormHeader.ViewModel( - "Doložení kritéria", - "Sestavte doklady poskytující důkaz kritéria a potvrďte odesláním formuláře. Případné limitace či jiné relevantní údaje vepište do pole Komentář." - ) - ), - FormFields( - FormRow( - "dokumenty", - "Dokumenty", - FilePicker(files.signal, availableFilesStream)(filesObserver) - .amend(idAttr := "dokumenty", cls("max-w-lg")) - ).toHtml, - FormRow( - "platnost", - "Platnost", - input( - idAttr := "platnost", - name := "platnost", - tpe := "date", - autoComplete := "date", - cls := "block max-w-lg w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" - ) - ).toHtml, - FormRow( - "komentar", - "Komentář", - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - textArea( - idAttr := "komentar", - name := "about", - rows := 3, - cls := "max-w-lg shadow-sm block w-full focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border border-gray-300 rounded-md" - ), - p( - cls := "mt-2 text-sm text-gray-500", - "Doplňte prosím potřebné informace související s doložením kritéria, včetně případných limitací." - ) - ) - ).toHtml - ) - ) - ), - submitButtons - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala deleted file mode 100644 index 41033f6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -import components._ - -object DirectoryPage: - - type ViewModel = List[UserRow.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - val (actionsStream, actionObserver) = - EventStream.withObserver[SearchForm.Action] - val $filter = actionsStream - .collect { - case SearchForm.ClearFilter => None - case SearchForm.SetFilter(t) => Some(t) - } - .startWith(None) - val byLetter = for { - d <- $m - f <- $filter - } yield for { - (letter, users) <- d - .filter { user => - f match - case None => true - case Some(t) => user.search.contains(t) - } - .groupBy(_.prijmeni.head) - .to(List) - .sortBy(_._1) - } yield (letter.toString, users.sortBy(_.prijmeni)) - - div( - cls := "h-full max-w-7xl mx-auto order-first flex flex-col flex-shrink-0", - SearchForm(actionObserver), - Directory(byLetter) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala deleted file mode 100644 index 73f915d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -object Directory: - - object Header: - type ViewModel = String - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500", - h3(child.text <-- $m) - ) - - object UserList: - type ViewModel = List[UserRow.ViewModel] - def apply($m: Signal[ViewModel]): HtmlElement = - ul( - role := "list", - cls := "relative z-0 divide-y divide-gray-200", - children <-- $m.split(_.osobniCislo)((_, _, s) => UserRow(s)) - ) - - type ViewModel = List[(String, List[UserRow.ViewModel])] - def apply($m: Signal[ViewModel]): HtmlElement = - nav( - cls := "flex-1 min-h-0 overflow-y-auto", - aria.label := "Directory", - children <-- $m.split(_._1)((_, _, s) => - div( - cls := "relative", - Header(s.map(_._1)), - UserList(s.map(_._2)) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala deleted file mode 100644 index e182910..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala +++ /dev/null @@ -1,54 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object SearchForm: - sealed trait Action - case object SubmitFilter extends Action - sealed trait FilterAction extends Action - case object ClearFilter extends FilterAction - case class SetFilter(value: String) extends FilterAction - - def apply(actions: Observer[Action]): HtmlElement = - div( - cls := "px-6 pt-4 pb-4", - form( - cls := "flex space-x-4", - onSubmit.mapTo(SubmitFilter) --> actions, - div( - cls := "flex-1 min-w-0", - label( - forId := "search", - cls := "sr-only", - "Hledat" - ), - div( - cls := "relative rounded-md shadow-sm", - div( - cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", - Icons.solid.search().amend(svg.cls := "text-gray-400") - ), - input( - tpe := "search", - name := "search", - idAttr := "search", - cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", - placeholder := "Hledat", - composeEvents(onInput.mapToValue.setAsValue.map(SetFilter(_)))( - _.throttle(500) - ) --> actions - ) - ) - ), - button( - tpe := "submit", - cls := "inline-flex justify-center px-3.5 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500", - Icons.solid.filter().amend(svg.cls := "text-gray-400"), - span( - cls := "sr-only", - "Hledat" - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala deleted file mode 100644 index 82f7723..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala +++ /dev/null @@ -1,52 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.airstream.core.Signal -import fiftyforms.ui.components.tailwind.Avatar - -object UserRow: - case class ViewModel( - osobniCislo: String, - celeJmeno: String, - prijmeni: String, - hlavniFunkce: Option[String], - img: Option[String], - container: () => HtmlElement = () => div() - ) { - val search = prijmeni.toLowerCase - } - - def apply($m: Signal[ViewModel]): HtmlElement = - inline def avatarImage = - Avatar($m.map(_.img)).avatarImage(10) - - li( - div( - cls := "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500", - div( - cls := "flex-shrink-0", - child <-- avatarImage - ), - div( - cls := "flex-1 min-w-0", - child <-- $m.map { o => - o.container() - .amend( - cls := "focus:outline-none", - span( - cls := "absolute inset-0", - aria.hidden := true - ), - p( - cls := "text-sm font-medium text-gray-900", - o.celeJmeno - ), - p( - cls := "text-sm text-gray-500 truncate", - o.hlavniFunkce - ) - ) - } - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala deleted file mode 100644 index 467c5f5..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala +++ /dev/null @@ -1,99 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router - -object ErrorPage: - case class ViewModel( - homePage: Page, - errorName: String, - title: String, - subTitle: String - ) - - def apply(m: ViewModel, actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - val ViewModel(homePage, errorName, title, subTitle) = m - div( - cls := "min-h-full pt-16 pb-12 flex flex-col bg-white", - main( - cls := "flex-grow flex flex-col justify-center max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex-shrink-0 flex justify-center", - a( - href := "/", - cls := "inline-flex", - span( - cls := "sr-only", - """Workflow""" - ), - img( - cls := "h-12 w-auto", - src := "https://tailwindui.com/img/logos/workflow-mark.svg?color=indigo&shade=600", - alt := "" - ) - ) - ), - div( - cls := "py-16", - div( - cls := "text-center", - p( - cls := "text-sm font-semibold text-indigo-600 uppercase tracking-wide", - errorName - ), - h1( - cls := "mt-2 text-4xl font-extrabold text-gray-900 tracking-tight sm:text-5xl", - title - ), - p( - cls := "mt-2 text-base text-gray-500", - subTitle - ), - div( - cls := "mt-6", - PageLink - .container(homePage, actionBus) - .amend( - cls := "text-base font-medium text-indigo-600 hover:text-indigo-500", - """Go back home""" - ) - ) - ) - ) - ), - footer( - cls := "flex-shrink-0 max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", - nav( - cls := "flex justify-center space-x-4", - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Contact Support""" - ), - span( - cls := "inline-block border-l border-gray-300", - aria.hidden := true - ), - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Status""" - ), - span( - cls := "inline-block border-l border-gray-300", - aria.hidden := true - ), - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Twitter""" - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala deleted file mode 100644 index bd9baec..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action - -object NotFoundPage: - def apply(homePage: Page, url: String, actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - ErrorPage( - ErrorPage.ViewModel( - homePage, - "404 error", - "Page not found.", - s"Sorry, but page $url doesn't exist." - ), - actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala deleted file mode 100644 index 476fe3c..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action - -object UnhandledErrorPage: - - case class ViewModel( - homePage: Page, - errorName: Option[String], - errorMessage: Option[String] - ) - - def apply(m: ViewModel, actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - ErrorPage( - ErrorPage.ViewModel( - m.homePage, - "Unexpected error occurred", - m.errorName.getOrElse( - "Uh oh!" - ), // TODO: translations, better text than uh oh - m.errorMessage.getOrElse( - "This wasn't supposed to happen! Please try again." - ) - ), - actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala deleted file mode 100644 index 25f059f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala +++ /dev/null @@ -1,6 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.services - -import com.raquo.airstream.core.Observer - -trait DataFetcher[K, A]: - def fetch(id: K, sink: Observer[A]): Unit diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala deleted file mode 100644 index a469830..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala +++ /dev/null @@ -1,120 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package state - -import com.raquo.airstream.core.EventStream -import cz.e_bs.cmi.mdr.pdb.{UserInfo, OsobniCislo} -import com.raquo.airstream.core.Observer -import scala.scalajs.js -import scala.scalajs.js.JSON -import zio.json._ -import com.raquo.airstream.eventbus.EventBus -import com.raquo.airstream.ownership.Owner -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.UserFunction -import cz.e_bs.cmi.mdr.pdb.UserContract -import fiftyforms.services.files.File - -trait AppState - extends connectors.DetailPageConnector.AppState - with connectors.DetailParametruPageConnector.AppState - with connectors.DetailKriteriaPageConnector.AppState - with pages.detail.UpravDukaz.State: - def users: EventStream[List[UserInfo]] - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - -class MockAppState(implicit owner: Owner, router: Router[Page]) - extends AppState: - - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonDecoder[UserFunction] = DeriveJsonDecoder.gen - given JsonDecoder[UserContract] = DeriveJsonDecoder.gen - given JsonDecoder[UserInfo] = DeriveJsonDecoder.gen - - given JsonDecoder[ParameterCriteria] = DeriveJsonDecoder.gen - given JsonDecoder[Parameter] = DeriveJsonDecoder.gen - - private val actions = EventBus[Action]() - private val (parametersStream, pushParameters) = - EventStream.withCallback[List[Parameter]] - private val (usersStream, pushUsers) = - EventStream.withCallback[List[UserInfo]] - private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] - private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] - - private val mockData: List[UserInfo] = - mockUsers - .asInstanceOf[js.Dictionary[js.Object]] - .values - // TODO: is there a more efficient way to parse from JS object directly? - .map { o => - val parsed = JSON.stringify(o).fromJson[UserInfo] - parsed.left.foreach(org.scalajs.dom.console.log(_)) - parsed - } - .collect { case Right(u) => - u - } - .toList - - private val mockParameters: List[Parameter] = - pdbParams - .asInstanceOf[js.Dictionary[js.Object]] - .values - .map(o => JSON.stringify(o).fromJson[Parameter]) - .collect { case Right(p) => p } - .toList - - // TODO: Extract to separate event handler - actions.events.foreach { - case FetchDirectory => pushUsers(mockData) - case FetchUserDetails(osc) => - mockData.find(_.personalNumber == osc).foreach { o => - pushDetails(o) - router.replaceState(Page.Detail(o)) - } - case FetchParameters(osc) => - pushParameters(mockParameters) - case FetchParameter(osc, paramId) => - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(Page.DetailParametru(o, p)) - case FetchParameterCriteria(osc, paramId, critId, page) => - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - c <- p.criteria.find(_.id == critId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(page(o, p, c)) - case NavigateTo(page) => router.pushState(page) - case FetchAvailableFiles(osc) => - pushFiles( - List( - File("https://tc163.cmi.cz/here", "Example file") - ) - ) - } - - override def users: EventStream[List[UserInfo]] = - usersStream.debugWithName("users") - - override def details: EventStream[UserInfo] = - detailsStream.debugWithName("details") - - override def parameters: EventStream[List[Parameter]] = - parametersStream.debugWithName("parameters") - - override def availableFiles: EventStream[List[File]] = - filesStream.debugWithName("available files") - - override def actionBus: Observer[Action] = - actions.writer.debugWithName("actions writer") diff --git a/app/src/main/scala/mdr/pdb/app/Main.scala b/app/src/main/scala/mdr/pdb/app/Main.scala new file mode 100644 index 0000000..081b4ee --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/Main.scala @@ -0,0 +1,103 @@ +package mdr.pdb.app + +import scala.scalajs.js.annotation.JSExportTopLevel +import scala.scalajs.js.annotation.JSExport +import scala.scalajs.js.annotation.JSImport +import scala.scalajs.js +import org.scalajs.dom +import com.raquo.laminar.api.L.{*, given} +import scala.scalajs.js.Date +import com.raquo.waypoint.Router +import com.raquo.waypoint.SplitRender +import mdr.pdb.app.services.DataFetcher +import scala.scalajs.js.JSON +import zio.json._ +import mdr.pdb.UserInfo +import mdr.pdb.app.state.AppState + +@js.native +@JSImport("stylesheets/main.css", JSImport.Namespace) +object Css extends js.Any + +@js.native +@JSImport("data/users.json", JSImport.Default) +object mockUsers extends js.Object + +@js.native +@JSImport("params/pdb-params.json", JSImport.Default) +object pdbParams extends js.Object + +@JSExportTopLevel("app") +object Main: + + @JSExport + def main(args: Array[String]): Unit = + import Routes.given + onLoad { + setupAirstream() + val appContainer = dom.document.querySelector("#app") + val _ = + render( + appContainer, + renderPage(state.MockAppState(using unsafeWindowOwner, router)) + ) + } + + private def onLoad(f: => Unit): Unit = + documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) + + private def setupAirstream()(using router: Router[Page]): Unit = + AirstreamError.registerUnhandledErrorCallback(err => + router.forcePage( + Page.UnhandledError( + Some(err.getClass.getName), // TODO: Fill only in dev mode + Some(err.getMessage) + ) + ) + ) + + def renderPage(state: AppState)(using + router: Router[Page] + ): HtmlElement = + val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) + .collectSignal[Page.Detail]( + connectors + .DetailPageConnector(state)(_) + .apply + ) + .collectSignal[Page.DetailParametru]( + connectors + .DetailParametruPageConnector(state)(_) + .apply + ) + .collectSignal[Page.DetailKriteria]( + connectors + .DetailKriteriaPageConnector(state)(_) + .apply + ) + .collectSignal[Page.UpravDukazKriteria]( + pages.detail.UpravDukaz.Connector(state)(_).apply + ) + .collectStatic(Page.Dashboard)( + connectors.DashboardPageConnector(state.actionBus).apply + ) + .collect[Page.NotFound](pg => + pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) + ) + .collect[Page.UnhandledError](pg => + pages.errors + .UnhandledErrorPage( + pages.errors.UnhandledErrorPage + .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), + state.actionBus + ) + ) + .collectStatic(Page.Directory)( + connectors + .DirectoryPageConnector(state.users, state.actionBus) + .apply + ) + div(cls := "h-full", child <-- pageSplitter.$view) + + // Pull in the stylesheet + val css: Css.type = Css diff --git a/app/src/main/scala/mdr/pdb/app/Routes.scala b/app/src/main/scala/mdr/pdb/app/Routes.scala new file mode 100644 index 0000000..2cc860c --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/Routes.scala @@ -0,0 +1,198 @@ +package mdr.pdb.app + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.* +import org.scalajs.dom +import zio.json.{*, given} +import mdr.pdb.OsobniCislo + +import scala.scalajs.js +import mdr.pdb.UserInfo +import mdr.pdb.Parameter +import mdr.pdb.ParameterCriteria +import mdr.pdb.app.Page.Titled + +// enum is not working with Waypoints' SplitRender collectStatic +sealed abstract class Page( + val id: String, + val title: String, + val parent: Option[Page] +) { + val path: Vector[Page] = + parent match + case None => Vector(this) + case Some(p) => p.path :+ this + + val isRoot: Boolean = parent.isEmpty +} + +object Page: + + case class Titled[V](value: V, title: Option[String] = None): + val show: String = title.getOrElse(value.toString) + + case object Directory extends Page("directory", "Adresář", None) + + case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) + + case class Detail(osobniCislo: Titled[OsobniCislo]) + extends Page("user", osobniCislo.show, Some(Directory)) + + object Detail { + def apply(o: UserInfo): Detail = Detail( + Titled(o.personalNumber, Some(o.name)) + ) + } + + case class DetailParametru( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String] + ) extends Page( + "parameter", + parametr.show, + Some(Detail(osobniCislo)) + ) + + object DetailParametru { + def apply(o: UserInfo, p: Parameter): DetailParametru = + DetailParametru( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)) + ) + } + + case class DetailKriteria( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String], + kriterium: Titled[String] + ) extends Page( + "criteria", + kriterium.show, + Some(DetailParametru(osobniCislo, parametr)) + ) + + object DetailKriteria { + def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = + DetailKriteria( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)), + Titled(k.id, Some(k.id)) + ) + } + + case class UpravDukazKriteria( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String], + kriterium: Titled[String] + ) extends Page( + "addProof", + "Důkaz", + Some(DetailKriteria(osobniCislo, parametr, kriterium)) + ) + + object UpravDukazKriteria { + def apply( + o: UserInfo, + p: Parameter, + k: ParameterCriteria + ): UpravDukazKriteria = + UpravDukazKriteria( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)), + Titled(k.id, Some(k.id)) + ) + } + + case class NotFound(url: String) extends Page("404", "404", Some(Directory)) + + case class UnhandledError( + errorName: Option[String], + errorMessage: Option[String] + ) extends Page("500", "Unexpected error", Some(Directory)) + +object Routes: + given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) + given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) + given [V: JsonEncoder]: JsonEncoder[Titled[V]] = + DeriveJsonEncoder.gen[Titled[V]] + given [V: JsonDecoder]: JsonDecoder[Titled[V]] = + DeriveJsonDecoder.gen[Titled[V]] + given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] + given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] + + val base = + js.`import`.meta.env.BASE_URL + .asInstanceOf[String] + "app" + + val homePage: Page = Page.Directory + + given router: Router[Page] = Router[Page]( + routes = List( + Route.static(homePage, root / endOfSegments, basePath = base), + Route.static( + Page.Dashboard, + root / "dashboard" / endOfSegments, + basePath = base + ), + Route[Page.Detail, String]( + encode = _.osobniCislo.value.toString, + decode = osc => Page.Detail(Titled(OsobniCislo(osc))), + root / "osoba" / segment[String] / endOfSegments, + basePath = base + ), + Route[Page.DetailParametru, (String, String)]( + encode = p => (p.osobniCislo.value.toString, p.parametr.value), + decode = + p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / endOfSegments, + basePath = base + ), + Route[Page.DetailKriteria, (String, String, String)]( + encode = p => + ( + p.osobniCislo.value.toString, + p.parametr.value, + p.kriterium.value.replaceAll("\\.", "--") + ), + decode = p => + Page.DetailKriteria( + Titled(OsobniCislo(p._1)), + Titled(p._2), + Titled(p._3.replaceAll("--", ".")) + ), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / "kriterium" / segment[String] / endOfSegments, + basePath = base + ), + Route[Page.UpravDukazKriteria, (String, String, String)]( + encode = p => + ( + p.osobniCislo.value.toString, + p.parametr.value, + p.kriterium.value.replaceAll("\\.", "--") + ), + decode = p => + Page.UpravDukazKriteria( + Titled(OsobniCislo(p._1)), + Titled(p._2), + Titled(p._3.replaceAll("--", ".")) + ), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / "kriterium" / segment[String] / "edit" / endOfSegments, + basePath = base + ) + ), + serializePage = _.toJson, + deserializePage = _.fromJson[Page] + .fold(s => throw IllegalStateException(s), identity), + getPageTitle = _.title, + routeFallback = url => Page.NotFound(url), + deserializeFallback = _ => Page.Dashboard + )( + $popStateEvent = windowEvents.onPopState, + owner = unsafeWindowOwner + ) diff --git a/app/src/main/scala/mdr/pdb/app/actions.scala b/app/src/main/scala/mdr/pdb/app/actions.scala new file mode 100644 index 0000000..d078b3a --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/actions.scala @@ -0,0 +1,21 @@ +package mdr.pdb.app + +import mdr.pdb.OsobniCislo +import mdr.pdb.UserInfo +import mdr.pdb.Parameter +import mdr.pdb.ParameterCriteria + +sealed trait Action + +case object FetchDirectory extends Action +case class FetchUserDetails(osc: OsobniCislo) extends Action +case class FetchParameters(osc: OsobniCislo) extends Action +case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action +case class FetchParameterCriteria( + osc: OsobniCislo, + paramId: String, + critId: String, + page: (UserInfo, Parameter, ParameterCriteria) => Page +) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action +case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/mdr/pdb/app/components/AppPage.scala new file mode 100644 index 0000000..130c817 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/AppPage.scala @@ -0,0 +1,82 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.Page +import mdr.pdb.{UserProfile, UserInfo, OsobniCislo} +import com.raquo.waypoint.Router +import mdr.pdb.app.Action +import mdr.pdb.app.NavigateTo +import mdr.pdb.UserFunction + +object AppPage: + // TODO: pages by logged in user + val pages: List[Page] = List(Page.Directory, Page.Dashboard) + + import NavigationBar.{Logo, MenuItem} + + val logo = Logo( + "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", + "Workflow" + ) + + // TODO: menu items by user profile + val userMenu = + List( + MenuItem("Your Profile"), + MenuItem("Settings"), + MenuItem("Sign out") + ) + + // TODO: load user profile + val $userProfile = Var( + UserProfile( + "tom", + UserInfo( + OsobniCislo("1031"), + "tom", + "Tom", + "Cook", + None, + None, + Some("tom@example.com"), + Some("+420 222 866 180"), + Some( + UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") + ), + Nil, + None + ) + ) + ) + + val $userInfo = $userProfile.signal.map(_.userInfo) + + type ViewModel = Option[HtmlElement] + def apply( + actionBus: Observer[Action] + )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using + router: Router[Page] + ): HtmlElement = + PageLayout(actionBus)( + $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => + PageLayout.ViewModel( + NavigationBar.ViewModel( + u, + pages.map(p => + NavigationBar.Link( + () => + PageLink( + p, + actionBus + ), + p == cp + ) + ), + userMenu, + logo + ), + c + ) + ), + mods + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala new file mode 100644 index 0000000..9375398 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala @@ -0,0 +1,143 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.tailwind.list.IconText.ViewModel +import fiftyforms.ui.components.tailwind.Icons +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.Page + +import com.raquo.laminar.api.L.{*, given} +import io.laminext.syntax.core.{*, given} +import mdr.pdb.app.Action + +object Breadcrumbs: + + private def slash = + import svg.{*, given} + svg( + cls := "flex-shrink-0 h-5 w-5 text-gray-300", + xmlns := "http://www.w3.org/2000/svg", + fill := "currentColor", + viewBox := "0 0 20 20", + ariaHidden := true, + path( + d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" + ) + ) + + def backIcon = + Icons.solid + .`arrow-narrow-left`() + .amend( + svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" + ) + + object Link: + case class ViewModel( + page: Page, + icon: Option[SvgElement], + extraClasses: String, + text: String, + textClasses: Option[String] = None + ) + + val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" + + def shortHome(p: Page) = ViewModel( + p, + Some(backIcon), + "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", + "Zpět na úvodní stránku", + None + ) + + def fullHome(p: Page) = ViewModel( + p, + Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), + "text-gray-400 hover:text-gray-500", + "Domů", + Some("sr-only") + ) + + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + PageLink + .container($m.map(_.page), actionBus) + .amend( + cls <-- $m.map(_.extraClasses), + child.maybe <-- $m.map(_.icon), + span( + cls <-- $m.map(_.textClasses.getOrElse("")), + child.text <-- $m.map(_.text) + ) + ) + + object FullBreadcrumbs: + type ViewModel = Page + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + ol( + role := "list", + cls := "flex items-center space-x-4", + children <-- $m.map(_.path).split(_.id)((_, _, $p) => + li( + div( + cls := "flex items-center", + child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), + Link( + $p.map(p => + if (p.isRoot) then Link.fullHome(p) + else + Link.ViewModel( + p, + None, + s"ml-4 max-w-xs truncate ${Link.baseClasses}", + p.title + ) + ), + actionBus + ) + ) + ) + ) + ) + + object ShortBreadcrumbs: + type ViewModel = Page + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + Link( + $m.map { p => + val target = p.parent.getOrElse(p) + if target.isRoot then Link.shortHome(target) + else + Link.ViewModel( + target, + Some(backIcon), + "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", + s"Zpět na ${target.title}" + ) + }, + actionBus + ) + + def apply(actionBus: Observer[Action])(using + router: Router[Page] + ): HtmlElement = + nav( + cls := "flex", + aria.label := "Breadcrumb", + div( + cls := "flex sm:hidden", + ShortBreadcrumbs(router.$currentPage, actionBus) + ), + div( + cls := "hidden sm:block", + FullBreadcrumbs(router.$currentPage, actionBus) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala new file mode 100644 index 0000000..4bb8b8d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala @@ -0,0 +1,242 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent +import fiftyforms.ui.components.tailwind.Icons +import fiftyforms.ui.components.tailwind.Avatar +import mdr.pdb.UserInfo +import io.laminext.syntax.core.* + +object NavigationBar: + + case class Logo(img: String, name: String) + case class Link(a: () => Anchor, active: Boolean) + case class MenuItem(title: String) + + case class ViewModel( + userInfo: UserInfo, + pages: List[Link], + userMenu: List[MenuItem], + logo: Logo + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + val $userInfo = $m.map(_.userInfo) + val mobileMenuOpen = Var(false) + + val desktopOnly = cls("hidden md:block") + val mobileOnly = cls("md:hidden") + + inline def avatarImage(size: Int = 8) = + Avatar($userInfo.map(_.img)).avatarImage(size) + + def notificationButton = button( + tpe := "button", + cls := List( + "bg-indigo-600", + "focus:outline-none", + "focus:ring-2", + "focus:ring-offset-2", + "focus:ring-offset-indigo-600", + "focus:ring-white", + "hover:text-white", + "p-1", + "rounded-full", + "text-indigo-200" + ), + span(cls := "sr-only", "View notifications"), + Icons.outline.bell() + ) + + def userProfile: HtmlElement = + val menuOpen = Var(false) + + def menuItem(item: MenuItem, idx: Int): HtmlElement = + a( + href := "#", + cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", + role := "menuitem", + tabIndex := -1, + idAttr := s"user-menu-item-$idx", + item.title + ) + + div( + cls := "ml-3 relative", + div( + button( + tpe := "button", + cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", + idAttr := "user-menu-button", + aria.expanded <-- menuOpen.signal, + aria.hasPopup := true, + span(cls := "sr-only", "Open user menu"), + child <-- avatarImage(), + onClick.preventDefault.mapTo( + !menuOpen.now() + ) --> menuOpen.writer + ) + ), + /* + * */ + div( + cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", + cls <-- menuOpen.signal.map { o => + if (o) "md:block" else "md:hidden" + }, + role := "menu", + aria.orientation := "vertical", + aria.labelledBy := "user-menu-button", + tabIndex := -1, + // : keyboard navigation + children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) + ) + ) + + def mobileProfile = + def menuItem(item: MenuItem): HtmlElement = + a( + href := "#", + cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", + item.title + ) + + div( + cls := "pt-4 pb-3 border-t border-indigo-700", + div( + cls := "flex items-center px-5", + div( + cls := "flex-shrink-0", + child <-- avatarImage(10) + ), + div( + cls := "ml-3", + div( + cls := "text-base font-medium text-white", + child.text <-- $userInfo.map(_.name) + ), + child.maybe <-- $userInfo.map( + _.email.map(e => + div( + cls := "text-sm font-medium text-indigo-300", + e + ) + ) + ) + ), + notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) + ), + div( + cls := "mt-3 px-2 space-y-1", + children <-- $m.map(_.userMenu.map(menuItem)) + ) + ) + + def pageLink(page: Link): Anchor = + page + .a() + .amend( + cls := "text-white px-3 py-2 rounded-md text-sm font-medium", + cls := Seq( + "bg-indigo-700" -> page.active, + "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active + ), + ariaCurrent := (if page.active then "page" else "false") + ) + + def logoImg: Image = + img( + cls := "h-8 w-8", + src <-- $m.map(_.logo.img), + alt <-- $m.map(_.logo.name) + ) + + def mobileMenuButton = button( + tpe := "button", + cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", + aria.controls := "mobile-menu", + aria.expanded <-- mobileMenuOpen.signal, + span(cls := "sr-only", "Open main menu"), + Icons.outline + .menu() + .amend( + svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") + ), + Icons.outline + .x() + .amend( + svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") + ), + onClick.preventDefault.mapTo( + !mobileMenuOpen.now() + ) --> mobileMenuOpen.writer + ) + + def navBarLeft = + div( + cls := "flex items-center", + div(cls := "flex-shrink-0", logoImg), + div( + desktopOnly, + div( + cls := "ml-10 flex items-baseline space-x-4", + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) + ) + ) + ) + + def navBarRight = + div( + desktopOnly, + div( + cls := "ml-4 flex items-center md:ml-6", + notificationButton, + userProfile + ) + ) + + def navBarMobile = + div( + cls := "-mr-2 flex", + mobileOnly, + mobileMenuButton + ) + + def navBar = + div( + cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", + div( + cls := "flex items-center justify-between h-16", + navBarLeft, + navBarRight, + navBarMobile + ) + ) + + def mobileMenu = + div( + mobileOnly, + cls <-- mobileMenuOpen.signal.map { o => + if (o) "block" else "hidden" + }, + idAttr := "mobile-menu", + div( + cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) + ), + mobileProfile + ) + + nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala new file mode 100644 index 0000000..c4cb10d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala @@ -0,0 +1,19 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.Page +import com.raquo.waypoint.Router +import mdr.pdb.app.Action + +object PageHeader: + def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = + header( + cls := "bg-white shadow-sm", + div( + cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", + h1( + cls := "text-lg leading-6 font-semibold text-gray-900", + Breadcrumbs(actionBus) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala new file mode 100644 index 0000000..aae4539 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala @@ -0,0 +1,30 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.Page +import mdr.pdb.app.Action +import fiftyforms.ui.components.tailwind.Loading +import io.laminext.syntax.core.* + +object PageLayout: + case class ViewModel( + navigation: NavigationBar.ViewModel, + content: Option[HtmlElement] + ) + def apply(actionBus: Observer[Action])( + $m: Signal[ViewModel], + mods: Modifier[HtmlElement]* + )(using router: Router[Page]): HtmlElement = + val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) + div( + cls := "h-full flex flex-col", + NavigationBar($m.map(_.navigation)), + child.maybe <-- router.$currentPage.map(_.isRoot) + .switch(None, Some(PageHeader(actionBus))), + main( + cls := "flex-grow-1 overflow-y-auto", + mods, + child <-- $maybeContent.map(_.getOrElse(Loading)) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/mdr/pdb/app/components/PageLink.scala new file mode 100644 index 0000000..e6e7052 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageLink.scala @@ -0,0 +1,40 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.LinkSupport.* +import mdr.pdb.app.Page +import mdr.pdb.app.Action +import com.raquo.waypoint.Router +import mdr.pdb.app.NavigateTo + +object PageLink: + type ViewModel = Page + def apply($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = + a(mods($m, actions), child.text <-- $m.map(_.title)) + + def apply(m: ViewModel, actions: Observer[Action])(using + router: Router[Page] + ): Anchor = apply(Val(m), actions) + + def container($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = a(mods($m, actions)) + + def container(m: ViewModel, actions: Observer[Action])(using + Router[Page] + ): Anchor = container(Val(m), actions) + + private def mods($m: Signal[Page], actions: Observer[Action])(using + router: Router[Page] + ): Modifier[Anchor] = + Seq( + href <-- $m.map(router.absoluteUrlForPage).recover { case _ => + Some("invalid url") + }, + composeEvents(onClick.noKeyMod.preventDefault)( + _.sample($m) + .map(NavigateTo.apply) + ) --> actions + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala new file mode 100644 index 0000000..7682772 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala @@ -0,0 +1,13 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.pages.dashboard.DashboardPage +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage + +class DashboardPageConnector(actionBus: Observer[Action])(using + router: Router[Page] +): + def apply: HtmlElement = + AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala new file mode 100644 index 0000000..76b9133 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala @@ -0,0 +1,76 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.Parameter +import mdr.pdb.UserInfo +import mdr.pdb.app.pages.detail.DetailParametruPage +import pages.detail.DetailKriteriaPage +import pages.detail.components.DukazyKriteria +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.ParameterCriteria + +object DetailKriteriaPageConnector { + trait AppState { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + } +} + +case class DetailKriteriaPageConnector( + state: DetailKriteriaPageConnector.AppState +)( + $page: Signal[Page.DetailKriteria] +)(using Router[Page]): + val $paramChangeSignal = + $page.splitOne(p => + (p.osobniCislo.value, p.parametr.value, p.kriterium.value) + )((x, _, _) => x) + val $pageChangeSignal = + $paramChangeSignal.map( + FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) + ) + + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + val $merged = + $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => + for { + da <- d + pa <- p + pb <- pa.find(_.id == pc._2) + ka <- pb.criteria.find(_.id == pc._3) + } yield (da, pb, ka) + ) + + def apply: HtmlElement = + AppPage(state.actionBus)( + $merged.map(_.map(buildModel)) + .split(_ => ())((_, s, $s) => + DetailKriteriaPage($s)(state.actionBus.contramap { + case DukazyKriteria.Add => + NavigateTo( + Page.UpravDukazKriteria( + Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), + Page.Titled(s.parametr.id, Some(s.parametr.nazev)), + Page.Titled(s.kriterium.id, Some(s.kriterium.id)) + ) + ) + }) + ), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: Parameter, + k: ParameterCriteria + ): DetailKriteriaPage.ViewModel = + DetailKriteriaPage.ViewModel( + o.toDetailOsoby, + p.toParametr(_ => a()), + k.toKriterium(_ => a()) + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DetailPageConnector.scala new file mode 100644 index 0000000..df3a6b7 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DetailPageConnector.scala @@ -0,0 +1,57 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.UserInfo +import pages.detail.DetailPage +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.pages.detail.components.DetailOsoby +import mdr.pdb.Parameter +import mdr.pdb.app.pages.detail.components.SeznamParametru +import fiftyforms.ui.components.tailwind.Color + +object DetailPageConnector { + trait AppState { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + } +} + +case class DetailPageConnector(state: DetailPageConnector.AppState)( + $page: Signal[Page.Detail] +)(using router: Router[Page]): + val $oscChangeSignal = $page.splitOne(_.osobniCislo.value)((osc, _, _) => osc) + val $pageChangeSignal = + $oscChangeSignal.flatMap(osc => + EventStream.fromSeq(Seq(FetchUserDetails(osc), FetchParameters(osc))) + ) + // TODO: filter the value based on the current osc + // OSC change will fetch new data, but still + // - we need to be sure that what we got is really what we ought to display + // - we want to display stale data accordingly (at least with loading indicator) + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + def apply: HtmlElement = + AppPage(state.actionBus)( + $data.combineWithFn($params)(_ zip _) + .map(_.map(buildModel)) + .split(_ => ())((_, _, s) => DetailPage(s)), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: List[Parameter] + ): DetailPage.ViewModel = + DetailPage.ViewModel( + o.toDetailOsoby, + p.map( + _.toParametr(param => + PageLink.container(Page.DetailParametru(o, param), state.actionBus) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DetailParametruPageConnector.scala new file mode 100644 index 0000000..fa7193d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DetailParametruPageConnector.scala @@ -0,0 +1,67 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.Parameter +import mdr.pdb.UserInfo +import mdr.pdb.app.pages.detail.DetailParametruPage +import pages.detail.DetailParametruPage +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.components.PageLink + +object DetailParametruPageConnector { + trait AppState { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + } +} + +case class DetailParametruPageConnector( + state: DetailParametruPageConnector.AppState +)( + $page: Signal[Page.DetailParametru] +)(using router: Router[Page]): + val $paramChangeSignal = + $page.splitOne(p => (p.osobniCislo.value, p.parametr.value))((x, _, _) => x) + val $pageChangeSignal = + $paramChangeSignal.map(FetchParameter(_, _)) + + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + val $merged = + $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => + for { + da <- d + pa <- p + pb <- pa.find(_.id == pc._2) + } yield (da, pb) + ) + + def apply: HtmlElement = + AppPage(state.actionBus)( + $merged.map(_.map(buildModel)) + .split(_ => ())((_, _, s) => DetailParametruPage(s)), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: Parameter + ): DetailParametruPage.ViewModel = + DetailParametruPage.ViewModel( + o.toDetailOsoby, + p.toParametr(p => + PageLink.container(Page.DetailParametru(o, p), state.actionBus) + ), + p.criteria.map( + _.toKriterium { c => + PageLink.container( + Page.DetailKriteria(o, p, c), + state.actionBus + ) + } + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DirectoryPageConnector.scala new file mode 100644 index 0000000..ab2c045 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DirectoryPageConnector.scala @@ -0,0 +1,34 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.UserInfo +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.components.AppPage + +case class DirectoryPageConnector( + $input: EventStream[List[UserInfo]], + actionBus: Observer[Action] +)(using router: Router[Page]): + val $data = $input.startWithNone + val $actionSignal = EventStream.fromValue(FetchDirectory) + + def apply: HtmlElement = + AppPage(actionBus)( + $data.split(_ => ())((_, _, s) => + pages.directory.DirectoryPage( + s.map( + _.map( + _.toUserRow(u => + PageLink.container( + Page.Detail(Page.Titled(u.personalNumber)), + actionBus + ) + ) + ) + ) + ) + ), + $actionSignal --> actionBus + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/mdr/pdb/app/connectors/conversions.scala new file mode 100644 index 0000000..f581796 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/conversions.scala @@ -0,0 +1,68 @@ +package mdr.pdb.app.connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.UserInfo +import mdr.pdb.app.pages.detail.components.DetailOsoby +import mdr.pdb.Parameter +import mdr.pdb.app.pages.detail.components.SeznamParametru +import mdr.pdb.ParameterCriteria +import mdr.pdb.app.pages.detail.components.SeznamKriterii +import mdr.pdb.app.pages.directory.components.UserRow +import mdr.pdb.app.pages.detail.components.DetailParametru +import mdr.pdb.app.pages.detail.components.DetailKriteria +import fiftyforms.ui.components.tailwind.Color + +extension (o: UserInfo) + def toDetailOsoby: DetailOsoby.ViewModel = + DetailOsoby.ViewModel( + o.personalNumber, + o.name, + o.email, + o.phone, + o.img, + o.mainFunction.map(f => + DetailOsoby.Funkce.ViewModel(f.name, f.dept, f.ou) + ), + o.userContracts.headOption.map(c => + DetailOsoby.PracovniPomer.ViewModel(c.rel, c.startDate, c.endDate) + ) + ) + +extension (param: Parameter) + def toParametr(container: Parameter => Anchor): DetailParametru.ViewModel = + DetailParametru.ViewModel( + id = param.id, + nazev = param.name, + popis = param.description, + status = "Nesplněno", + statusColor = Color.red, + a = container(param) + ) + +extension (crit: ParameterCriteria) + def toKriterium( + container: ParameterCriteria => Anchor + ): DetailKriteria.ViewModel = + DetailKriteria.ViewModel( + nazev = crit.criteriumText, + kapitola = crit.chapterId, + bod = crit.itemId, + status = "Nesplněno", + statusColor = Color.red, + splneno = false, + dukazy = Nil, + container = container(crit) + ) + +extension (user: UserInfo) + def toUserRow( + container: UserInfo => HtmlElement = (_: UserInfo) => div() + ): UserRow.ViewModel = + UserRow.ViewModel( + osobniCislo = user.personalNumber.toString, + celeJmeno = user.name, + prijmeni = user.surname, + hlavniFunkce = user.mainFunction.map(_.name), + img = user.img, + container = () => container(user) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/dashboard/DashboardPage.scala b/app/src/main/scala/mdr/pdb/app/pages/dashboard/DashboardPage.scala new file mode 100644 index 0000000..0eb8f7a --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/dashboard/DashboardPage.scala @@ -0,0 +1,11 @@ +package mdr.pdb.app.pages.dashboard + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.Page + +object DashboardPage: + + def render: HtmlElement = + div("Dashboard page") diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala new file mode 100644 index 0000000..ff3a25f --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala @@ -0,0 +1,31 @@ +package mdr.pdb.app.pages.detail + +import com.raquo.laminar.api.L.{*, given} + +import components._ + +object DetailKriteriaPage: + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametr: DetailParametru.ViewModel, + kriterium: DetailKriteria.ViewModel + ) + + def apply($m: Signal[ViewModel])( + action: Observer[DukazyKriteria.Action] + ): HtmlElement = + div( + cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + div( + DetailOsoby.header($m.map(_.osoba)), + DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") + ), + div( + DetailKriteria($m.map(_.kriterium)), + DukazyKriteria($m.map(_.kriterium.dukazy))(action) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/DetailPage.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailPage.scala new file mode 100644 index 0000000..b591cc9 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailPage.scala @@ -0,0 +1,25 @@ +package mdr.pdb.app.pages.detail + +import com.raquo.laminar.api.L.{*, given} + +import components._ +import mdr.pdb.app.Page +import com.raquo.waypoint.Router +import mdr.pdb.app.Action + +object DetailPage: + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametry: SeznamParametru.ViewModel + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + DetailOsoby($m.map(_.osoba)), + SeznamParametru($m.map(_.parametry)) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/DetailParametruPage.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailParametruPage.scala new file mode 100644 index 0000000..3322279 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailParametruPage.scala @@ -0,0 +1,24 @@ +package mdr.pdb.app.pages.detail + +import com.raquo.laminar.api.L.{*, given} + +import components._ + +object DetailParametruPage: + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametr: DetailParametru.ViewModel, + kriteria: SeznamKriterii.ViewModel + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + DetailOsoby.header($m.map(_.osoba)), + DetailParametru($m.map(_.parametr)), + SeznamKriterii($m.map(_.kriteria)) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala new file mode 100644 index 0000000..b58618a --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala @@ -0,0 +1,107 @@ +package mdr.pdb +package app +package pages.detail + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.pages.detail.components.UpravDukazForm +import fiftyforms.services.files.File + +object UpravDukaz: + + type ThisPage = Page.UpravDukazKriteria + type PageKey = (OsobniCislo, String, String) + + trait State { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def availableFiles: EventStream[List[File]] + def actionBus: Observer[Action] + } + + def keyOfPage(page: ThisPage): PageKey = + (page.osobniCislo.value, page.parametr.value, page.kriterium.value) + + def onChangeAction(key: PageKey): Action = + FetchParameterCriteria( + key._1, + key._2, + key._3, + Page.UpravDukazKriteria(_, _, _) + ) + + class Connector(state: State)($page: Signal[ThisPage])(using Router[Page]): + val $paramChangeSignal = $page.splitOne(keyOfPage)((x, _, _) => x) + val $pageChangeSignal = $paramChangeSignal.map(onChangeAction) + + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + val $merged = + $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => + for { + da <- d + pa <- p + pb <- pa.find(_.id == pc._2) + ka <- pb.criteria.find(_.id == pc._3) + } yield (da, pb, ka) + ) + + def apply: HtmlElement = + AppPage(state.actionBus)( + $merged.split(_ => ())((_, s, $s) => + PageComponent( + $s.map(buildModel), + state.availableFiles, + state.actionBus.contramap { + case UpravDukazForm.Cancelled => + NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) + case UpravDukazForm.AvailableFilesRequested => + FetchAvailableFiles(s._1.personalNumber) + } + ) + ), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: Parameter, + k: ParameterCriteria + ): PageComponent.ViewModel = + import connectors.* + PageComponent.ViewModel( + o.toDetailOsoby, + p.toParametr(_ => a()), + k.toKriterium(_ => a()) + ) + + object PageComponent: + import components.* + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametr: DetailParametru.ViewModel, + kriterium: DetailKriteria.ViewModel + ) + + def apply( + $m: Signal[ViewModel], + availableFilesStream: EventStream[List[File]], + events: Observer[UpravDukazForm.Event] + ): HtmlElement = + div( + cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + div( + DetailOsoby.header($m.map(_.osoba)), + DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") + ), + div( + DetailKriteria($m.map(_.kriterium)), + UpravDukazForm(availableFilesStream)(events) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailKriteria.scala new file mode 100644 index 0000000..a3ab634 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailKriteria.scala @@ -0,0 +1,37 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.CustomAttrs +import fiftyforms.ui.components.tailwind.Icons +import java.time.LocalDate +import fiftyforms.ui.components.tailwind.Color + +object DetailKriteria: + case class ViewModel( + nazev: String, + kapitola: String, + bod: String, + status: String, + statusColor: Color, + splneno: Boolean, + dukazy: List[DukazKriteria.ViewModel], + container: HtmlElement = div() + ) { + val id = s"${kapitola}${bod}" + } + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "pb-5 border-b border-gray-200", + h3( + cls := "text-xl leading-6 font-bold text-gray-900", + child.text <-- $m.map(_.nazev) + ), + h3( + cls := "mt-2 max-w-4xl text-sm text-gray-500", + "Kapitola ", + child.text <-- $m.map(_.kapitola), + " bod ", + child <-- $m.map(_.bod) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailOsoby.scala new file mode 100644 index 0000000..0fe2ba5 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailOsoby.scala @@ -0,0 +1,100 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.OsobniCislo + +import java.time.LocalDate +import fiftyforms.ui.components.tailwind.Avatar + +object DetailOsoby: + + object Funkce: + case class ViewModel( + nazev: String, + stredisko: String, + voj: String + ) + + def render($m: Signal[ViewModel]): HtmlElement = + p( + cls := "text-sm font-medium text-gray-500", + child.text <-- $m.map(_.nazev), + span( + cls := "hidden md:inline", + " @ ", + child.text <-- $m.map(_.stredisko), + ", ", + child.text <-- $m.map(_.voj) + ) + ) + + object PracovniPomer: + case class ViewModel( + druh: String, + pocatek: LocalDate, + konec: Option[LocalDate] + ) + + def render($m: Signal[ViewModel]): HtmlElement = + import fiftyforms.ui.components.tailwind.CustomAttrs.datetime + p( + cls := "text-sm font-medium text-gray-500", + child.text <-- $m.map(_.druh), + " od ", + time( + datetime <-- $m.map(_.pocatek.toString), + child.text <-- $m.map(_.pocatek.toString) + ) + ) + + case class ViewModel( + osobniCislo: OsobniCislo, + jmeno: String, + email: Option[String], + telefon: Option[String], + img: Option[String], + hlavniFunkce: Option[Funkce.ViewModel], + pracovniPomer: Option[PracovniPomer.ViewModel] + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "md:flex md:items-center md:justify-between md:space-x-4", + div( + cls := "flex items-start space-x-4", + div( + cls := "flex-shrink-0", + Avatar($m.map(_.img)).avatar(16) + ), + div( + h1( + cls := "text-2xl font-bold text-gray-900", + child.text <-- $m.map(_.jmeno) + ), + child.maybe <-- $m.map(_.hlavniFunkce).split(_ => ())((_, _, d) => + Funkce.render(d) + ), + child.maybe <-- $m.map(_.pracovniPomer).split(_ => ())((_, _, d) => + PracovniPomer.render(d) + ) + ) + ) + ) + + def header($m: Signal[ViewModel]): HtmlElement = + div( + cls := "md:flex md:items-center md:justify-between md:space-x-5", + div( + cls := "flex items-start space-x-4", + div( + cls := "flex-shrink-0", + Avatar($m.map(_.img)).avatar(8) + ), + div( + h1( + cls := "text-lg font-medium text-gray-800", + child.text <-- $m.map(_.jmeno) + ) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailParametru.scala new file mode 100644 index 0000000..6310dde --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailParametru.scala @@ -0,0 +1,34 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.Color + +object DetailParametru: + case class ViewModel( + id: String, + nazev: String, + popis: String, + status: String, + statusColor: Color, + a: Anchor + ) + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "pb-5 border-b border-gray-200", + h2( + cls := "text-2xl leading-6 font-medium text-gray-900", + child.text <-- $m.map(_.nazev) + ), + p( + cls := "mt-2 max-w-4xl text-sm text-gray-500", + child.text <-- $m.map(_.popis) + ) + ) + + def header($m: Signal[ViewModel]): HtmlElement = + div( + h2( + cls := "text-lg leading-6 font-medium text-gray-600", + child.text <-- $m.map(_.nazev) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazKriteria.scala new file mode 100644 index 0000000..c292919 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazKriteria.scala @@ -0,0 +1,177 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import java.time.LocalDate +import fiftyforms.ui.components.tailwind.CustomAttrs + +object DukazKriteria: + case class Osoba(osobniCislo: String, jmeno: String) + case class Udalost(osoba: Osoba, datum: LocalDate) + case class Dokument( + url: String, + nazev: String, + pridal: Udalost, + odebral: Option[Udalost] + ) + case class ViewModel( + dokumenty: List[Dokument], + autorizoval: Option[Udalost], + platiDo: Option[LocalDate], + poznámka: Option[String] + ) + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "bg-white shadow overflow-hidden sm:rounded-lg", + div( + cls := "px-4 py-5 sm:px-6", + h3( + cls := "text-lg leading-6 font-medium text-gray-900", + """Applicant Information""" + ), + p( + cls := "mt-1 max-w-2xl text-sm text-gray-500", + """Personal details and application.""" + ) + ), + div( + cls := "border-t border-gray-200 px-4 py-5 sm:px-6", + dl( + cls := "grid grid-cols-1 gap-x-4 gap-y-8 sm:grid-cols-2", + div( + cls := "sm:col-span-1", + dt( + cls := "text-sm font-medium text-gray-500", + """Full name""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """Margot Foster""" + ) + ), + div( + cls := "sm:col-span-1", + dt( + cls := "text-sm font-medium text-gray-500", + """Application for""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """Backend Developer""" + ) + ), + div( + cls := "sm:col-span-1", + dt( + cls := "text-sm font-medium text-gray-500", + """Email address""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """margotfoster@example.com""" + ) + ), + div( + cls := "sm:col-span-1", + dt( + cls := "text-sm font-medium text-gray-500", + """Salary expectation""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """$120,000""" + ) + ), + div( + cls := "sm:col-span-2", + dt( + cls := "text-sm font-medium text-gray-500", + """About""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """Fugiat ipsum ipsum deserunt culpa aute sint do nostrud anim incididunt cillum culpa consequat. Excepteur qui ipsum aliquip consequat sint. Sit id mollit nulla mollit nostrud in ea officia proident. Irure nostrud pariatur mollit ad adipisicing reprehenderit deserunt qui eu.""" + ) + ), + div( + cls := "sm:col-span-2", + dt( + cls := "text-sm font-medium text-gray-500", + """Attachments""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + ul( + role := "list", + cls := "border border-gray-200 rounded-md divide-y divide-gray-200", + li( + cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", + div( + cls := "w-0 flex-1 flex items-center", { + import svg.* + import CustomAttrs.svg.ariaHidden + svg( + cls := "flex-shrink-0 h-5 w-5 text-gray-400", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + ariaHidden := true, + path( + fillRule := "evenodd", + d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", + clipRule := "evenodd" + ) + ) + }, + span( + cls := "ml-2 flex-1 w-0 truncate", + """resume_back_end_developer.pdf""" + ) + ), + div( + cls := "ml-4 flex-shrink-0", + a( + href := "#", + cls := "font-medium text-indigo-600 hover:text-indigo-500", + """Download""" + ) + ) + ), + li( + cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", + div( + cls := "w-0 flex-1 flex items-center", { + import svg.* + import CustomAttrs.svg.ariaHidden + svg( + cls := "flex-shrink-0 h-5 w-5 text-gray-400", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + ariaHidden := true, + path( + fillRule := "evenodd", + d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", + clipRule := "evenodd" + ) + ) + }, + span( + cls := "ml-2 flex-1 w-0 truncate", + """coverletter_back_end_developer.pdf""" + ) + ), + div( + cls := "ml-4 flex-shrink-0", + a( + href := "#", + cls := "font-medium text-indigo-600 hover:text-indigo-500", + """Download""" + ) + ) + ) + ) + ) + ) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala new file mode 100644 index 0000000..d604860 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala @@ -0,0 +1,35 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.Icons + +object DukazyKriteria: + sealed trait Action + case object Add extends Action + + type ViewModel = List[DukazKriteria.ViewModel] + + def apply($m: Signal[ViewModel])(actions: Observer[Action]): HtmlElement = + div( + child.maybe <-- $m.map(m => + if (m.isEmpty) then Some(prazdnyDukaz(actions)) + else None + ), + children <-- $m.map(_.zipWithIndex).split(_._2)((_, _, $s) => + DukazKriteria($s.map(_._1)) + ) + ) + + private def prazdnyDukaz(actions: Observer[Action]): HtmlElement = + button( + tpe := "button", + onClick.preventDefault.mapTo(Add) --> actions, + cls := "relative block w-full border-2 border-gray-300 border-dashed rounded-lg p-12 text-center hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + Icons.outline + .`document-add`(12) + .amend(svg.cls := "mx-auto text-gray-400"), + span( + cls := "mt-2 block text-sm font-medium text-gray-900", + "Přidat důkaz" + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala new file mode 100644 index 0000000..7e77a7b --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala @@ -0,0 +1,34 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.Color +import fiftyforms.ui.components.tailwind.list.StackedList +import fiftyforms.ui.components.tailwind.list.ListRow +import fiftyforms.ui.components.tailwind.list.RowTag +import fiftyforms.ui.components.tailwind.list.RowNext +import java.time.LocalDate + +object SeznamKriterii: + type ViewModel = List[DetailKriteria.ViewModel] + + private val kritList = new StackedList[DetailKriteria.ViewModel] + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "bg-white shadow overflow-hidden sm:rounded-md", + kritList($m, _.id) { $i => + $i.map { i => + ListRow.ViewModel( + title = i.nazev, + topRight = RowTag.render( + $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) + ), + bottomLeft = + p(cls := "text-sm text-gray-500", s"${i.kapitola}${i.bod}"), + bottomRight = emptyNode, + farRight = RowNext.render, + containerElement = i.container + ) + } + } + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamParametru.scala new file mode 100644 index 0000000..0760b32 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamParametru.scala @@ -0,0 +1,37 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.list.{ + StackedList, + ListRow, + RowTag, + PropList, + IconText, + RowNext +} +import fiftyforms.ui.components.tailwind.Color +import fiftyforms.ui.components.tailwind.LinkSupport.* + +object SeznamParametru: + type ViewModel = List[DetailParametru.ViewModel] + + private val parametrList = new StackedList[DetailParametru.ViewModel] + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "bg-white shadow overflow-hidden sm:rounded-md", + parametrList($m, _.id) { $i => + $i.map { i => + ListRow.ViewModel( + title = i.nazev, + topRight = RowTag.render( + $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) + ), + bottomLeft = emptyNode, + bottomRight = emptyNode, + farRight = RowNext.render, + containerElement = i.a + ) + } + } + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala deleted file mode 100644 index a3b00af..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala +++ /dev/null @@ -1,103 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import scala.scalajs.js.annotation.JSExportTopLevel -import scala.scalajs.js.annotation.JSExport -import scala.scalajs.js.annotation.JSImport -import scala.scalajs.js -import org.scalajs.dom -import com.raquo.laminar.api.L.{*, given} -import scala.scalajs.js.Date -import com.raquo.waypoint.Router -import com.raquo.waypoint.SplitRender -import cz.e_bs.cmi.mdr.pdb.app.services.DataFetcher -import scala.scalajs.js.JSON -import zio.json._ -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.state.AppState - -@js.native -@JSImport("stylesheets/main.css", JSImport.Namespace) -object Css extends js.Any - -@js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native -@JSImport("params/pdb-params.json", JSImport.Default) -object pdbParams extends js.Object - -@JSExportTopLevel("app") -object Main: - - @JSExport - def main(args: Array[String]): Unit = - import Routes.given - onLoad { - setupAirstream() - val appContainer = dom.document.querySelector("#app") - val _ = - render( - appContainer, - renderPage(state.MockAppState(using unsafeWindowOwner, router)) - ) - } - - private def onLoad(f: => Unit): Unit = - documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) - - private def setupAirstream()(using router: Router[Page]): Unit = - AirstreamError.registerUnhandledErrorCallback(err => - router.forcePage( - Page.UnhandledError( - Some(err.getClass.getName), // TODO: Fill only in dev mode - Some(err.getMessage) - ) - ) - ) - - def renderPage(state: AppState)(using - router: Router[Page] - ): HtmlElement = - val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) - .collectSignal[Page.Detail]( - connectors - .DetailPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailParametru]( - connectors - .DetailParametruPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailKriteria]( - connectors - .DetailKriteriaPageConnector(state)(_) - .apply - ) - .collectSignal[Page.UpravDukazKriteria]( - pages.detail.UpravDukaz.Connector(state)(_).apply - ) - .collectStatic(Page.Dashboard)( - connectors.DashboardPageConnector(state.actionBus).apply - ) - .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) - ) - .collect[Page.UnhandledError](pg => - pages.errors - .UnhandledErrorPage( - pages.errors.UnhandledErrorPage - .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), - state.actionBus - ) - ) - .collectStatic(Page.Directory)( - connectors - .DirectoryPageConnector(state.users, state.actionBus) - .apply - ) - div(cls := "h-full", child <-- pageSplitter.$view) - - // Pull in the stylesheet - val css: Css.type = Css diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala deleted file mode 100644 index 3bbc8c0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ /dev/null @@ -1,198 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.* -import org.scalajs.dom -import zio.json.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import scala.scalajs.js -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.Page.Titled - -// enum is not working with Waypoints' SplitRender collectStatic -sealed abstract class Page( - val id: String, - val title: String, - val parent: Option[Page] -) { - val path: Vector[Page] = - parent match - case None => Vector(this) - case Some(p) => p.path :+ this - - val isRoot: Boolean = parent.isEmpty -} - -object Page: - - case class Titled[V](value: V, title: Option[String] = None): - val show: String = title.getOrElse(value.toString) - - case object Directory extends Page("directory", "Adresář", None) - - case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) - - case class Detail(osobniCislo: Titled[OsobniCislo]) - extends Page("user", osobniCislo.show, Some(Directory)) - - object Detail { - def apply(o: UserInfo): Detail = Detail( - Titled(o.personalNumber, Some(o.name)) - ) - } - - case class DetailParametru( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String] - ) extends Page( - "parameter", - parametr.show, - Some(Detail(osobniCislo)) - ) - - object DetailParametru { - def apply(o: UserInfo, p: Parameter): DetailParametru = - DetailParametru( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)) - ) - } - - case class DetailKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "criteria", - kriterium.show, - Some(DetailParametru(osobniCislo, parametr)) - ) - - object DetailKriteria { - def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = - DetailKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class UpravDukazKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "addProof", - "Důkaz", - Some(DetailKriteria(osobniCislo, parametr, kriterium)) - ) - - object UpravDukazKriteria { - def apply( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): UpravDukazKriteria = - UpravDukazKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class NotFound(url: String) extends Page("404", "404", Some(Directory)) - - case class UnhandledError( - errorName: Option[String], - errorMessage: Option[String] - ) extends Page("500", "Unexpected error", Some(Directory)) - -object Routes: - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) - given [V: JsonEncoder]: JsonEncoder[Titled[V]] = - DeriveJsonEncoder.gen[Titled[V]] - given [V: JsonDecoder]: JsonDecoder[Titled[V]] = - DeriveJsonDecoder.gen[Titled[V]] - given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] - given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] - - val base = - js.`import`.meta.env.BASE_URL - .asInstanceOf[String] + "app" - - val homePage: Page = Page.Directory - - given router: Router[Page] = Router[Page]( - routes = List( - Route.static(homePage, root / endOfSegments, basePath = base), - Route.static( - Page.Dashboard, - root / "dashboard" / endOfSegments, - basePath = base - ), - Route[Page.Detail, String]( - encode = _.osobniCislo.value.toString, - decode = osc => Page.Detail(Titled(OsobniCislo(osc))), - root / "osoba" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.DetailParametru, (String, String)]( - encode = p => (p.osobniCislo.value.toString, p.parametr.value), - decode = - p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / endOfSegments, - basePath = base - ), - Route[Page.DetailKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.DetailKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.UpravDukazKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.UpravDukazKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / "edit" / endOfSegments, - basePath = base - ) - ), - serializePage = _.toJson, - deserializePage = _.fromJson[Page] - .fold(s => throw IllegalStateException(s), identity), - getPageTitle = _.title, - routeFallback = url => Page.NotFound(url), - deserializeFallback = _ => Page.Dashboard - )( - $popStateEvent = windowEvents.onPopState, - owner = unsafeWindowOwner - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala deleted file mode 100644 index b24b82b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -sealed trait Action - -case object FetchDirectory extends Action -case class FetchUserDetails(osc: OsobniCislo) extends Action -case class FetchParameters(osc: OsobniCislo) extends Action -case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action -case class FetchParameterCriteria( - osc: OsobniCislo, - paramId: String, - critId: String, - page: (UserInfo, Parameter, ParameterCriteria) => Page -) extends Action -case class FetchAvailableFiles(osc: OsobniCislo) extends Action -case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala deleted file mode 100644 index c637d35..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala +++ /dev/null @@ -1,82 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.{UserProfile, UserInfo, OsobniCislo} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo -import cz.e_bs.cmi.mdr.pdb.UserFunction - -object AppPage: - // TODO: pages by logged in user - val pages: List[Page] = List(Page.Directory, Page.Dashboard) - - import NavigationBar.{Logo, MenuItem} - - val logo = Logo( - "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", - "Workflow" - ) - - // TODO: menu items by user profile - val userMenu = - List( - MenuItem("Your Profile"), - MenuItem("Settings"), - MenuItem("Sign out") - ) - - // TODO: load user profile - val $userProfile = Var( - UserProfile( - "tom", - UserInfo( - OsobniCislo("1031"), - "tom", - "Tom", - "Cook", - None, - None, - Some("tom@example.com"), - Some("+420 222 866 180"), - Some( - UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") - ), - Nil, - None - ) - ) - ) - - val $userInfo = $userProfile.signal.map(_.userInfo) - - type ViewModel = Option[HtmlElement] - def apply( - actionBus: Observer[Action] - )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using - router: Router[Page] - ): HtmlElement = - PageLayout(actionBus)( - $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => - PageLayout.ViewModel( - NavigationBar.ViewModel( - u, - pages.map(p => - NavigationBar.Link( - () => - PageLink( - p, - actionBus - ), - p == cp - ) - ), - userMenu, - logo - ), - c - ) - ), - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala deleted file mode 100644 index bf473c6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ /dev/null @@ -1,143 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden -import fiftyforms.ui.components.tailwind.list.IconText.ViewModel -import fiftyforms.ui.components.tailwind.Icons -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Action - -object Breadcrumbs: - - private def slash = - import svg.{*, given} - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-300", - xmlns := "http://www.w3.org/2000/svg", - fill := "currentColor", - viewBox := "0 0 20 20", - ariaHidden := true, - path( - d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" - ) - ) - - def backIcon = - Icons.solid - .`arrow-narrow-left`() - .amend( - svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" - ) - - object Link: - case class ViewModel( - page: Page, - icon: Option[SvgElement], - extraClasses: String, - text: String, - textClasses: Option[String] = None - ) - - val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" - - def shortHome(p: Page) = ViewModel( - p, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - "Zpět na úvodní stránku", - None - ) - - def fullHome(p: Page) = ViewModel( - p, - Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), - "text-gray-400 hover:text-gray-500", - "Domů", - Some("sr-only") - ) - - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - PageLink - .container($m.map(_.page), actionBus) - .amend( - cls <-- $m.map(_.extraClasses), - child.maybe <-- $m.map(_.icon), - span( - cls <-- $m.map(_.textClasses.getOrElse("")), - child.text <-- $m.map(_.text) - ) - ) - - object FullBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - ol( - role := "list", - cls := "flex items-center space-x-4", - children <-- $m.map(_.path).split(_.id)((_, _, $p) => - li( - div( - cls := "flex items-center", - child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), - Link( - $p.map(p => - if (p.isRoot) then Link.fullHome(p) - else - Link.ViewModel( - p, - None, - s"ml-4 max-w-xs truncate ${Link.baseClasses}", - p.title - ) - ), - actionBus - ) - ) - ) - ) - ) - - object ShortBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - Link( - $m.map { p => - val target = p.parent.getOrElse(p) - if target.isRoot then Link.shortHome(target) - else - Link.ViewModel( - target, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - s"Zpět na ${target.title}" - ) - }, - actionBus - ) - - def apply(actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - nav( - cls := "flex", - aria.label := "Breadcrumb", - div( - cls := "flex sm:hidden", - ShortBreadcrumbs(router.$currentPage, actionBus) - ), - div( - cls := "hidden sm:block", - FullBreadcrumbs(router.$currentPage, actionBus) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala deleted file mode 100644 index 0db2047..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ /dev/null @@ -1,242 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent -import fiftyforms.ui.components.tailwind.Icons -import fiftyforms.ui.components.tailwind.Avatar -import cz.e_bs.cmi.mdr.pdb.UserInfo -import io.laminext.syntax.core.* - -object NavigationBar: - - case class Logo(img: String, name: String) - case class Link(a: () => Anchor, active: Boolean) - case class MenuItem(title: String) - - case class ViewModel( - userInfo: UserInfo, - pages: List[Link], - userMenu: List[MenuItem], - logo: Logo - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - val $userInfo = $m.map(_.userInfo) - val mobileMenuOpen = Var(false) - - val desktopOnly = cls("hidden md:block") - val mobileOnly = cls("md:hidden") - - inline def avatarImage(size: Int = 8) = - Avatar($userInfo.map(_.img)).avatarImage(size) - - def notificationButton = button( - tpe := "button", - cls := List( - "bg-indigo-600", - "focus:outline-none", - "focus:ring-2", - "focus:ring-offset-2", - "focus:ring-offset-indigo-600", - "focus:ring-white", - "hover:text-white", - "p-1", - "rounded-full", - "text-indigo-200" - ), - span(cls := "sr-only", "View notifications"), - Icons.outline.bell() - ) - - def userProfile: HtmlElement = - val menuOpen = Var(false) - - def menuItem(item: MenuItem, idx: Int): HtmlElement = - a( - href := "#", - cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", - role := "menuitem", - tabIndex := -1, - idAttr := s"user-menu-item-$idx", - item.title - ) - - div( - cls := "ml-3 relative", - div( - button( - tpe := "button", - cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - idAttr := "user-menu-button", - aria.expanded <-- menuOpen.signal, - aria.hasPopup := true, - span(cls := "sr-only", "Open user menu"), - child <-- avatarImage(), - onClick.preventDefault.mapTo( - !menuOpen.now() - ) --> menuOpen.writer - ) - ), - /* - * */ - div( - cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", - cls <-- menuOpen.signal.map { o => - if (o) "md:block" else "md:hidden" - }, - role := "menu", - aria.orientation := "vertical", - aria.labelledBy := "user-menu-button", - tabIndex := -1, - // : keyboard navigation - children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) - ) - ) - - def mobileProfile = - def menuItem(item: MenuItem): HtmlElement = - a( - href := "#", - cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", - item.title - ) - - div( - cls := "pt-4 pb-3 border-t border-indigo-700", - div( - cls := "flex items-center px-5", - div( - cls := "flex-shrink-0", - child <-- avatarImage(10) - ), - div( - cls := "ml-3", - div( - cls := "text-base font-medium text-white", - child.text <-- $userInfo.map(_.name) - ), - child.maybe <-- $userInfo.map( - _.email.map(e => - div( - cls := "text-sm font-medium text-indigo-300", - e - ) - ) - ) - ), - notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) - ), - div( - cls := "mt-3 px-2 space-y-1", - children <-- $m.map(_.userMenu.map(menuItem)) - ) - ) - - def pageLink(page: Link): Anchor = - page - .a() - .amend( - cls := "text-white px-3 py-2 rounded-md text-sm font-medium", - cls := Seq( - "bg-indigo-700" -> page.active, - "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active - ), - ariaCurrent := (if page.active then "page" else "false") - ) - - def logoImg: Image = - img( - cls := "h-8 w-8", - src <-- $m.map(_.logo.img), - alt <-- $m.map(_.logo.name) - ) - - def mobileMenuButton = button( - tpe := "button", - cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - aria.controls := "mobile-menu", - aria.expanded <-- mobileMenuOpen.signal, - span(cls := "sr-only", "Open main menu"), - Icons.outline - .menu() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") - ), - Icons.outline - .x() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") - ), - onClick.preventDefault.mapTo( - !mobileMenuOpen.now() - ) --> mobileMenuOpen.writer - ) - - def navBarLeft = - div( - cls := "flex items-center", - div(cls := "flex-shrink-0", logoImg), - div( - desktopOnly, - div( - cls := "ml-10 flex items-baseline space-x-4", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ) - ) - ) - - def navBarRight = - div( - desktopOnly, - div( - cls := "ml-4 flex items-center md:ml-6", - notificationButton, - userProfile - ) - ) - - def navBarMobile = - div( - cls := "-mr-2 flex", - mobileOnly, - mobileMenuButton - ) - - def navBar = - div( - cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex items-center justify-between h-16", - navBarLeft, - navBarRight, - navBarMobile - ) - ) - - def mobileMenu = - div( - mobileOnly, - cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }, - idAttr := "mobile-menu", - div( - cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ), - mobileProfile - ) - - nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala deleted file mode 100644 index 75339df..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object PageHeader: - def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = - header( - cls := "bg-white shadow-sm", - div( - cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", - h1( - cls := "text-lg leading-6 font-semibold text-gray-900", - Breadcrumbs(actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala deleted file mode 100644 index 5096463..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ /dev/null @@ -1,30 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import fiftyforms.ui.components.tailwind.Loading -import io.laminext.syntax.core.* - -object PageLayout: - case class ViewModel( - navigation: NavigationBar.ViewModel, - content: Option[HtmlElement] - ) - def apply(actionBus: Observer[Action])( - $m: Signal[ViewModel], - mods: Modifier[HtmlElement]* - )(using router: Router[Page]): HtmlElement = - val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) - div( - cls := "h-full flex flex-col", - NavigationBar($m.map(_.navigation)), - child.maybe <-- router.$currentPage.map(_.isRoot) - .switch(None, Some(PageHeader(actionBus))), - main( - cls := "flex-grow-1 overflow-y-auto", - mods, - child <-- $maybeContent.map(_.getOrElse(Loading)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala deleted file mode 100644 index 62d820f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.LinkSupport.* -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo - -object PageLink: - type ViewModel = Page - def apply($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = - a(mods($m, actions), child.text <-- $m.map(_.title)) - - def apply(m: ViewModel, actions: Observer[Action])(using - router: Router[Page] - ): Anchor = apply(Val(m), actions) - - def container($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = a(mods($m, actions)) - - def container(m: ViewModel, actions: Observer[Action])(using - Router[Page] - ): Anchor = container(Val(m), actions) - - private def mods($m: Signal[Page], actions: Observer[Action])(using - router: Router[Page] - ): Modifier[Anchor] = - Seq( - href <-- $m.map(router.absoluteUrlForPage).recover { case _ => - Some("invalid url") - }, - composeEvents(onClick.noKeyMod.preventDefault)( - _.sample($m) - .map(NavigateTo.apply) - ) --> actions - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala deleted file mode 100644 index 87f825a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.pages.dashboard.DashboardPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -class DashboardPageConnector(actionBus: Observer[Action])(using - router: Router[Page] -): - def apply: HtmlElement = - AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala deleted file mode 100644 index e9b0f71..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala +++ /dev/null @@ -1,76 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailKriteriaPage -import pages.detail.components.DukazyKriteria -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -object DetailKriteriaPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailKriteriaPageConnector( - state: DetailKriteriaPageConnector.AppState -)( - $page: Signal[Page.DetailKriteria] -)(using Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => - (p.osobniCislo.value, p.parametr.value, p.kriterium.value) - )((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map( - FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) - ) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, s, $s) => - DetailKriteriaPage($s)(state.actionBus.contramap { - case DukazyKriteria.Add => - NavigateTo( - Page.UpravDukazKriteria( - Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), - Page.Titled(s.parametr.id, Some(s.parametr.nazev)), - Page.Titled(s.kriterium.id, Some(s.kriterium.id)) - ) - ) - }) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): DetailKriteriaPage.ViewModel = - DetailKriteriaPage.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala deleted file mode 100644 index 4d8024f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ /dev/null @@ -1,57 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import pages.detail.DetailPage -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import fiftyforms.ui.components.tailwind.Color - -object DetailPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailPageConnector(state: DetailPageConnector.AppState)( - $page: Signal[Page.Detail] -)(using router: Router[Page]): - val $oscChangeSignal = $page.splitOne(_.osobniCislo.value)((osc, _, _) => osc) - val $pageChangeSignal = - $oscChangeSignal.flatMap(osc => - EventStream.fromSeq(Seq(FetchUserDetails(osc), FetchParameters(osc))) - ) - // TODO: filter the value based on the current osc - // OSC change will fetch new data, but still - // - we need to be sure that what we got is really what we ought to display - // - we want to display stale data accordingly (at least with loading indicator) - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - def apply: HtmlElement = - AppPage(state.actionBus)( - $data.combineWithFn($params)(_ zip _) - .map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: List[Parameter] - ): DetailPage.ViewModel = - DetailPage.ViewModel( - o.toDetailOsoby, - p.map( - _.toParametr(param => - PageLink.container(Page.DetailParametru(o, param), state.actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala deleted file mode 100644 index fdaa7f8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailParametruPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink - -object DetailParametruPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailParametruPageConnector( - state: DetailParametruPageConnector.AppState -)( - $page: Signal[Page.DetailParametru] -)(using router: Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => (p.osobniCislo.value, p.parametr.value))((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map(FetchParameter(_, _)) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - } yield (da, pb) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailParametruPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter - ): DetailParametruPage.ViewModel = - DetailParametruPage.ViewModel( - o.toDetailOsoby, - p.toParametr(p => - PageLink.container(Page.DetailParametru(o, p), state.actionBus) - ), - p.criteria.map( - _.toKriterium { c => - PageLink.container( - Page.DetailKriteria(o, p, c), - state.actionBus - ) - } - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala deleted file mode 100644 index 049aa22..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -case class DirectoryPageConnector( - $input: EventStream[List[UserInfo]], - actionBus: Observer[Action] -)(using router: Router[Page]): - val $data = $input.startWithNone - val $actionSignal = EventStream.fromValue(FetchDirectory) - - def apply: HtmlElement = - AppPage(actionBus)( - $data.split(_ => ())((_, _, s) => - pages.directory.DirectoryPage( - s.map( - _.map( - _.toUserRow(u => - PageLink.container( - Page.Detail(Page.Titled(u.personalNumber)), - actionBus - ) - ) - ) - ) - ) - ), - $actionSignal --> actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala deleted file mode 100644 index 88e7450..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ /dev/null @@ -1,68 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii -import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria -import fiftyforms.ui.components.tailwind.Color - -extension (o: UserInfo) - def toDetailOsoby: DetailOsoby.ViewModel = - DetailOsoby.ViewModel( - o.personalNumber, - o.name, - o.email, - o.phone, - o.img, - o.mainFunction.map(f => - DetailOsoby.Funkce.ViewModel(f.name, f.dept, f.ou) - ), - o.userContracts.headOption.map(c => - DetailOsoby.PracovniPomer.ViewModel(c.rel, c.startDate, c.endDate) - ) - ) - -extension (param: Parameter) - def toParametr(container: Parameter => Anchor): DetailParametru.ViewModel = - DetailParametru.ViewModel( - id = param.id, - nazev = param.name, - popis = param.description, - status = "Nesplněno", - statusColor = Color.red, - a = container(param) - ) - -extension (crit: ParameterCriteria) - def toKriterium( - container: ParameterCriteria => Anchor - ): DetailKriteria.ViewModel = - DetailKriteria.ViewModel( - nazev = crit.criteriumText, - kapitola = crit.chapterId, - bod = crit.itemId, - status = "Nesplněno", - statusColor = Color.red, - splneno = false, - dukazy = Nil, - container = container(crit) - ) - -extension (user: UserInfo) - def toUserRow( - container: UserInfo => HtmlElement = (_: UserInfo) => div() - ): UserRow.ViewModel = - UserRow.ViewModel( - osobniCislo = user.personalNumber.toString, - celeJmeno = user.name, - prijmeni = user.surname, - hlavniFunkce = user.mainFunction.map(_.name), - img = user.img, - container = () => container(user) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala deleted file mode 100644 index 7d5b2c1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.dashboard - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.Page - -object DashboardPage: - - def render: HtmlElement = - div("Dashboard page") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala deleted file mode 100644 index 9ef0698..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala +++ /dev/null @@ -1,31 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailKriteriaPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply($m: Signal[ViewModel])( - action: Observer[DukazyKriteria.Action] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - DukazyKriteria($m.map(_.kriterium.dukazy))(action) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala deleted file mode 100644 index 3b316e1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object DetailPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametry: SeznamParametru.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby($m.map(_.osoba)), - SeznamParametru($m.map(_.parametry)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala deleted file mode 100644 index 17224cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala +++ /dev/null @@ -1,24 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailParametruPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriteria: SeznamKriterii.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby.header($m.map(_.osoba)), - DetailParametru($m.map(_.parametr)), - SeznamKriterii($m.map(_.kriteria)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala deleted file mode 100644 index dc912fa..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ /dev/null @@ -1,107 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb -package app -package pages.detail - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.UpravDukazForm -import fiftyforms.services.files.File - -object UpravDukaz: - - type ThisPage = Page.UpravDukazKriteria - type PageKey = (OsobniCislo, String, String) - - trait State { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def availableFiles: EventStream[List[File]] - def actionBus: Observer[Action] - } - - def keyOfPage(page: ThisPage): PageKey = - (page.osobniCislo.value, page.parametr.value, page.kriterium.value) - - def onChangeAction(key: PageKey): Action = - FetchParameterCriteria( - key._1, - key._2, - key._3, - Page.UpravDukazKriteria(_, _, _) - ) - - class Connector(state: State)($page: Signal[ThisPage])(using Router[Page]): - val $paramChangeSignal = $page.splitOne(keyOfPage)((x, _, _) => x) - val $pageChangeSignal = $paramChangeSignal.map(onChangeAction) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.split(_ => ())((_, s, $s) => - PageComponent( - $s.map(buildModel), - state.availableFiles, - state.actionBus.contramap { - case UpravDukazForm.Cancelled => - NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) - case UpravDukazForm.AvailableFilesRequested => - FetchAvailableFiles(s._1.personalNumber) - } - ) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): PageComponent.ViewModel = - import connectors.* - PageComponent.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) - - object PageComponent: - import components.* - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply( - $m: Signal[ViewModel], - availableFilesStream: EventStream[List[File]], - events: Observer[UpravDukazForm.Event] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - UpravDukazForm(availableFilesStream)(events) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala deleted file mode 100644 index ca311b0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.Icons -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Color - -object DetailKriteria: - case class ViewModel( - nazev: String, - kapitola: String, - bod: String, - status: String, - statusColor: Color, - splneno: Boolean, - dukazy: List[DukazKriteria.ViewModel], - container: HtmlElement = div() - ) { - val id = s"${kapitola}${bod}" - } - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h3( - cls := "text-xl leading-6 font-bold text-gray-900", - child.text <-- $m.map(_.nazev) - ), - h3( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - "Kapitola ", - child.text <-- $m.map(_.kapitola), - " bod ", - child <-- $m.map(_.bod) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala deleted file mode 100644 index ae90290..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala +++ /dev/null @@ -1,100 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Avatar - -object DetailOsoby: - - object Funkce: - case class ViewModel( - nazev: String, - stredisko: String, - voj: String - ) - - def render($m: Signal[ViewModel]): HtmlElement = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.nazev), - span( - cls := "hidden md:inline", - " @ ", - child.text <-- $m.map(_.stredisko), - ", ", - child.text <-- $m.map(_.voj) - ) - ) - - object PracovniPomer: - case class ViewModel( - druh: String, - pocatek: LocalDate, - konec: Option[LocalDate] - ) - - def render($m: Signal[ViewModel]): HtmlElement = - import fiftyforms.ui.components.tailwind.CustomAttrs.datetime - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.druh), - " od ", - time( - datetime <-- $m.map(_.pocatek.toString), - child.text <-- $m.map(_.pocatek.toString) - ) - ) - - case class ViewModel( - osobniCislo: OsobniCislo, - jmeno: String, - email: Option[String], - telefon: Option[String], - img: Option[String], - hlavniFunkce: Option[Funkce.ViewModel], - pracovniPomer: Option[PracovniPomer.ViewModel] - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-4", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(16) - ), - div( - h1( - cls := "text-2xl font-bold text-gray-900", - child.text <-- $m.map(_.jmeno) - ), - child.maybe <-- $m.map(_.hlavniFunkce).split(_ => ())((_, _, d) => - Funkce.render(d) - ), - child.maybe <-- $m.map(_.pracovniPomer).split(_ => ())((_, _, d) => - PracovniPomer.render(d) - ) - ) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-5", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(8) - ), - div( - h1( - cls := "text-lg font-medium text-gray-800", - child.text <-- $m.map(_.jmeno) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala deleted file mode 100644 index e11cb62..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color - -object DetailParametru: - case class ViewModel( - id: String, - nazev: String, - popis: String, - status: String, - statusColor: Color, - a: Anchor - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h2( - cls := "text-2xl leading-6 font-medium text-gray-900", - child.text <-- $m.map(_.nazev) - ), - p( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - child.text <-- $m.map(_.popis) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - h2( - cls := "text-lg leading-6 font-medium text-gray-600", - child.text <-- $m.map(_.nazev) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala deleted file mode 100644 index a00ede8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala +++ /dev/null @@ -1,177 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.CustomAttrs - -object DukazKriteria: - case class Osoba(osobniCislo: String, jmeno: String) - case class Udalost(osoba: Osoba, datum: LocalDate) - case class Dokument( - url: String, - nazev: String, - pridal: Udalost, - odebral: Option[Udalost] - ) - case class ViewModel( - dokumenty: List[Dokument], - autorizoval: Option[Udalost], - platiDo: Option[LocalDate], - poznámka: Option[String] - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-lg", - div( - cls := "px-4 py-5 sm:px-6", - h3( - cls := "text-lg leading-6 font-medium text-gray-900", - """Applicant Information""" - ), - p( - cls := "mt-1 max-w-2xl text-sm text-gray-500", - """Personal details and application.""" - ) - ), - div( - cls := "border-t border-gray-200 px-4 py-5 sm:px-6", - dl( - cls := "grid grid-cols-1 gap-x-4 gap-y-8 sm:grid-cols-2", - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Full name""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Margot Foster""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Application for""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Backend Developer""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Email address""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """margotfoster@example.com""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Salary expectation""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """$120,000""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """About""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Fugiat ipsum ipsum deserunt culpa aute sint do nostrud anim incididunt cillum culpa consequat. Excepteur qui ipsum aliquip consequat sint. Sit id mollit nulla mollit nostrud in ea officia proident. Irure nostrud pariatur mollit ad adipisicing reprehenderit deserunt qui eu.""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """Attachments""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - ul( - role := "list", - cls := "border border-gray-200 rounded-md divide-y divide-gray-200", - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """resume_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ), - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """coverletter_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala deleted file mode 100644 index 22f134f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala +++ /dev/null @@ -1,35 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object DukazyKriteria: - sealed trait Action - case object Add extends Action - - type ViewModel = List[DukazKriteria.ViewModel] - - def apply($m: Signal[ViewModel])(actions: Observer[Action]): HtmlElement = - div( - child.maybe <-- $m.map(m => - if (m.isEmpty) then Some(prazdnyDukaz(actions)) - else None - ), - children <-- $m.map(_.zipWithIndex).split(_._2)((_, _, $s) => - DukazKriteria($s.map(_._1)) - ) - ) - - private def prazdnyDukaz(actions: Observer[Action]): HtmlElement = - button( - tpe := "button", - onClick.preventDefault.mapTo(Add) --> actions, - cls := "relative block w-full border-2 border-gray-300 border-dashed rounded-lg p-12 text-center hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - Icons.outline - .`document-add`(12) - .amend(svg.cls := "mx-auto text-gray-400"), - span( - cls := "mt-2 block text-sm font-medium text-gray-900", - "Přidat důkaz" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala deleted file mode 100644 index f628697..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.list.StackedList -import fiftyforms.ui.components.tailwind.list.ListRow -import fiftyforms.ui.components.tailwind.list.RowTag -import fiftyforms.ui.components.tailwind.list.RowNext -import java.time.LocalDate - -object SeznamKriterii: - type ViewModel = List[DetailKriteria.ViewModel] - - private val kritList = new StackedList[DetailKriteria.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - kritList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = - p(cls := "text-sm text-gray-500", s"${i.kapitola}${i.bod}"), - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.container - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala deleted file mode 100644 index 2bfe4bc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.list.{ - StackedList, - ListRow, - RowTag, - PropList, - IconText, - RowNext -} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.LinkSupport.* - -object SeznamParametru: - type ViewModel = List[DetailParametru.ViewModel] - - private val parametrList = new StackedList[DetailParametru.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - parametrList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = emptyNode, - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.a - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala deleted file mode 100644 index f048e9d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ /dev/null @@ -1,101 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.form.* -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement -import fiftyforms.services.files.components.tailwind.FilePicker -import cz.e_bs.cmi.mdr.pdb.frontend.AutorizujDukaz -import cz.e_bs.cmi.mdr.pdb.frontend.DocumentRef -import fiftyforms.services.files.File - -object UpravDukazForm: - sealed trait Event - case object Cancelled extends Event - case object AvailableFilesRequested extends Event - def apply(availableFilesStream: EventStream[List[File]])( - updates: Observer[Event] - ): HtmlElement = - val (filesStream, filesObserver) = - EventStream.withObserver[FilePicker.Event] - val files = Var[List[File]](Nil) - def submitButtons: HtmlElement = - div( - cls := "pt-5", - div( - cls := "flex justify-end", - button( - tpe := "button", - cls := "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zrušit", - onClick.mapTo(Cancelled) --> updates - ), - button( - tpe := "submit", - cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - disabled <-- files.signal.map(_.isEmpty), - "Autorizovat" - ) - ) - ) - - div( - filesStream.collect { case FilePicker.SelectionUpdated(files) => - files.to(List) - } --> files.writer, - filesStream.collect { case FilePicker.AvailableFilesRequested => - AvailableFilesRequested - } --> updates, - cls := "bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6", - Form( - Form.Body( - Form.Section( - FormHeader( - FormHeader.ViewModel( - "Doložení kritéria", - "Sestavte doklady poskytující důkaz kritéria a potvrďte odesláním formuláře. Případné limitace či jiné relevantní údaje vepište do pole Komentář." - ) - ), - FormFields( - FormRow( - "dokumenty", - "Dokumenty", - FilePicker(files.signal, availableFilesStream)(filesObserver) - .amend(idAttr := "dokumenty", cls("max-w-lg")) - ).toHtml, - FormRow( - "platnost", - "Platnost", - input( - idAttr := "platnost", - name := "platnost", - tpe := "date", - autoComplete := "date", - cls := "block max-w-lg w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" - ) - ).toHtml, - FormRow( - "komentar", - "Komentář", - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - textArea( - idAttr := "komentar", - name := "about", - rows := 3, - cls := "max-w-lg shadow-sm block w-full focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border border-gray-300 rounded-md" - ), - p( - cls := "mt-2 text-sm text-gray-500", - "Doplňte prosím potřebné informace související s doložením kritéria, včetně případných limitací." - ) - ) - ).toHtml - ) - ) - ), - submitButtons - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala deleted file mode 100644 index 41033f6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -import components._ - -object DirectoryPage: - - type ViewModel = List[UserRow.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - val (actionsStream, actionObserver) = - EventStream.withObserver[SearchForm.Action] - val $filter = actionsStream - .collect { - case SearchForm.ClearFilter => None - case SearchForm.SetFilter(t) => Some(t) - } - .startWith(None) - val byLetter = for { - d <- $m - f <- $filter - } yield for { - (letter, users) <- d - .filter { user => - f match - case None => true - case Some(t) => user.search.contains(t) - } - .groupBy(_.prijmeni.head) - .to(List) - .sortBy(_._1) - } yield (letter.toString, users.sortBy(_.prijmeni)) - - div( - cls := "h-full max-w-7xl mx-auto order-first flex flex-col flex-shrink-0", - SearchForm(actionObserver), - Directory(byLetter) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala deleted file mode 100644 index 73f915d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -object Directory: - - object Header: - type ViewModel = String - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500", - h3(child.text <-- $m) - ) - - object UserList: - type ViewModel = List[UserRow.ViewModel] - def apply($m: Signal[ViewModel]): HtmlElement = - ul( - role := "list", - cls := "relative z-0 divide-y divide-gray-200", - children <-- $m.split(_.osobniCislo)((_, _, s) => UserRow(s)) - ) - - type ViewModel = List[(String, List[UserRow.ViewModel])] - def apply($m: Signal[ViewModel]): HtmlElement = - nav( - cls := "flex-1 min-h-0 overflow-y-auto", - aria.label := "Directory", - children <-- $m.split(_._1)((_, _, s) => - div( - cls := "relative", - Header(s.map(_._1)), - UserList(s.map(_._2)) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala deleted file mode 100644 index e182910..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala +++ /dev/null @@ -1,54 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object SearchForm: - sealed trait Action - case object SubmitFilter extends Action - sealed trait FilterAction extends Action - case object ClearFilter extends FilterAction - case class SetFilter(value: String) extends FilterAction - - def apply(actions: Observer[Action]): HtmlElement = - div( - cls := "px-6 pt-4 pb-4", - form( - cls := "flex space-x-4", - onSubmit.mapTo(SubmitFilter) --> actions, - div( - cls := "flex-1 min-w-0", - label( - forId := "search", - cls := "sr-only", - "Hledat" - ), - div( - cls := "relative rounded-md shadow-sm", - div( - cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", - Icons.solid.search().amend(svg.cls := "text-gray-400") - ), - input( - tpe := "search", - name := "search", - idAttr := "search", - cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", - placeholder := "Hledat", - composeEvents(onInput.mapToValue.setAsValue.map(SetFilter(_)))( - _.throttle(500) - ) --> actions - ) - ) - ), - button( - tpe := "submit", - cls := "inline-flex justify-center px-3.5 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500", - Icons.solid.filter().amend(svg.cls := "text-gray-400"), - span( - cls := "sr-only", - "Hledat" - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala deleted file mode 100644 index 82f7723..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala +++ /dev/null @@ -1,52 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.airstream.core.Signal -import fiftyforms.ui.components.tailwind.Avatar - -object UserRow: - case class ViewModel( - osobniCislo: String, - celeJmeno: String, - prijmeni: String, - hlavniFunkce: Option[String], - img: Option[String], - container: () => HtmlElement = () => div() - ) { - val search = prijmeni.toLowerCase - } - - def apply($m: Signal[ViewModel]): HtmlElement = - inline def avatarImage = - Avatar($m.map(_.img)).avatarImage(10) - - li( - div( - cls := "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500", - div( - cls := "flex-shrink-0", - child <-- avatarImage - ), - div( - cls := "flex-1 min-w-0", - child <-- $m.map { o => - o.container() - .amend( - cls := "focus:outline-none", - span( - cls := "absolute inset-0", - aria.hidden := true - ), - p( - cls := "text-sm font-medium text-gray-900", - o.celeJmeno - ), - p( - cls := "text-sm text-gray-500 truncate", - o.hlavniFunkce - ) - ) - } - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala deleted file mode 100644 index 467c5f5..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala +++ /dev/null @@ -1,99 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router - -object ErrorPage: - case class ViewModel( - homePage: Page, - errorName: String, - title: String, - subTitle: String - ) - - def apply(m: ViewModel, actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - val ViewModel(homePage, errorName, title, subTitle) = m - div( - cls := "min-h-full pt-16 pb-12 flex flex-col bg-white", - main( - cls := "flex-grow flex flex-col justify-center max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex-shrink-0 flex justify-center", - a( - href := "/", - cls := "inline-flex", - span( - cls := "sr-only", - """Workflow""" - ), - img( - cls := "h-12 w-auto", - src := "https://tailwindui.com/img/logos/workflow-mark.svg?color=indigo&shade=600", - alt := "" - ) - ) - ), - div( - cls := "py-16", - div( - cls := "text-center", - p( - cls := "text-sm font-semibold text-indigo-600 uppercase tracking-wide", - errorName - ), - h1( - cls := "mt-2 text-4xl font-extrabold text-gray-900 tracking-tight sm:text-5xl", - title - ), - p( - cls := "mt-2 text-base text-gray-500", - subTitle - ), - div( - cls := "mt-6", - PageLink - .container(homePage, actionBus) - .amend( - cls := "text-base font-medium text-indigo-600 hover:text-indigo-500", - """Go back home""" - ) - ) - ) - ) - ), - footer( - cls := "flex-shrink-0 max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", - nav( - cls := "flex justify-center space-x-4", - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Contact Support""" - ), - span( - cls := "inline-block border-l border-gray-300", - aria.hidden := true - ), - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Status""" - ), - span( - cls := "inline-block border-l border-gray-300", - aria.hidden := true - ), - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Twitter""" - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala deleted file mode 100644 index bd9baec..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action - -object NotFoundPage: - def apply(homePage: Page, url: String, actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - ErrorPage( - ErrorPage.ViewModel( - homePage, - "404 error", - "Page not found.", - s"Sorry, but page $url doesn't exist." - ), - actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala deleted file mode 100644 index 476fe3c..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action - -object UnhandledErrorPage: - - case class ViewModel( - homePage: Page, - errorName: Option[String], - errorMessage: Option[String] - ) - - def apply(m: ViewModel, actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - ErrorPage( - ErrorPage.ViewModel( - m.homePage, - "Unexpected error occurred", - m.errorName.getOrElse( - "Uh oh!" - ), // TODO: translations, better text than uh oh - m.errorMessage.getOrElse( - "This wasn't supposed to happen! Please try again." - ) - ), - actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala deleted file mode 100644 index 25f059f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala +++ /dev/null @@ -1,6 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.services - -import com.raquo.airstream.core.Observer - -trait DataFetcher[K, A]: - def fetch(id: K, sink: Observer[A]): Unit diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala deleted file mode 100644 index a469830..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala +++ /dev/null @@ -1,120 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package state - -import com.raquo.airstream.core.EventStream -import cz.e_bs.cmi.mdr.pdb.{UserInfo, OsobniCislo} -import com.raquo.airstream.core.Observer -import scala.scalajs.js -import scala.scalajs.js.JSON -import zio.json._ -import com.raquo.airstream.eventbus.EventBus -import com.raquo.airstream.ownership.Owner -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.UserFunction -import cz.e_bs.cmi.mdr.pdb.UserContract -import fiftyforms.services.files.File - -trait AppState - extends connectors.DetailPageConnector.AppState - with connectors.DetailParametruPageConnector.AppState - with connectors.DetailKriteriaPageConnector.AppState - with pages.detail.UpravDukaz.State: - def users: EventStream[List[UserInfo]] - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - -class MockAppState(implicit owner: Owner, router: Router[Page]) - extends AppState: - - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonDecoder[UserFunction] = DeriveJsonDecoder.gen - given JsonDecoder[UserContract] = DeriveJsonDecoder.gen - given JsonDecoder[UserInfo] = DeriveJsonDecoder.gen - - given JsonDecoder[ParameterCriteria] = DeriveJsonDecoder.gen - given JsonDecoder[Parameter] = DeriveJsonDecoder.gen - - private val actions = EventBus[Action]() - private val (parametersStream, pushParameters) = - EventStream.withCallback[List[Parameter]] - private val (usersStream, pushUsers) = - EventStream.withCallback[List[UserInfo]] - private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] - private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] - - private val mockData: List[UserInfo] = - mockUsers - .asInstanceOf[js.Dictionary[js.Object]] - .values - // TODO: is there a more efficient way to parse from JS object directly? - .map { o => - val parsed = JSON.stringify(o).fromJson[UserInfo] - parsed.left.foreach(org.scalajs.dom.console.log(_)) - parsed - } - .collect { case Right(u) => - u - } - .toList - - private val mockParameters: List[Parameter] = - pdbParams - .asInstanceOf[js.Dictionary[js.Object]] - .values - .map(o => JSON.stringify(o).fromJson[Parameter]) - .collect { case Right(p) => p } - .toList - - // TODO: Extract to separate event handler - actions.events.foreach { - case FetchDirectory => pushUsers(mockData) - case FetchUserDetails(osc) => - mockData.find(_.personalNumber == osc).foreach { o => - pushDetails(o) - router.replaceState(Page.Detail(o)) - } - case FetchParameters(osc) => - pushParameters(mockParameters) - case FetchParameter(osc, paramId) => - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(Page.DetailParametru(o, p)) - case FetchParameterCriteria(osc, paramId, critId, page) => - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - c <- p.criteria.find(_.id == critId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(page(o, p, c)) - case NavigateTo(page) => router.pushState(page) - case FetchAvailableFiles(osc) => - pushFiles( - List( - File("https://tc163.cmi.cz/here", "Example file") - ) - ) - } - - override def users: EventStream[List[UserInfo]] = - usersStream.debugWithName("users") - - override def details: EventStream[UserInfo] = - detailsStream.debugWithName("details") - - override def parameters: EventStream[List[Parameter]] = - parametersStream.debugWithName("parameters") - - override def availableFiles: EventStream[List[File]] = - filesStream.debugWithName("available files") - - override def actionBus: Observer[Action] = - actions.writer.debugWithName("actions writer") diff --git a/app/src/main/scala/mdr/pdb/app/Main.scala b/app/src/main/scala/mdr/pdb/app/Main.scala new file mode 100644 index 0000000..081b4ee --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/Main.scala @@ -0,0 +1,103 @@ +package mdr.pdb.app + +import scala.scalajs.js.annotation.JSExportTopLevel +import scala.scalajs.js.annotation.JSExport +import scala.scalajs.js.annotation.JSImport +import scala.scalajs.js +import org.scalajs.dom +import com.raquo.laminar.api.L.{*, given} +import scala.scalajs.js.Date +import com.raquo.waypoint.Router +import com.raquo.waypoint.SplitRender +import mdr.pdb.app.services.DataFetcher +import scala.scalajs.js.JSON +import zio.json._ +import mdr.pdb.UserInfo +import mdr.pdb.app.state.AppState + +@js.native +@JSImport("stylesheets/main.css", JSImport.Namespace) +object Css extends js.Any + +@js.native +@JSImport("data/users.json", JSImport.Default) +object mockUsers extends js.Object + +@js.native +@JSImport("params/pdb-params.json", JSImport.Default) +object pdbParams extends js.Object + +@JSExportTopLevel("app") +object Main: + + @JSExport + def main(args: Array[String]): Unit = + import Routes.given + onLoad { + setupAirstream() + val appContainer = dom.document.querySelector("#app") + val _ = + render( + appContainer, + renderPage(state.MockAppState(using unsafeWindowOwner, router)) + ) + } + + private def onLoad(f: => Unit): Unit = + documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) + + private def setupAirstream()(using router: Router[Page]): Unit = + AirstreamError.registerUnhandledErrorCallback(err => + router.forcePage( + Page.UnhandledError( + Some(err.getClass.getName), // TODO: Fill only in dev mode + Some(err.getMessage) + ) + ) + ) + + def renderPage(state: AppState)(using + router: Router[Page] + ): HtmlElement = + val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) + .collectSignal[Page.Detail]( + connectors + .DetailPageConnector(state)(_) + .apply + ) + .collectSignal[Page.DetailParametru]( + connectors + .DetailParametruPageConnector(state)(_) + .apply + ) + .collectSignal[Page.DetailKriteria]( + connectors + .DetailKriteriaPageConnector(state)(_) + .apply + ) + .collectSignal[Page.UpravDukazKriteria]( + pages.detail.UpravDukaz.Connector(state)(_).apply + ) + .collectStatic(Page.Dashboard)( + connectors.DashboardPageConnector(state.actionBus).apply + ) + .collect[Page.NotFound](pg => + pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) + ) + .collect[Page.UnhandledError](pg => + pages.errors + .UnhandledErrorPage( + pages.errors.UnhandledErrorPage + .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), + state.actionBus + ) + ) + .collectStatic(Page.Directory)( + connectors + .DirectoryPageConnector(state.users, state.actionBus) + .apply + ) + div(cls := "h-full", child <-- pageSplitter.$view) + + // Pull in the stylesheet + val css: Css.type = Css diff --git a/app/src/main/scala/mdr/pdb/app/Routes.scala b/app/src/main/scala/mdr/pdb/app/Routes.scala new file mode 100644 index 0000000..2cc860c --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/Routes.scala @@ -0,0 +1,198 @@ +package mdr.pdb.app + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.* +import org.scalajs.dom +import zio.json.{*, given} +import mdr.pdb.OsobniCislo + +import scala.scalajs.js +import mdr.pdb.UserInfo +import mdr.pdb.Parameter +import mdr.pdb.ParameterCriteria +import mdr.pdb.app.Page.Titled + +// enum is not working with Waypoints' SplitRender collectStatic +sealed abstract class Page( + val id: String, + val title: String, + val parent: Option[Page] +) { + val path: Vector[Page] = + parent match + case None => Vector(this) + case Some(p) => p.path :+ this + + val isRoot: Boolean = parent.isEmpty +} + +object Page: + + case class Titled[V](value: V, title: Option[String] = None): + val show: String = title.getOrElse(value.toString) + + case object Directory extends Page("directory", "Adresář", None) + + case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) + + case class Detail(osobniCislo: Titled[OsobniCislo]) + extends Page("user", osobniCislo.show, Some(Directory)) + + object Detail { + def apply(o: UserInfo): Detail = Detail( + Titled(o.personalNumber, Some(o.name)) + ) + } + + case class DetailParametru( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String] + ) extends Page( + "parameter", + parametr.show, + Some(Detail(osobniCislo)) + ) + + object DetailParametru { + def apply(o: UserInfo, p: Parameter): DetailParametru = + DetailParametru( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)) + ) + } + + case class DetailKriteria( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String], + kriterium: Titled[String] + ) extends Page( + "criteria", + kriterium.show, + Some(DetailParametru(osobniCislo, parametr)) + ) + + object DetailKriteria { + def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = + DetailKriteria( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)), + Titled(k.id, Some(k.id)) + ) + } + + case class UpravDukazKriteria( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String], + kriterium: Titled[String] + ) extends Page( + "addProof", + "Důkaz", + Some(DetailKriteria(osobniCislo, parametr, kriterium)) + ) + + object UpravDukazKriteria { + def apply( + o: UserInfo, + p: Parameter, + k: ParameterCriteria + ): UpravDukazKriteria = + UpravDukazKriteria( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)), + Titled(k.id, Some(k.id)) + ) + } + + case class NotFound(url: String) extends Page("404", "404", Some(Directory)) + + case class UnhandledError( + errorName: Option[String], + errorMessage: Option[String] + ) extends Page("500", "Unexpected error", Some(Directory)) + +object Routes: + given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) + given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) + given [V: JsonEncoder]: JsonEncoder[Titled[V]] = + DeriveJsonEncoder.gen[Titled[V]] + given [V: JsonDecoder]: JsonDecoder[Titled[V]] = + DeriveJsonDecoder.gen[Titled[V]] + given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] + given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] + + val base = + js.`import`.meta.env.BASE_URL + .asInstanceOf[String] + "app" + + val homePage: Page = Page.Directory + + given router: Router[Page] = Router[Page]( + routes = List( + Route.static(homePage, root / endOfSegments, basePath = base), + Route.static( + Page.Dashboard, + root / "dashboard" / endOfSegments, + basePath = base + ), + Route[Page.Detail, String]( + encode = _.osobniCislo.value.toString, + decode = osc => Page.Detail(Titled(OsobniCislo(osc))), + root / "osoba" / segment[String] / endOfSegments, + basePath = base + ), + Route[Page.DetailParametru, (String, String)]( + encode = p => (p.osobniCislo.value.toString, p.parametr.value), + decode = + p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / endOfSegments, + basePath = base + ), + Route[Page.DetailKriteria, (String, String, String)]( + encode = p => + ( + p.osobniCislo.value.toString, + p.parametr.value, + p.kriterium.value.replaceAll("\\.", "--") + ), + decode = p => + Page.DetailKriteria( + Titled(OsobniCislo(p._1)), + Titled(p._2), + Titled(p._3.replaceAll("--", ".")) + ), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / "kriterium" / segment[String] / endOfSegments, + basePath = base + ), + Route[Page.UpravDukazKriteria, (String, String, String)]( + encode = p => + ( + p.osobniCislo.value.toString, + p.parametr.value, + p.kriterium.value.replaceAll("\\.", "--") + ), + decode = p => + Page.UpravDukazKriteria( + Titled(OsobniCislo(p._1)), + Titled(p._2), + Titled(p._3.replaceAll("--", ".")) + ), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / "kriterium" / segment[String] / "edit" / endOfSegments, + basePath = base + ) + ), + serializePage = _.toJson, + deserializePage = _.fromJson[Page] + .fold(s => throw IllegalStateException(s), identity), + getPageTitle = _.title, + routeFallback = url => Page.NotFound(url), + deserializeFallback = _ => Page.Dashboard + )( + $popStateEvent = windowEvents.onPopState, + owner = unsafeWindowOwner + ) diff --git a/app/src/main/scala/mdr/pdb/app/actions.scala b/app/src/main/scala/mdr/pdb/app/actions.scala new file mode 100644 index 0000000..d078b3a --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/actions.scala @@ -0,0 +1,21 @@ +package mdr.pdb.app + +import mdr.pdb.OsobniCislo +import mdr.pdb.UserInfo +import mdr.pdb.Parameter +import mdr.pdb.ParameterCriteria + +sealed trait Action + +case object FetchDirectory extends Action +case class FetchUserDetails(osc: OsobniCislo) extends Action +case class FetchParameters(osc: OsobniCislo) extends Action +case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action +case class FetchParameterCriteria( + osc: OsobniCislo, + paramId: String, + critId: String, + page: (UserInfo, Parameter, ParameterCriteria) => Page +) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action +case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/mdr/pdb/app/components/AppPage.scala new file mode 100644 index 0000000..130c817 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/AppPage.scala @@ -0,0 +1,82 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.Page +import mdr.pdb.{UserProfile, UserInfo, OsobniCislo} +import com.raquo.waypoint.Router +import mdr.pdb.app.Action +import mdr.pdb.app.NavigateTo +import mdr.pdb.UserFunction + +object AppPage: + // TODO: pages by logged in user + val pages: List[Page] = List(Page.Directory, Page.Dashboard) + + import NavigationBar.{Logo, MenuItem} + + val logo = Logo( + "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", + "Workflow" + ) + + // TODO: menu items by user profile + val userMenu = + List( + MenuItem("Your Profile"), + MenuItem("Settings"), + MenuItem("Sign out") + ) + + // TODO: load user profile + val $userProfile = Var( + UserProfile( + "tom", + UserInfo( + OsobniCislo("1031"), + "tom", + "Tom", + "Cook", + None, + None, + Some("tom@example.com"), + Some("+420 222 866 180"), + Some( + UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") + ), + Nil, + None + ) + ) + ) + + val $userInfo = $userProfile.signal.map(_.userInfo) + + type ViewModel = Option[HtmlElement] + def apply( + actionBus: Observer[Action] + )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using + router: Router[Page] + ): HtmlElement = + PageLayout(actionBus)( + $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => + PageLayout.ViewModel( + NavigationBar.ViewModel( + u, + pages.map(p => + NavigationBar.Link( + () => + PageLink( + p, + actionBus + ), + p == cp + ) + ), + userMenu, + logo + ), + c + ) + ), + mods + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala new file mode 100644 index 0000000..9375398 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala @@ -0,0 +1,143 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.tailwind.list.IconText.ViewModel +import fiftyforms.ui.components.tailwind.Icons +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.Page + +import com.raquo.laminar.api.L.{*, given} +import io.laminext.syntax.core.{*, given} +import mdr.pdb.app.Action + +object Breadcrumbs: + + private def slash = + import svg.{*, given} + svg( + cls := "flex-shrink-0 h-5 w-5 text-gray-300", + xmlns := "http://www.w3.org/2000/svg", + fill := "currentColor", + viewBox := "0 0 20 20", + ariaHidden := true, + path( + d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" + ) + ) + + def backIcon = + Icons.solid + .`arrow-narrow-left`() + .amend( + svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" + ) + + object Link: + case class ViewModel( + page: Page, + icon: Option[SvgElement], + extraClasses: String, + text: String, + textClasses: Option[String] = None + ) + + val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" + + def shortHome(p: Page) = ViewModel( + p, + Some(backIcon), + "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", + "Zpět na úvodní stránku", + None + ) + + def fullHome(p: Page) = ViewModel( + p, + Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), + "text-gray-400 hover:text-gray-500", + "Domů", + Some("sr-only") + ) + + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + PageLink + .container($m.map(_.page), actionBus) + .amend( + cls <-- $m.map(_.extraClasses), + child.maybe <-- $m.map(_.icon), + span( + cls <-- $m.map(_.textClasses.getOrElse("")), + child.text <-- $m.map(_.text) + ) + ) + + object FullBreadcrumbs: + type ViewModel = Page + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + ol( + role := "list", + cls := "flex items-center space-x-4", + children <-- $m.map(_.path).split(_.id)((_, _, $p) => + li( + div( + cls := "flex items-center", + child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), + Link( + $p.map(p => + if (p.isRoot) then Link.fullHome(p) + else + Link.ViewModel( + p, + None, + s"ml-4 max-w-xs truncate ${Link.baseClasses}", + p.title + ) + ), + actionBus + ) + ) + ) + ) + ) + + object ShortBreadcrumbs: + type ViewModel = Page + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + Link( + $m.map { p => + val target = p.parent.getOrElse(p) + if target.isRoot then Link.shortHome(target) + else + Link.ViewModel( + target, + Some(backIcon), + "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", + s"Zpět na ${target.title}" + ) + }, + actionBus + ) + + def apply(actionBus: Observer[Action])(using + router: Router[Page] + ): HtmlElement = + nav( + cls := "flex", + aria.label := "Breadcrumb", + div( + cls := "flex sm:hidden", + ShortBreadcrumbs(router.$currentPage, actionBus) + ), + div( + cls := "hidden sm:block", + FullBreadcrumbs(router.$currentPage, actionBus) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala new file mode 100644 index 0000000..4bb8b8d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala @@ -0,0 +1,242 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent +import fiftyforms.ui.components.tailwind.Icons +import fiftyforms.ui.components.tailwind.Avatar +import mdr.pdb.UserInfo +import io.laminext.syntax.core.* + +object NavigationBar: + + case class Logo(img: String, name: String) + case class Link(a: () => Anchor, active: Boolean) + case class MenuItem(title: String) + + case class ViewModel( + userInfo: UserInfo, + pages: List[Link], + userMenu: List[MenuItem], + logo: Logo + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + val $userInfo = $m.map(_.userInfo) + val mobileMenuOpen = Var(false) + + val desktopOnly = cls("hidden md:block") + val mobileOnly = cls("md:hidden") + + inline def avatarImage(size: Int = 8) = + Avatar($userInfo.map(_.img)).avatarImage(size) + + def notificationButton = button( + tpe := "button", + cls := List( + "bg-indigo-600", + "focus:outline-none", + "focus:ring-2", + "focus:ring-offset-2", + "focus:ring-offset-indigo-600", + "focus:ring-white", + "hover:text-white", + "p-1", + "rounded-full", + "text-indigo-200" + ), + span(cls := "sr-only", "View notifications"), + Icons.outline.bell() + ) + + def userProfile: HtmlElement = + val menuOpen = Var(false) + + def menuItem(item: MenuItem, idx: Int): HtmlElement = + a( + href := "#", + cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", + role := "menuitem", + tabIndex := -1, + idAttr := s"user-menu-item-$idx", + item.title + ) + + div( + cls := "ml-3 relative", + div( + button( + tpe := "button", + cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", + idAttr := "user-menu-button", + aria.expanded <-- menuOpen.signal, + aria.hasPopup := true, + span(cls := "sr-only", "Open user menu"), + child <-- avatarImage(), + onClick.preventDefault.mapTo( + !menuOpen.now() + ) --> menuOpen.writer + ) + ), + /* + * */ + div( + cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", + cls <-- menuOpen.signal.map { o => + if (o) "md:block" else "md:hidden" + }, + role := "menu", + aria.orientation := "vertical", + aria.labelledBy := "user-menu-button", + tabIndex := -1, + // : keyboard navigation + children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) + ) + ) + + def mobileProfile = + def menuItem(item: MenuItem): HtmlElement = + a( + href := "#", + cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", + item.title + ) + + div( + cls := "pt-4 pb-3 border-t border-indigo-700", + div( + cls := "flex items-center px-5", + div( + cls := "flex-shrink-0", + child <-- avatarImage(10) + ), + div( + cls := "ml-3", + div( + cls := "text-base font-medium text-white", + child.text <-- $userInfo.map(_.name) + ), + child.maybe <-- $userInfo.map( + _.email.map(e => + div( + cls := "text-sm font-medium text-indigo-300", + e + ) + ) + ) + ), + notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) + ), + div( + cls := "mt-3 px-2 space-y-1", + children <-- $m.map(_.userMenu.map(menuItem)) + ) + ) + + def pageLink(page: Link): Anchor = + page + .a() + .amend( + cls := "text-white px-3 py-2 rounded-md text-sm font-medium", + cls := Seq( + "bg-indigo-700" -> page.active, + "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active + ), + ariaCurrent := (if page.active then "page" else "false") + ) + + def logoImg: Image = + img( + cls := "h-8 w-8", + src <-- $m.map(_.logo.img), + alt <-- $m.map(_.logo.name) + ) + + def mobileMenuButton = button( + tpe := "button", + cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", + aria.controls := "mobile-menu", + aria.expanded <-- mobileMenuOpen.signal, + span(cls := "sr-only", "Open main menu"), + Icons.outline + .menu() + .amend( + svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") + ), + Icons.outline + .x() + .amend( + svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") + ), + onClick.preventDefault.mapTo( + !mobileMenuOpen.now() + ) --> mobileMenuOpen.writer + ) + + def navBarLeft = + div( + cls := "flex items-center", + div(cls := "flex-shrink-0", logoImg), + div( + desktopOnly, + div( + cls := "ml-10 flex items-baseline space-x-4", + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) + ) + ) + ) + + def navBarRight = + div( + desktopOnly, + div( + cls := "ml-4 flex items-center md:ml-6", + notificationButton, + userProfile + ) + ) + + def navBarMobile = + div( + cls := "-mr-2 flex", + mobileOnly, + mobileMenuButton + ) + + def navBar = + div( + cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", + div( + cls := "flex items-center justify-between h-16", + navBarLeft, + navBarRight, + navBarMobile + ) + ) + + def mobileMenu = + div( + mobileOnly, + cls <-- mobileMenuOpen.signal.map { o => + if (o) "block" else "hidden" + }, + idAttr := "mobile-menu", + div( + cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) + ), + mobileProfile + ) + + nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala new file mode 100644 index 0000000..c4cb10d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala @@ -0,0 +1,19 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.Page +import com.raquo.waypoint.Router +import mdr.pdb.app.Action + +object PageHeader: + def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = + header( + cls := "bg-white shadow-sm", + div( + cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", + h1( + cls := "text-lg leading-6 font-semibold text-gray-900", + Breadcrumbs(actionBus) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala new file mode 100644 index 0000000..aae4539 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala @@ -0,0 +1,30 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.Page +import mdr.pdb.app.Action +import fiftyforms.ui.components.tailwind.Loading +import io.laminext.syntax.core.* + +object PageLayout: + case class ViewModel( + navigation: NavigationBar.ViewModel, + content: Option[HtmlElement] + ) + def apply(actionBus: Observer[Action])( + $m: Signal[ViewModel], + mods: Modifier[HtmlElement]* + )(using router: Router[Page]): HtmlElement = + val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) + div( + cls := "h-full flex flex-col", + NavigationBar($m.map(_.navigation)), + child.maybe <-- router.$currentPage.map(_.isRoot) + .switch(None, Some(PageHeader(actionBus))), + main( + cls := "flex-grow-1 overflow-y-auto", + mods, + child <-- $maybeContent.map(_.getOrElse(Loading)) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/mdr/pdb/app/components/PageLink.scala new file mode 100644 index 0000000..e6e7052 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageLink.scala @@ -0,0 +1,40 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.LinkSupport.* +import mdr.pdb.app.Page +import mdr.pdb.app.Action +import com.raquo.waypoint.Router +import mdr.pdb.app.NavigateTo + +object PageLink: + type ViewModel = Page + def apply($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = + a(mods($m, actions), child.text <-- $m.map(_.title)) + + def apply(m: ViewModel, actions: Observer[Action])(using + router: Router[Page] + ): Anchor = apply(Val(m), actions) + + def container($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = a(mods($m, actions)) + + def container(m: ViewModel, actions: Observer[Action])(using + Router[Page] + ): Anchor = container(Val(m), actions) + + private def mods($m: Signal[Page], actions: Observer[Action])(using + router: Router[Page] + ): Modifier[Anchor] = + Seq( + href <-- $m.map(router.absoluteUrlForPage).recover { case _ => + Some("invalid url") + }, + composeEvents(onClick.noKeyMod.preventDefault)( + _.sample($m) + .map(NavigateTo.apply) + ) --> actions + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala new file mode 100644 index 0000000..7682772 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala @@ -0,0 +1,13 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.pages.dashboard.DashboardPage +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage + +class DashboardPageConnector(actionBus: Observer[Action])(using + router: Router[Page] +): + def apply: HtmlElement = + AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala new file mode 100644 index 0000000..76b9133 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala @@ -0,0 +1,76 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.Parameter +import mdr.pdb.UserInfo +import mdr.pdb.app.pages.detail.DetailParametruPage +import pages.detail.DetailKriteriaPage +import pages.detail.components.DukazyKriteria +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.ParameterCriteria + +object DetailKriteriaPageConnector { + trait AppState { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + } +} + +case class DetailKriteriaPageConnector( + state: DetailKriteriaPageConnector.AppState +)( + $page: Signal[Page.DetailKriteria] +)(using Router[Page]): + val $paramChangeSignal = + $page.splitOne(p => + (p.osobniCislo.value, p.parametr.value, p.kriterium.value) + )((x, _, _) => x) + val $pageChangeSignal = + $paramChangeSignal.map( + FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) + ) + + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + val $merged = + $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => + for { + da <- d + pa <- p + pb <- pa.find(_.id == pc._2) + ka <- pb.criteria.find(_.id == pc._3) + } yield (da, pb, ka) + ) + + def apply: HtmlElement = + AppPage(state.actionBus)( + $merged.map(_.map(buildModel)) + .split(_ => ())((_, s, $s) => + DetailKriteriaPage($s)(state.actionBus.contramap { + case DukazyKriteria.Add => + NavigateTo( + Page.UpravDukazKriteria( + Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), + Page.Titled(s.parametr.id, Some(s.parametr.nazev)), + Page.Titled(s.kriterium.id, Some(s.kriterium.id)) + ) + ) + }) + ), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: Parameter, + k: ParameterCriteria + ): DetailKriteriaPage.ViewModel = + DetailKriteriaPage.ViewModel( + o.toDetailOsoby, + p.toParametr(_ => a()), + k.toKriterium(_ => a()) + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DetailPageConnector.scala new file mode 100644 index 0000000..df3a6b7 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DetailPageConnector.scala @@ -0,0 +1,57 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.UserInfo +import pages.detail.DetailPage +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.pages.detail.components.DetailOsoby +import mdr.pdb.Parameter +import mdr.pdb.app.pages.detail.components.SeznamParametru +import fiftyforms.ui.components.tailwind.Color + +object DetailPageConnector { + trait AppState { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + } +} + +case class DetailPageConnector(state: DetailPageConnector.AppState)( + $page: Signal[Page.Detail] +)(using router: Router[Page]): + val $oscChangeSignal = $page.splitOne(_.osobniCislo.value)((osc, _, _) => osc) + val $pageChangeSignal = + $oscChangeSignal.flatMap(osc => + EventStream.fromSeq(Seq(FetchUserDetails(osc), FetchParameters(osc))) + ) + // TODO: filter the value based on the current osc + // OSC change will fetch new data, but still + // - we need to be sure that what we got is really what we ought to display + // - we want to display stale data accordingly (at least with loading indicator) + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + def apply: HtmlElement = + AppPage(state.actionBus)( + $data.combineWithFn($params)(_ zip _) + .map(_.map(buildModel)) + .split(_ => ())((_, _, s) => DetailPage(s)), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: List[Parameter] + ): DetailPage.ViewModel = + DetailPage.ViewModel( + o.toDetailOsoby, + p.map( + _.toParametr(param => + PageLink.container(Page.DetailParametru(o, param), state.actionBus) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DetailParametruPageConnector.scala new file mode 100644 index 0000000..fa7193d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DetailParametruPageConnector.scala @@ -0,0 +1,67 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.Parameter +import mdr.pdb.UserInfo +import mdr.pdb.app.pages.detail.DetailParametruPage +import pages.detail.DetailParametruPage +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.components.PageLink + +object DetailParametruPageConnector { + trait AppState { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + } +} + +case class DetailParametruPageConnector( + state: DetailParametruPageConnector.AppState +)( + $page: Signal[Page.DetailParametru] +)(using router: Router[Page]): + val $paramChangeSignal = + $page.splitOne(p => (p.osobniCislo.value, p.parametr.value))((x, _, _) => x) + val $pageChangeSignal = + $paramChangeSignal.map(FetchParameter(_, _)) + + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + val $merged = + $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => + for { + da <- d + pa <- p + pb <- pa.find(_.id == pc._2) + } yield (da, pb) + ) + + def apply: HtmlElement = + AppPage(state.actionBus)( + $merged.map(_.map(buildModel)) + .split(_ => ())((_, _, s) => DetailParametruPage(s)), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: Parameter + ): DetailParametruPage.ViewModel = + DetailParametruPage.ViewModel( + o.toDetailOsoby, + p.toParametr(p => + PageLink.container(Page.DetailParametru(o, p), state.actionBus) + ), + p.criteria.map( + _.toKriterium { c => + PageLink.container( + Page.DetailKriteria(o, p, c), + state.actionBus + ) + } + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DirectoryPageConnector.scala new file mode 100644 index 0000000..ab2c045 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DirectoryPageConnector.scala @@ -0,0 +1,34 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.UserInfo +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.components.AppPage + +case class DirectoryPageConnector( + $input: EventStream[List[UserInfo]], + actionBus: Observer[Action] +)(using router: Router[Page]): + val $data = $input.startWithNone + val $actionSignal = EventStream.fromValue(FetchDirectory) + + def apply: HtmlElement = + AppPage(actionBus)( + $data.split(_ => ())((_, _, s) => + pages.directory.DirectoryPage( + s.map( + _.map( + _.toUserRow(u => + PageLink.container( + Page.Detail(Page.Titled(u.personalNumber)), + actionBus + ) + ) + ) + ) + ) + ), + $actionSignal --> actionBus + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/mdr/pdb/app/connectors/conversions.scala new file mode 100644 index 0000000..f581796 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/conversions.scala @@ -0,0 +1,68 @@ +package mdr.pdb.app.connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.UserInfo +import mdr.pdb.app.pages.detail.components.DetailOsoby +import mdr.pdb.Parameter +import mdr.pdb.app.pages.detail.components.SeznamParametru +import mdr.pdb.ParameterCriteria +import mdr.pdb.app.pages.detail.components.SeznamKriterii +import mdr.pdb.app.pages.directory.components.UserRow +import mdr.pdb.app.pages.detail.components.DetailParametru +import mdr.pdb.app.pages.detail.components.DetailKriteria +import fiftyforms.ui.components.tailwind.Color + +extension (o: UserInfo) + def toDetailOsoby: DetailOsoby.ViewModel = + DetailOsoby.ViewModel( + o.personalNumber, + o.name, + o.email, + o.phone, + o.img, + o.mainFunction.map(f => + DetailOsoby.Funkce.ViewModel(f.name, f.dept, f.ou) + ), + o.userContracts.headOption.map(c => + DetailOsoby.PracovniPomer.ViewModel(c.rel, c.startDate, c.endDate) + ) + ) + +extension (param: Parameter) + def toParametr(container: Parameter => Anchor): DetailParametru.ViewModel = + DetailParametru.ViewModel( + id = param.id, + nazev = param.name, + popis = param.description, + status = "Nesplněno", + statusColor = Color.red, + a = container(param) + ) + +extension (crit: ParameterCriteria) + def toKriterium( + container: ParameterCriteria => Anchor + ): DetailKriteria.ViewModel = + DetailKriteria.ViewModel( + nazev = crit.criteriumText, + kapitola = crit.chapterId, + bod = crit.itemId, + status = "Nesplněno", + statusColor = Color.red, + splneno = false, + dukazy = Nil, + container = container(crit) + ) + +extension (user: UserInfo) + def toUserRow( + container: UserInfo => HtmlElement = (_: UserInfo) => div() + ): UserRow.ViewModel = + UserRow.ViewModel( + osobniCislo = user.personalNumber.toString, + celeJmeno = user.name, + prijmeni = user.surname, + hlavniFunkce = user.mainFunction.map(_.name), + img = user.img, + container = () => container(user) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/dashboard/DashboardPage.scala b/app/src/main/scala/mdr/pdb/app/pages/dashboard/DashboardPage.scala new file mode 100644 index 0000000..0eb8f7a --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/dashboard/DashboardPage.scala @@ -0,0 +1,11 @@ +package mdr.pdb.app.pages.dashboard + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.Page + +object DashboardPage: + + def render: HtmlElement = + div("Dashboard page") diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala new file mode 100644 index 0000000..ff3a25f --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala @@ -0,0 +1,31 @@ +package mdr.pdb.app.pages.detail + +import com.raquo.laminar.api.L.{*, given} + +import components._ + +object DetailKriteriaPage: + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametr: DetailParametru.ViewModel, + kriterium: DetailKriteria.ViewModel + ) + + def apply($m: Signal[ViewModel])( + action: Observer[DukazyKriteria.Action] + ): HtmlElement = + div( + cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + div( + DetailOsoby.header($m.map(_.osoba)), + DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") + ), + div( + DetailKriteria($m.map(_.kriterium)), + DukazyKriteria($m.map(_.kriterium.dukazy))(action) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/DetailPage.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailPage.scala new file mode 100644 index 0000000..b591cc9 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailPage.scala @@ -0,0 +1,25 @@ +package mdr.pdb.app.pages.detail + +import com.raquo.laminar.api.L.{*, given} + +import components._ +import mdr.pdb.app.Page +import com.raquo.waypoint.Router +import mdr.pdb.app.Action + +object DetailPage: + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametry: SeznamParametru.ViewModel + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + DetailOsoby($m.map(_.osoba)), + SeznamParametru($m.map(_.parametry)) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/DetailParametruPage.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailParametruPage.scala new file mode 100644 index 0000000..3322279 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailParametruPage.scala @@ -0,0 +1,24 @@ +package mdr.pdb.app.pages.detail + +import com.raquo.laminar.api.L.{*, given} + +import components._ + +object DetailParametruPage: + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametr: DetailParametru.ViewModel, + kriteria: SeznamKriterii.ViewModel + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + DetailOsoby.header($m.map(_.osoba)), + DetailParametru($m.map(_.parametr)), + SeznamKriterii($m.map(_.kriteria)) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala new file mode 100644 index 0000000..b58618a --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala @@ -0,0 +1,107 @@ +package mdr.pdb +package app +package pages.detail + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.pages.detail.components.UpravDukazForm +import fiftyforms.services.files.File + +object UpravDukaz: + + type ThisPage = Page.UpravDukazKriteria + type PageKey = (OsobniCislo, String, String) + + trait State { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def availableFiles: EventStream[List[File]] + def actionBus: Observer[Action] + } + + def keyOfPage(page: ThisPage): PageKey = + (page.osobniCislo.value, page.parametr.value, page.kriterium.value) + + def onChangeAction(key: PageKey): Action = + FetchParameterCriteria( + key._1, + key._2, + key._3, + Page.UpravDukazKriteria(_, _, _) + ) + + class Connector(state: State)($page: Signal[ThisPage])(using Router[Page]): + val $paramChangeSignal = $page.splitOne(keyOfPage)((x, _, _) => x) + val $pageChangeSignal = $paramChangeSignal.map(onChangeAction) + + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + val $merged = + $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => + for { + da <- d + pa <- p + pb <- pa.find(_.id == pc._2) + ka <- pb.criteria.find(_.id == pc._3) + } yield (da, pb, ka) + ) + + def apply: HtmlElement = + AppPage(state.actionBus)( + $merged.split(_ => ())((_, s, $s) => + PageComponent( + $s.map(buildModel), + state.availableFiles, + state.actionBus.contramap { + case UpravDukazForm.Cancelled => + NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) + case UpravDukazForm.AvailableFilesRequested => + FetchAvailableFiles(s._1.personalNumber) + } + ) + ), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: Parameter, + k: ParameterCriteria + ): PageComponent.ViewModel = + import connectors.* + PageComponent.ViewModel( + o.toDetailOsoby, + p.toParametr(_ => a()), + k.toKriterium(_ => a()) + ) + + object PageComponent: + import components.* + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametr: DetailParametru.ViewModel, + kriterium: DetailKriteria.ViewModel + ) + + def apply( + $m: Signal[ViewModel], + availableFilesStream: EventStream[List[File]], + events: Observer[UpravDukazForm.Event] + ): HtmlElement = + div( + cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + div( + DetailOsoby.header($m.map(_.osoba)), + DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") + ), + div( + DetailKriteria($m.map(_.kriterium)), + UpravDukazForm(availableFilesStream)(events) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailKriteria.scala new file mode 100644 index 0000000..a3ab634 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailKriteria.scala @@ -0,0 +1,37 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.CustomAttrs +import fiftyforms.ui.components.tailwind.Icons +import java.time.LocalDate +import fiftyforms.ui.components.tailwind.Color + +object DetailKriteria: + case class ViewModel( + nazev: String, + kapitola: String, + bod: String, + status: String, + statusColor: Color, + splneno: Boolean, + dukazy: List[DukazKriteria.ViewModel], + container: HtmlElement = div() + ) { + val id = s"${kapitola}${bod}" + } + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "pb-5 border-b border-gray-200", + h3( + cls := "text-xl leading-6 font-bold text-gray-900", + child.text <-- $m.map(_.nazev) + ), + h3( + cls := "mt-2 max-w-4xl text-sm text-gray-500", + "Kapitola ", + child.text <-- $m.map(_.kapitola), + " bod ", + child <-- $m.map(_.bod) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailOsoby.scala new file mode 100644 index 0000000..0fe2ba5 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailOsoby.scala @@ -0,0 +1,100 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.OsobniCislo + +import java.time.LocalDate +import fiftyforms.ui.components.tailwind.Avatar + +object DetailOsoby: + + object Funkce: + case class ViewModel( + nazev: String, + stredisko: String, + voj: String + ) + + def render($m: Signal[ViewModel]): HtmlElement = + p( + cls := "text-sm font-medium text-gray-500", + child.text <-- $m.map(_.nazev), + span( + cls := "hidden md:inline", + " @ ", + child.text <-- $m.map(_.stredisko), + ", ", + child.text <-- $m.map(_.voj) + ) + ) + + object PracovniPomer: + case class ViewModel( + druh: String, + pocatek: LocalDate, + konec: Option[LocalDate] + ) + + def render($m: Signal[ViewModel]): HtmlElement = + import fiftyforms.ui.components.tailwind.CustomAttrs.datetime + p( + cls := "text-sm font-medium text-gray-500", + child.text <-- $m.map(_.druh), + " od ", + time( + datetime <-- $m.map(_.pocatek.toString), + child.text <-- $m.map(_.pocatek.toString) + ) + ) + + case class ViewModel( + osobniCislo: OsobniCislo, + jmeno: String, + email: Option[String], + telefon: Option[String], + img: Option[String], + hlavniFunkce: Option[Funkce.ViewModel], + pracovniPomer: Option[PracovniPomer.ViewModel] + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "md:flex md:items-center md:justify-between md:space-x-4", + div( + cls := "flex items-start space-x-4", + div( + cls := "flex-shrink-0", + Avatar($m.map(_.img)).avatar(16) + ), + div( + h1( + cls := "text-2xl font-bold text-gray-900", + child.text <-- $m.map(_.jmeno) + ), + child.maybe <-- $m.map(_.hlavniFunkce).split(_ => ())((_, _, d) => + Funkce.render(d) + ), + child.maybe <-- $m.map(_.pracovniPomer).split(_ => ())((_, _, d) => + PracovniPomer.render(d) + ) + ) + ) + ) + + def header($m: Signal[ViewModel]): HtmlElement = + div( + cls := "md:flex md:items-center md:justify-between md:space-x-5", + div( + cls := "flex items-start space-x-4", + div( + cls := "flex-shrink-0", + Avatar($m.map(_.img)).avatar(8) + ), + div( + h1( + cls := "text-lg font-medium text-gray-800", + child.text <-- $m.map(_.jmeno) + ) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailParametru.scala new file mode 100644 index 0000000..6310dde --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailParametru.scala @@ -0,0 +1,34 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.Color + +object DetailParametru: + case class ViewModel( + id: String, + nazev: String, + popis: String, + status: String, + statusColor: Color, + a: Anchor + ) + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "pb-5 border-b border-gray-200", + h2( + cls := "text-2xl leading-6 font-medium text-gray-900", + child.text <-- $m.map(_.nazev) + ), + p( + cls := "mt-2 max-w-4xl text-sm text-gray-500", + child.text <-- $m.map(_.popis) + ) + ) + + def header($m: Signal[ViewModel]): HtmlElement = + div( + h2( + cls := "text-lg leading-6 font-medium text-gray-600", + child.text <-- $m.map(_.nazev) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazKriteria.scala new file mode 100644 index 0000000..c292919 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazKriteria.scala @@ -0,0 +1,177 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import java.time.LocalDate +import fiftyforms.ui.components.tailwind.CustomAttrs + +object DukazKriteria: + case class Osoba(osobniCislo: String, jmeno: String) + case class Udalost(osoba: Osoba, datum: LocalDate) + case class Dokument( + url: String, + nazev: String, + pridal: Udalost, + odebral: Option[Udalost] + ) + case class ViewModel( + dokumenty: List[Dokument], + autorizoval: Option[Udalost], + platiDo: Option[LocalDate], + poznámka: Option[String] + ) + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "bg-white shadow overflow-hidden sm:rounded-lg", + div( + cls := "px-4 py-5 sm:px-6", + h3( + cls := "text-lg leading-6 font-medium text-gray-900", + """Applicant Information""" + ), + p( + cls := "mt-1 max-w-2xl text-sm text-gray-500", + """Personal details and application.""" + ) + ), + div( + cls := "border-t border-gray-200 px-4 py-5 sm:px-6", + dl( + cls := "grid grid-cols-1 gap-x-4 gap-y-8 sm:grid-cols-2", + div( + cls := "sm:col-span-1", + dt( + cls := "text-sm font-medium text-gray-500", + """Full name""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """Margot Foster""" + ) + ), + div( + cls := "sm:col-span-1", + dt( + cls := "text-sm font-medium text-gray-500", + """Application for""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """Backend Developer""" + ) + ), + div( + cls := "sm:col-span-1", + dt( + cls := "text-sm font-medium text-gray-500", + """Email address""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """margotfoster@example.com""" + ) + ), + div( + cls := "sm:col-span-1", + dt( + cls := "text-sm font-medium text-gray-500", + """Salary expectation""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """$120,000""" + ) + ), + div( + cls := "sm:col-span-2", + dt( + cls := "text-sm font-medium text-gray-500", + """About""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """Fugiat ipsum ipsum deserunt culpa aute sint do nostrud anim incididunt cillum culpa consequat. Excepteur qui ipsum aliquip consequat sint. Sit id mollit nulla mollit nostrud in ea officia proident. Irure nostrud pariatur mollit ad adipisicing reprehenderit deserunt qui eu.""" + ) + ), + div( + cls := "sm:col-span-2", + dt( + cls := "text-sm font-medium text-gray-500", + """Attachments""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + ul( + role := "list", + cls := "border border-gray-200 rounded-md divide-y divide-gray-200", + li( + cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", + div( + cls := "w-0 flex-1 flex items-center", { + import svg.* + import CustomAttrs.svg.ariaHidden + svg( + cls := "flex-shrink-0 h-5 w-5 text-gray-400", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + ariaHidden := true, + path( + fillRule := "evenodd", + d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", + clipRule := "evenodd" + ) + ) + }, + span( + cls := "ml-2 flex-1 w-0 truncate", + """resume_back_end_developer.pdf""" + ) + ), + div( + cls := "ml-4 flex-shrink-0", + a( + href := "#", + cls := "font-medium text-indigo-600 hover:text-indigo-500", + """Download""" + ) + ) + ), + li( + cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", + div( + cls := "w-0 flex-1 flex items-center", { + import svg.* + import CustomAttrs.svg.ariaHidden + svg( + cls := "flex-shrink-0 h-5 w-5 text-gray-400", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + ariaHidden := true, + path( + fillRule := "evenodd", + d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", + clipRule := "evenodd" + ) + ) + }, + span( + cls := "ml-2 flex-1 w-0 truncate", + """coverletter_back_end_developer.pdf""" + ) + ), + div( + cls := "ml-4 flex-shrink-0", + a( + href := "#", + cls := "font-medium text-indigo-600 hover:text-indigo-500", + """Download""" + ) + ) + ) + ) + ) + ) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala new file mode 100644 index 0000000..d604860 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala @@ -0,0 +1,35 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.Icons + +object DukazyKriteria: + sealed trait Action + case object Add extends Action + + type ViewModel = List[DukazKriteria.ViewModel] + + def apply($m: Signal[ViewModel])(actions: Observer[Action]): HtmlElement = + div( + child.maybe <-- $m.map(m => + if (m.isEmpty) then Some(prazdnyDukaz(actions)) + else None + ), + children <-- $m.map(_.zipWithIndex).split(_._2)((_, _, $s) => + DukazKriteria($s.map(_._1)) + ) + ) + + private def prazdnyDukaz(actions: Observer[Action]): HtmlElement = + button( + tpe := "button", + onClick.preventDefault.mapTo(Add) --> actions, + cls := "relative block w-full border-2 border-gray-300 border-dashed rounded-lg p-12 text-center hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + Icons.outline + .`document-add`(12) + .amend(svg.cls := "mx-auto text-gray-400"), + span( + cls := "mt-2 block text-sm font-medium text-gray-900", + "Přidat důkaz" + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala new file mode 100644 index 0000000..7e77a7b --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala @@ -0,0 +1,34 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.Color +import fiftyforms.ui.components.tailwind.list.StackedList +import fiftyforms.ui.components.tailwind.list.ListRow +import fiftyforms.ui.components.tailwind.list.RowTag +import fiftyforms.ui.components.tailwind.list.RowNext +import java.time.LocalDate + +object SeznamKriterii: + type ViewModel = List[DetailKriteria.ViewModel] + + private val kritList = new StackedList[DetailKriteria.ViewModel] + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "bg-white shadow overflow-hidden sm:rounded-md", + kritList($m, _.id) { $i => + $i.map { i => + ListRow.ViewModel( + title = i.nazev, + topRight = RowTag.render( + $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) + ), + bottomLeft = + p(cls := "text-sm text-gray-500", s"${i.kapitola}${i.bod}"), + bottomRight = emptyNode, + farRight = RowNext.render, + containerElement = i.container + ) + } + } + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamParametru.scala new file mode 100644 index 0000000..0760b32 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamParametru.scala @@ -0,0 +1,37 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.list.{ + StackedList, + ListRow, + RowTag, + PropList, + IconText, + RowNext +} +import fiftyforms.ui.components.tailwind.Color +import fiftyforms.ui.components.tailwind.LinkSupport.* + +object SeznamParametru: + type ViewModel = List[DetailParametru.ViewModel] + + private val parametrList = new StackedList[DetailParametru.ViewModel] + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "bg-white shadow overflow-hidden sm:rounded-md", + parametrList($m, _.id) { $i => + $i.map { i => + ListRow.ViewModel( + title = i.nazev, + topRight = RowTag.render( + $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) + ), + bottomLeft = emptyNode, + bottomRight = emptyNode, + farRight = RowNext.render, + containerElement = i.a + ) + } + } + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala new file mode 100644 index 0000000..28753c9 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala @@ -0,0 +1,101 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import io.laminext.syntax.core.* +import fiftyforms.ui.components.tailwind.CustomAttrs +import fiftyforms.ui.components.tailwind.form.* +import org.scalajs.dom +import com.raquo.laminar.nodes.ReactiveHtmlElement +import fiftyforms.services.files.components.tailwind.FilePicker +import mdr.pdb.frontend.AutorizujDukaz +import mdr.pdb.frontend.DocumentRef +import fiftyforms.services.files.File + +object UpravDukazForm: + sealed trait Event + case object Cancelled extends Event + case object AvailableFilesRequested extends Event + def apply(availableFilesStream: EventStream[List[File]])( + updates: Observer[Event] + ): HtmlElement = + val (filesStream, filesObserver) = + EventStream.withObserver[FilePicker.Event] + val files = Var[List[File]](Nil) + def submitButtons: HtmlElement = + div( + cls := "pt-5", + div( + cls := "flex justify-end", + button( + tpe := "button", + cls := "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + "Zrušit", + onClick.mapTo(Cancelled) --> updates + ), + button( + tpe := "submit", + cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + disabled <-- files.signal.map(_.isEmpty), + "Autorizovat" + ) + ) + ) + + div( + filesStream.collect { case FilePicker.SelectionUpdated(files) => + files.to(List) + } --> files.writer, + filesStream.collect { case FilePicker.AvailableFilesRequested => + AvailableFilesRequested + } --> updates, + cls := "bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6", + Form( + Form.Body( + Form.Section( + FormHeader( + FormHeader.ViewModel( + "Doložení kritéria", + "Sestavte doklady poskytující důkaz kritéria a potvrďte odesláním formuláře. Případné limitace či jiné relevantní údaje vepište do pole Komentář." + ) + ), + FormFields( + FormRow( + "dokumenty", + "Dokumenty", + FilePicker(files.signal, availableFilesStream)(filesObserver) + .amend(idAttr := "dokumenty", cls("max-w-lg")) + ).toHtml, + FormRow( + "platnost", + "Platnost", + input( + idAttr := "platnost", + name := "platnost", + tpe := "date", + autoComplete := "date", + cls := "block max-w-lg w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" + ) + ).toHtml, + FormRow( + "komentar", + "Komentář", + div( + cls := "mt-1 sm:mt-0 sm:col-span-2", + textArea( + idAttr := "komentar", + name := "about", + rows := 3, + cls := "max-w-lg shadow-sm block w-full focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border border-gray-300 rounded-md" + ), + p( + cls := "mt-2 text-sm text-gray-500", + "Doplňte prosím potřebné informace související s doložením kritéria, včetně případných limitací." + ) + ) + ).toHtml + ) + ) + ), + submitButtons + ) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala deleted file mode 100644 index a3b00af..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala +++ /dev/null @@ -1,103 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import scala.scalajs.js.annotation.JSExportTopLevel -import scala.scalajs.js.annotation.JSExport -import scala.scalajs.js.annotation.JSImport -import scala.scalajs.js -import org.scalajs.dom -import com.raquo.laminar.api.L.{*, given} -import scala.scalajs.js.Date -import com.raquo.waypoint.Router -import com.raquo.waypoint.SplitRender -import cz.e_bs.cmi.mdr.pdb.app.services.DataFetcher -import scala.scalajs.js.JSON -import zio.json._ -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.state.AppState - -@js.native -@JSImport("stylesheets/main.css", JSImport.Namespace) -object Css extends js.Any - -@js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native -@JSImport("params/pdb-params.json", JSImport.Default) -object pdbParams extends js.Object - -@JSExportTopLevel("app") -object Main: - - @JSExport - def main(args: Array[String]): Unit = - import Routes.given - onLoad { - setupAirstream() - val appContainer = dom.document.querySelector("#app") - val _ = - render( - appContainer, - renderPage(state.MockAppState(using unsafeWindowOwner, router)) - ) - } - - private def onLoad(f: => Unit): Unit = - documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) - - private def setupAirstream()(using router: Router[Page]): Unit = - AirstreamError.registerUnhandledErrorCallback(err => - router.forcePage( - Page.UnhandledError( - Some(err.getClass.getName), // TODO: Fill only in dev mode - Some(err.getMessage) - ) - ) - ) - - def renderPage(state: AppState)(using - router: Router[Page] - ): HtmlElement = - val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) - .collectSignal[Page.Detail]( - connectors - .DetailPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailParametru]( - connectors - .DetailParametruPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailKriteria]( - connectors - .DetailKriteriaPageConnector(state)(_) - .apply - ) - .collectSignal[Page.UpravDukazKriteria]( - pages.detail.UpravDukaz.Connector(state)(_).apply - ) - .collectStatic(Page.Dashboard)( - connectors.DashboardPageConnector(state.actionBus).apply - ) - .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) - ) - .collect[Page.UnhandledError](pg => - pages.errors - .UnhandledErrorPage( - pages.errors.UnhandledErrorPage - .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), - state.actionBus - ) - ) - .collectStatic(Page.Directory)( - connectors - .DirectoryPageConnector(state.users, state.actionBus) - .apply - ) - div(cls := "h-full", child <-- pageSplitter.$view) - - // Pull in the stylesheet - val css: Css.type = Css diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala deleted file mode 100644 index 3bbc8c0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ /dev/null @@ -1,198 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.* -import org.scalajs.dom -import zio.json.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import scala.scalajs.js -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.Page.Titled - -// enum is not working with Waypoints' SplitRender collectStatic -sealed abstract class Page( - val id: String, - val title: String, - val parent: Option[Page] -) { - val path: Vector[Page] = - parent match - case None => Vector(this) - case Some(p) => p.path :+ this - - val isRoot: Boolean = parent.isEmpty -} - -object Page: - - case class Titled[V](value: V, title: Option[String] = None): - val show: String = title.getOrElse(value.toString) - - case object Directory extends Page("directory", "Adresář", None) - - case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) - - case class Detail(osobniCislo: Titled[OsobniCislo]) - extends Page("user", osobniCislo.show, Some(Directory)) - - object Detail { - def apply(o: UserInfo): Detail = Detail( - Titled(o.personalNumber, Some(o.name)) - ) - } - - case class DetailParametru( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String] - ) extends Page( - "parameter", - parametr.show, - Some(Detail(osobniCislo)) - ) - - object DetailParametru { - def apply(o: UserInfo, p: Parameter): DetailParametru = - DetailParametru( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)) - ) - } - - case class DetailKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "criteria", - kriterium.show, - Some(DetailParametru(osobniCislo, parametr)) - ) - - object DetailKriteria { - def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = - DetailKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class UpravDukazKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "addProof", - "Důkaz", - Some(DetailKriteria(osobniCislo, parametr, kriterium)) - ) - - object UpravDukazKriteria { - def apply( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): UpravDukazKriteria = - UpravDukazKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class NotFound(url: String) extends Page("404", "404", Some(Directory)) - - case class UnhandledError( - errorName: Option[String], - errorMessage: Option[String] - ) extends Page("500", "Unexpected error", Some(Directory)) - -object Routes: - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) - given [V: JsonEncoder]: JsonEncoder[Titled[V]] = - DeriveJsonEncoder.gen[Titled[V]] - given [V: JsonDecoder]: JsonDecoder[Titled[V]] = - DeriveJsonDecoder.gen[Titled[V]] - given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] - given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] - - val base = - js.`import`.meta.env.BASE_URL - .asInstanceOf[String] + "app" - - val homePage: Page = Page.Directory - - given router: Router[Page] = Router[Page]( - routes = List( - Route.static(homePage, root / endOfSegments, basePath = base), - Route.static( - Page.Dashboard, - root / "dashboard" / endOfSegments, - basePath = base - ), - Route[Page.Detail, String]( - encode = _.osobniCislo.value.toString, - decode = osc => Page.Detail(Titled(OsobniCislo(osc))), - root / "osoba" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.DetailParametru, (String, String)]( - encode = p => (p.osobniCislo.value.toString, p.parametr.value), - decode = - p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / endOfSegments, - basePath = base - ), - Route[Page.DetailKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.DetailKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.UpravDukazKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.UpravDukazKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / "edit" / endOfSegments, - basePath = base - ) - ), - serializePage = _.toJson, - deserializePage = _.fromJson[Page] - .fold(s => throw IllegalStateException(s), identity), - getPageTitle = _.title, - routeFallback = url => Page.NotFound(url), - deserializeFallback = _ => Page.Dashboard - )( - $popStateEvent = windowEvents.onPopState, - owner = unsafeWindowOwner - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala deleted file mode 100644 index b24b82b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -sealed trait Action - -case object FetchDirectory extends Action -case class FetchUserDetails(osc: OsobniCislo) extends Action -case class FetchParameters(osc: OsobniCislo) extends Action -case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action -case class FetchParameterCriteria( - osc: OsobniCislo, - paramId: String, - critId: String, - page: (UserInfo, Parameter, ParameterCriteria) => Page -) extends Action -case class FetchAvailableFiles(osc: OsobniCislo) extends Action -case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala deleted file mode 100644 index c637d35..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala +++ /dev/null @@ -1,82 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.{UserProfile, UserInfo, OsobniCislo} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo -import cz.e_bs.cmi.mdr.pdb.UserFunction - -object AppPage: - // TODO: pages by logged in user - val pages: List[Page] = List(Page.Directory, Page.Dashboard) - - import NavigationBar.{Logo, MenuItem} - - val logo = Logo( - "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", - "Workflow" - ) - - // TODO: menu items by user profile - val userMenu = - List( - MenuItem("Your Profile"), - MenuItem("Settings"), - MenuItem("Sign out") - ) - - // TODO: load user profile - val $userProfile = Var( - UserProfile( - "tom", - UserInfo( - OsobniCislo("1031"), - "tom", - "Tom", - "Cook", - None, - None, - Some("tom@example.com"), - Some("+420 222 866 180"), - Some( - UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") - ), - Nil, - None - ) - ) - ) - - val $userInfo = $userProfile.signal.map(_.userInfo) - - type ViewModel = Option[HtmlElement] - def apply( - actionBus: Observer[Action] - )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using - router: Router[Page] - ): HtmlElement = - PageLayout(actionBus)( - $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => - PageLayout.ViewModel( - NavigationBar.ViewModel( - u, - pages.map(p => - NavigationBar.Link( - () => - PageLink( - p, - actionBus - ), - p == cp - ) - ), - userMenu, - logo - ), - c - ) - ), - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala deleted file mode 100644 index bf473c6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ /dev/null @@ -1,143 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden -import fiftyforms.ui.components.tailwind.list.IconText.ViewModel -import fiftyforms.ui.components.tailwind.Icons -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Action - -object Breadcrumbs: - - private def slash = - import svg.{*, given} - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-300", - xmlns := "http://www.w3.org/2000/svg", - fill := "currentColor", - viewBox := "0 0 20 20", - ariaHidden := true, - path( - d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" - ) - ) - - def backIcon = - Icons.solid - .`arrow-narrow-left`() - .amend( - svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" - ) - - object Link: - case class ViewModel( - page: Page, - icon: Option[SvgElement], - extraClasses: String, - text: String, - textClasses: Option[String] = None - ) - - val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" - - def shortHome(p: Page) = ViewModel( - p, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - "Zpět na úvodní stránku", - None - ) - - def fullHome(p: Page) = ViewModel( - p, - Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), - "text-gray-400 hover:text-gray-500", - "Domů", - Some("sr-only") - ) - - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - PageLink - .container($m.map(_.page), actionBus) - .amend( - cls <-- $m.map(_.extraClasses), - child.maybe <-- $m.map(_.icon), - span( - cls <-- $m.map(_.textClasses.getOrElse("")), - child.text <-- $m.map(_.text) - ) - ) - - object FullBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - ol( - role := "list", - cls := "flex items-center space-x-4", - children <-- $m.map(_.path).split(_.id)((_, _, $p) => - li( - div( - cls := "flex items-center", - child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), - Link( - $p.map(p => - if (p.isRoot) then Link.fullHome(p) - else - Link.ViewModel( - p, - None, - s"ml-4 max-w-xs truncate ${Link.baseClasses}", - p.title - ) - ), - actionBus - ) - ) - ) - ) - ) - - object ShortBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - Link( - $m.map { p => - val target = p.parent.getOrElse(p) - if target.isRoot then Link.shortHome(target) - else - Link.ViewModel( - target, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - s"Zpět na ${target.title}" - ) - }, - actionBus - ) - - def apply(actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - nav( - cls := "flex", - aria.label := "Breadcrumb", - div( - cls := "flex sm:hidden", - ShortBreadcrumbs(router.$currentPage, actionBus) - ), - div( - cls := "hidden sm:block", - FullBreadcrumbs(router.$currentPage, actionBus) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala deleted file mode 100644 index 0db2047..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ /dev/null @@ -1,242 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent -import fiftyforms.ui.components.tailwind.Icons -import fiftyforms.ui.components.tailwind.Avatar -import cz.e_bs.cmi.mdr.pdb.UserInfo -import io.laminext.syntax.core.* - -object NavigationBar: - - case class Logo(img: String, name: String) - case class Link(a: () => Anchor, active: Boolean) - case class MenuItem(title: String) - - case class ViewModel( - userInfo: UserInfo, - pages: List[Link], - userMenu: List[MenuItem], - logo: Logo - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - val $userInfo = $m.map(_.userInfo) - val mobileMenuOpen = Var(false) - - val desktopOnly = cls("hidden md:block") - val mobileOnly = cls("md:hidden") - - inline def avatarImage(size: Int = 8) = - Avatar($userInfo.map(_.img)).avatarImage(size) - - def notificationButton = button( - tpe := "button", - cls := List( - "bg-indigo-600", - "focus:outline-none", - "focus:ring-2", - "focus:ring-offset-2", - "focus:ring-offset-indigo-600", - "focus:ring-white", - "hover:text-white", - "p-1", - "rounded-full", - "text-indigo-200" - ), - span(cls := "sr-only", "View notifications"), - Icons.outline.bell() - ) - - def userProfile: HtmlElement = - val menuOpen = Var(false) - - def menuItem(item: MenuItem, idx: Int): HtmlElement = - a( - href := "#", - cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", - role := "menuitem", - tabIndex := -1, - idAttr := s"user-menu-item-$idx", - item.title - ) - - div( - cls := "ml-3 relative", - div( - button( - tpe := "button", - cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - idAttr := "user-menu-button", - aria.expanded <-- menuOpen.signal, - aria.hasPopup := true, - span(cls := "sr-only", "Open user menu"), - child <-- avatarImage(), - onClick.preventDefault.mapTo( - !menuOpen.now() - ) --> menuOpen.writer - ) - ), - /* - * */ - div( - cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", - cls <-- menuOpen.signal.map { o => - if (o) "md:block" else "md:hidden" - }, - role := "menu", - aria.orientation := "vertical", - aria.labelledBy := "user-menu-button", - tabIndex := -1, - // : keyboard navigation - children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) - ) - ) - - def mobileProfile = - def menuItem(item: MenuItem): HtmlElement = - a( - href := "#", - cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", - item.title - ) - - div( - cls := "pt-4 pb-3 border-t border-indigo-700", - div( - cls := "flex items-center px-5", - div( - cls := "flex-shrink-0", - child <-- avatarImage(10) - ), - div( - cls := "ml-3", - div( - cls := "text-base font-medium text-white", - child.text <-- $userInfo.map(_.name) - ), - child.maybe <-- $userInfo.map( - _.email.map(e => - div( - cls := "text-sm font-medium text-indigo-300", - e - ) - ) - ) - ), - notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) - ), - div( - cls := "mt-3 px-2 space-y-1", - children <-- $m.map(_.userMenu.map(menuItem)) - ) - ) - - def pageLink(page: Link): Anchor = - page - .a() - .amend( - cls := "text-white px-3 py-2 rounded-md text-sm font-medium", - cls := Seq( - "bg-indigo-700" -> page.active, - "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active - ), - ariaCurrent := (if page.active then "page" else "false") - ) - - def logoImg: Image = - img( - cls := "h-8 w-8", - src <-- $m.map(_.logo.img), - alt <-- $m.map(_.logo.name) - ) - - def mobileMenuButton = button( - tpe := "button", - cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - aria.controls := "mobile-menu", - aria.expanded <-- mobileMenuOpen.signal, - span(cls := "sr-only", "Open main menu"), - Icons.outline - .menu() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") - ), - Icons.outline - .x() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") - ), - onClick.preventDefault.mapTo( - !mobileMenuOpen.now() - ) --> mobileMenuOpen.writer - ) - - def navBarLeft = - div( - cls := "flex items-center", - div(cls := "flex-shrink-0", logoImg), - div( - desktopOnly, - div( - cls := "ml-10 flex items-baseline space-x-4", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ) - ) - ) - - def navBarRight = - div( - desktopOnly, - div( - cls := "ml-4 flex items-center md:ml-6", - notificationButton, - userProfile - ) - ) - - def navBarMobile = - div( - cls := "-mr-2 flex", - mobileOnly, - mobileMenuButton - ) - - def navBar = - div( - cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex items-center justify-between h-16", - navBarLeft, - navBarRight, - navBarMobile - ) - ) - - def mobileMenu = - div( - mobileOnly, - cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }, - idAttr := "mobile-menu", - div( - cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ), - mobileProfile - ) - - nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala deleted file mode 100644 index 75339df..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object PageHeader: - def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = - header( - cls := "bg-white shadow-sm", - div( - cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", - h1( - cls := "text-lg leading-6 font-semibold text-gray-900", - Breadcrumbs(actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala deleted file mode 100644 index 5096463..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ /dev/null @@ -1,30 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import fiftyforms.ui.components.tailwind.Loading -import io.laminext.syntax.core.* - -object PageLayout: - case class ViewModel( - navigation: NavigationBar.ViewModel, - content: Option[HtmlElement] - ) - def apply(actionBus: Observer[Action])( - $m: Signal[ViewModel], - mods: Modifier[HtmlElement]* - )(using router: Router[Page]): HtmlElement = - val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) - div( - cls := "h-full flex flex-col", - NavigationBar($m.map(_.navigation)), - child.maybe <-- router.$currentPage.map(_.isRoot) - .switch(None, Some(PageHeader(actionBus))), - main( - cls := "flex-grow-1 overflow-y-auto", - mods, - child <-- $maybeContent.map(_.getOrElse(Loading)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala deleted file mode 100644 index 62d820f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.LinkSupport.* -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo - -object PageLink: - type ViewModel = Page - def apply($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = - a(mods($m, actions), child.text <-- $m.map(_.title)) - - def apply(m: ViewModel, actions: Observer[Action])(using - router: Router[Page] - ): Anchor = apply(Val(m), actions) - - def container($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = a(mods($m, actions)) - - def container(m: ViewModel, actions: Observer[Action])(using - Router[Page] - ): Anchor = container(Val(m), actions) - - private def mods($m: Signal[Page], actions: Observer[Action])(using - router: Router[Page] - ): Modifier[Anchor] = - Seq( - href <-- $m.map(router.absoluteUrlForPage).recover { case _ => - Some("invalid url") - }, - composeEvents(onClick.noKeyMod.preventDefault)( - _.sample($m) - .map(NavigateTo.apply) - ) --> actions - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala deleted file mode 100644 index 87f825a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.pages.dashboard.DashboardPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -class DashboardPageConnector(actionBus: Observer[Action])(using - router: Router[Page] -): - def apply: HtmlElement = - AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala deleted file mode 100644 index e9b0f71..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala +++ /dev/null @@ -1,76 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailKriteriaPage -import pages.detail.components.DukazyKriteria -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -object DetailKriteriaPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailKriteriaPageConnector( - state: DetailKriteriaPageConnector.AppState -)( - $page: Signal[Page.DetailKriteria] -)(using Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => - (p.osobniCislo.value, p.parametr.value, p.kriterium.value) - )((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map( - FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) - ) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, s, $s) => - DetailKriteriaPage($s)(state.actionBus.contramap { - case DukazyKriteria.Add => - NavigateTo( - Page.UpravDukazKriteria( - Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), - Page.Titled(s.parametr.id, Some(s.parametr.nazev)), - Page.Titled(s.kriterium.id, Some(s.kriterium.id)) - ) - ) - }) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): DetailKriteriaPage.ViewModel = - DetailKriteriaPage.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala deleted file mode 100644 index 4d8024f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ /dev/null @@ -1,57 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import pages.detail.DetailPage -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import fiftyforms.ui.components.tailwind.Color - -object DetailPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailPageConnector(state: DetailPageConnector.AppState)( - $page: Signal[Page.Detail] -)(using router: Router[Page]): - val $oscChangeSignal = $page.splitOne(_.osobniCislo.value)((osc, _, _) => osc) - val $pageChangeSignal = - $oscChangeSignal.flatMap(osc => - EventStream.fromSeq(Seq(FetchUserDetails(osc), FetchParameters(osc))) - ) - // TODO: filter the value based on the current osc - // OSC change will fetch new data, but still - // - we need to be sure that what we got is really what we ought to display - // - we want to display stale data accordingly (at least with loading indicator) - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - def apply: HtmlElement = - AppPage(state.actionBus)( - $data.combineWithFn($params)(_ zip _) - .map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: List[Parameter] - ): DetailPage.ViewModel = - DetailPage.ViewModel( - o.toDetailOsoby, - p.map( - _.toParametr(param => - PageLink.container(Page.DetailParametru(o, param), state.actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala deleted file mode 100644 index fdaa7f8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailParametruPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink - -object DetailParametruPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailParametruPageConnector( - state: DetailParametruPageConnector.AppState -)( - $page: Signal[Page.DetailParametru] -)(using router: Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => (p.osobniCislo.value, p.parametr.value))((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map(FetchParameter(_, _)) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - } yield (da, pb) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailParametruPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter - ): DetailParametruPage.ViewModel = - DetailParametruPage.ViewModel( - o.toDetailOsoby, - p.toParametr(p => - PageLink.container(Page.DetailParametru(o, p), state.actionBus) - ), - p.criteria.map( - _.toKriterium { c => - PageLink.container( - Page.DetailKriteria(o, p, c), - state.actionBus - ) - } - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala deleted file mode 100644 index 049aa22..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -case class DirectoryPageConnector( - $input: EventStream[List[UserInfo]], - actionBus: Observer[Action] -)(using router: Router[Page]): - val $data = $input.startWithNone - val $actionSignal = EventStream.fromValue(FetchDirectory) - - def apply: HtmlElement = - AppPage(actionBus)( - $data.split(_ => ())((_, _, s) => - pages.directory.DirectoryPage( - s.map( - _.map( - _.toUserRow(u => - PageLink.container( - Page.Detail(Page.Titled(u.personalNumber)), - actionBus - ) - ) - ) - ) - ) - ), - $actionSignal --> actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala deleted file mode 100644 index 88e7450..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ /dev/null @@ -1,68 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii -import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria -import fiftyforms.ui.components.tailwind.Color - -extension (o: UserInfo) - def toDetailOsoby: DetailOsoby.ViewModel = - DetailOsoby.ViewModel( - o.personalNumber, - o.name, - o.email, - o.phone, - o.img, - o.mainFunction.map(f => - DetailOsoby.Funkce.ViewModel(f.name, f.dept, f.ou) - ), - o.userContracts.headOption.map(c => - DetailOsoby.PracovniPomer.ViewModel(c.rel, c.startDate, c.endDate) - ) - ) - -extension (param: Parameter) - def toParametr(container: Parameter => Anchor): DetailParametru.ViewModel = - DetailParametru.ViewModel( - id = param.id, - nazev = param.name, - popis = param.description, - status = "Nesplněno", - statusColor = Color.red, - a = container(param) - ) - -extension (crit: ParameterCriteria) - def toKriterium( - container: ParameterCriteria => Anchor - ): DetailKriteria.ViewModel = - DetailKriteria.ViewModel( - nazev = crit.criteriumText, - kapitola = crit.chapterId, - bod = crit.itemId, - status = "Nesplněno", - statusColor = Color.red, - splneno = false, - dukazy = Nil, - container = container(crit) - ) - -extension (user: UserInfo) - def toUserRow( - container: UserInfo => HtmlElement = (_: UserInfo) => div() - ): UserRow.ViewModel = - UserRow.ViewModel( - osobniCislo = user.personalNumber.toString, - celeJmeno = user.name, - prijmeni = user.surname, - hlavniFunkce = user.mainFunction.map(_.name), - img = user.img, - container = () => container(user) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala deleted file mode 100644 index 7d5b2c1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.dashboard - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.Page - -object DashboardPage: - - def render: HtmlElement = - div("Dashboard page") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala deleted file mode 100644 index 9ef0698..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala +++ /dev/null @@ -1,31 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailKriteriaPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply($m: Signal[ViewModel])( - action: Observer[DukazyKriteria.Action] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - DukazyKriteria($m.map(_.kriterium.dukazy))(action) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala deleted file mode 100644 index 3b316e1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object DetailPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametry: SeznamParametru.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby($m.map(_.osoba)), - SeznamParametru($m.map(_.parametry)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala deleted file mode 100644 index 17224cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala +++ /dev/null @@ -1,24 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailParametruPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriteria: SeznamKriterii.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby.header($m.map(_.osoba)), - DetailParametru($m.map(_.parametr)), - SeznamKriterii($m.map(_.kriteria)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala deleted file mode 100644 index dc912fa..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ /dev/null @@ -1,107 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb -package app -package pages.detail - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.UpravDukazForm -import fiftyforms.services.files.File - -object UpravDukaz: - - type ThisPage = Page.UpravDukazKriteria - type PageKey = (OsobniCislo, String, String) - - trait State { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def availableFiles: EventStream[List[File]] - def actionBus: Observer[Action] - } - - def keyOfPage(page: ThisPage): PageKey = - (page.osobniCislo.value, page.parametr.value, page.kriterium.value) - - def onChangeAction(key: PageKey): Action = - FetchParameterCriteria( - key._1, - key._2, - key._3, - Page.UpravDukazKriteria(_, _, _) - ) - - class Connector(state: State)($page: Signal[ThisPage])(using Router[Page]): - val $paramChangeSignal = $page.splitOne(keyOfPage)((x, _, _) => x) - val $pageChangeSignal = $paramChangeSignal.map(onChangeAction) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.split(_ => ())((_, s, $s) => - PageComponent( - $s.map(buildModel), - state.availableFiles, - state.actionBus.contramap { - case UpravDukazForm.Cancelled => - NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) - case UpravDukazForm.AvailableFilesRequested => - FetchAvailableFiles(s._1.personalNumber) - } - ) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): PageComponent.ViewModel = - import connectors.* - PageComponent.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) - - object PageComponent: - import components.* - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply( - $m: Signal[ViewModel], - availableFilesStream: EventStream[List[File]], - events: Observer[UpravDukazForm.Event] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - UpravDukazForm(availableFilesStream)(events) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala deleted file mode 100644 index ca311b0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.Icons -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Color - -object DetailKriteria: - case class ViewModel( - nazev: String, - kapitola: String, - bod: String, - status: String, - statusColor: Color, - splneno: Boolean, - dukazy: List[DukazKriteria.ViewModel], - container: HtmlElement = div() - ) { - val id = s"${kapitola}${bod}" - } - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h3( - cls := "text-xl leading-6 font-bold text-gray-900", - child.text <-- $m.map(_.nazev) - ), - h3( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - "Kapitola ", - child.text <-- $m.map(_.kapitola), - " bod ", - child <-- $m.map(_.bod) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala deleted file mode 100644 index ae90290..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala +++ /dev/null @@ -1,100 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Avatar - -object DetailOsoby: - - object Funkce: - case class ViewModel( - nazev: String, - stredisko: String, - voj: String - ) - - def render($m: Signal[ViewModel]): HtmlElement = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.nazev), - span( - cls := "hidden md:inline", - " @ ", - child.text <-- $m.map(_.stredisko), - ", ", - child.text <-- $m.map(_.voj) - ) - ) - - object PracovniPomer: - case class ViewModel( - druh: String, - pocatek: LocalDate, - konec: Option[LocalDate] - ) - - def render($m: Signal[ViewModel]): HtmlElement = - import fiftyforms.ui.components.tailwind.CustomAttrs.datetime - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.druh), - " od ", - time( - datetime <-- $m.map(_.pocatek.toString), - child.text <-- $m.map(_.pocatek.toString) - ) - ) - - case class ViewModel( - osobniCislo: OsobniCislo, - jmeno: String, - email: Option[String], - telefon: Option[String], - img: Option[String], - hlavniFunkce: Option[Funkce.ViewModel], - pracovniPomer: Option[PracovniPomer.ViewModel] - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-4", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(16) - ), - div( - h1( - cls := "text-2xl font-bold text-gray-900", - child.text <-- $m.map(_.jmeno) - ), - child.maybe <-- $m.map(_.hlavniFunkce).split(_ => ())((_, _, d) => - Funkce.render(d) - ), - child.maybe <-- $m.map(_.pracovniPomer).split(_ => ())((_, _, d) => - PracovniPomer.render(d) - ) - ) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-5", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(8) - ), - div( - h1( - cls := "text-lg font-medium text-gray-800", - child.text <-- $m.map(_.jmeno) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala deleted file mode 100644 index e11cb62..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color - -object DetailParametru: - case class ViewModel( - id: String, - nazev: String, - popis: String, - status: String, - statusColor: Color, - a: Anchor - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h2( - cls := "text-2xl leading-6 font-medium text-gray-900", - child.text <-- $m.map(_.nazev) - ), - p( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - child.text <-- $m.map(_.popis) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - h2( - cls := "text-lg leading-6 font-medium text-gray-600", - child.text <-- $m.map(_.nazev) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala deleted file mode 100644 index a00ede8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala +++ /dev/null @@ -1,177 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.CustomAttrs - -object DukazKriteria: - case class Osoba(osobniCislo: String, jmeno: String) - case class Udalost(osoba: Osoba, datum: LocalDate) - case class Dokument( - url: String, - nazev: String, - pridal: Udalost, - odebral: Option[Udalost] - ) - case class ViewModel( - dokumenty: List[Dokument], - autorizoval: Option[Udalost], - platiDo: Option[LocalDate], - poznámka: Option[String] - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-lg", - div( - cls := "px-4 py-5 sm:px-6", - h3( - cls := "text-lg leading-6 font-medium text-gray-900", - """Applicant Information""" - ), - p( - cls := "mt-1 max-w-2xl text-sm text-gray-500", - """Personal details and application.""" - ) - ), - div( - cls := "border-t border-gray-200 px-4 py-5 sm:px-6", - dl( - cls := "grid grid-cols-1 gap-x-4 gap-y-8 sm:grid-cols-2", - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Full name""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Margot Foster""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Application for""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Backend Developer""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Email address""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """margotfoster@example.com""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Salary expectation""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """$120,000""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """About""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Fugiat ipsum ipsum deserunt culpa aute sint do nostrud anim incididunt cillum culpa consequat. Excepteur qui ipsum aliquip consequat sint. Sit id mollit nulla mollit nostrud in ea officia proident. Irure nostrud pariatur mollit ad adipisicing reprehenderit deserunt qui eu.""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """Attachments""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - ul( - role := "list", - cls := "border border-gray-200 rounded-md divide-y divide-gray-200", - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """resume_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ), - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """coverletter_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala deleted file mode 100644 index 22f134f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala +++ /dev/null @@ -1,35 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object DukazyKriteria: - sealed trait Action - case object Add extends Action - - type ViewModel = List[DukazKriteria.ViewModel] - - def apply($m: Signal[ViewModel])(actions: Observer[Action]): HtmlElement = - div( - child.maybe <-- $m.map(m => - if (m.isEmpty) then Some(prazdnyDukaz(actions)) - else None - ), - children <-- $m.map(_.zipWithIndex).split(_._2)((_, _, $s) => - DukazKriteria($s.map(_._1)) - ) - ) - - private def prazdnyDukaz(actions: Observer[Action]): HtmlElement = - button( - tpe := "button", - onClick.preventDefault.mapTo(Add) --> actions, - cls := "relative block w-full border-2 border-gray-300 border-dashed rounded-lg p-12 text-center hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - Icons.outline - .`document-add`(12) - .amend(svg.cls := "mx-auto text-gray-400"), - span( - cls := "mt-2 block text-sm font-medium text-gray-900", - "Přidat důkaz" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala deleted file mode 100644 index f628697..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.list.StackedList -import fiftyforms.ui.components.tailwind.list.ListRow -import fiftyforms.ui.components.tailwind.list.RowTag -import fiftyforms.ui.components.tailwind.list.RowNext -import java.time.LocalDate - -object SeznamKriterii: - type ViewModel = List[DetailKriteria.ViewModel] - - private val kritList = new StackedList[DetailKriteria.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - kritList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = - p(cls := "text-sm text-gray-500", s"${i.kapitola}${i.bod}"), - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.container - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala deleted file mode 100644 index 2bfe4bc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.list.{ - StackedList, - ListRow, - RowTag, - PropList, - IconText, - RowNext -} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.LinkSupport.* - -object SeznamParametru: - type ViewModel = List[DetailParametru.ViewModel] - - private val parametrList = new StackedList[DetailParametru.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - parametrList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = emptyNode, - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.a - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala deleted file mode 100644 index f048e9d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ /dev/null @@ -1,101 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.form.* -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement -import fiftyforms.services.files.components.tailwind.FilePicker -import cz.e_bs.cmi.mdr.pdb.frontend.AutorizujDukaz -import cz.e_bs.cmi.mdr.pdb.frontend.DocumentRef -import fiftyforms.services.files.File - -object UpravDukazForm: - sealed trait Event - case object Cancelled extends Event - case object AvailableFilesRequested extends Event - def apply(availableFilesStream: EventStream[List[File]])( - updates: Observer[Event] - ): HtmlElement = - val (filesStream, filesObserver) = - EventStream.withObserver[FilePicker.Event] - val files = Var[List[File]](Nil) - def submitButtons: HtmlElement = - div( - cls := "pt-5", - div( - cls := "flex justify-end", - button( - tpe := "button", - cls := "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zrušit", - onClick.mapTo(Cancelled) --> updates - ), - button( - tpe := "submit", - cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - disabled <-- files.signal.map(_.isEmpty), - "Autorizovat" - ) - ) - ) - - div( - filesStream.collect { case FilePicker.SelectionUpdated(files) => - files.to(List) - } --> files.writer, - filesStream.collect { case FilePicker.AvailableFilesRequested => - AvailableFilesRequested - } --> updates, - cls := "bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6", - Form( - Form.Body( - Form.Section( - FormHeader( - FormHeader.ViewModel( - "Doložení kritéria", - "Sestavte doklady poskytující důkaz kritéria a potvrďte odesláním formuláře. Případné limitace či jiné relevantní údaje vepište do pole Komentář." - ) - ), - FormFields( - FormRow( - "dokumenty", - "Dokumenty", - FilePicker(files.signal, availableFilesStream)(filesObserver) - .amend(idAttr := "dokumenty", cls("max-w-lg")) - ).toHtml, - FormRow( - "platnost", - "Platnost", - input( - idAttr := "platnost", - name := "platnost", - tpe := "date", - autoComplete := "date", - cls := "block max-w-lg w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" - ) - ).toHtml, - FormRow( - "komentar", - "Komentář", - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - textArea( - idAttr := "komentar", - name := "about", - rows := 3, - cls := "max-w-lg shadow-sm block w-full focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border border-gray-300 rounded-md" - ), - p( - cls := "mt-2 text-sm text-gray-500", - "Doplňte prosím potřebné informace související s doložením kritéria, včetně případných limitací." - ) - ) - ).toHtml - ) - ) - ), - submitButtons - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala deleted file mode 100644 index 41033f6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -import components._ - -object DirectoryPage: - - type ViewModel = List[UserRow.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - val (actionsStream, actionObserver) = - EventStream.withObserver[SearchForm.Action] - val $filter = actionsStream - .collect { - case SearchForm.ClearFilter => None - case SearchForm.SetFilter(t) => Some(t) - } - .startWith(None) - val byLetter = for { - d <- $m - f <- $filter - } yield for { - (letter, users) <- d - .filter { user => - f match - case None => true - case Some(t) => user.search.contains(t) - } - .groupBy(_.prijmeni.head) - .to(List) - .sortBy(_._1) - } yield (letter.toString, users.sortBy(_.prijmeni)) - - div( - cls := "h-full max-w-7xl mx-auto order-first flex flex-col flex-shrink-0", - SearchForm(actionObserver), - Directory(byLetter) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala deleted file mode 100644 index 73f915d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -object Directory: - - object Header: - type ViewModel = String - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500", - h3(child.text <-- $m) - ) - - object UserList: - type ViewModel = List[UserRow.ViewModel] - def apply($m: Signal[ViewModel]): HtmlElement = - ul( - role := "list", - cls := "relative z-0 divide-y divide-gray-200", - children <-- $m.split(_.osobniCislo)((_, _, s) => UserRow(s)) - ) - - type ViewModel = List[(String, List[UserRow.ViewModel])] - def apply($m: Signal[ViewModel]): HtmlElement = - nav( - cls := "flex-1 min-h-0 overflow-y-auto", - aria.label := "Directory", - children <-- $m.split(_._1)((_, _, s) => - div( - cls := "relative", - Header(s.map(_._1)), - UserList(s.map(_._2)) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala deleted file mode 100644 index e182910..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala +++ /dev/null @@ -1,54 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object SearchForm: - sealed trait Action - case object SubmitFilter extends Action - sealed trait FilterAction extends Action - case object ClearFilter extends FilterAction - case class SetFilter(value: String) extends FilterAction - - def apply(actions: Observer[Action]): HtmlElement = - div( - cls := "px-6 pt-4 pb-4", - form( - cls := "flex space-x-4", - onSubmit.mapTo(SubmitFilter) --> actions, - div( - cls := "flex-1 min-w-0", - label( - forId := "search", - cls := "sr-only", - "Hledat" - ), - div( - cls := "relative rounded-md shadow-sm", - div( - cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", - Icons.solid.search().amend(svg.cls := "text-gray-400") - ), - input( - tpe := "search", - name := "search", - idAttr := "search", - cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", - placeholder := "Hledat", - composeEvents(onInput.mapToValue.setAsValue.map(SetFilter(_)))( - _.throttle(500) - ) --> actions - ) - ) - ), - button( - tpe := "submit", - cls := "inline-flex justify-center px-3.5 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500", - Icons.solid.filter().amend(svg.cls := "text-gray-400"), - span( - cls := "sr-only", - "Hledat" - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala deleted file mode 100644 index 82f7723..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala +++ /dev/null @@ -1,52 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.airstream.core.Signal -import fiftyforms.ui.components.tailwind.Avatar - -object UserRow: - case class ViewModel( - osobniCislo: String, - celeJmeno: String, - prijmeni: String, - hlavniFunkce: Option[String], - img: Option[String], - container: () => HtmlElement = () => div() - ) { - val search = prijmeni.toLowerCase - } - - def apply($m: Signal[ViewModel]): HtmlElement = - inline def avatarImage = - Avatar($m.map(_.img)).avatarImage(10) - - li( - div( - cls := "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500", - div( - cls := "flex-shrink-0", - child <-- avatarImage - ), - div( - cls := "flex-1 min-w-0", - child <-- $m.map { o => - o.container() - .amend( - cls := "focus:outline-none", - span( - cls := "absolute inset-0", - aria.hidden := true - ), - p( - cls := "text-sm font-medium text-gray-900", - o.celeJmeno - ), - p( - cls := "text-sm text-gray-500 truncate", - o.hlavniFunkce - ) - ) - } - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala deleted file mode 100644 index 467c5f5..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala +++ /dev/null @@ -1,99 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router - -object ErrorPage: - case class ViewModel( - homePage: Page, - errorName: String, - title: String, - subTitle: String - ) - - def apply(m: ViewModel, actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - val ViewModel(homePage, errorName, title, subTitle) = m - div( - cls := "min-h-full pt-16 pb-12 flex flex-col bg-white", - main( - cls := "flex-grow flex flex-col justify-center max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex-shrink-0 flex justify-center", - a( - href := "/", - cls := "inline-flex", - span( - cls := "sr-only", - """Workflow""" - ), - img( - cls := "h-12 w-auto", - src := "https://tailwindui.com/img/logos/workflow-mark.svg?color=indigo&shade=600", - alt := "" - ) - ) - ), - div( - cls := "py-16", - div( - cls := "text-center", - p( - cls := "text-sm font-semibold text-indigo-600 uppercase tracking-wide", - errorName - ), - h1( - cls := "mt-2 text-4xl font-extrabold text-gray-900 tracking-tight sm:text-5xl", - title - ), - p( - cls := "mt-2 text-base text-gray-500", - subTitle - ), - div( - cls := "mt-6", - PageLink - .container(homePage, actionBus) - .amend( - cls := "text-base font-medium text-indigo-600 hover:text-indigo-500", - """Go back home""" - ) - ) - ) - ) - ), - footer( - cls := "flex-shrink-0 max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", - nav( - cls := "flex justify-center space-x-4", - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Contact Support""" - ), - span( - cls := "inline-block border-l border-gray-300", - aria.hidden := true - ), - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Status""" - ), - span( - cls := "inline-block border-l border-gray-300", - aria.hidden := true - ), - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Twitter""" - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala deleted file mode 100644 index bd9baec..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action - -object NotFoundPage: - def apply(homePage: Page, url: String, actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - ErrorPage( - ErrorPage.ViewModel( - homePage, - "404 error", - "Page not found.", - s"Sorry, but page $url doesn't exist." - ), - actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala deleted file mode 100644 index 476fe3c..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action - -object UnhandledErrorPage: - - case class ViewModel( - homePage: Page, - errorName: Option[String], - errorMessage: Option[String] - ) - - def apply(m: ViewModel, actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - ErrorPage( - ErrorPage.ViewModel( - m.homePage, - "Unexpected error occurred", - m.errorName.getOrElse( - "Uh oh!" - ), // TODO: translations, better text than uh oh - m.errorMessage.getOrElse( - "This wasn't supposed to happen! Please try again." - ) - ), - actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala deleted file mode 100644 index 25f059f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala +++ /dev/null @@ -1,6 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.services - -import com.raquo.airstream.core.Observer - -trait DataFetcher[K, A]: - def fetch(id: K, sink: Observer[A]): Unit diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala deleted file mode 100644 index a469830..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala +++ /dev/null @@ -1,120 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package state - -import com.raquo.airstream.core.EventStream -import cz.e_bs.cmi.mdr.pdb.{UserInfo, OsobniCislo} -import com.raquo.airstream.core.Observer -import scala.scalajs.js -import scala.scalajs.js.JSON -import zio.json._ -import com.raquo.airstream.eventbus.EventBus -import com.raquo.airstream.ownership.Owner -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.UserFunction -import cz.e_bs.cmi.mdr.pdb.UserContract -import fiftyforms.services.files.File - -trait AppState - extends connectors.DetailPageConnector.AppState - with connectors.DetailParametruPageConnector.AppState - with connectors.DetailKriteriaPageConnector.AppState - with pages.detail.UpravDukaz.State: - def users: EventStream[List[UserInfo]] - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - -class MockAppState(implicit owner: Owner, router: Router[Page]) - extends AppState: - - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonDecoder[UserFunction] = DeriveJsonDecoder.gen - given JsonDecoder[UserContract] = DeriveJsonDecoder.gen - given JsonDecoder[UserInfo] = DeriveJsonDecoder.gen - - given JsonDecoder[ParameterCriteria] = DeriveJsonDecoder.gen - given JsonDecoder[Parameter] = DeriveJsonDecoder.gen - - private val actions = EventBus[Action]() - private val (parametersStream, pushParameters) = - EventStream.withCallback[List[Parameter]] - private val (usersStream, pushUsers) = - EventStream.withCallback[List[UserInfo]] - private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] - private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] - - private val mockData: List[UserInfo] = - mockUsers - .asInstanceOf[js.Dictionary[js.Object]] - .values - // TODO: is there a more efficient way to parse from JS object directly? - .map { o => - val parsed = JSON.stringify(o).fromJson[UserInfo] - parsed.left.foreach(org.scalajs.dom.console.log(_)) - parsed - } - .collect { case Right(u) => - u - } - .toList - - private val mockParameters: List[Parameter] = - pdbParams - .asInstanceOf[js.Dictionary[js.Object]] - .values - .map(o => JSON.stringify(o).fromJson[Parameter]) - .collect { case Right(p) => p } - .toList - - // TODO: Extract to separate event handler - actions.events.foreach { - case FetchDirectory => pushUsers(mockData) - case FetchUserDetails(osc) => - mockData.find(_.personalNumber == osc).foreach { o => - pushDetails(o) - router.replaceState(Page.Detail(o)) - } - case FetchParameters(osc) => - pushParameters(mockParameters) - case FetchParameter(osc, paramId) => - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(Page.DetailParametru(o, p)) - case FetchParameterCriteria(osc, paramId, critId, page) => - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - c <- p.criteria.find(_.id == critId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(page(o, p, c)) - case NavigateTo(page) => router.pushState(page) - case FetchAvailableFiles(osc) => - pushFiles( - List( - File("https://tc163.cmi.cz/here", "Example file") - ) - ) - } - - override def users: EventStream[List[UserInfo]] = - usersStream.debugWithName("users") - - override def details: EventStream[UserInfo] = - detailsStream.debugWithName("details") - - override def parameters: EventStream[List[Parameter]] = - parametersStream.debugWithName("parameters") - - override def availableFiles: EventStream[List[File]] = - filesStream.debugWithName("available files") - - override def actionBus: Observer[Action] = - actions.writer.debugWithName("actions writer") diff --git a/app/src/main/scala/mdr/pdb/app/Main.scala b/app/src/main/scala/mdr/pdb/app/Main.scala new file mode 100644 index 0000000..081b4ee --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/Main.scala @@ -0,0 +1,103 @@ +package mdr.pdb.app + +import scala.scalajs.js.annotation.JSExportTopLevel +import scala.scalajs.js.annotation.JSExport +import scala.scalajs.js.annotation.JSImport +import scala.scalajs.js +import org.scalajs.dom +import com.raquo.laminar.api.L.{*, given} +import scala.scalajs.js.Date +import com.raquo.waypoint.Router +import com.raquo.waypoint.SplitRender +import mdr.pdb.app.services.DataFetcher +import scala.scalajs.js.JSON +import zio.json._ +import mdr.pdb.UserInfo +import mdr.pdb.app.state.AppState + +@js.native +@JSImport("stylesheets/main.css", JSImport.Namespace) +object Css extends js.Any + +@js.native +@JSImport("data/users.json", JSImport.Default) +object mockUsers extends js.Object + +@js.native +@JSImport("params/pdb-params.json", JSImport.Default) +object pdbParams extends js.Object + +@JSExportTopLevel("app") +object Main: + + @JSExport + def main(args: Array[String]): Unit = + import Routes.given + onLoad { + setupAirstream() + val appContainer = dom.document.querySelector("#app") + val _ = + render( + appContainer, + renderPage(state.MockAppState(using unsafeWindowOwner, router)) + ) + } + + private def onLoad(f: => Unit): Unit = + documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) + + private def setupAirstream()(using router: Router[Page]): Unit = + AirstreamError.registerUnhandledErrorCallback(err => + router.forcePage( + Page.UnhandledError( + Some(err.getClass.getName), // TODO: Fill only in dev mode + Some(err.getMessage) + ) + ) + ) + + def renderPage(state: AppState)(using + router: Router[Page] + ): HtmlElement = + val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) + .collectSignal[Page.Detail]( + connectors + .DetailPageConnector(state)(_) + .apply + ) + .collectSignal[Page.DetailParametru]( + connectors + .DetailParametruPageConnector(state)(_) + .apply + ) + .collectSignal[Page.DetailKriteria]( + connectors + .DetailKriteriaPageConnector(state)(_) + .apply + ) + .collectSignal[Page.UpravDukazKriteria]( + pages.detail.UpravDukaz.Connector(state)(_).apply + ) + .collectStatic(Page.Dashboard)( + connectors.DashboardPageConnector(state.actionBus).apply + ) + .collect[Page.NotFound](pg => + pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) + ) + .collect[Page.UnhandledError](pg => + pages.errors + .UnhandledErrorPage( + pages.errors.UnhandledErrorPage + .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), + state.actionBus + ) + ) + .collectStatic(Page.Directory)( + connectors + .DirectoryPageConnector(state.users, state.actionBus) + .apply + ) + div(cls := "h-full", child <-- pageSplitter.$view) + + // Pull in the stylesheet + val css: Css.type = Css diff --git a/app/src/main/scala/mdr/pdb/app/Routes.scala b/app/src/main/scala/mdr/pdb/app/Routes.scala new file mode 100644 index 0000000..2cc860c --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/Routes.scala @@ -0,0 +1,198 @@ +package mdr.pdb.app + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.* +import org.scalajs.dom +import zio.json.{*, given} +import mdr.pdb.OsobniCislo + +import scala.scalajs.js +import mdr.pdb.UserInfo +import mdr.pdb.Parameter +import mdr.pdb.ParameterCriteria +import mdr.pdb.app.Page.Titled + +// enum is not working with Waypoints' SplitRender collectStatic +sealed abstract class Page( + val id: String, + val title: String, + val parent: Option[Page] +) { + val path: Vector[Page] = + parent match + case None => Vector(this) + case Some(p) => p.path :+ this + + val isRoot: Boolean = parent.isEmpty +} + +object Page: + + case class Titled[V](value: V, title: Option[String] = None): + val show: String = title.getOrElse(value.toString) + + case object Directory extends Page("directory", "Adresář", None) + + case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) + + case class Detail(osobniCislo: Titled[OsobniCislo]) + extends Page("user", osobniCislo.show, Some(Directory)) + + object Detail { + def apply(o: UserInfo): Detail = Detail( + Titled(o.personalNumber, Some(o.name)) + ) + } + + case class DetailParametru( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String] + ) extends Page( + "parameter", + parametr.show, + Some(Detail(osobniCislo)) + ) + + object DetailParametru { + def apply(o: UserInfo, p: Parameter): DetailParametru = + DetailParametru( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)) + ) + } + + case class DetailKriteria( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String], + kriterium: Titled[String] + ) extends Page( + "criteria", + kriterium.show, + Some(DetailParametru(osobniCislo, parametr)) + ) + + object DetailKriteria { + def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = + DetailKriteria( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)), + Titled(k.id, Some(k.id)) + ) + } + + case class UpravDukazKriteria( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String], + kriterium: Titled[String] + ) extends Page( + "addProof", + "Důkaz", + Some(DetailKriteria(osobniCislo, parametr, kriterium)) + ) + + object UpravDukazKriteria { + def apply( + o: UserInfo, + p: Parameter, + k: ParameterCriteria + ): UpravDukazKriteria = + UpravDukazKriteria( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)), + Titled(k.id, Some(k.id)) + ) + } + + case class NotFound(url: String) extends Page("404", "404", Some(Directory)) + + case class UnhandledError( + errorName: Option[String], + errorMessage: Option[String] + ) extends Page("500", "Unexpected error", Some(Directory)) + +object Routes: + given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) + given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) + given [V: JsonEncoder]: JsonEncoder[Titled[V]] = + DeriveJsonEncoder.gen[Titled[V]] + given [V: JsonDecoder]: JsonDecoder[Titled[V]] = + DeriveJsonDecoder.gen[Titled[V]] + given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] + given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] + + val base = + js.`import`.meta.env.BASE_URL + .asInstanceOf[String] + "app" + + val homePage: Page = Page.Directory + + given router: Router[Page] = Router[Page]( + routes = List( + Route.static(homePage, root / endOfSegments, basePath = base), + Route.static( + Page.Dashboard, + root / "dashboard" / endOfSegments, + basePath = base + ), + Route[Page.Detail, String]( + encode = _.osobniCislo.value.toString, + decode = osc => Page.Detail(Titled(OsobniCislo(osc))), + root / "osoba" / segment[String] / endOfSegments, + basePath = base + ), + Route[Page.DetailParametru, (String, String)]( + encode = p => (p.osobniCislo.value.toString, p.parametr.value), + decode = + p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / endOfSegments, + basePath = base + ), + Route[Page.DetailKriteria, (String, String, String)]( + encode = p => + ( + p.osobniCislo.value.toString, + p.parametr.value, + p.kriterium.value.replaceAll("\\.", "--") + ), + decode = p => + Page.DetailKriteria( + Titled(OsobniCislo(p._1)), + Titled(p._2), + Titled(p._3.replaceAll("--", ".")) + ), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / "kriterium" / segment[String] / endOfSegments, + basePath = base + ), + Route[Page.UpravDukazKriteria, (String, String, String)]( + encode = p => + ( + p.osobniCislo.value.toString, + p.parametr.value, + p.kriterium.value.replaceAll("\\.", "--") + ), + decode = p => + Page.UpravDukazKriteria( + Titled(OsobniCislo(p._1)), + Titled(p._2), + Titled(p._3.replaceAll("--", ".")) + ), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / "kriterium" / segment[String] / "edit" / endOfSegments, + basePath = base + ) + ), + serializePage = _.toJson, + deserializePage = _.fromJson[Page] + .fold(s => throw IllegalStateException(s), identity), + getPageTitle = _.title, + routeFallback = url => Page.NotFound(url), + deserializeFallback = _ => Page.Dashboard + )( + $popStateEvent = windowEvents.onPopState, + owner = unsafeWindowOwner + ) diff --git a/app/src/main/scala/mdr/pdb/app/actions.scala b/app/src/main/scala/mdr/pdb/app/actions.scala new file mode 100644 index 0000000..d078b3a --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/actions.scala @@ -0,0 +1,21 @@ +package mdr.pdb.app + +import mdr.pdb.OsobniCislo +import mdr.pdb.UserInfo +import mdr.pdb.Parameter +import mdr.pdb.ParameterCriteria + +sealed trait Action + +case object FetchDirectory extends Action +case class FetchUserDetails(osc: OsobniCislo) extends Action +case class FetchParameters(osc: OsobniCislo) extends Action +case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action +case class FetchParameterCriteria( + osc: OsobniCislo, + paramId: String, + critId: String, + page: (UserInfo, Parameter, ParameterCriteria) => Page +) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action +case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/mdr/pdb/app/components/AppPage.scala new file mode 100644 index 0000000..130c817 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/AppPage.scala @@ -0,0 +1,82 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.Page +import mdr.pdb.{UserProfile, UserInfo, OsobniCislo} +import com.raquo.waypoint.Router +import mdr.pdb.app.Action +import mdr.pdb.app.NavigateTo +import mdr.pdb.UserFunction + +object AppPage: + // TODO: pages by logged in user + val pages: List[Page] = List(Page.Directory, Page.Dashboard) + + import NavigationBar.{Logo, MenuItem} + + val logo = Logo( + "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", + "Workflow" + ) + + // TODO: menu items by user profile + val userMenu = + List( + MenuItem("Your Profile"), + MenuItem("Settings"), + MenuItem("Sign out") + ) + + // TODO: load user profile + val $userProfile = Var( + UserProfile( + "tom", + UserInfo( + OsobniCislo("1031"), + "tom", + "Tom", + "Cook", + None, + None, + Some("tom@example.com"), + Some("+420 222 866 180"), + Some( + UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") + ), + Nil, + None + ) + ) + ) + + val $userInfo = $userProfile.signal.map(_.userInfo) + + type ViewModel = Option[HtmlElement] + def apply( + actionBus: Observer[Action] + )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using + router: Router[Page] + ): HtmlElement = + PageLayout(actionBus)( + $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => + PageLayout.ViewModel( + NavigationBar.ViewModel( + u, + pages.map(p => + NavigationBar.Link( + () => + PageLink( + p, + actionBus + ), + p == cp + ) + ), + userMenu, + logo + ), + c + ) + ), + mods + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala new file mode 100644 index 0000000..9375398 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala @@ -0,0 +1,143 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.tailwind.list.IconText.ViewModel +import fiftyforms.ui.components.tailwind.Icons +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.Page + +import com.raquo.laminar.api.L.{*, given} +import io.laminext.syntax.core.{*, given} +import mdr.pdb.app.Action + +object Breadcrumbs: + + private def slash = + import svg.{*, given} + svg( + cls := "flex-shrink-0 h-5 w-5 text-gray-300", + xmlns := "http://www.w3.org/2000/svg", + fill := "currentColor", + viewBox := "0 0 20 20", + ariaHidden := true, + path( + d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" + ) + ) + + def backIcon = + Icons.solid + .`arrow-narrow-left`() + .amend( + svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" + ) + + object Link: + case class ViewModel( + page: Page, + icon: Option[SvgElement], + extraClasses: String, + text: String, + textClasses: Option[String] = None + ) + + val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" + + def shortHome(p: Page) = ViewModel( + p, + Some(backIcon), + "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", + "Zpět na úvodní stránku", + None + ) + + def fullHome(p: Page) = ViewModel( + p, + Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), + "text-gray-400 hover:text-gray-500", + "Domů", + Some("sr-only") + ) + + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + PageLink + .container($m.map(_.page), actionBus) + .amend( + cls <-- $m.map(_.extraClasses), + child.maybe <-- $m.map(_.icon), + span( + cls <-- $m.map(_.textClasses.getOrElse("")), + child.text <-- $m.map(_.text) + ) + ) + + object FullBreadcrumbs: + type ViewModel = Page + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + ol( + role := "list", + cls := "flex items-center space-x-4", + children <-- $m.map(_.path).split(_.id)((_, _, $p) => + li( + div( + cls := "flex items-center", + child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), + Link( + $p.map(p => + if (p.isRoot) then Link.fullHome(p) + else + Link.ViewModel( + p, + None, + s"ml-4 max-w-xs truncate ${Link.baseClasses}", + p.title + ) + ), + actionBus + ) + ) + ) + ) + ) + + object ShortBreadcrumbs: + type ViewModel = Page + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + Link( + $m.map { p => + val target = p.parent.getOrElse(p) + if target.isRoot then Link.shortHome(target) + else + Link.ViewModel( + target, + Some(backIcon), + "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", + s"Zpět na ${target.title}" + ) + }, + actionBus + ) + + def apply(actionBus: Observer[Action])(using + router: Router[Page] + ): HtmlElement = + nav( + cls := "flex", + aria.label := "Breadcrumb", + div( + cls := "flex sm:hidden", + ShortBreadcrumbs(router.$currentPage, actionBus) + ), + div( + cls := "hidden sm:block", + FullBreadcrumbs(router.$currentPage, actionBus) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala new file mode 100644 index 0000000..4bb8b8d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala @@ -0,0 +1,242 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent +import fiftyforms.ui.components.tailwind.Icons +import fiftyforms.ui.components.tailwind.Avatar +import mdr.pdb.UserInfo +import io.laminext.syntax.core.* + +object NavigationBar: + + case class Logo(img: String, name: String) + case class Link(a: () => Anchor, active: Boolean) + case class MenuItem(title: String) + + case class ViewModel( + userInfo: UserInfo, + pages: List[Link], + userMenu: List[MenuItem], + logo: Logo + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + val $userInfo = $m.map(_.userInfo) + val mobileMenuOpen = Var(false) + + val desktopOnly = cls("hidden md:block") + val mobileOnly = cls("md:hidden") + + inline def avatarImage(size: Int = 8) = + Avatar($userInfo.map(_.img)).avatarImage(size) + + def notificationButton = button( + tpe := "button", + cls := List( + "bg-indigo-600", + "focus:outline-none", + "focus:ring-2", + "focus:ring-offset-2", + "focus:ring-offset-indigo-600", + "focus:ring-white", + "hover:text-white", + "p-1", + "rounded-full", + "text-indigo-200" + ), + span(cls := "sr-only", "View notifications"), + Icons.outline.bell() + ) + + def userProfile: HtmlElement = + val menuOpen = Var(false) + + def menuItem(item: MenuItem, idx: Int): HtmlElement = + a( + href := "#", + cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", + role := "menuitem", + tabIndex := -1, + idAttr := s"user-menu-item-$idx", + item.title + ) + + div( + cls := "ml-3 relative", + div( + button( + tpe := "button", + cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", + idAttr := "user-menu-button", + aria.expanded <-- menuOpen.signal, + aria.hasPopup := true, + span(cls := "sr-only", "Open user menu"), + child <-- avatarImage(), + onClick.preventDefault.mapTo( + !menuOpen.now() + ) --> menuOpen.writer + ) + ), + /* + * */ + div( + cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", + cls <-- menuOpen.signal.map { o => + if (o) "md:block" else "md:hidden" + }, + role := "menu", + aria.orientation := "vertical", + aria.labelledBy := "user-menu-button", + tabIndex := -1, + // : keyboard navigation + children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) + ) + ) + + def mobileProfile = + def menuItem(item: MenuItem): HtmlElement = + a( + href := "#", + cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", + item.title + ) + + div( + cls := "pt-4 pb-3 border-t border-indigo-700", + div( + cls := "flex items-center px-5", + div( + cls := "flex-shrink-0", + child <-- avatarImage(10) + ), + div( + cls := "ml-3", + div( + cls := "text-base font-medium text-white", + child.text <-- $userInfo.map(_.name) + ), + child.maybe <-- $userInfo.map( + _.email.map(e => + div( + cls := "text-sm font-medium text-indigo-300", + e + ) + ) + ) + ), + notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) + ), + div( + cls := "mt-3 px-2 space-y-1", + children <-- $m.map(_.userMenu.map(menuItem)) + ) + ) + + def pageLink(page: Link): Anchor = + page + .a() + .amend( + cls := "text-white px-3 py-2 rounded-md text-sm font-medium", + cls := Seq( + "bg-indigo-700" -> page.active, + "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active + ), + ariaCurrent := (if page.active then "page" else "false") + ) + + def logoImg: Image = + img( + cls := "h-8 w-8", + src <-- $m.map(_.logo.img), + alt <-- $m.map(_.logo.name) + ) + + def mobileMenuButton = button( + tpe := "button", + cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", + aria.controls := "mobile-menu", + aria.expanded <-- mobileMenuOpen.signal, + span(cls := "sr-only", "Open main menu"), + Icons.outline + .menu() + .amend( + svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") + ), + Icons.outline + .x() + .amend( + svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") + ), + onClick.preventDefault.mapTo( + !mobileMenuOpen.now() + ) --> mobileMenuOpen.writer + ) + + def navBarLeft = + div( + cls := "flex items-center", + div(cls := "flex-shrink-0", logoImg), + div( + desktopOnly, + div( + cls := "ml-10 flex items-baseline space-x-4", + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) + ) + ) + ) + + def navBarRight = + div( + desktopOnly, + div( + cls := "ml-4 flex items-center md:ml-6", + notificationButton, + userProfile + ) + ) + + def navBarMobile = + div( + cls := "-mr-2 flex", + mobileOnly, + mobileMenuButton + ) + + def navBar = + div( + cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", + div( + cls := "flex items-center justify-between h-16", + navBarLeft, + navBarRight, + navBarMobile + ) + ) + + def mobileMenu = + div( + mobileOnly, + cls <-- mobileMenuOpen.signal.map { o => + if (o) "block" else "hidden" + }, + idAttr := "mobile-menu", + div( + cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) + ), + mobileProfile + ) + + nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala new file mode 100644 index 0000000..c4cb10d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala @@ -0,0 +1,19 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.Page +import com.raquo.waypoint.Router +import mdr.pdb.app.Action + +object PageHeader: + def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = + header( + cls := "bg-white shadow-sm", + div( + cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", + h1( + cls := "text-lg leading-6 font-semibold text-gray-900", + Breadcrumbs(actionBus) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala new file mode 100644 index 0000000..aae4539 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala @@ -0,0 +1,30 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.Page +import mdr.pdb.app.Action +import fiftyforms.ui.components.tailwind.Loading +import io.laminext.syntax.core.* + +object PageLayout: + case class ViewModel( + navigation: NavigationBar.ViewModel, + content: Option[HtmlElement] + ) + def apply(actionBus: Observer[Action])( + $m: Signal[ViewModel], + mods: Modifier[HtmlElement]* + )(using router: Router[Page]): HtmlElement = + val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) + div( + cls := "h-full flex flex-col", + NavigationBar($m.map(_.navigation)), + child.maybe <-- router.$currentPage.map(_.isRoot) + .switch(None, Some(PageHeader(actionBus))), + main( + cls := "flex-grow-1 overflow-y-auto", + mods, + child <-- $maybeContent.map(_.getOrElse(Loading)) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/mdr/pdb/app/components/PageLink.scala new file mode 100644 index 0000000..e6e7052 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageLink.scala @@ -0,0 +1,40 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.LinkSupport.* +import mdr.pdb.app.Page +import mdr.pdb.app.Action +import com.raquo.waypoint.Router +import mdr.pdb.app.NavigateTo + +object PageLink: + type ViewModel = Page + def apply($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = + a(mods($m, actions), child.text <-- $m.map(_.title)) + + def apply(m: ViewModel, actions: Observer[Action])(using + router: Router[Page] + ): Anchor = apply(Val(m), actions) + + def container($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = a(mods($m, actions)) + + def container(m: ViewModel, actions: Observer[Action])(using + Router[Page] + ): Anchor = container(Val(m), actions) + + private def mods($m: Signal[Page], actions: Observer[Action])(using + router: Router[Page] + ): Modifier[Anchor] = + Seq( + href <-- $m.map(router.absoluteUrlForPage).recover { case _ => + Some("invalid url") + }, + composeEvents(onClick.noKeyMod.preventDefault)( + _.sample($m) + .map(NavigateTo.apply) + ) --> actions + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala new file mode 100644 index 0000000..7682772 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala @@ -0,0 +1,13 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.pages.dashboard.DashboardPage +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage + +class DashboardPageConnector(actionBus: Observer[Action])(using + router: Router[Page] +): + def apply: HtmlElement = + AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala new file mode 100644 index 0000000..76b9133 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala @@ -0,0 +1,76 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.Parameter +import mdr.pdb.UserInfo +import mdr.pdb.app.pages.detail.DetailParametruPage +import pages.detail.DetailKriteriaPage +import pages.detail.components.DukazyKriteria +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.ParameterCriteria + +object DetailKriteriaPageConnector { + trait AppState { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + } +} + +case class DetailKriteriaPageConnector( + state: DetailKriteriaPageConnector.AppState +)( + $page: Signal[Page.DetailKriteria] +)(using Router[Page]): + val $paramChangeSignal = + $page.splitOne(p => + (p.osobniCislo.value, p.parametr.value, p.kriterium.value) + )((x, _, _) => x) + val $pageChangeSignal = + $paramChangeSignal.map( + FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) + ) + + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + val $merged = + $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => + for { + da <- d + pa <- p + pb <- pa.find(_.id == pc._2) + ka <- pb.criteria.find(_.id == pc._3) + } yield (da, pb, ka) + ) + + def apply: HtmlElement = + AppPage(state.actionBus)( + $merged.map(_.map(buildModel)) + .split(_ => ())((_, s, $s) => + DetailKriteriaPage($s)(state.actionBus.contramap { + case DukazyKriteria.Add => + NavigateTo( + Page.UpravDukazKriteria( + Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), + Page.Titled(s.parametr.id, Some(s.parametr.nazev)), + Page.Titled(s.kriterium.id, Some(s.kriterium.id)) + ) + ) + }) + ), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: Parameter, + k: ParameterCriteria + ): DetailKriteriaPage.ViewModel = + DetailKriteriaPage.ViewModel( + o.toDetailOsoby, + p.toParametr(_ => a()), + k.toKriterium(_ => a()) + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DetailPageConnector.scala new file mode 100644 index 0000000..df3a6b7 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DetailPageConnector.scala @@ -0,0 +1,57 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.UserInfo +import pages.detail.DetailPage +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.pages.detail.components.DetailOsoby +import mdr.pdb.Parameter +import mdr.pdb.app.pages.detail.components.SeznamParametru +import fiftyforms.ui.components.tailwind.Color + +object DetailPageConnector { + trait AppState { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + } +} + +case class DetailPageConnector(state: DetailPageConnector.AppState)( + $page: Signal[Page.Detail] +)(using router: Router[Page]): + val $oscChangeSignal = $page.splitOne(_.osobniCislo.value)((osc, _, _) => osc) + val $pageChangeSignal = + $oscChangeSignal.flatMap(osc => + EventStream.fromSeq(Seq(FetchUserDetails(osc), FetchParameters(osc))) + ) + // TODO: filter the value based on the current osc + // OSC change will fetch new data, but still + // - we need to be sure that what we got is really what we ought to display + // - we want to display stale data accordingly (at least with loading indicator) + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + def apply: HtmlElement = + AppPage(state.actionBus)( + $data.combineWithFn($params)(_ zip _) + .map(_.map(buildModel)) + .split(_ => ())((_, _, s) => DetailPage(s)), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: List[Parameter] + ): DetailPage.ViewModel = + DetailPage.ViewModel( + o.toDetailOsoby, + p.map( + _.toParametr(param => + PageLink.container(Page.DetailParametru(o, param), state.actionBus) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DetailParametruPageConnector.scala new file mode 100644 index 0000000..fa7193d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DetailParametruPageConnector.scala @@ -0,0 +1,67 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.Parameter +import mdr.pdb.UserInfo +import mdr.pdb.app.pages.detail.DetailParametruPage +import pages.detail.DetailParametruPage +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.components.PageLink + +object DetailParametruPageConnector { + trait AppState { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + } +} + +case class DetailParametruPageConnector( + state: DetailParametruPageConnector.AppState +)( + $page: Signal[Page.DetailParametru] +)(using router: Router[Page]): + val $paramChangeSignal = + $page.splitOne(p => (p.osobniCislo.value, p.parametr.value))((x, _, _) => x) + val $pageChangeSignal = + $paramChangeSignal.map(FetchParameter(_, _)) + + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + val $merged = + $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => + for { + da <- d + pa <- p + pb <- pa.find(_.id == pc._2) + } yield (da, pb) + ) + + def apply: HtmlElement = + AppPage(state.actionBus)( + $merged.map(_.map(buildModel)) + .split(_ => ())((_, _, s) => DetailParametruPage(s)), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: Parameter + ): DetailParametruPage.ViewModel = + DetailParametruPage.ViewModel( + o.toDetailOsoby, + p.toParametr(p => + PageLink.container(Page.DetailParametru(o, p), state.actionBus) + ), + p.criteria.map( + _.toKriterium { c => + PageLink.container( + Page.DetailKriteria(o, p, c), + state.actionBus + ) + } + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DirectoryPageConnector.scala new file mode 100644 index 0000000..ab2c045 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DirectoryPageConnector.scala @@ -0,0 +1,34 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.UserInfo +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.components.AppPage + +case class DirectoryPageConnector( + $input: EventStream[List[UserInfo]], + actionBus: Observer[Action] +)(using router: Router[Page]): + val $data = $input.startWithNone + val $actionSignal = EventStream.fromValue(FetchDirectory) + + def apply: HtmlElement = + AppPage(actionBus)( + $data.split(_ => ())((_, _, s) => + pages.directory.DirectoryPage( + s.map( + _.map( + _.toUserRow(u => + PageLink.container( + Page.Detail(Page.Titled(u.personalNumber)), + actionBus + ) + ) + ) + ) + ) + ), + $actionSignal --> actionBus + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/mdr/pdb/app/connectors/conversions.scala new file mode 100644 index 0000000..f581796 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/conversions.scala @@ -0,0 +1,68 @@ +package mdr.pdb.app.connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.UserInfo +import mdr.pdb.app.pages.detail.components.DetailOsoby +import mdr.pdb.Parameter +import mdr.pdb.app.pages.detail.components.SeznamParametru +import mdr.pdb.ParameterCriteria +import mdr.pdb.app.pages.detail.components.SeznamKriterii +import mdr.pdb.app.pages.directory.components.UserRow +import mdr.pdb.app.pages.detail.components.DetailParametru +import mdr.pdb.app.pages.detail.components.DetailKriteria +import fiftyforms.ui.components.tailwind.Color + +extension (o: UserInfo) + def toDetailOsoby: DetailOsoby.ViewModel = + DetailOsoby.ViewModel( + o.personalNumber, + o.name, + o.email, + o.phone, + o.img, + o.mainFunction.map(f => + DetailOsoby.Funkce.ViewModel(f.name, f.dept, f.ou) + ), + o.userContracts.headOption.map(c => + DetailOsoby.PracovniPomer.ViewModel(c.rel, c.startDate, c.endDate) + ) + ) + +extension (param: Parameter) + def toParametr(container: Parameter => Anchor): DetailParametru.ViewModel = + DetailParametru.ViewModel( + id = param.id, + nazev = param.name, + popis = param.description, + status = "Nesplněno", + statusColor = Color.red, + a = container(param) + ) + +extension (crit: ParameterCriteria) + def toKriterium( + container: ParameterCriteria => Anchor + ): DetailKriteria.ViewModel = + DetailKriteria.ViewModel( + nazev = crit.criteriumText, + kapitola = crit.chapterId, + bod = crit.itemId, + status = "Nesplněno", + statusColor = Color.red, + splneno = false, + dukazy = Nil, + container = container(crit) + ) + +extension (user: UserInfo) + def toUserRow( + container: UserInfo => HtmlElement = (_: UserInfo) => div() + ): UserRow.ViewModel = + UserRow.ViewModel( + osobniCislo = user.personalNumber.toString, + celeJmeno = user.name, + prijmeni = user.surname, + hlavniFunkce = user.mainFunction.map(_.name), + img = user.img, + container = () => container(user) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/dashboard/DashboardPage.scala b/app/src/main/scala/mdr/pdb/app/pages/dashboard/DashboardPage.scala new file mode 100644 index 0000000..0eb8f7a --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/dashboard/DashboardPage.scala @@ -0,0 +1,11 @@ +package mdr.pdb.app.pages.dashboard + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.Page + +object DashboardPage: + + def render: HtmlElement = + div("Dashboard page") diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala new file mode 100644 index 0000000..ff3a25f --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala @@ -0,0 +1,31 @@ +package mdr.pdb.app.pages.detail + +import com.raquo.laminar.api.L.{*, given} + +import components._ + +object DetailKriteriaPage: + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametr: DetailParametru.ViewModel, + kriterium: DetailKriteria.ViewModel + ) + + def apply($m: Signal[ViewModel])( + action: Observer[DukazyKriteria.Action] + ): HtmlElement = + div( + cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + div( + DetailOsoby.header($m.map(_.osoba)), + DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") + ), + div( + DetailKriteria($m.map(_.kriterium)), + DukazyKriteria($m.map(_.kriterium.dukazy))(action) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/DetailPage.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailPage.scala new file mode 100644 index 0000000..b591cc9 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailPage.scala @@ -0,0 +1,25 @@ +package mdr.pdb.app.pages.detail + +import com.raquo.laminar.api.L.{*, given} + +import components._ +import mdr.pdb.app.Page +import com.raquo.waypoint.Router +import mdr.pdb.app.Action + +object DetailPage: + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametry: SeznamParametru.ViewModel + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + DetailOsoby($m.map(_.osoba)), + SeznamParametru($m.map(_.parametry)) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/DetailParametruPage.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailParametruPage.scala new file mode 100644 index 0000000..3322279 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailParametruPage.scala @@ -0,0 +1,24 @@ +package mdr.pdb.app.pages.detail + +import com.raquo.laminar.api.L.{*, given} + +import components._ + +object DetailParametruPage: + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametr: DetailParametru.ViewModel, + kriteria: SeznamKriterii.ViewModel + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + DetailOsoby.header($m.map(_.osoba)), + DetailParametru($m.map(_.parametr)), + SeznamKriterii($m.map(_.kriteria)) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala new file mode 100644 index 0000000..b58618a --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala @@ -0,0 +1,107 @@ +package mdr.pdb +package app +package pages.detail + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.pages.detail.components.UpravDukazForm +import fiftyforms.services.files.File + +object UpravDukaz: + + type ThisPage = Page.UpravDukazKriteria + type PageKey = (OsobniCislo, String, String) + + trait State { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def availableFiles: EventStream[List[File]] + def actionBus: Observer[Action] + } + + def keyOfPage(page: ThisPage): PageKey = + (page.osobniCislo.value, page.parametr.value, page.kriterium.value) + + def onChangeAction(key: PageKey): Action = + FetchParameterCriteria( + key._1, + key._2, + key._3, + Page.UpravDukazKriteria(_, _, _) + ) + + class Connector(state: State)($page: Signal[ThisPage])(using Router[Page]): + val $paramChangeSignal = $page.splitOne(keyOfPage)((x, _, _) => x) + val $pageChangeSignal = $paramChangeSignal.map(onChangeAction) + + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + val $merged = + $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => + for { + da <- d + pa <- p + pb <- pa.find(_.id == pc._2) + ka <- pb.criteria.find(_.id == pc._3) + } yield (da, pb, ka) + ) + + def apply: HtmlElement = + AppPage(state.actionBus)( + $merged.split(_ => ())((_, s, $s) => + PageComponent( + $s.map(buildModel), + state.availableFiles, + state.actionBus.contramap { + case UpravDukazForm.Cancelled => + NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) + case UpravDukazForm.AvailableFilesRequested => + FetchAvailableFiles(s._1.personalNumber) + } + ) + ), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: Parameter, + k: ParameterCriteria + ): PageComponent.ViewModel = + import connectors.* + PageComponent.ViewModel( + o.toDetailOsoby, + p.toParametr(_ => a()), + k.toKriterium(_ => a()) + ) + + object PageComponent: + import components.* + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametr: DetailParametru.ViewModel, + kriterium: DetailKriteria.ViewModel + ) + + def apply( + $m: Signal[ViewModel], + availableFilesStream: EventStream[List[File]], + events: Observer[UpravDukazForm.Event] + ): HtmlElement = + div( + cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + div( + DetailOsoby.header($m.map(_.osoba)), + DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") + ), + div( + DetailKriteria($m.map(_.kriterium)), + UpravDukazForm(availableFilesStream)(events) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailKriteria.scala new file mode 100644 index 0000000..a3ab634 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailKriteria.scala @@ -0,0 +1,37 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.CustomAttrs +import fiftyforms.ui.components.tailwind.Icons +import java.time.LocalDate +import fiftyforms.ui.components.tailwind.Color + +object DetailKriteria: + case class ViewModel( + nazev: String, + kapitola: String, + bod: String, + status: String, + statusColor: Color, + splneno: Boolean, + dukazy: List[DukazKriteria.ViewModel], + container: HtmlElement = div() + ) { + val id = s"${kapitola}${bod}" + } + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "pb-5 border-b border-gray-200", + h3( + cls := "text-xl leading-6 font-bold text-gray-900", + child.text <-- $m.map(_.nazev) + ), + h3( + cls := "mt-2 max-w-4xl text-sm text-gray-500", + "Kapitola ", + child.text <-- $m.map(_.kapitola), + " bod ", + child <-- $m.map(_.bod) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailOsoby.scala new file mode 100644 index 0000000..0fe2ba5 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailOsoby.scala @@ -0,0 +1,100 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.OsobniCislo + +import java.time.LocalDate +import fiftyforms.ui.components.tailwind.Avatar + +object DetailOsoby: + + object Funkce: + case class ViewModel( + nazev: String, + stredisko: String, + voj: String + ) + + def render($m: Signal[ViewModel]): HtmlElement = + p( + cls := "text-sm font-medium text-gray-500", + child.text <-- $m.map(_.nazev), + span( + cls := "hidden md:inline", + " @ ", + child.text <-- $m.map(_.stredisko), + ", ", + child.text <-- $m.map(_.voj) + ) + ) + + object PracovniPomer: + case class ViewModel( + druh: String, + pocatek: LocalDate, + konec: Option[LocalDate] + ) + + def render($m: Signal[ViewModel]): HtmlElement = + import fiftyforms.ui.components.tailwind.CustomAttrs.datetime + p( + cls := "text-sm font-medium text-gray-500", + child.text <-- $m.map(_.druh), + " od ", + time( + datetime <-- $m.map(_.pocatek.toString), + child.text <-- $m.map(_.pocatek.toString) + ) + ) + + case class ViewModel( + osobniCislo: OsobniCislo, + jmeno: String, + email: Option[String], + telefon: Option[String], + img: Option[String], + hlavniFunkce: Option[Funkce.ViewModel], + pracovniPomer: Option[PracovniPomer.ViewModel] + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "md:flex md:items-center md:justify-between md:space-x-4", + div( + cls := "flex items-start space-x-4", + div( + cls := "flex-shrink-0", + Avatar($m.map(_.img)).avatar(16) + ), + div( + h1( + cls := "text-2xl font-bold text-gray-900", + child.text <-- $m.map(_.jmeno) + ), + child.maybe <-- $m.map(_.hlavniFunkce).split(_ => ())((_, _, d) => + Funkce.render(d) + ), + child.maybe <-- $m.map(_.pracovniPomer).split(_ => ())((_, _, d) => + PracovniPomer.render(d) + ) + ) + ) + ) + + def header($m: Signal[ViewModel]): HtmlElement = + div( + cls := "md:flex md:items-center md:justify-between md:space-x-5", + div( + cls := "flex items-start space-x-4", + div( + cls := "flex-shrink-0", + Avatar($m.map(_.img)).avatar(8) + ), + div( + h1( + cls := "text-lg font-medium text-gray-800", + child.text <-- $m.map(_.jmeno) + ) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailParametru.scala new file mode 100644 index 0000000..6310dde --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailParametru.scala @@ -0,0 +1,34 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.Color + +object DetailParametru: + case class ViewModel( + id: String, + nazev: String, + popis: String, + status: String, + statusColor: Color, + a: Anchor + ) + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "pb-5 border-b border-gray-200", + h2( + cls := "text-2xl leading-6 font-medium text-gray-900", + child.text <-- $m.map(_.nazev) + ), + p( + cls := "mt-2 max-w-4xl text-sm text-gray-500", + child.text <-- $m.map(_.popis) + ) + ) + + def header($m: Signal[ViewModel]): HtmlElement = + div( + h2( + cls := "text-lg leading-6 font-medium text-gray-600", + child.text <-- $m.map(_.nazev) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazKriteria.scala new file mode 100644 index 0000000..c292919 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazKriteria.scala @@ -0,0 +1,177 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import java.time.LocalDate +import fiftyforms.ui.components.tailwind.CustomAttrs + +object DukazKriteria: + case class Osoba(osobniCislo: String, jmeno: String) + case class Udalost(osoba: Osoba, datum: LocalDate) + case class Dokument( + url: String, + nazev: String, + pridal: Udalost, + odebral: Option[Udalost] + ) + case class ViewModel( + dokumenty: List[Dokument], + autorizoval: Option[Udalost], + platiDo: Option[LocalDate], + poznámka: Option[String] + ) + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "bg-white shadow overflow-hidden sm:rounded-lg", + div( + cls := "px-4 py-5 sm:px-6", + h3( + cls := "text-lg leading-6 font-medium text-gray-900", + """Applicant Information""" + ), + p( + cls := "mt-1 max-w-2xl text-sm text-gray-500", + """Personal details and application.""" + ) + ), + div( + cls := "border-t border-gray-200 px-4 py-5 sm:px-6", + dl( + cls := "grid grid-cols-1 gap-x-4 gap-y-8 sm:grid-cols-2", + div( + cls := "sm:col-span-1", + dt( + cls := "text-sm font-medium text-gray-500", + """Full name""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """Margot Foster""" + ) + ), + div( + cls := "sm:col-span-1", + dt( + cls := "text-sm font-medium text-gray-500", + """Application for""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """Backend Developer""" + ) + ), + div( + cls := "sm:col-span-1", + dt( + cls := "text-sm font-medium text-gray-500", + """Email address""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """margotfoster@example.com""" + ) + ), + div( + cls := "sm:col-span-1", + dt( + cls := "text-sm font-medium text-gray-500", + """Salary expectation""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """$120,000""" + ) + ), + div( + cls := "sm:col-span-2", + dt( + cls := "text-sm font-medium text-gray-500", + """About""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """Fugiat ipsum ipsum deserunt culpa aute sint do nostrud anim incididunt cillum culpa consequat. Excepteur qui ipsum aliquip consequat sint. Sit id mollit nulla mollit nostrud in ea officia proident. Irure nostrud pariatur mollit ad adipisicing reprehenderit deserunt qui eu.""" + ) + ), + div( + cls := "sm:col-span-2", + dt( + cls := "text-sm font-medium text-gray-500", + """Attachments""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + ul( + role := "list", + cls := "border border-gray-200 rounded-md divide-y divide-gray-200", + li( + cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", + div( + cls := "w-0 flex-1 flex items-center", { + import svg.* + import CustomAttrs.svg.ariaHidden + svg( + cls := "flex-shrink-0 h-5 w-5 text-gray-400", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + ariaHidden := true, + path( + fillRule := "evenodd", + d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", + clipRule := "evenodd" + ) + ) + }, + span( + cls := "ml-2 flex-1 w-0 truncate", + """resume_back_end_developer.pdf""" + ) + ), + div( + cls := "ml-4 flex-shrink-0", + a( + href := "#", + cls := "font-medium text-indigo-600 hover:text-indigo-500", + """Download""" + ) + ) + ), + li( + cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", + div( + cls := "w-0 flex-1 flex items-center", { + import svg.* + import CustomAttrs.svg.ariaHidden + svg( + cls := "flex-shrink-0 h-5 w-5 text-gray-400", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + ariaHidden := true, + path( + fillRule := "evenodd", + d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", + clipRule := "evenodd" + ) + ) + }, + span( + cls := "ml-2 flex-1 w-0 truncate", + """coverletter_back_end_developer.pdf""" + ) + ), + div( + cls := "ml-4 flex-shrink-0", + a( + href := "#", + cls := "font-medium text-indigo-600 hover:text-indigo-500", + """Download""" + ) + ) + ) + ) + ) + ) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala new file mode 100644 index 0000000..d604860 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala @@ -0,0 +1,35 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.Icons + +object DukazyKriteria: + sealed trait Action + case object Add extends Action + + type ViewModel = List[DukazKriteria.ViewModel] + + def apply($m: Signal[ViewModel])(actions: Observer[Action]): HtmlElement = + div( + child.maybe <-- $m.map(m => + if (m.isEmpty) then Some(prazdnyDukaz(actions)) + else None + ), + children <-- $m.map(_.zipWithIndex).split(_._2)((_, _, $s) => + DukazKriteria($s.map(_._1)) + ) + ) + + private def prazdnyDukaz(actions: Observer[Action]): HtmlElement = + button( + tpe := "button", + onClick.preventDefault.mapTo(Add) --> actions, + cls := "relative block w-full border-2 border-gray-300 border-dashed rounded-lg p-12 text-center hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + Icons.outline + .`document-add`(12) + .amend(svg.cls := "mx-auto text-gray-400"), + span( + cls := "mt-2 block text-sm font-medium text-gray-900", + "Přidat důkaz" + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala new file mode 100644 index 0000000..7e77a7b --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala @@ -0,0 +1,34 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.Color +import fiftyforms.ui.components.tailwind.list.StackedList +import fiftyforms.ui.components.tailwind.list.ListRow +import fiftyforms.ui.components.tailwind.list.RowTag +import fiftyforms.ui.components.tailwind.list.RowNext +import java.time.LocalDate + +object SeznamKriterii: + type ViewModel = List[DetailKriteria.ViewModel] + + private val kritList = new StackedList[DetailKriteria.ViewModel] + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "bg-white shadow overflow-hidden sm:rounded-md", + kritList($m, _.id) { $i => + $i.map { i => + ListRow.ViewModel( + title = i.nazev, + topRight = RowTag.render( + $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) + ), + bottomLeft = + p(cls := "text-sm text-gray-500", s"${i.kapitola}${i.bod}"), + bottomRight = emptyNode, + farRight = RowNext.render, + containerElement = i.container + ) + } + } + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamParametru.scala new file mode 100644 index 0000000..0760b32 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamParametru.scala @@ -0,0 +1,37 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.list.{ + StackedList, + ListRow, + RowTag, + PropList, + IconText, + RowNext +} +import fiftyforms.ui.components.tailwind.Color +import fiftyforms.ui.components.tailwind.LinkSupport.* + +object SeznamParametru: + type ViewModel = List[DetailParametru.ViewModel] + + private val parametrList = new StackedList[DetailParametru.ViewModel] + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "bg-white shadow overflow-hidden sm:rounded-md", + parametrList($m, _.id) { $i => + $i.map { i => + ListRow.ViewModel( + title = i.nazev, + topRight = RowTag.render( + $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) + ), + bottomLeft = emptyNode, + bottomRight = emptyNode, + farRight = RowNext.render, + containerElement = i.a + ) + } + } + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala new file mode 100644 index 0000000..28753c9 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala @@ -0,0 +1,101 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import io.laminext.syntax.core.* +import fiftyforms.ui.components.tailwind.CustomAttrs +import fiftyforms.ui.components.tailwind.form.* +import org.scalajs.dom +import com.raquo.laminar.nodes.ReactiveHtmlElement +import fiftyforms.services.files.components.tailwind.FilePicker +import mdr.pdb.frontend.AutorizujDukaz +import mdr.pdb.frontend.DocumentRef +import fiftyforms.services.files.File + +object UpravDukazForm: + sealed trait Event + case object Cancelled extends Event + case object AvailableFilesRequested extends Event + def apply(availableFilesStream: EventStream[List[File]])( + updates: Observer[Event] + ): HtmlElement = + val (filesStream, filesObserver) = + EventStream.withObserver[FilePicker.Event] + val files = Var[List[File]](Nil) + def submitButtons: HtmlElement = + div( + cls := "pt-5", + div( + cls := "flex justify-end", + button( + tpe := "button", + cls := "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + "Zrušit", + onClick.mapTo(Cancelled) --> updates + ), + button( + tpe := "submit", + cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + disabled <-- files.signal.map(_.isEmpty), + "Autorizovat" + ) + ) + ) + + div( + filesStream.collect { case FilePicker.SelectionUpdated(files) => + files.to(List) + } --> files.writer, + filesStream.collect { case FilePicker.AvailableFilesRequested => + AvailableFilesRequested + } --> updates, + cls := "bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6", + Form( + Form.Body( + Form.Section( + FormHeader( + FormHeader.ViewModel( + "Doložení kritéria", + "Sestavte doklady poskytující důkaz kritéria a potvrďte odesláním formuláře. Případné limitace či jiné relevantní údaje vepište do pole Komentář." + ) + ), + FormFields( + FormRow( + "dokumenty", + "Dokumenty", + FilePicker(files.signal, availableFilesStream)(filesObserver) + .amend(idAttr := "dokumenty", cls("max-w-lg")) + ).toHtml, + FormRow( + "platnost", + "Platnost", + input( + idAttr := "platnost", + name := "platnost", + tpe := "date", + autoComplete := "date", + cls := "block max-w-lg w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" + ) + ).toHtml, + FormRow( + "komentar", + "Komentář", + div( + cls := "mt-1 sm:mt-0 sm:col-span-2", + textArea( + idAttr := "komentar", + name := "about", + rows := 3, + cls := "max-w-lg shadow-sm block w-full focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border border-gray-300 rounded-md" + ), + p( + cls := "mt-2 text-sm text-gray-500", + "Doplňte prosím potřebné informace související s doložením kritéria, včetně případných limitací." + ) + ) + ).toHtml + ) + ) + ), + submitButtons + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/directory/DirectoryPage.scala b/app/src/main/scala/mdr/pdb/app/pages/directory/DirectoryPage.scala new file mode 100644 index 0000000..7a72ca3 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/directory/DirectoryPage.scala @@ -0,0 +1,40 @@ +package mdr.pdb.app.pages.directory + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.UserInfo + +import components._ + +object DirectoryPage: + + type ViewModel = List[UserRow.ViewModel] + + def apply($m: Signal[ViewModel]): HtmlElement = + val (actionsStream, actionObserver) = + EventStream.withObserver[SearchForm.Action] + val $filter = actionsStream + .collect { + case SearchForm.ClearFilter => None + case SearchForm.SetFilter(t) => Some(t) + } + .startWith(None) + val byLetter = for { + d <- $m + f <- $filter + } yield for { + (letter, users) <- d + .filter { user => + f match + case None => true + case Some(t) => user.search.contains(t) + } + .groupBy(_.prijmeni.head) + .to(List) + .sortBy(_._1) + } yield (letter.toString, users.sortBy(_.prijmeni)) + + div( + cls := "h-full max-w-7xl mx-auto order-first flex flex-col flex-shrink-0", + SearchForm(actionObserver), + Directory(byLetter) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala deleted file mode 100644 index a3b00af..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala +++ /dev/null @@ -1,103 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import scala.scalajs.js.annotation.JSExportTopLevel -import scala.scalajs.js.annotation.JSExport -import scala.scalajs.js.annotation.JSImport -import scala.scalajs.js -import org.scalajs.dom -import com.raquo.laminar.api.L.{*, given} -import scala.scalajs.js.Date -import com.raquo.waypoint.Router -import com.raquo.waypoint.SplitRender -import cz.e_bs.cmi.mdr.pdb.app.services.DataFetcher -import scala.scalajs.js.JSON -import zio.json._ -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.state.AppState - -@js.native -@JSImport("stylesheets/main.css", JSImport.Namespace) -object Css extends js.Any - -@js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native -@JSImport("params/pdb-params.json", JSImport.Default) -object pdbParams extends js.Object - -@JSExportTopLevel("app") -object Main: - - @JSExport - def main(args: Array[String]): Unit = - import Routes.given - onLoad { - setupAirstream() - val appContainer = dom.document.querySelector("#app") - val _ = - render( - appContainer, - renderPage(state.MockAppState(using unsafeWindowOwner, router)) - ) - } - - private def onLoad(f: => Unit): Unit = - documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) - - private def setupAirstream()(using router: Router[Page]): Unit = - AirstreamError.registerUnhandledErrorCallback(err => - router.forcePage( - Page.UnhandledError( - Some(err.getClass.getName), // TODO: Fill only in dev mode - Some(err.getMessage) - ) - ) - ) - - def renderPage(state: AppState)(using - router: Router[Page] - ): HtmlElement = - val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) - .collectSignal[Page.Detail]( - connectors - .DetailPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailParametru]( - connectors - .DetailParametruPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailKriteria]( - connectors - .DetailKriteriaPageConnector(state)(_) - .apply - ) - .collectSignal[Page.UpravDukazKriteria]( - pages.detail.UpravDukaz.Connector(state)(_).apply - ) - .collectStatic(Page.Dashboard)( - connectors.DashboardPageConnector(state.actionBus).apply - ) - .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) - ) - .collect[Page.UnhandledError](pg => - pages.errors - .UnhandledErrorPage( - pages.errors.UnhandledErrorPage - .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), - state.actionBus - ) - ) - .collectStatic(Page.Directory)( - connectors - .DirectoryPageConnector(state.users, state.actionBus) - .apply - ) - div(cls := "h-full", child <-- pageSplitter.$view) - - // Pull in the stylesheet - val css: Css.type = Css diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala deleted file mode 100644 index 3bbc8c0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ /dev/null @@ -1,198 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.* -import org.scalajs.dom -import zio.json.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import scala.scalajs.js -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.Page.Titled - -// enum is not working with Waypoints' SplitRender collectStatic -sealed abstract class Page( - val id: String, - val title: String, - val parent: Option[Page] -) { - val path: Vector[Page] = - parent match - case None => Vector(this) - case Some(p) => p.path :+ this - - val isRoot: Boolean = parent.isEmpty -} - -object Page: - - case class Titled[V](value: V, title: Option[String] = None): - val show: String = title.getOrElse(value.toString) - - case object Directory extends Page("directory", "Adresář", None) - - case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) - - case class Detail(osobniCislo: Titled[OsobniCislo]) - extends Page("user", osobniCislo.show, Some(Directory)) - - object Detail { - def apply(o: UserInfo): Detail = Detail( - Titled(o.personalNumber, Some(o.name)) - ) - } - - case class DetailParametru( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String] - ) extends Page( - "parameter", - parametr.show, - Some(Detail(osobniCislo)) - ) - - object DetailParametru { - def apply(o: UserInfo, p: Parameter): DetailParametru = - DetailParametru( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)) - ) - } - - case class DetailKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "criteria", - kriterium.show, - Some(DetailParametru(osobniCislo, parametr)) - ) - - object DetailKriteria { - def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = - DetailKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class UpravDukazKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "addProof", - "Důkaz", - Some(DetailKriteria(osobniCislo, parametr, kriterium)) - ) - - object UpravDukazKriteria { - def apply( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): UpravDukazKriteria = - UpravDukazKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class NotFound(url: String) extends Page("404", "404", Some(Directory)) - - case class UnhandledError( - errorName: Option[String], - errorMessage: Option[String] - ) extends Page("500", "Unexpected error", Some(Directory)) - -object Routes: - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) - given [V: JsonEncoder]: JsonEncoder[Titled[V]] = - DeriveJsonEncoder.gen[Titled[V]] - given [V: JsonDecoder]: JsonDecoder[Titled[V]] = - DeriveJsonDecoder.gen[Titled[V]] - given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] - given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] - - val base = - js.`import`.meta.env.BASE_URL - .asInstanceOf[String] + "app" - - val homePage: Page = Page.Directory - - given router: Router[Page] = Router[Page]( - routes = List( - Route.static(homePage, root / endOfSegments, basePath = base), - Route.static( - Page.Dashboard, - root / "dashboard" / endOfSegments, - basePath = base - ), - Route[Page.Detail, String]( - encode = _.osobniCislo.value.toString, - decode = osc => Page.Detail(Titled(OsobniCislo(osc))), - root / "osoba" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.DetailParametru, (String, String)]( - encode = p => (p.osobniCislo.value.toString, p.parametr.value), - decode = - p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / endOfSegments, - basePath = base - ), - Route[Page.DetailKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.DetailKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.UpravDukazKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.UpravDukazKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / "edit" / endOfSegments, - basePath = base - ) - ), - serializePage = _.toJson, - deserializePage = _.fromJson[Page] - .fold(s => throw IllegalStateException(s), identity), - getPageTitle = _.title, - routeFallback = url => Page.NotFound(url), - deserializeFallback = _ => Page.Dashboard - )( - $popStateEvent = windowEvents.onPopState, - owner = unsafeWindowOwner - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala deleted file mode 100644 index b24b82b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -sealed trait Action - -case object FetchDirectory extends Action -case class FetchUserDetails(osc: OsobniCislo) extends Action -case class FetchParameters(osc: OsobniCislo) extends Action -case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action -case class FetchParameterCriteria( - osc: OsobniCislo, - paramId: String, - critId: String, - page: (UserInfo, Parameter, ParameterCriteria) => Page -) extends Action -case class FetchAvailableFiles(osc: OsobniCislo) extends Action -case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala deleted file mode 100644 index c637d35..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala +++ /dev/null @@ -1,82 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.{UserProfile, UserInfo, OsobniCislo} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo -import cz.e_bs.cmi.mdr.pdb.UserFunction - -object AppPage: - // TODO: pages by logged in user - val pages: List[Page] = List(Page.Directory, Page.Dashboard) - - import NavigationBar.{Logo, MenuItem} - - val logo = Logo( - "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", - "Workflow" - ) - - // TODO: menu items by user profile - val userMenu = - List( - MenuItem("Your Profile"), - MenuItem("Settings"), - MenuItem("Sign out") - ) - - // TODO: load user profile - val $userProfile = Var( - UserProfile( - "tom", - UserInfo( - OsobniCislo("1031"), - "tom", - "Tom", - "Cook", - None, - None, - Some("tom@example.com"), - Some("+420 222 866 180"), - Some( - UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") - ), - Nil, - None - ) - ) - ) - - val $userInfo = $userProfile.signal.map(_.userInfo) - - type ViewModel = Option[HtmlElement] - def apply( - actionBus: Observer[Action] - )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using - router: Router[Page] - ): HtmlElement = - PageLayout(actionBus)( - $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => - PageLayout.ViewModel( - NavigationBar.ViewModel( - u, - pages.map(p => - NavigationBar.Link( - () => - PageLink( - p, - actionBus - ), - p == cp - ) - ), - userMenu, - logo - ), - c - ) - ), - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala deleted file mode 100644 index bf473c6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ /dev/null @@ -1,143 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden -import fiftyforms.ui.components.tailwind.list.IconText.ViewModel -import fiftyforms.ui.components.tailwind.Icons -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Action - -object Breadcrumbs: - - private def slash = - import svg.{*, given} - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-300", - xmlns := "http://www.w3.org/2000/svg", - fill := "currentColor", - viewBox := "0 0 20 20", - ariaHidden := true, - path( - d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" - ) - ) - - def backIcon = - Icons.solid - .`arrow-narrow-left`() - .amend( - svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" - ) - - object Link: - case class ViewModel( - page: Page, - icon: Option[SvgElement], - extraClasses: String, - text: String, - textClasses: Option[String] = None - ) - - val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" - - def shortHome(p: Page) = ViewModel( - p, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - "Zpět na úvodní stránku", - None - ) - - def fullHome(p: Page) = ViewModel( - p, - Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), - "text-gray-400 hover:text-gray-500", - "Domů", - Some("sr-only") - ) - - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - PageLink - .container($m.map(_.page), actionBus) - .amend( - cls <-- $m.map(_.extraClasses), - child.maybe <-- $m.map(_.icon), - span( - cls <-- $m.map(_.textClasses.getOrElse("")), - child.text <-- $m.map(_.text) - ) - ) - - object FullBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - ol( - role := "list", - cls := "flex items-center space-x-4", - children <-- $m.map(_.path).split(_.id)((_, _, $p) => - li( - div( - cls := "flex items-center", - child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), - Link( - $p.map(p => - if (p.isRoot) then Link.fullHome(p) - else - Link.ViewModel( - p, - None, - s"ml-4 max-w-xs truncate ${Link.baseClasses}", - p.title - ) - ), - actionBus - ) - ) - ) - ) - ) - - object ShortBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - Link( - $m.map { p => - val target = p.parent.getOrElse(p) - if target.isRoot then Link.shortHome(target) - else - Link.ViewModel( - target, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - s"Zpět na ${target.title}" - ) - }, - actionBus - ) - - def apply(actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - nav( - cls := "flex", - aria.label := "Breadcrumb", - div( - cls := "flex sm:hidden", - ShortBreadcrumbs(router.$currentPage, actionBus) - ), - div( - cls := "hidden sm:block", - FullBreadcrumbs(router.$currentPage, actionBus) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala deleted file mode 100644 index 0db2047..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ /dev/null @@ -1,242 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent -import fiftyforms.ui.components.tailwind.Icons -import fiftyforms.ui.components.tailwind.Avatar -import cz.e_bs.cmi.mdr.pdb.UserInfo -import io.laminext.syntax.core.* - -object NavigationBar: - - case class Logo(img: String, name: String) - case class Link(a: () => Anchor, active: Boolean) - case class MenuItem(title: String) - - case class ViewModel( - userInfo: UserInfo, - pages: List[Link], - userMenu: List[MenuItem], - logo: Logo - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - val $userInfo = $m.map(_.userInfo) - val mobileMenuOpen = Var(false) - - val desktopOnly = cls("hidden md:block") - val mobileOnly = cls("md:hidden") - - inline def avatarImage(size: Int = 8) = - Avatar($userInfo.map(_.img)).avatarImage(size) - - def notificationButton = button( - tpe := "button", - cls := List( - "bg-indigo-600", - "focus:outline-none", - "focus:ring-2", - "focus:ring-offset-2", - "focus:ring-offset-indigo-600", - "focus:ring-white", - "hover:text-white", - "p-1", - "rounded-full", - "text-indigo-200" - ), - span(cls := "sr-only", "View notifications"), - Icons.outline.bell() - ) - - def userProfile: HtmlElement = - val menuOpen = Var(false) - - def menuItem(item: MenuItem, idx: Int): HtmlElement = - a( - href := "#", - cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", - role := "menuitem", - tabIndex := -1, - idAttr := s"user-menu-item-$idx", - item.title - ) - - div( - cls := "ml-3 relative", - div( - button( - tpe := "button", - cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - idAttr := "user-menu-button", - aria.expanded <-- menuOpen.signal, - aria.hasPopup := true, - span(cls := "sr-only", "Open user menu"), - child <-- avatarImage(), - onClick.preventDefault.mapTo( - !menuOpen.now() - ) --> menuOpen.writer - ) - ), - /* - * */ - div( - cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", - cls <-- menuOpen.signal.map { o => - if (o) "md:block" else "md:hidden" - }, - role := "menu", - aria.orientation := "vertical", - aria.labelledBy := "user-menu-button", - tabIndex := -1, - // : keyboard navigation - children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) - ) - ) - - def mobileProfile = - def menuItem(item: MenuItem): HtmlElement = - a( - href := "#", - cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", - item.title - ) - - div( - cls := "pt-4 pb-3 border-t border-indigo-700", - div( - cls := "flex items-center px-5", - div( - cls := "flex-shrink-0", - child <-- avatarImage(10) - ), - div( - cls := "ml-3", - div( - cls := "text-base font-medium text-white", - child.text <-- $userInfo.map(_.name) - ), - child.maybe <-- $userInfo.map( - _.email.map(e => - div( - cls := "text-sm font-medium text-indigo-300", - e - ) - ) - ) - ), - notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) - ), - div( - cls := "mt-3 px-2 space-y-1", - children <-- $m.map(_.userMenu.map(menuItem)) - ) - ) - - def pageLink(page: Link): Anchor = - page - .a() - .amend( - cls := "text-white px-3 py-2 rounded-md text-sm font-medium", - cls := Seq( - "bg-indigo-700" -> page.active, - "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active - ), - ariaCurrent := (if page.active then "page" else "false") - ) - - def logoImg: Image = - img( - cls := "h-8 w-8", - src <-- $m.map(_.logo.img), - alt <-- $m.map(_.logo.name) - ) - - def mobileMenuButton = button( - tpe := "button", - cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - aria.controls := "mobile-menu", - aria.expanded <-- mobileMenuOpen.signal, - span(cls := "sr-only", "Open main menu"), - Icons.outline - .menu() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") - ), - Icons.outline - .x() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") - ), - onClick.preventDefault.mapTo( - !mobileMenuOpen.now() - ) --> mobileMenuOpen.writer - ) - - def navBarLeft = - div( - cls := "flex items-center", - div(cls := "flex-shrink-0", logoImg), - div( - desktopOnly, - div( - cls := "ml-10 flex items-baseline space-x-4", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ) - ) - ) - - def navBarRight = - div( - desktopOnly, - div( - cls := "ml-4 flex items-center md:ml-6", - notificationButton, - userProfile - ) - ) - - def navBarMobile = - div( - cls := "-mr-2 flex", - mobileOnly, - mobileMenuButton - ) - - def navBar = - div( - cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex items-center justify-between h-16", - navBarLeft, - navBarRight, - navBarMobile - ) - ) - - def mobileMenu = - div( - mobileOnly, - cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }, - idAttr := "mobile-menu", - div( - cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ), - mobileProfile - ) - - nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala deleted file mode 100644 index 75339df..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object PageHeader: - def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = - header( - cls := "bg-white shadow-sm", - div( - cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", - h1( - cls := "text-lg leading-6 font-semibold text-gray-900", - Breadcrumbs(actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala deleted file mode 100644 index 5096463..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ /dev/null @@ -1,30 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import fiftyforms.ui.components.tailwind.Loading -import io.laminext.syntax.core.* - -object PageLayout: - case class ViewModel( - navigation: NavigationBar.ViewModel, - content: Option[HtmlElement] - ) - def apply(actionBus: Observer[Action])( - $m: Signal[ViewModel], - mods: Modifier[HtmlElement]* - )(using router: Router[Page]): HtmlElement = - val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) - div( - cls := "h-full flex flex-col", - NavigationBar($m.map(_.navigation)), - child.maybe <-- router.$currentPage.map(_.isRoot) - .switch(None, Some(PageHeader(actionBus))), - main( - cls := "flex-grow-1 overflow-y-auto", - mods, - child <-- $maybeContent.map(_.getOrElse(Loading)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala deleted file mode 100644 index 62d820f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.LinkSupport.* -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo - -object PageLink: - type ViewModel = Page - def apply($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = - a(mods($m, actions), child.text <-- $m.map(_.title)) - - def apply(m: ViewModel, actions: Observer[Action])(using - router: Router[Page] - ): Anchor = apply(Val(m), actions) - - def container($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = a(mods($m, actions)) - - def container(m: ViewModel, actions: Observer[Action])(using - Router[Page] - ): Anchor = container(Val(m), actions) - - private def mods($m: Signal[Page], actions: Observer[Action])(using - router: Router[Page] - ): Modifier[Anchor] = - Seq( - href <-- $m.map(router.absoluteUrlForPage).recover { case _ => - Some("invalid url") - }, - composeEvents(onClick.noKeyMod.preventDefault)( - _.sample($m) - .map(NavigateTo.apply) - ) --> actions - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala deleted file mode 100644 index 87f825a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.pages.dashboard.DashboardPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -class DashboardPageConnector(actionBus: Observer[Action])(using - router: Router[Page] -): - def apply: HtmlElement = - AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala deleted file mode 100644 index e9b0f71..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala +++ /dev/null @@ -1,76 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailKriteriaPage -import pages.detail.components.DukazyKriteria -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -object DetailKriteriaPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailKriteriaPageConnector( - state: DetailKriteriaPageConnector.AppState -)( - $page: Signal[Page.DetailKriteria] -)(using Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => - (p.osobniCislo.value, p.parametr.value, p.kriterium.value) - )((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map( - FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) - ) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, s, $s) => - DetailKriteriaPage($s)(state.actionBus.contramap { - case DukazyKriteria.Add => - NavigateTo( - Page.UpravDukazKriteria( - Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), - Page.Titled(s.parametr.id, Some(s.parametr.nazev)), - Page.Titled(s.kriterium.id, Some(s.kriterium.id)) - ) - ) - }) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): DetailKriteriaPage.ViewModel = - DetailKriteriaPage.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala deleted file mode 100644 index 4d8024f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ /dev/null @@ -1,57 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import pages.detail.DetailPage -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import fiftyforms.ui.components.tailwind.Color - -object DetailPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailPageConnector(state: DetailPageConnector.AppState)( - $page: Signal[Page.Detail] -)(using router: Router[Page]): - val $oscChangeSignal = $page.splitOne(_.osobniCislo.value)((osc, _, _) => osc) - val $pageChangeSignal = - $oscChangeSignal.flatMap(osc => - EventStream.fromSeq(Seq(FetchUserDetails(osc), FetchParameters(osc))) - ) - // TODO: filter the value based on the current osc - // OSC change will fetch new data, but still - // - we need to be sure that what we got is really what we ought to display - // - we want to display stale data accordingly (at least with loading indicator) - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - def apply: HtmlElement = - AppPage(state.actionBus)( - $data.combineWithFn($params)(_ zip _) - .map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: List[Parameter] - ): DetailPage.ViewModel = - DetailPage.ViewModel( - o.toDetailOsoby, - p.map( - _.toParametr(param => - PageLink.container(Page.DetailParametru(o, param), state.actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala deleted file mode 100644 index fdaa7f8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailParametruPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink - -object DetailParametruPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailParametruPageConnector( - state: DetailParametruPageConnector.AppState -)( - $page: Signal[Page.DetailParametru] -)(using router: Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => (p.osobniCislo.value, p.parametr.value))((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map(FetchParameter(_, _)) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - } yield (da, pb) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailParametruPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter - ): DetailParametruPage.ViewModel = - DetailParametruPage.ViewModel( - o.toDetailOsoby, - p.toParametr(p => - PageLink.container(Page.DetailParametru(o, p), state.actionBus) - ), - p.criteria.map( - _.toKriterium { c => - PageLink.container( - Page.DetailKriteria(o, p, c), - state.actionBus - ) - } - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala deleted file mode 100644 index 049aa22..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -case class DirectoryPageConnector( - $input: EventStream[List[UserInfo]], - actionBus: Observer[Action] -)(using router: Router[Page]): - val $data = $input.startWithNone - val $actionSignal = EventStream.fromValue(FetchDirectory) - - def apply: HtmlElement = - AppPage(actionBus)( - $data.split(_ => ())((_, _, s) => - pages.directory.DirectoryPage( - s.map( - _.map( - _.toUserRow(u => - PageLink.container( - Page.Detail(Page.Titled(u.personalNumber)), - actionBus - ) - ) - ) - ) - ) - ), - $actionSignal --> actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala deleted file mode 100644 index 88e7450..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ /dev/null @@ -1,68 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii -import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria -import fiftyforms.ui.components.tailwind.Color - -extension (o: UserInfo) - def toDetailOsoby: DetailOsoby.ViewModel = - DetailOsoby.ViewModel( - o.personalNumber, - o.name, - o.email, - o.phone, - o.img, - o.mainFunction.map(f => - DetailOsoby.Funkce.ViewModel(f.name, f.dept, f.ou) - ), - o.userContracts.headOption.map(c => - DetailOsoby.PracovniPomer.ViewModel(c.rel, c.startDate, c.endDate) - ) - ) - -extension (param: Parameter) - def toParametr(container: Parameter => Anchor): DetailParametru.ViewModel = - DetailParametru.ViewModel( - id = param.id, - nazev = param.name, - popis = param.description, - status = "Nesplněno", - statusColor = Color.red, - a = container(param) - ) - -extension (crit: ParameterCriteria) - def toKriterium( - container: ParameterCriteria => Anchor - ): DetailKriteria.ViewModel = - DetailKriteria.ViewModel( - nazev = crit.criteriumText, - kapitola = crit.chapterId, - bod = crit.itemId, - status = "Nesplněno", - statusColor = Color.red, - splneno = false, - dukazy = Nil, - container = container(crit) - ) - -extension (user: UserInfo) - def toUserRow( - container: UserInfo => HtmlElement = (_: UserInfo) => div() - ): UserRow.ViewModel = - UserRow.ViewModel( - osobniCislo = user.personalNumber.toString, - celeJmeno = user.name, - prijmeni = user.surname, - hlavniFunkce = user.mainFunction.map(_.name), - img = user.img, - container = () => container(user) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala deleted file mode 100644 index 7d5b2c1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.dashboard - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.Page - -object DashboardPage: - - def render: HtmlElement = - div("Dashboard page") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala deleted file mode 100644 index 9ef0698..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala +++ /dev/null @@ -1,31 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailKriteriaPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply($m: Signal[ViewModel])( - action: Observer[DukazyKriteria.Action] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - DukazyKriteria($m.map(_.kriterium.dukazy))(action) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala deleted file mode 100644 index 3b316e1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object DetailPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametry: SeznamParametru.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby($m.map(_.osoba)), - SeznamParametru($m.map(_.parametry)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala deleted file mode 100644 index 17224cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala +++ /dev/null @@ -1,24 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailParametruPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriteria: SeznamKriterii.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby.header($m.map(_.osoba)), - DetailParametru($m.map(_.parametr)), - SeznamKriterii($m.map(_.kriteria)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala deleted file mode 100644 index dc912fa..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ /dev/null @@ -1,107 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb -package app -package pages.detail - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.UpravDukazForm -import fiftyforms.services.files.File - -object UpravDukaz: - - type ThisPage = Page.UpravDukazKriteria - type PageKey = (OsobniCislo, String, String) - - trait State { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def availableFiles: EventStream[List[File]] - def actionBus: Observer[Action] - } - - def keyOfPage(page: ThisPage): PageKey = - (page.osobniCislo.value, page.parametr.value, page.kriterium.value) - - def onChangeAction(key: PageKey): Action = - FetchParameterCriteria( - key._1, - key._2, - key._3, - Page.UpravDukazKriteria(_, _, _) - ) - - class Connector(state: State)($page: Signal[ThisPage])(using Router[Page]): - val $paramChangeSignal = $page.splitOne(keyOfPage)((x, _, _) => x) - val $pageChangeSignal = $paramChangeSignal.map(onChangeAction) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.split(_ => ())((_, s, $s) => - PageComponent( - $s.map(buildModel), - state.availableFiles, - state.actionBus.contramap { - case UpravDukazForm.Cancelled => - NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) - case UpravDukazForm.AvailableFilesRequested => - FetchAvailableFiles(s._1.personalNumber) - } - ) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): PageComponent.ViewModel = - import connectors.* - PageComponent.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) - - object PageComponent: - import components.* - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply( - $m: Signal[ViewModel], - availableFilesStream: EventStream[List[File]], - events: Observer[UpravDukazForm.Event] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - UpravDukazForm(availableFilesStream)(events) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala deleted file mode 100644 index ca311b0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.Icons -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Color - -object DetailKriteria: - case class ViewModel( - nazev: String, - kapitola: String, - bod: String, - status: String, - statusColor: Color, - splneno: Boolean, - dukazy: List[DukazKriteria.ViewModel], - container: HtmlElement = div() - ) { - val id = s"${kapitola}${bod}" - } - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h3( - cls := "text-xl leading-6 font-bold text-gray-900", - child.text <-- $m.map(_.nazev) - ), - h3( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - "Kapitola ", - child.text <-- $m.map(_.kapitola), - " bod ", - child <-- $m.map(_.bod) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala deleted file mode 100644 index ae90290..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala +++ /dev/null @@ -1,100 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Avatar - -object DetailOsoby: - - object Funkce: - case class ViewModel( - nazev: String, - stredisko: String, - voj: String - ) - - def render($m: Signal[ViewModel]): HtmlElement = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.nazev), - span( - cls := "hidden md:inline", - " @ ", - child.text <-- $m.map(_.stredisko), - ", ", - child.text <-- $m.map(_.voj) - ) - ) - - object PracovniPomer: - case class ViewModel( - druh: String, - pocatek: LocalDate, - konec: Option[LocalDate] - ) - - def render($m: Signal[ViewModel]): HtmlElement = - import fiftyforms.ui.components.tailwind.CustomAttrs.datetime - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.druh), - " od ", - time( - datetime <-- $m.map(_.pocatek.toString), - child.text <-- $m.map(_.pocatek.toString) - ) - ) - - case class ViewModel( - osobniCislo: OsobniCislo, - jmeno: String, - email: Option[String], - telefon: Option[String], - img: Option[String], - hlavniFunkce: Option[Funkce.ViewModel], - pracovniPomer: Option[PracovniPomer.ViewModel] - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-4", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(16) - ), - div( - h1( - cls := "text-2xl font-bold text-gray-900", - child.text <-- $m.map(_.jmeno) - ), - child.maybe <-- $m.map(_.hlavniFunkce).split(_ => ())((_, _, d) => - Funkce.render(d) - ), - child.maybe <-- $m.map(_.pracovniPomer).split(_ => ())((_, _, d) => - PracovniPomer.render(d) - ) - ) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-5", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(8) - ), - div( - h1( - cls := "text-lg font-medium text-gray-800", - child.text <-- $m.map(_.jmeno) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala deleted file mode 100644 index e11cb62..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color - -object DetailParametru: - case class ViewModel( - id: String, - nazev: String, - popis: String, - status: String, - statusColor: Color, - a: Anchor - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h2( - cls := "text-2xl leading-6 font-medium text-gray-900", - child.text <-- $m.map(_.nazev) - ), - p( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - child.text <-- $m.map(_.popis) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - h2( - cls := "text-lg leading-6 font-medium text-gray-600", - child.text <-- $m.map(_.nazev) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala deleted file mode 100644 index a00ede8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala +++ /dev/null @@ -1,177 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.CustomAttrs - -object DukazKriteria: - case class Osoba(osobniCislo: String, jmeno: String) - case class Udalost(osoba: Osoba, datum: LocalDate) - case class Dokument( - url: String, - nazev: String, - pridal: Udalost, - odebral: Option[Udalost] - ) - case class ViewModel( - dokumenty: List[Dokument], - autorizoval: Option[Udalost], - platiDo: Option[LocalDate], - poznámka: Option[String] - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-lg", - div( - cls := "px-4 py-5 sm:px-6", - h3( - cls := "text-lg leading-6 font-medium text-gray-900", - """Applicant Information""" - ), - p( - cls := "mt-1 max-w-2xl text-sm text-gray-500", - """Personal details and application.""" - ) - ), - div( - cls := "border-t border-gray-200 px-4 py-5 sm:px-6", - dl( - cls := "grid grid-cols-1 gap-x-4 gap-y-8 sm:grid-cols-2", - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Full name""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Margot Foster""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Application for""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Backend Developer""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Email address""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """margotfoster@example.com""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Salary expectation""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """$120,000""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """About""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Fugiat ipsum ipsum deserunt culpa aute sint do nostrud anim incididunt cillum culpa consequat. Excepteur qui ipsum aliquip consequat sint. Sit id mollit nulla mollit nostrud in ea officia proident. Irure nostrud pariatur mollit ad adipisicing reprehenderit deserunt qui eu.""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """Attachments""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - ul( - role := "list", - cls := "border border-gray-200 rounded-md divide-y divide-gray-200", - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """resume_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ), - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """coverletter_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala deleted file mode 100644 index 22f134f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala +++ /dev/null @@ -1,35 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object DukazyKriteria: - sealed trait Action - case object Add extends Action - - type ViewModel = List[DukazKriteria.ViewModel] - - def apply($m: Signal[ViewModel])(actions: Observer[Action]): HtmlElement = - div( - child.maybe <-- $m.map(m => - if (m.isEmpty) then Some(prazdnyDukaz(actions)) - else None - ), - children <-- $m.map(_.zipWithIndex).split(_._2)((_, _, $s) => - DukazKriteria($s.map(_._1)) - ) - ) - - private def prazdnyDukaz(actions: Observer[Action]): HtmlElement = - button( - tpe := "button", - onClick.preventDefault.mapTo(Add) --> actions, - cls := "relative block w-full border-2 border-gray-300 border-dashed rounded-lg p-12 text-center hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - Icons.outline - .`document-add`(12) - .amend(svg.cls := "mx-auto text-gray-400"), - span( - cls := "mt-2 block text-sm font-medium text-gray-900", - "Přidat důkaz" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala deleted file mode 100644 index f628697..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.list.StackedList -import fiftyforms.ui.components.tailwind.list.ListRow -import fiftyforms.ui.components.tailwind.list.RowTag -import fiftyforms.ui.components.tailwind.list.RowNext -import java.time.LocalDate - -object SeznamKriterii: - type ViewModel = List[DetailKriteria.ViewModel] - - private val kritList = new StackedList[DetailKriteria.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - kritList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = - p(cls := "text-sm text-gray-500", s"${i.kapitola}${i.bod}"), - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.container - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala deleted file mode 100644 index 2bfe4bc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.list.{ - StackedList, - ListRow, - RowTag, - PropList, - IconText, - RowNext -} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.LinkSupport.* - -object SeznamParametru: - type ViewModel = List[DetailParametru.ViewModel] - - private val parametrList = new StackedList[DetailParametru.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - parametrList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = emptyNode, - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.a - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala deleted file mode 100644 index f048e9d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ /dev/null @@ -1,101 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.form.* -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement -import fiftyforms.services.files.components.tailwind.FilePicker -import cz.e_bs.cmi.mdr.pdb.frontend.AutorizujDukaz -import cz.e_bs.cmi.mdr.pdb.frontend.DocumentRef -import fiftyforms.services.files.File - -object UpravDukazForm: - sealed trait Event - case object Cancelled extends Event - case object AvailableFilesRequested extends Event - def apply(availableFilesStream: EventStream[List[File]])( - updates: Observer[Event] - ): HtmlElement = - val (filesStream, filesObserver) = - EventStream.withObserver[FilePicker.Event] - val files = Var[List[File]](Nil) - def submitButtons: HtmlElement = - div( - cls := "pt-5", - div( - cls := "flex justify-end", - button( - tpe := "button", - cls := "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zrušit", - onClick.mapTo(Cancelled) --> updates - ), - button( - tpe := "submit", - cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - disabled <-- files.signal.map(_.isEmpty), - "Autorizovat" - ) - ) - ) - - div( - filesStream.collect { case FilePicker.SelectionUpdated(files) => - files.to(List) - } --> files.writer, - filesStream.collect { case FilePicker.AvailableFilesRequested => - AvailableFilesRequested - } --> updates, - cls := "bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6", - Form( - Form.Body( - Form.Section( - FormHeader( - FormHeader.ViewModel( - "Doložení kritéria", - "Sestavte doklady poskytující důkaz kritéria a potvrďte odesláním formuláře. Případné limitace či jiné relevantní údaje vepište do pole Komentář." - ) - ), - FormFields( - FormRow( - "dokumenty", - "Dokumenty", - FilePicker(files.signal, availableFilesStream)(filesObserver) - .amend(idAttr := "dokumenty", cls("max-w-lg")) - ).toHtml, - FormRow( - "platnost", - "Platnost", - input( - idAttr := "platnost", - name := "platnost", - tpe := "date", - autoComplete := "date", - cls := "block max-w-lg w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" - ) - ).toHtml, - FormRow( - "komentar", - "Komentář", - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - textArea( - idAttr := "komentar", - name := "about", - rows := 3, - cls := "max-w-lg shadow-sm block w-full focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border border-gray-300 rounded-md" - ), - p( - cls := "mt-2 text-sm text-gray-500", - "Doplňte prosím potřebné informace související s doložením kritéria, včetně případných limitací." - ) - ) - ).toHtml - ) - ) - ), - submitButtons - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala deleted file mode 100644 index 41033f6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -import components._ - -object DirectoryPage: - - type ViewModel = List[UserRow.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - val (actionsStream, actionObserver) = - EventStream.withObserver[SearchForm.Action] - val $filter = actionsStream - .collect { - case SearchForm.ClearFilter => None - case SearchForm.SetFilter(t) => Some(t) - } - .startWith(None) - val byLetter = for { - d <- $m - f <- $filter - } yield for { - (letter, users) <- d - .filter { user => - f match - case None => true - case Some(t) => user.search.contains(t) - } - .groupBy(_.prijmeni.head) - .to(List) - .sortBy(_._1) - } yield (letter.toString, users.sortBy(_.prijmeni)) - - div( - cls := "h-full max-w-7xl mx-auto order-first flex flex-col flex-shrink-0", - SearchForm(actionObserver), - Directory(byLetter) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala deleted file mode 100644 index 73f915d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -object Directory: - - object Header: - type ViewModel = String - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500", - h3(child.text <-- $m) - ) - - object UserList: - type ViewModel = List[UserRow.ViewModel] - def apply($m: Signal[ViewModel]): HtmlElement = - ul( - role := "list", - cls := "relative z-0 divide-y divide-gray-200", - children <-- $m.split(_.osobniCislo)((_, _, s) => UserRow(s)) - ) - - type ViewModel = List[(String, List[UserRow.ViewModel])] - def apply($m: Signal[ViewModel]): HtmlElement = - nav( - cls := "flex-1 min-h-0 overflow-y-auto", - aria.label := "Directory", - children <-- $m.split(_._1)((_, _, s) => - div( - cls := "relative", - Header(s.map(_._1)), - UserList(s.map(_._2)) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala deleted file mode 100644 index e182910..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala +++ /dev/null @@ -1,54 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object SearchForm: - sealed trait Action - case object SubmitFilter extends Action - sealed trait FilterAction extends Action - case object ClearFilter extends FilterAction - case class SetFilter(value: String) extends FilterAction - - def apply(actions: Observer[Action]): HtmlElement = - div( - cls := "px-6 pt-4 pb-4", - form( - cls := "flex space-x-4", - onSubmit.mapTo(SubmitFilter) --> actions, - div( - cls := "flex-1 min-w-0", - label( - forId := "search", - cls := "sr-only", - "Hledat" - ), - div( - cls := "relative rounded-md shadow-sm", - div( - cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", - Icons.solid.search().amend(svg.cls := "text-gray-400") - ), - input( - tpe := "search", - name := "search", - idAttr := "search", - cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", - placeholder := "Hledat", - composeEvents(onInput.mapToValue.setAsValue.map(SetFilter(_)))( - _.throttle(500) - ) --> actions - ) - ) - ), - button( - tpe := "submit", - cls := "inline-flex justify-center px-3.5 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500", - Icons.solid.filter().amend(svg.cls := "text-gray-400"), - span( - cls := "sr-only", - "Hledat" - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala deleted file mode 100644 index 82f7723..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala +++ /dev/null @@ -1,52 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.airstream.core.Signal -import fiftyforms.ui.components.tailwind.Avatar - -object UserRow: - case class ViewModel( - osobniCislo: String, - celeJmeno: String, - prijmeni: String, - hlavniFunkce: Option[String], - img: Option[String], - container: () => HtmlElement = () => div() - ) { - val search = prijmeni.toLowerCase - } - - def apply($m: Signal[ViewModel]): HtmlElement = - inline def avatarImage = - Avatar($m.map(_.img)).avatarImage(10) - - li( - div( - cls := "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500", - div( - cls := "flex-shrink-0", - child <-- avatarImage - ), - div( - cls := "flex-1 min-w-0", - child <-- $m.map { o => - o.container() - .amend( - cls := "focus:outline-none", - span( - cls := "absolute inset-0", - aria.hidden := true - ), - p( - cls := "text-sm font-medium text-gray-900", - o.celeJmeno - ), - p( - cls := "text-sm text-gray-500 truncate", - o.hlavniFunkce - ) - ) - } - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala deleted file mode 100644 index 467c5f5..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala +++ /dev/null @@ -1,99 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router - -object ErrorPage: - case class ViewModel( - homePage: Page, - errorName: String, - title: String, - subTitle: String - ) - - def apply(m: ViewModel, actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - val ViewModel(homePage, errorName, title, subTitle) = m - div( - cls := "min-h-full pt-16 pb-12 flex flex-col bg-white", - main( - cls := "flex-grow flex flex-col justify-center max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex-shrink-0 flex justify-center", - a( - href := "/", - cls := "inline-flex", - span( - cls := "sr-only", - """Workflow""" - ), - img( - cls := "h-12 w-auto", - src := "https://tailwindui.com/img/logos/workflow-mark.svg?color=indigo&shade=600", - alt := "" - ) - ) - ), - div( - cls := "py-16", - div( - cls := "text-center", - p( - cls := "text-sm font-semibold text-indigo-600 uppercase tracking-wide", - errorName - ), - h1( - cls := "mt-2 text-4xl font-extrabold text-gray-900 tracking-tight sm:text-5xl", - title - ), - p( - cls := "mt-2 text-base text-gray-500", - subTitle - ), - div( - cls := "mt-6", - PageLink - .container(homePage, actionBus) - .amend( - cls := "text-base font-medium text-indigo-600 hover:text-indigo-500", - """Go back home""" - ) - ) - ) - ) - ), - footer( - cls := "flex-shrink-0 max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", - nav( - cls := "flex justify-center space-x-4", - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Contact Support""" - ), - span( - cls := "inline-block border-l border-gray-300", - aria.hidden := true - ), - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Status""" - ), - span( - cls := "inline-block border-l border-gray-300", - aria.hidden := true - ), - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Twitter""" - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala deleted file mode 100644 index bd9baec..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action - -object NotFoundPage: - def apply(homePage: Page, url: String, actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - ErrorPage( - ErrorPage.ViewModel( - homePage, - "404 error", - "Page not found.", - s"Sorry, but page $url doesn't exist." - ), - actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala deleted file mode 100644 index 476fe3c..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action - -object UnhandledErrorPage: - - case class ViewModel( - homePage: Page, - errorName: Option[String], - errorMessage: Option[String] - ) - - def apply(m: ViewModel, actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - ErrorPage( - ErrorPage.ViewModel( - m.homePage, - "Unexpected error occurred", - m.errorName.getOrElse( - "Uh oh!" - ), // TODO: translations, better text than uh oh - m.errorMessage.getOrElse( - "This wasn't supposed to happen! Please try again." - ) - ), - actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala deleted file mode 100644 index 25f059f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala +++ /dev/null @@ -1,6 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.services - -import com.raquo.airstream.core.Observer - -trait DataFetcher[K, A]: - def fetch(id: K, sink: Observer[A]): Unit diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala deleted file mode 100644 index a469830..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala +++ /dev/null @@ -1,120 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package state - -import com.raquo.airstream.core.EventStream -import cz.e_bs.cmi.mdr.pdb.{UserInfo, OsobniCislo} -import com.raquo.airstream.core.Observer -import scala.scalajs.js -import scala.scalajs.js.JSON -import zio.json._ -import com.raquo.airstream.eventbus.EventBus -import com.raquo.airstream.ownership.Owner -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.UserFunction -import cz.e_bs.cmi.mdr.pdb.UserContract -import fiftyforms.services.files.File - -trait AppState - extends connectors.DetailPageConnector.AppState - with connectors.DetailParametruPageConnector.AppState - with connectors.DetailKriteriaPageConnector.AppState - with pages.detail.UpravDukaz.State: - def users: EventStream[List[UserInfo]] - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - -class MockAppState(implicit owner: Owner, router: Router[Page]) - extends AppState: - - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonDecoder[UserFunction] = DeriveJsonDecoder.gen - given JsonDecoder[UserContract] = DeriveJsonDecoder.gen - given JsonDecoder[UserInfo] = DeriveJsonDecoder.gen - - given JsonDecoder[ParameterCriteria] = DeriveJsonDecoder.gen - given JsonDecoder[Parameter] = DeriveJsonDecoder.gen - - private val actions = EventBus[Action]() - private val (parametersStream, pushParameters) = - EventStream.withCallback[List[Parameter]] - private val (usersStream, pushUsers) = - EventStream.withCallback[List[UserInfo]] - private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] - private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] - - private val mockData: List[UserInfo] = - mockUsers - .asInstanceOf[js.Dictionary[js.Object]] - .values - // TODO: is there a more efficient way to parse from JS object directly? - .map { o => - val parsed = JSON.stringify(o).fromJson[UserInfo] - parsed.left.foreach(org.scalajs.dom.console.log(_)) - parsed - } - .collect { case Right(u) => - u - } - .toList - - private val mockParameters: List[Parameter] = - pdbParams - .asInstanceOf[js.Dictionary[js.Object]] - .values - .map(o => JSON.stringify(o).fromJson[Parameter]) - .collect { case Right(p) => p } - .toList - - // TODO: Extract to separate event handler - actions.events.foreach { - case FetchDirectory => pushUsers(mockData) - case FetchUserDetails(osc) => - mockData.find(_.personalNumber == osc).foreach { o => - pushDetails(o) - router.replaceState(Page.Detail(o)) - } - case FetchParameters(osc) => - pushParameters(mockParameters) - case FetchParameter(osc, paramId) => - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(Page.DetailParametru(o, p)) - case FetchParameterCriteria(osc, paramId, critId, page) => - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - c <- p.criteria.find(_.id == critId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(page(o, p, c)) - case NavigateTo(page) => router.pushState(page) - case FetchAvailableFiles(osc) => - pushFiles( - List( - File("https://tc163.cmi.cz/here", "Example file") - ) - ) - } - - override def users: EventStream[List[UserInfo]] = - usersStream.debugWithName("users") - - override def details: EventStream[UserInfo] = - detailsStream.debugWithName("details") - - override def parameters: EventStream[List[Parameter]] = - parametersStream.debugWithName("parameters") - - override def availableFiles: EventStream[List[File]] = - filesStream.debugWithName("available files") - - override def actionBus: Observer[Action] = - actions.writer.debugWithName("actions writer") diff --git a/app/src/main/scala/mdr/pdb/app/Main.scala b/app/src/main/scala/mdr/pdb/app/Main.scala new file mode 100644 index 0000000..081b4ee --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/Main.scala @@ -0,0 +1,103 @@ +package mdr.pdb.app + +import scala.scalajs.js.annotation.JSExportTopLevel +import scala.scalajs.js.annotation.JSExport +import scala.scalajs.js.annotation.JSImport +import scala.scalajs.js +import org.scalajs.dom +import com.raquo.laminar.api.L.{*, given} +import scala.scalajs.js.Date +import com.raquo.waypoint.Router +import com.raquo.waypoint.SplitRender +import mdr.pdb.app.services.DataFetcher +import scala.scalajs.js.JSON +import zio.json._ +import mdr.pdb.UserInfo +import mdr.pdb.app.state.AppState + +@js.native +@JSImport("stylesheets/main.css", JSImport.Namespace) +object Css extends js.Any + +@js.native +@JSImport("data/users.json", JSImport.Default) +object mockUsers extends js.Object + +@js.native +@JSImport("params/pdb-params.json", JSImport.Default) +object pdbParams extends js.Object + +@JSExportTopLevel("app") +object Main: + + @JSExport + def main(args: Array[String]): Unit = + import Routes.given + onLoad { + setupAirstream() + val appContainer = dom.document.querySelector("#app") + val _ = + render( + appContainer, + renderPage(state.MockAppState(using unsafeWindowOwner, router)) + ) + } + + private def onLoad(f: => Unit): Unit = + documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) + + private def setupAirstream()(using router: Router[Page]): Unit = + AirstreamError.registerUnhandledErrorCallback(err => + router.forcePage( + Page.UnhandledError( + Some(err.getClass.getName), // TODO: Fill only in dev mode + Some(err.getMessage) + ) + ) + ) + + def renderPage(state: AppState)(using + router: Router[Page] + ): HtmlElement = + val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) + .collectSignal[Page.Detail]( + connectors + .DetailPageConnector(state)(_) + .apply + ) + .collectSignal[Page.DetailParametru]( + connectors + .DetailParametruPageConnector(state)(_) + .apply + ) + .collectSignal[Page.DetailKriteria]( + connectors + .DetailKriteriaPageConnector(state)(_) + .apply + ) + .collectSignal[Page.UpravDukazKriteria]( + pages.detail.UpravDukaz.Connector(state)(_).apply + ) + .collectStatic(Page.Dashboard)( + connectors.DashboardPageConnector(state.actionBus).apply + ) + .collect[Page.NotFound](pg => + pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) + ) + .collect[Page.UnhandledError](pg => + pages.errors + .UnhandledErrorPage( + pages.errors.UnhandledErrorPage + .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), + state.actionBus + ) + ) + .collectStatic(Page.Directory)( + connectors + .DirectoryPageConnector(state.users, state.actionBus) + .apply + ) + div(cls := "h-full", child <-- pageSplitter.$view) + + // Pull in the stylesheet + val css: Css.type = Css diff --git a/app/src/main/scala/mdr/pdb/app/Routes.scala b/app/src/main/scala/mdr/pdb/app/Routes.scala new file mode 100644 index 0000000..2cc860c --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/Routes.scala @@ -0,0 +1,198 @@ +package mdr.pdb.app + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.* +import org.scalajs.dom +import zio.json.{*, given} +import mdr.pdb.OsobniCislo + +import scala.scalajs.js +import mdr.pdb.UserInfo +import mdr.pdb.Parameter +import mdr.pdb.ParameterCriteria +import mdr.pdb.app.Page.Titled + +// enum is not working with Waypoints' SplitRender collectStatic +sealed abstract class Page( + val id: String, + val title: String, + val parent: Option[Page] +) { + val path: Vector[Page] = + parent match + case None => Vector(this) + case Some(p) => p.path :+ this + + val isRoot: Boolean = parent.isEmpty +} + +object Page: + + case class Titled[V](value: V, title: Option[String] = None): + val show: String = title.getOrElse(value.toString) + + case object Directory extends Page("directory", "Adresář", None) + + case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) + + case class Detail(osobniCislo: Titled[OsobniCislo]) + extends Page("user", osobniCislo.show, Some(Directory)) + + object Detail { + def apply(o: UserInfo): Detail = Detail( + Titled(o.personalNumber, Some(o.name)) + ) + } + + case class DetailParametru( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String] + ) extends Page( + "parameter", + parametr.show, + Some(Detail(osobniCislo)) + ) + + object DetailParametru { + def apply(o: UserInfo, p: Parameter): DetailParametru = + DetailParametru( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)) + ) + } + + case class DetailKriteria( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String], + kriterium: Titled[String] + ) extends Page( + "criteria", + kriterium.show, + Some(DetailParametru(osobniCislo, parametr)) + ) + + object DetailKriteria { + def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = + DetailKriteria( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)), + Titled(k.id, Some(k.id)) + ) + } + + case class UpravDukazKriteria( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String], + kriterium: Titled[String] + ) extends Page( + "addProof", + "Důkaz", + Some(DetailKriteria(osobniCislo, parametr, kriterium)) + ) + + object UpravDukazKriteria { + def apply( + o: UserInfo, + p: Parameter, + k: ParameterCriteria + ): UpravDukazKriteria = + UpravDukazKriteria( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)), + Titled(k.id, Some(k.id)) + ) + } + + case class NotFound(url: String) extends Page("404", "404", Some(Directory)) + + case class UnhandledError( + errorName: Option[String], + errorMessage: Option[String] + ) extends Page("500", "Unexpected error", Some(Directory)) + +object Routes: + given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) + given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) + given [V: JsonEncoder]: JsonEncoder[Titled[V]] = + DeriveJsonEncoder.gen[Titled[V]] + given [V: JsonDecoder]: JsonDecoder[Titled[V]] = + DeriveJsonDecoder.gen[Titled[V]] + given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] + given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] + + val base = + js.`import`.meta.env.BASE_URL + .asInstanceOf[String] + "app" + + val homePage: Page = Page.Directory + + given router: Router[Page] = Router[Page]( + routes = List( + Route.static(homePage, root / endOfSegments, basePath = base), + Route.static( + Page.Dashboard, + root / "dashboard" / endOfSegments, + basePath = base + ), + Route[Page.Detail, String]( + encode = _.osobniCislo.value.toString, + decode = osc => Page.Detail(Titled(OsobniCislo(osc))), + root / "osoba" / segment[String] / endOfSegments, + basePath = base + ), + Route[Page.DetailParametru, (String, String)]( + encode = p => (p.osobniCislo.value.toString, p.parametr.value), + decode = + p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / endOfSegments, + basePath = base + ), + Route[Page.DetailKriteria, (String, String, String)]( + encode = p => + ( + p.osobniCislo.value.toString, + p.parametr.value, + p.kriterium.value.replaceAll("\\.", "--") + ), + decode = p => + Page.DetailKriteria( + Titled(OsobniCislo(p._1)), + Titled(p._2), + Titled(p._3.replaceAll("--", ".")) + ), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / "kriterium" / segment[String] / endOfSegments, + basePath = base + ), + Route[Page.UpravDukazKriteria, (String, String, String)]( + encode = p => + ( + p.osobniCislo.value.toString, + p.parametr.value, + p.kriterium.value.replaceAll("\\.", "--") + ), + decode = p => + Page.UpravDukazKriteria( + Titled(OsobniCislo(p._1)), + Titled(p._2), + Titled(p._3.replaceAll("--", ".")) + ), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / "kriterium" / segment[String] / "edit" / endOfSegments, + basePath = base + ) + ), + serializePage = _.toJson, + deserializePage = _.fromJson[Page] + .fold(s => throw IllegalStateException(s), identity), + getPageTitle = _.title, + routeFallback = url => Page.NotFound(url), + deserializeFallback = _ => Page.Dashboard + )( + $popStateEvent = windowEvents.onPopState, + owner = unsafeWindowOwner + ) diff --git a/app/src/main/scala/mdr/pdb/app/actions.scala b/app/src/main/scala/mdr/pdb/app/actions.scala new file mode 100644 index 0000000..d078b3a --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/actions.scala @@ -0,0 +1,21 @@ +package mdr.pdb.app + +import mdr.pdb.OsobniCislo +import mdr.pdb.UserInfo +import mdr.pdb.Parameter +import mdr.pdb.ParameterCriteria + +sealed trait Action + +case object FetchDirectory extends Action +case class FetchUserDetails(osc: OsobniCislo) extends Action +case class FetchParameters(osc: OsobniCislo) extends Action +case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action +case class FetchParameterCriteria( + osc: OsobniCislo, + paramId: String, + critId: String, + page: (UserInfo, Parameter, ParameterCriteria) => Page +) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action +case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/mdr/pdb/app/components/AppPage.scala new file mode 100644 index 0000000..130c817 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/AppPage.scala @@ -0,0 +1,82 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.Page +import mdr.pdb.{UserProfile, UserInfo, OsobniCislo} +import com.raquo.waypoint.Router +import mdr.pdb.app.Action +import mdr.pdb.app.NavigateTo +import mdr.pdb.UserFunction + +object AppPage: + // TODO: pages by logged in user + val pages: List[Page] = List(Page.Directory, Page.Dashboard) + + import NavigationBar.{Logo, MenuItem} + + val logo = Logo( + "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", + "Workflow" + ) + + // TODO: menu items by user profile + val userMenu = + List( + MenuItem("Your Profile"), + MenuItem("Settings"), + MenuItem("Sign out") + ) + + // TODO: load user profile + val $userProfile = Var( + UserProfile( + "tom", + UserInfo( + OsobniCislo("1031"), + "tom", + "Tom", + "Cook", + None, + None, + Some("tom@example.com"), + Some("+420 222 866 180"), + Some( + UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") + ), + Nil, + None + ) + ) + ) + + val $userInfo = $userProfile.signal.map(_.userInfo) + + type ViewModel = Option[HtmlElement] + def apply( + actionBus: Observer[Action] + )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using + router: Router[Page] + ): HtmlElement = + PageLayout(actionBus)( + $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => + PageLayout.ViewModel( + NavigationBar.ViewModel( + u, + pages.map(p => + NavigationBar.Link( + () => + PageLink( + p, + actionBus + ), + p == cp + ) + ), + userMenu, + logo + ), + c + ) + ), + mods + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala new file mode 100644 index 0000000..9375398 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala @@ -0,0 +1,143 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.tailwind.list.IconText.ViewModel +import fiftyforms.ui.components.tailwind.Icons +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.Page + +import com.raquo.laminar.api.L.{*, given} +import io.laminext.syntax.core.{*, given} +import mdr.pdb.app.Action + +object Breadcrumbs: + + private def slash = + import svg.{*, given} + svg( + cls := "flex-shrink-0 h-5 w-5 text-gray-300", + xmlns := "http://www.w3.org/2000/svg", + fill := "currentColor", + viewBox := "0 0 20 20", + ariaHidden := true, + path( + d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" + ) + ) + + def backIcon = + Icons.solid + .`arrow-narrow-left`() + .amend( + svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" + ) + + object Link: + case class ViewModel( + page: Page, + icon: Option[SvgElement], + extraClasses: String, + text: String, + textClasses: Option[String] = None + ) + + val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" + + def shortHome(p: Page) = ViewModel( + p, + Some(backIcon), + "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", + "Zpět na úvodní stránku", + None + ) + + def fullHome(p: Page) = ViewModel( + p, + Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), + "text-gray-400 hover:text-gray-500", + "Domů", + Some("sr-only") + ) + + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + PageLink + .container($m.map(_.page), actionBus) + .amend( + cls <-- $m.map(_.extraClasses), + child.maybe <-- $m.map(_.icon), + span( + cls <-- $m.map(_.textClasses.getOrElse("")), + child.text <-- $m.map(_.text) + ) + ) + + object FullBreadcrumbs: + type ViewModel = Page + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + ol( + role := "list", + cls := "flex items-center space-x-4", + children <-- $m.map(_.path).split(_.id)((_, _, $p) => + li( + div( + cls := "flex items-center", + child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), + Link( + $p.map(p => + if (p.isRoot) then Link.fullHome(p) + else + Link.ViewModel( + p, + None, + s"ml-4 max-w-xs truncate ${Link.baseClasses}", + p.title + ) + ), + actionBus + ) + ) + ) + ) + ) + + object ShortBreadcrumbs: + type ViewModel = Page + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + Link( + $m.map { p => + val target = p.parent.getOrElse(p) + if target.isRoot then Link.shortHome(target) + else + Link.ViewModel( + target, + Some(backIcon), + "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", + s"Zpět na ${target.title}" + ) + }, + actionBus + ) + + def apply(actionBus: Observer[Action])(using + router: Router[Page] + ): HtmlElement = + nav( + cls := "flex", + aria.label := "Breadcrumb", + div( + cls := "flex sm:hidden", + ShortBreadcrumbs(router.$currentPage, actionBus) + ), + div( + cls := "hidden sm:block", + FullBreadcrumbs(router.$currentPage, actionBus) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala new file mode 100644 index 0000000..4bb8b8d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala @@ -0,0 +1,242 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent +import fiftyforms.ui.components.tailwind.Icons +import fiftyforms.ui.components.tailwind.Avatar +import mdr.pdb.UserInfo +import io.laminext.syntax.core.* + +object NavigationBar: + + case class Logo(img: String, name: String) + case class Link(a: () => Anchor, active: Boolean) + case class MenuItem(title: String) + + case class ViewModel( + userInfo: UserInfo, + pages: List[Link], + userMenu: List[MenuItem], + logo: Logo + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + val $userInfo = $m.map(_.userInfo) + val mobileMenuOpen = Var(false) + + val desktopOnly = cls("hidden md:block") + val mobileOnly = cls("md:hidden") + + inline def avatarImage(size: Int = 8) = + Avatar($userInfo.map(_.img)).avatarImage(size) + + def notificationButton = button( + tpe := "button", + cls := List( + "bg-indigo-600", + "focus:outline-none", + "focus:ring-2", + "focus:ring-offset-2", + "focus:ring-offset-indigo-600", + "focus:ring-white", + "hover:text-white", + "p-1", + "rounded-full", + "text-indigo-200" + ), + span(cls := "sr-only", "View notifications"), + Icons.outline.bell() + ) + + def userProfile: HtmlElement = + val menuOpen = Var(false) + + def menuItem(item: MenuItem, idx: Int): HtmlElement = + a( + href := "#", + cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", + role := "menuitem", + tabIndex := -1, + idAttr := s"user-menu-item-$idx", + item.title + ) + + div( + cls := "ml-3 relative", + div( + button( + tpe := "button", + cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", + idAttr := "user-menu-button", + aria.expanded <-- menuOpen.signal, + aria.hasPopup := true, + span(cls := "sr-only", "Open user menu"), + child <-- avatarImage(), + onClick.preventDefault.mapTo( + !menuOpen.now() + ) --> menuOpen.writer + ) + ), + /* + * */ + div( + cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", + cls <-- menuOpen.signal.map { o => + if (o) "md:block" else "md:hidden" + }, + role := "menu", + aria.orientation := "vertical", + aria.labelledBy := "user-menu-button", + tabIndex := -1, + // : keyboard navigation + children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) + ) + ) + + def mobileProfile = + def menuItem(item: MenuItem): HtmlElement = + a( + href := "#", + cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", + item.title + ) + + div( + cls := "pt-4 pb-3 border-t border-indigo-700", + div( + cls := "flex items-center px-5", + div( + cls := "flex-shrink-0", + child <-- avatarImage(10) + ), + div( + cls := "ml-3", + div( + cls := "text-base font-medium text-white", + child.text <-- $userInfo.map(_.name) + ), + child.maybe <-- $userInfo.map( + _.email.map(e => + div( + cls := "text-sm font-medium text-indigo-300", + e + ) + ) + ) + ), + notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) + ), + div( + cls := "mt-3 px-2 space-y-1", + children <-- $m.map(_.userMenu.map(menuItem)) + ) + ) + + def pageLink(page: Link): Anchor = + page + .a() + .amend( + cls := "text-white px-3 py-2 rounded-md text-sm font-medium", + cls := Seq( + "bg-indigo-700" -> page.active, + "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active + ), + ariaCurrent := (if page.active then "page" else "false") + ) + + def logoImg: Image = + img( + cls := "h-8 w-8", + src <-- $m.map(_.logo.img), + alt <-- $m.map(_.logo.name) + ) + + def mobileMenuButton = button( + tpe := "button", + cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", + aria.controls := "mobile-menu", + aria.expanded <-- mobileMenuOpen.signal, + span(cls := "sr-only", "Open main menu"), + Icons.outline + .menu() + .amend( + svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") + ), + Icons.outline + .x() + .amend( + svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") + ), + onClick.preventDefault.mapTo( + !mobileMenuOpen.now() + ) --> mobileMenuOpen.writer + ) + + def navBarLeft = + div( + cls := "flex items-center", + div(cls := "flex-shrink-0", logoImg), + div( + desktopOnly, + div( + cls := "ml-10 flex items-baseline space-x-4", + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) + ) + ) + ) + + def navBarRight = + div( + desktopOnly, + div( + cls := "ml-4 flex items-center md:ml-6", + notificationButton, + userProfile + ) + ) + + def navBarMobile = + div( + cls := "-mr-2 flex", + mobileOnly, + mobileMenuButton + ) + + def navBar = + div( + cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", + div( + cls := "flex items-center justify-between h-16", + navBarLeft, + navBarRight, + navBarMobile + ) + ) + + def mobileMenu = + div( + mobileOnly, + cls <-- mobileMenuOpen.signal.map { o => + if (o) "block" else "hidden" + }, + idAttr := "mobile-menu", + div( + cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) + ), + mobileProfile + ) + + nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala new file mode 100644 index 0000000..c4cb10d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala @@ -0,0 +1,19 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.Page +import com.raquo.waypoint.Router +import mdr.pdb.app.Action + +object PageHeader: + def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = + header( + cls := "bg-white shadow-sm", + div( + cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", + h1( + cls := "text-lg leading-6 font-semibold text-gray-900", + Breadcrumbs(actionBus) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala new file mode 100644 index 0000000..aae4539 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala @@ -0,0 +1,30 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.Page +import mdr.pdb.app.Action +import fiftyforms.ui.components.tailwind.Loading +import io.laminext.syntax.core.* + +object PageLayout: + case class ViewModel( + navigation: NavigationBar.ViewModel, + content: Option[HtmlElement] + ) + def apply(actionBus: Observer[Action])( + $m: Signal[ViewModel], + mods: Modifier[HtmlElement]* + )(using router: Router[Page]): HtmlElement = + val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) + div( + cls := "h-full flex flex-col", + NavigationBar($m.map(_.navigation)), + child.maybe <-- router.$currentPage.map(_.isRoot) + .switch(None, Some(PageHeader(actionBus))), + main( + cls := "flex-grow-1 overflow-y-auto", + mods, + child <-- $maybeContent.map(_.getOrElse(Loading)) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/mdr/pdb/app/components/PageLink.scala new file mode 100644 index 0000000..e6e7052 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageLink.scala @@ -0,0 +1,40 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.LinkSupport.* +import mdr.pdb.app.Page +import mdr.pdb.app.Action +import com.raquo.waypoint.Router +import mdr.pdb.app.NavigateTo + +object PageLink: + type ViewModel = Page + def apply($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = + a(mods($m, actions), child.text <-- $m.map(_.title)) + + def apply(m: ViewModel, actions: Observer[Action])(using + router: Router[Page] + ): Anchor = apply(Val(m), actions) + + def container($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = a(mods($m, actions)) + + def container(m: ViewModel, actions: Observer[Action])(using + Router[Page] + ): Anchor = container(Val(m), actions) + + private def mods($m: Signal[Page], actions: Observer[Action])(using + router: Router[Page] + ): Modifier[Anchor] = + Seq( + href <-- $m.map(router.absoluteUrlForPage).recover { case _ => + Some("invalid url") + }, + composeEvents(onClick.noKeyMod.preventDefault)( + _.sample($m) + .map(NavigateTo.apply) + ) --> actions + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala new file mode 100644 index 0000000..7682772 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala @@ -0,0 +1,13 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.pages.dashboard.DashboardPage +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage + +class DashboardPageConnector(actionBus: Observer[Action])(using + router: Router[Page] +): + def apply: HtmlElement = + AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala new file mode 100644 index 0000000..76b9133 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala @@ -0,0 +1,76 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.Parameter +import mdr.pdb.UserInfo +import mdr.pdb.app.pages.detail.DetailParametruPage +import pages.detail.DetailKriteriaPage +import pages.detail.components.DukazyKriteria +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.ParameterCriteria + +object DetailKriteriaPageConnector { + trait AppState { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + } +} + +case class DetailKriteriaPageConnector( + state: DetailKriteriaPageConnector.AppState +)( + $page: Signal[Page.DetailKriteria] +)(using Router[Page]): + val $paramChangeSignal = + $page.splitOne(p => + (p.osobniCislo.value, p.parametr.value, p.kriterium.value) + )((x, _, _) => x) + val $pageChangeSignal = + $paramChangeSignal.map( + FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) + ) + + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + val $merged = + $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => + for { + da <- d + pa <- p + pb <- pa.find(_.id == pc._2) + ka <- pb.criteria.find(_.id == pc._3) + } yield (da, pb, ka) + ) + + def apply: HtmlElement = + AppPage(state.actionBus)( + $merged.map(_.map(buildModel)) + .split(_ => ())((_, s, $s) => + DetailKriteriaPage($s)(state.actionBus.contramap { + case DukazyKriteria.Add => + NavigateTo( + Page.UpravDukazKriteria( + Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), + Page.Titled(s.parametr.id, Some(s.parametr.nazev)), + Page.Titled(s.kriterium.id, Some(s.kriterium.id)) + ) + ) + }) + ), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: Parameter, + k: ParameterCriteria + ): DetailKriteriaPage.ViewModel = + DetailKriteriaPage.ViewModel( + o.toDetailOsoby, + p.toParametr(_ => a()), + k.toKriterium(_ => a()) + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DetailPageConnector.scala new file mode 100644 index 0000000..df3a6b7 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DetailPageConnector.scala @@ -0,0 +1,57 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.UserInfo +import pages.detail.DetailPage +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.pages.detail.components.DetailOsoby +import mdr.pdb.Parameter +import mdr.pdb.app.pages.detail.components.SeznamParametru +import fiftyforms.ui.components.tailwind.Color + +object DetailPageConnector { + trait AppState { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + } +} + +case class DetailPageConnector(state: DetailPageConnector.AppState)( + $page: Signal[Page.Detail] +)(using router: Router[Page]): + val $oscChangeSignal = $page.splitOne(_.osobniCislo.value)((osc, _, _) => osc) + val $pageChangeSignal = + $oscChangeSignal.flatMap(osc => + EventStream.fromSeq(Seq(FetchUserDetails(osc), FetchParameters(osc))) + ) + // TODO: filter the value based on the current osc + // OSC change will fetch new data, but still + // - we need to be sure that what we got is really what we ought to display + // - we want to display stale data accordingly (at least with loading indicator) + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + def apply: HtmlElement = + AppPage(state.actionBus)( + $data.combineWithFn($params)(_ zip _) + .map(_.map(buildModel)) + .split(_ => ())((_, _, s) => DetailPage(s)), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: List[Parameter] + ): DetailPage.ViewModel = + DetailPage.ViewModel( + o.toDetailOsoby, + p.map( + _.toParametr(param => + PageLink.container(Page.DetailParametru(o, param), state.actionBus) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DetailParametruPageConnector.scala new file mode 100644 index 0000000..fa7193d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DetailParametruPageConnector.scala @@ -0,0 +1,67 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.Parameter +import mdr.pdb.UserInfo +import mdr.pdb.app.pages.detail.DetailParametruPage +import pages.detail.DetailParametruPage +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.components.PageLink + +object DetailParametruPageConnector { + trait AppState { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + } +} + +case class DetailParametruPageConnector( + state: DetailParametruPageConnector.AppState +)( + $page: Signal[Page.DetailParametru] +)(using router: Router[Page]): + val $paramChangeSignal = + $page.splitOne(p => (p.osobniCislo.value, p.parametr.value))((x, _, _) => x) + val $pageChangeSignal = + $paramChangeSignal.map(FetchParameter(_, _)) + + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + val $merged = + $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => + for { + da <- d + pa <- p + pb <- pa.find(_.id == pc._2) + } yield (da, pb) + ) + + def apply: HtmlElement = + AppPage(state.actionBus)( + $merged.map(_.map(buildModel)) + .split(_ => ())((_, _, s) => DetailParametruPage(s)), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: Parameter + ): DetailParametruPage.ViewModel = + DetailParametruPage.ViewModel( + o.toDetailOsoby, + p.toParametr(p => + PageLink.container(Page.DetailParametru(o, p), state.actionBus) + ), + p.criteria.map( + _.toKriterium { c => + PageLink.container( + Page.DetailKriteria(o, p, c), + state.actionBus + ) + } + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DirectoryPageConnector.scala new file mode 100644 index 0000000..ab2c045 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DirectoryPageConnector.scala @@ -0,0 +1,34 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.UserInfo +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.components.AppPage + +case class DirectoryPageConnector( + $input: EventStream[List[UserInfo]], + actionBus: Observer[Action] +)(using router: Router[Page]): + val $data = $input.startWithNone + val $actionSignal = EventStream.fromValue(FetchDirectory) + + def apply: HtmlElement = + AppPage(actionBus)( + $data.split(_ => ())((_, _, s) => + pages.directory.DirectoryPage( + s.map( + _.map( + _.toUserRow(u => + PageLink.container( + Page.Detail(Page.Titled(u.personalNumber)), + actionBus + ) + ) + ) + ) + ) + ), + $actionSignal --> actionBus + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/mdr/pdb/app/connectors/conversions.scala new file mode 100644 index 0000000..f581796 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/conversions.scala @@ -0,0 +1,68 @@ +package mdr.pdb.app.connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.UserInfo +import mdr.pdb.app.pages.detail.components.DetailOsoby +import mdr.pdb.Parameter +import mdr.pdb.app.pages.detail.components.SeznamParametru +import mdr.pdb.ParameterCriteria +import mdr.pdb.app.pages.detail.components.SeznamKriterii +import mdr.pdb.app.pages.directory.components.UserRow +import mdr.pdb.app.pages.detail.components.DetailParametru +import mdr.pdb.app.pages.detail.components.DetailKriteria +import fiftyforms.ui.components.tailwind.Color + +extension (o: UserInfo) + def toDetailOsoby: DetailOsoby.ViewModel = + DetailOsoby.ViewModel( + o.personalNumber, + o.name, + o.email, + o.phone, + o.img, + o.mainFunction.map(f => + DetailOsoby.Funkce.ViewModel(f.name, f.dept, f.ou) + ), + o.userContracts.headOption.map(c => + DetailOsoby.PracovniPomer.ViewModel(c.rel, c.startDate, c.endDate) + ) + ) + +extension (param: Parameter) + def toParametr(container: Parameter => Anchor): DetailParametru.ViewModel = + DetailParametru.ViewModel( + id = param.id, + nazev = param.name, + popis = param.description, + status = "Nesplněno", + statusColor = Color.red, + a = container(param) + ) + +extension (crit: ParameterCriteria) + def toKriterium( + container: ParameterCriteria => Anchor + ): DetailKriteria.ViewModel = + DetailKriteria.ViewModel( + nazev = crit.criteriumText, + kapitola = crit.chapterId, + bod = crit.itemId, + status = "Nesplněno", + statusColor = Color.red, + splneno = false, + dukazy = Nil, + container = container(crit) + ) + +extension (user: UserInfo) + def toUserRow( + container: UserInfo => HtmlElement = (_: UserInfo) => div() + ): UserRow.ViewModel = + UserRow.ViewModel( + osobniCislo = user.personalNumber.toString, + celeJmeno = user.name, + prijmeni = user.surname, + hlavniFunkce = user.mainFunction.map(_.name), + img = user.img, + container = () => container(user) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/dashboard/DashboardPage.scala b/app/src/main/scala/mdr/pdb/app/pages/dashboard/DashboardPage.scala new file mode 100644 index 0000000..0eb8f7a --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/dashboard/DashboardPage.scala @@ -0,0 +1,11 @@ +package mdr.pdb.app.pages.dashboard + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.Page + +object DashboardPage: + + def render: HtmlElement = + div("Dashboard page") diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala new file mode 100644 index 0000000..ff3a25f --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala @@ -0,0 +1,31 @@ +package mdr.pdb.app.pages.detail + +import com.raquo.laminar.api.L.{*, given} + +import components._ + +object DetailKriteriaPage: + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametr: DetailParametru.ViewModel, + kriterium: DetailKriteria.ViewModel + ) + + def apply($m: Signal[ViewModel])( + action: Observer[DukazyKriteria.Action] + ): HtmlElement = + div( + cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + div( + DetailOsoby.header($m.map(_.osoba)), + DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") + ), + div( + DetailKriteria($m.map(_.kriterium)), + DukazyKriteria($m.map(_.kriterium.dukazy))(action) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/DetailPage.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailPage.scala new file mode 100644 index 0000000..b591cc9 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailPage.scala @@ -0,0 +1,25 @@ +package mdr.pdb.app.pages.detail + +import com.raquo.laminar.api.L.{*, given} + +import components._ +import mdr.pdb.app.Page +import com.raquo.waypoint.Router +import mdr.pdb.app.Action + +object DetailPage: + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametry: SeznamParametru.ViewModel + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + DetailOsoby($m.map(_.osoba)), + SeznamParametru($m.map(_.parametry)) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/DetailParametruPage.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailParametruPage.scala new file mode 100644 index 0000000..3322279 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailParametruPage.scala @@ -0,0 +1,24 @@ +package mdr.pdb.app.pages.detail + +import com.raquo.laminar.api.L.{*, given} + +import components._ + +object DetailParametruPage: + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametr: DetailParametru.ViewModel, + kriteria: SeznamKriterii.ViewModel + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + DetailOsoby.header($m.map(_.osoba)), + DetailParametru($m.map(_.parametr)), + SeznamKriterii($m.map(_.kriteria)) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala new file mode 100644 index 0000000..b58618a --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala @@ -0,0 +1,107 @@ +package mdr.pdb +package app +package pages.detail + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.pages.detail.components.UpravDukazForm +import fiftyforms.services.files.File + +object UpravDukaz: + + type ThisPage = Page.UpravDukazKriteria + type PageKey = (OsobniCislo, String, String) + + trait State { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def availableFiles: EventStream[List[File]] + def actionBus: Observer[Action] + } + + def keyOfPage(page: ThisPage): PageKey = + (page.osobniCislo.value, page.parametr.value, page.kriterium.value) + + def onChangeAction(key: PageKey): Action = + FetchParameterCriteria( + key._1, + key._2, + key._3, + Page.UpravDukazKriteria(_, _, _) + ) + + class Connector(state: State)($page: Signal[ThisPage])(using Router[Page]): + val $paramChangeSignal = $page.splitOne(keyOfPage)((x, _, _) => x) + val $pageChangeSignal = $paramChangeSignal.map(onChangeAction) + + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + val $merged = + $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => + for { + da <- d + pa <- p + pb <- pa.find(_.id == pc._2) + ka <- pb.criteria.find(_.id == pc._3) + } yield (da, pb, ka) + ) + + def apply: HtmlElement = + AppPage(state.actionBus)( + $merged.split(_ => ())((_, s, $s) => + PageComponent( + $s.map(buildModel), + state.availableFiles, + state.actionBus.contramap { + case UpravDukazForm.Cancelled => + NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) + case UpravDukazForm.AvailableFilesRequested => + FetchAvailableFiles(s._1.personalNumber) + } + ) + ), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: Parameter, + k: ParameterCriteria + ): PageComponent.ViewModel = + import connectors.* + PageComponent.ViewModel( + o.toDetailOsoby, + p.toParametr(_ => a()), + k.toKriterium(_ => a()) + ) + + object PageComponent: + import components.* + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametr: DetailParametru.ViewModel, + kriterium: DetailKriteria.ViewModel + ) + + def apply( + $m: Signal[ViewModel], + availableFilesStream: EventStream[List[File]], + events: Observer[UpravDukazForm.Event] + ): HtmlElement = + div( + cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + div( + DetailOsoby.header($m.map(_.osoba)), + DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") + ), + div( + DetailKriteria($m.map(_.kriterium)), + UpravDukazForm(availableFilesStream)(events) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailKriteria.scala new file mode 100644 index 0000000..a3ab634 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailKriteria.scala @@ -0,0 +1,37 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.CustomAttrs +import fiftyforms.ui.components.tailwind.Icons +import java.time.LocalDate +import fiftyforms.ui.components.tailwind.Color + +object DetailKriteria: + case class ViewModel( + nazev: String, + kapitola: String, + bod: String, + status: String, + statusColor: Color, + splneno: Boolean, + dukazy: List[DukazKriteria.ViewModel], + container: HtmlElement = div() + ) { + val id = s"${kapitola}${bod}" + } + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "pb-5 border-b border-gray-200", + h3( + cls := "text-xl leading-6 font-bold text-gray-900", + child.text <-- $m.map(_.nazev) + ), + h3( + cls := "mt-2 max-w-4xl text-sm text-gray-500", + "Kapitola ", + child.text <-- $m.map(_.kapitola), + " bod ", + child <-- $m.map(_.bod) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailOsoby.scala new file mode 100644 index 0000000..0fe2ba5 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailOsoby.scala @@ -0,0 +1,100 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.OsobniCislo + +import java.time.LocalDate +import fiftyforms.ui.components.tailwind.Avatar + +object DetailOsoby: + + object Funkce: + case class ViewModel( + nazev: String, + stredisko: String, + voj: String + ) + + def render($m: Signal[ViewModel]): HtmlElement = + p( + cls := "text-sm font-medium text-gray-500", + child.text <-- $m.map(_.nazev), + span( + cls := "hidden md:inline", + " @ ", + child.text <-- $m.map(_.stredisko), + ", ", + child.text <-- $m.map(_.voj) + ) + ) + + object PracovniPomer: + case class ViewModel( + druh: String, + pocatek: LocalDate, + konec: Option[LocalDate] + ) + + def render($m: Signal[ViewModel]): HtmlElement = + import fiftyforms.ui.components.tailwind.CustomAttrs.datetime + p( + cls := "text-sm font-medium text-gray-500", + child.text <-- $m.map(_.druh), + " od ", + time( + datetime <-- $m.map(_.pocatek.toString), + child.text <-- $m.map(_.pocatek.toString) + ) + ) + + case class ViewModel( + osobniCislo: OsobniCislo, + jmeno: String, + email: Option[String], + telefon: Option[String], + img: Option[String], + hlavniFunkce: Option[Funkce.ViewModel], + pracovniPomer: Option[PracovniPomer.ViewModel] + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "md:flex md:items-center md:justify-between md:space-x-4", + div( + cls := "flex items-start space-x-4", + div( + cls := "flex-shrink-0", + Avatar($m.map(_.img)).avatar(16) + ), + div( + h1( + cls := "text-2xl font-bold text-gray-900", + child.text <-- $m.map(_.jmeno) + ), + child.maybe <-- $m.map(_.hlavniFunkce).split(_ => ())((_, _, d) => + Funkce.render(d) + ), + child.maybe <-- $m.map(_.pracovniPomer).split(_ => ())((_, _, d) => + PracovniPomer.render(d) + ) + ) + ) + ) + + def header($m: Signal[ViewModel]): HtmlElement = + div( + cls := "md:flex md:items-center md:justify-between md:space-x-5", + div( + cls := "flex items-start space-x-4", + div( + cls := "flex-shrink-0", + Avatar($m.map(_.img)).avatar(8) + ), + div( + h1( + cls := "text-lg font-medium text-gray-800", + child.text <-- $m.map(_.jmeno) + ) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailParametru.scala new file mode 100644 index 0000000..6310dde --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailParametru.scala @@ -0,0 +1,34 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.Color + +object DetailParametru: + case class ViewModel( + id: String, + nazev: String, + popis: String, + status: String, + statusColor: Color, + a: Anchor + ) + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "pb-5 border-b border-gray-200", + h2( + cls := "text-2xl leading-6 font-medium text-gray-900", + child.text <-- $m.map(_.nazev) + ), + p( + cls := "mt-2 max-w-4xl text-sm text-gray-500", + child.text <-- $m.map(_.popis) + ) + ) + + def header($m: Signal[ViewModel]): HtmlElement = + div( + h2( + cls := "text-lg leading-6 font-medium text-gray-600", + child.text <-- $m.map(_.nazev) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazKriteria.scala new file mode 100644 index 0000000..c292919 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazKriteria.scala @@ -0,0 +1,177 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import java.time.LocalDate +import fiftyforms.ui.components.tailwind.CustomAttrs + +object DukazKriteria: + case class Osoba(osobniCislo: String, jmeno: String) + case class Udalost(osoba: Osoba, datum: LocalDate) + case class Dokument( + url: String, + nazev: String, + pridal: Udalost, + odebral: Option[Udalost] + ) + case class ViewModel( + dokumenty: List[Dokument], + autorizoval: Option[Udalost], + platiDo: Option[LocalDate], + poznámka: Option[String] + ) + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "bg-white shadow overflow-hidden sm:rounded-lg", + div( + cls := "px-4 py-5 sm:px-6", + h3( + cls := "text-lg leading-6 font-medium text-gray-900", + """Applicant Information""" + ), + p( + cls := "mt-1 max-w-2xl text-sm text-gray-500", + """Personal details and application.""" + ) + ), + div( + cls := "border-t border-gray-200 px-4 py-5 sm:px-6", + dl( + cls := "grid grid-cols-1 gap-x-4 gap-y-8 sm:grid-cols-2", + div( + cls := "sm:col-span-1", + dt( + cls := "text-sm font-medium text-gray-500", + """Full name""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """Margot Foster""" + ) + ), + div( + cls := "sm:col-span-1", + dt( + cls := "text-sm font-medium text-gray-500", + """Application for""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """Backend Developer""" + ) + ), + div( + cls := "sm:col-span-1", + dt( + cls := "text-sm font-medium text-gray-500", + """Email address""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """margotfoster@example.com""" + ) + ), + div( + cls := "sm:col-span-1", + dt( + cls := "text-sm font-medium text-gray-500", + """Salary expectation""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """$120,000""" + ) + ), + div( + cls := "sm:col-span-2", + dt( + cls := "text-sm font-medium text-gray-500", + """About""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """Fugiat ipsum ipsum deserunt culpa aute sint do nostrud anim incididunt cillum culpa consequat. Excepteur qui ipsum aliquip consequat sint. Sit id mollit nulla mollit nostrud in ea officia proident. Irure nostrud pariatur mollit ad adipisicing reprehenderit deserunt qui eu.""" + ) + ), + div( + cls := "sm:col-span-2", + dt( + cls := "text-sm font-medium text-gray-500", + """Attachments""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + ul( + role := "list", + cls := "border border-gray-200 rounded-md divide-y divide-gray-200", + li( + cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", + div( + cls := "w-0 flex-1 flex items-center", { + import svg.* + import CustomAttrs.svg.ariaHidden + svg( + cls := "flex-shrink-0 h-5 w-5 text-gray-400", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + ariaHidden := true, + path( + fillRule := "evenodd", + d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", + clipRule := "evenodd" + ) + ) + }, + span( + cls := "ml-2 flex-1 w-0 truncate", + """resume_back_end_developer.pdf""" + ) + ), + div( + cls := "ml-4 flex-shrink-0", + a( + href := "#", + cls := "font-medium text-indigo-600 hover:text-indigo-500", + """Download""" + ) + ) + ), + li( + cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", + div( + cls := "w-0 flex-1 flex items-center", { + import svg.* + import CustomAttrs.svg.ariaHidden + svg( + cls := "flex-shrink-0 h-5 w-5 text-gray-400", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + ariaHidden := true, + path( + fillRule := "evenodd", + d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", + clipRule := "evenodd" + ) + ) + }, + span( + cls := "ml-2 flex-1 w-0 truncate", + """coverletter_back_end_developer.pdf""" + ) + ), + div( + cls := "ml-4 flex-shrink-0", + a( + href := "#", + cls := "font-medium text-indigo-600 hover:text-indigo-500", + """Download""" + ) + ) + ) + ) + ) + ) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala new file mode 100644 index 0000000..d604860 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala @@ -0,0 +1,35 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.Icons + +object DukazyKriteria: + sealed trait Action + case object Add extends Action + + type ViewModel = List[DukazKriteria.ViewModel] + + def apply($m: Signal[ViewModel])(actions: Observer[Action]): HtmlElement = + div( + child.maybe <-- $m.map(m => + if (m.isEmpty) then Some(prazdnyDukaz(actions)) + else None + ), + children <-- $m.map(_.zipWithIndex).split(_._2)((_, _, $s) => + DukazKriteria($s.map(_._1)) + ) + ) + + private def prazdnyDukaz(actions: Observer[Action]): HtmlElement = + button( + tpe := "button", + onClick.preventDefault.mapTo(Add) --> actions, + cls := "relative block w-full border-2 border-gray-300 border-dashed rounded-lg p-12 text-center hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + Icons.outline + .`document-add`(12) + .amend(svg.cls := "mx-auto text-gray-400"), + span( + cls := "mt-2 block text-sm font-medium text-gray-900", + "Přidat důkaz" + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala new file mode 100644 index 0000000..7e77a7b --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala @@ -0,0 +1,34 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.Color +import fiftyforms.ui.components.tailwind.list.StackedList +import fiftyforms.ui.components.tailwind.list.ListRow +import fiftyforms.ui.components.tailwind.list.RowTag +import fiftyforms.ui.components.tailwind.list.RowNext +import java.time.LocalDate + +object SeznamKriterii: + type ViewModel = List[DetailKriteria.ViewModel] + + private val kritList = new StackedList[DetailKriteria.ViewModel] + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "bg-white shadow overflow-hidden sm:rounded-md", + kritList($m, _.id) { $i => + $i.map { i => + ListRow.ViewModel( + title = i.nazev, + topRight = RowTag.render( + $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) + ), + bottomLeft = + p(cls := "text-sm text-gray-500", s"${i.kapitola}${i.bod}"), + bottomRight = emptyNode, + farRight = RowNext.render, + containerElement = i.container + ) + } + } + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamParametru.scala new file mode 100644 index 0000000..0760b32 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamParametru.scala @@ -0,0 +1,37 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.list.{ + StackedList, + ListRow, + RowTag, + PropList, + IconText, + RowNext +} +import fiftyforms.ui.components.tailwind.Color +import fiftyforms.ui.components.tailwind.LinkSupport.* + +object SeznamParametru: + type ViewModel = List[DetailParametru.ViewModel] + + private val parametrList = new StackedList[DetailParametru.ViewModel] + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "bg-white shadow overflow-hidden sm:rounded-md", + parametrList($m, _.id) { $i => + $i.map { i => + ListRow.ViewModel( + title = i.nazev, + topRight = RowTag.render( + $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) + ), + bottomLeft = emptyNode, + bottomRight = emptyNode, + farRight = RowNext.render, + containerElement = i.a + ) + } + } + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala new file mode 100644 index 0000000..28753c9 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala @@ -0,0 +1,101 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import io.laminext.syntax.core.* +import fiftyforms.ui.components.tailwind.CustomAttrs +import fiftyforms.ui.components.tailwind.form.* +import org.scalajs.dom +import com.raquo.laminar.nodes.ReactiveHtmlElement +import fiftyforms.services.files.components.tailwind.FilePicker +import mdr.pdb.frontend.AutorizujDukaz +import mdr.pdb.frontend.DocumentRef +import fiftyforms.services.files.File + +object UpravDukazForm: + sealed trait Event + case object Cancelled extends Event + case object AvailableFilesRequested extends Event + def apply(availableFilesStream: EventStream[List[File]])( + updates: Observer[Event] + ): HtmlElement = + val (filesStream, filesObserver) = + EventStream.withObserver[FilePicker.Event] + val files = Var[List[File]](Nil) + def submitButtons: HtmlElement = + div( + cls := "pt-5", + div( + cls := "flex justify-end", + button( + tpe := "button", + cls := "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + "Zrušit", + onClick.mapTo(Cancelled) --> updates + ), + button( + tpe := "submit", + cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + disabled <-- files.signal.map(_.isEmpty), + "Autorizovat" + ) + ) + ) + + div( + filesStream.collect { case FilePicker.SelectionUpdated(files) => + files.to(List) + } --> files.writer, + filesStream.collect { case FilePicker.AvailableFilesRequested => + AvailableFilesRequested + } --> updates, + cls := "bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6", + Form( + Form.Body( + Form.Section( + FormHeader( + FormHeader.ViewModel( + "Doložení kritéria", + "Sestavte doklady poskytující důkaz kritéria a potvrďte odesláním formuláře. Případné limitace či jiné relevantní údaje vepište do pole Komentář." + ) + ), + FormFields( + FormRow( + "dokumenty", + "Dokumenty", + FilePicker(files.signal, availableFilesStream)(filesObserver) + .amend(idAttr := "dokumenty", cls("max-w-lg")) + ).toHtml, + FormRow( + "platnost", + "Platnost", + input( + idAttr := "platnost", + name := "platnost", + tpe := "date", + autoComplete := "date", + cls := "block max-w-lg w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" + ) + ).toHtml, + FormRow( + "komentar", + "Komentář", + div( + cls := "mt-1 sm:mt-0 sm:col-span-2", + textArea( + idAttr := "komentar", + name := "about", + rows := 3, + cls := "max-w-lg shadow-sm block w-full focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border border-gray-300 rounded-md" + ), + p( + cls := "mt-2 text-sm text-gray-500", + "Doplňte prosím potřebné informace související s doložením kritéria, včetně případných limitací." + ) + ) + ).toHtml + ) + ) + ), + submitButtons + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/directory/DirectoryPage.scala b/app/src/main/scala/mdr/pdb/app/pages/directory/DirectoryPage.scala new file mode 100644 index 0000000..7a72ca3 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/directory/DirectoryPage.scala @@ -0,0 +1,40 @@ +package mdr.pdb.app.pages.directory + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.UserInfo + +import components._ + +object DirectoryPage: + + type ViewModel = List[UserRow.ViewModel] + + def apply($m: Signal[ViewModel]): HtmlElement = + val (actionsStream, actionObserver) = + EventStream.withObserver[SearchForm.Action] + val $filter = actionsStream + .collect { + case SearchForm.ClearFilter => None + case SearchForm.SetFilter(t) => Some(t) + } + .startWith(None) + val byLetter = for { + d <- $m + f <- $filter + } yield for { + (letter, users) <- d + .filter { user => + f match + case None => true + case Some(t) => user.search.contains(t) + } + .groupBy(_.prijmeni.head) + .to(List) + .sortBy(_._1) + } yield (letter.toString, users.sortBy(_.prijmeni)) + + div( + cls := "h-full max-w-7xl mx-auto order-first flex flex-col flex-shrink-0", + SearchForm(actionObserver), + Directory(byLetter) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/directory/components/Directory.scala b/app/src/main/scala/mdr/pdb/app/pages/directory/components/Directory.scala new file mode 100644 index 0000000..f7d6f99 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/directory/components/Directory.scala @@ -0,0 +1,37 @@ +package mdr.pdb.app.pages.directory.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.UserInfo + +object Directory: + + object Header: + type ViewModel = String + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500", + h3(child.text <-- $m) + ) + + object UserList: + type ViewModel = List[UserRow.ViewModel] + def apply($m: Signal[ViewModel]): HtmlElement = + ul( + role := "list", + cls := "relative z-0 divide-y divide-gray-200", + children <-- $m.split(_.osobniCislo)((_, _, s) => UserRow(s)) + ) + + type ViewModel = List[(String, List[UserRow.ViewModel])] + def apply($m: Signal[ViewModel]): HtmlElement = + nav( + cls := "flex-1 min-h-0 overflow-y-auto", + aria.label := "Directory", + children <-- $m.split(_._1)((_, _, s) => + div( + cls := "relative", + Header(s.map(_._1)), + UserList(s.map(_._2)) + ) + ) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala deleted file mode 100644 index a3b00af..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala +++ /dev/null @@ -1,103 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import scala.scalajs.js.annotation.JSExportTopLevel -import scala.scalajs.js.annotation.JSExport -import scala.scalajs.js.annotation.JSImport -import scala.scalajs.js -import org.scalajs.dom -import com.raquo.laminar.api.L.{*, given} -import scala.scalajs.js.Date -import com.raquo.waypoint.Router -import com.raquo.waypoint.SplitRender -import cz.e_bs.cmi.mdr.pdb.app.services.DataFetcher -import scala.scalajs.js.JSON -import zio.json._ -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.state.AppState - -@js.native -@JSImport("stylesheets/main.css", JSImport.Namespace) -object Css extends js.Any - -@js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native -@JSImport("params/pdb-params.json", JSImport.Default) -object pdbParams extends js.Object - -@JSExportTopLevel("app") -object Main: - - @JSExport - def main(args: Array[String]): Unit = - import Routes.given - onLoad { - setupAirstream() - val appContainer = dom.document.querySelector("#app") - val _ = - render( - appContainer, - renderPage(state.MockAppState(using unsafeWindowOwner, router)) - ) - } - - private def onLoad(f: => Unit): Unit = - documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) - - private def setupAirstream()(using router: Router[Page]): Unit = - AirstreamError.registerUnhandledErrorCallback(err => - router.forcePage( - Page.UnhandledError( - Some(err.getClass.getName), // TODO: Fill only in dev mode - Some(err.getMessage) - ) - ) - ) - - def renderPage(state: AppState)(using - router: Router[Page] - ): HtmlElement = - val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) - .collectSignal[Page.Detail]( - connectors - .DetailPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailParametru]( - connectors - .DetailParametruPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailKriteria]( - connectors - .DetailKriteriaPageConnector(state)(_) - .apply - ) - .collectSignal[Page.UpravDukazKriteria]( - pages.detail.UpravDukaz.Connector(state)(_).apply - ) - .collectStatic(Page.Dashboard)( - connectors.DashboardPageConnector(state.actionBus).apply - ) - .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) - ) - .collect[Page.UnhandledError](pg => - pages.errors - .UnhandledErrorPage( - pages.errors.UnhandledErrorPage - .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), - state.actionBus - ) - ) - .collectStatic(Page.Directory)( - connectors - .DirectoryPageConnector(state.users, state.actionBus) - .apply - ) - div(cls := "h-full", child <-- pageSplitter.$view) - - // Pull in the stylesheet - val css: Css.type = Css diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala deleted file mode 100644 index 3bbc8c0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ /dev/null @@ -1,198 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.* -import org.scalajs.dom -import zio.json.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import scala.scalajs.js -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.Page.Titled - -// enum is not working with Waypoints' SplitRender collectStatic -sealed abstract class Page( - val id: String, - val title: String, - val parent: Option[Page] -) { - val path: Vector[Page] = - parent match - case None => Vector(this) - case Some(p) => p.path :+ this - - val isRoot: Boolean = parent.isEmpty -} - -object Page: - - case class Titled[V](value: V, title: Option[String] = None): - val show: String = title.getOrElse(value.toString) - - case object Directory extends Page("directory", "Adresář", None) - - case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) - - case class Detail(osobniCislo: Titled[OsobniCislo]) - extends Page("user", osobniCislo.show, Some(Directory)) - - object Detail { - def apply(o: UserInfo): Detail = Detail( - Titled(o.personalNumber, Some(o.name)) - ) - } - - case class DetailParametru( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String] - ) extends Page( - "parameter", - parametr.show, - Some(Detail(osobniCislo)) - ) - - object DetailParametru { - def apply(o: UserInfo, p: Parameter): DetailParametru = - DetailParametru( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)) - ) - } - - case class DetailKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "criteria", - kriterium.show, - Some(DetailParametru(osobniCislo, parametr)) - ) - - object DetailKriteria { - def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = - DetailKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class UpravDukazKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "addProof", - "Důkaz", - Some(DetailKriteria(osobniCislo, parametr, kriterium)) - ) - - object UpravDukazKriteria { - def apply( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): UpravDukazKriteria = - UpravDukazKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class NotFound(url: String) extends Page("404", "404", Some(Directory)) - - case class UnhandledError( - errorName: Option[String], - errorMessage: Option[String] - ) extends Page("500", "Unexpected error", Some(Directory)) - -object Routes: - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) - given [V: JsonEncoder]: JsonEncoder[Titled[V]] = - DeriveJsonEncoder.gen[Titled[V]] - given [V: JsonDecoder]: JsonDecoder[Titled[V]] = - DeriveJsonDecoder.gen[Titled[V]] - given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] - given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] - - val base = - js.`import`.meta.env.BASE_URL - .asInstanceOf[String] + "app" - - val homePage: Page = Page.Directory - - given router: Router[Page] = Router[Page]( - routes = List( - Route.static(homePage, root / endOfSegments, basePath = base), - Route.static( - Page.Dashboard, - root / "dashboard" / endOfSegments, - basePath = base - ), - Route[Page.Detail, String]( - encode = _.osobniCislo.value.toString, - decode = osc => Page.Detail(Titled(OsobniCislo(osc))), - root / "osoba" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.DetailParametru, (String, String)]( - encode = p => (p.osobniCislo.value.toString, p.parametr.value), - decode = - p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / endOfSegments, - basePath = base - ), - Route[Page.DetailKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.DetailKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.UpravDukazKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.UpravDukazKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / "edit" / endOfSegments, - basePath = base - ) - ), - serializePage = _.toJson, - deserializePage = _.fromJson[Page] - .fold(s => throw IllegalStateException(s), identity), - getPageTitle = _.title, - routeFallback = url => Page.NotFound(url), - deserializeFallback = _ => Page.Dashboard - )( - $popStateEvent = windowEvents.onPopState, - owner = unsafeWindowOwner - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala deleted file mode 100644 index b24b82b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -sealed trait Action - -case object FetchDirectory extends Action -case class FetchUserDetails(osc: OsobniCislo) extends Action -case class FetchParameters(osc: OsobniCislo) extends Action -case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action -case class FetchParameterCriteria( - osc: OsobniCislo, - paramId: String, - critId: String, - page: (UserInfo, Parameter, ParameterCriteria) => Page -) extends Action -case class FetchAvailableFiles(osc: OsobniCislo) extends Action -case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala deleted file mode 100644 index c637d35..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala +++ /dev/null @@ -1,82 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.{UserProfile, UserInfo, OsobniCislo} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo -import cz.e_bs.cmi.mdr.pdb.UserFunction - -object AppPage: - // TODO: pages by logged in user - val pages: List[Page] = List(Page.Directory, Page.Dashboard) - - import NavigationBar.{Logo, MenuItem} - - val logo = Logo( - "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", - "Workflow" - ) - - // TODO: menu items by user profile - val userMenu = - List( - MenuItem("Your Profile"), - MenuItem("Settings"), - MenuItem("Sign out") - ) - - // TODO: load user profile - val $userProfile = Var( - UserProfile( - "tom", - UserInfo( - OsobniCislo("1031"), - "tom", - "Tom", - "Cook", - None, - None, - Some("tom@example.com"), - Some("+420 222 866 180"), - Some( - UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") - ), - Nil, - None - ) - ) - ) - - val $userInfo = $userProfile.signal.map(_.userInfo) - - type ViewModel = Option[HtmlElement] - def apply( - actionBus: Observer[Action] - )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using - router: Router[Page] - ): HtmlElement = - PageLayout(actionBus)( - $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => - PageLayout.ViewModel( - NavigationBar.ViewModel( - u, - pages.map(p => - NavigationBar.Link( - () => - PageLink( - p, - actionBus - ), - p == cp - ) - ), - userMenu, - logo - ), - c - ) - ), - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala deleted file mode 100644 index bf473c6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ /dev/null @@ -1,143 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden -import fiftyforms.ui.components.tailwind.list.IconText.ViewModel -import fiftyforms.ui.components.tailwind.Icons -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Action - -object Breadcrumbs: - - private def slash = - import svg.{*, given} - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-300", - xmlns := "http://www.w3.org/2000/svg", - fill := "currentColor", - viewBox := "0 0 20 20", - ariaHidden := true, - path( - d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" - ) - ) - - def backIcon = - Icons.solid - .`arrow-narrow-left`() - .amend( - svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" - ) - - object Link: - case class ViewModel( - page: Page, - icon: Option[SvgElement], - extraClasses: String, - text: String, - textClasses: Option[String] = None - ) - - val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" - - def shortHome(p: Page) = ViewModel( - p, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - "Zpět na úvodní stránku", - None - ) - - def fullHome(p: Page) = ViewModel( - p, - Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), - "text-gray-400 hover:text-gray-500", - "Domů", - Some("sr-only") - ) - - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - PageLink - .container($m.map(_.page), actionBus) - .amend( - cls <-- $m.map(_.extraClasses), - child.maybe <-- $m.map(_.icon), - span( - cls <-- $m.map(_.textClasses.getOrElse("")), - child.text <-- $m.map(_.text) - ) - ) - - object FullBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - ol( - role := "list", - cls := "flex items-center space-x-4", - children <-- $m.map(_.path).split(_.id)((_, _, $p) => - li( - div( - cls := "flex items-center", - child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), - Link( - $p.map(p => - if (p.isRoot) then Link.fullHome(p) - else - Link.ViewModel( - p, - None, - s"ml-4 max-w-xs truncate ${Link.baseClasses}", - p.title - ) - ), - actionBus - ) - ) - ) - ) - ) - - object ShortBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - Link( - $m.map { p => - val target = p.parent.getOrElse(p) - if target.isRoot then Link.shortHome(target) - else - Link.ViewModel( - target, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - s"Zpět na ${target.title}" - ) - }, - actionBus - ) - - def apply(actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - nav( - cls := "flex", - aria.label := "Breadcrumb", - div( - cls := "flex sm:hidden", - ShortBreadcrumbs(router.$currentPage, actionBus) - ), - div( - cls := "hidden sm:block", - FullBreadcrumbs(router.$currentPage, actionBus) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala deleted file mode 100644 index 0db2047..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ /dev/null @@ -1,242 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent -import fiftyforms.ui.components.tailwind.Icons -import fiftyforms.ui.components.tailwind.Avatar -import cz.e_bs.cmi.mdr.pdb.UserInfo -import io.laminext.syntax.core.* - -object NavigationBar: - - case class Logo(img: String, name: String) - case class Link(a: () => Anchor, active: Boolean) - case class MenuItem(title: String) - - case class ViewModel( - userInfo: UserInfo, - pages: List[Link], - userMenu: List[MenuItem], - logo: Logo - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - val $userInfo = $m.map(_.userInfo) - val mobileMenuOpen = Var(false) - - val desktopOnly = cls("hidden md:block") - val mobileOnly = cls("md:hidden") - - inline def avatarImage(size: Int = 8) = - Avatar($userInfo.map(_.img)).avatarImage(size) - - def notificationButton = button( - tpe := "button", - cls := List( - "bg-indigo-600", - "focus:outline-none", - "focus:ring-2", - "focus:ring-offset-2", - "focus:ring-offset-indigo-600", - "focus:ring-white", - "hover:text-white", - "p-1", - "rounded-full", - "text-indigo-200" - ), - span(cls := "sr-only", "View notifications"), - Icons.outline.bell() - ) - - def userProfile: HtmlElement = - val menuOpen = Var(false) - - def menuItem(item: MenuItem, idx: Int): HtmlElement = - a( - href := "#", - cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", - role := "menuitem", - tabIndex := -1, - idAttr := s"user-menu-item-$idx", - item.title - ) - - div( - cls := "ml-3 relative", - div( - button( - tpe := "button", - cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - idAttr := "user-menu-button", - aria.expanded <-- menuOpen.signal, - aria.hasPopup := true, - span(cls := "sr-only", "Open user menu"), - child <-- avatarImage(), - onClick.preventDefault.mapTo( - !menuOpen.now() - ) --> menuOpen.writer - ) - ), - /* - * */ - div( - cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", - cls <-- menuOpen.signal.map { o => - if (o) "md:block" else "md:hidden" - }, - role := "menu", - aria.orientation := "vertical", - aria.labelledBy := "user-menu-button", - tabIndex := -1, - // : keyboard navigation - children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) - ) - ) - - def mobileProfile = - def menuItem(item: MenuItem): HtmlElement = - a( - href := "#", - cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", - item.title - ) - - div( - cls := "pt-4 pb-3 border-t border-indigo-700", - div( - cls := "flex items-center px-5", - div( - cls := "flex-shrink-0", - child <-- avatarImage(10) - ), - div( - cls := "ml-3", - div( - cls := "text-base font-medium text-white", - child.text <-- $userInfo.map(_.name) - ), - child.maybe <-- $userInfo.map( - _.email.map(e => - div( - cls := "text-sm font-medium text-indigo-300", - e - ) - ) - ) - ), - notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) - ), - div( - cls := "mt-3 px-2 space-y-1", - children <-- $m.map(_.userMenu.map(menuItem)) - ) - ) - - def pageLink(page: Link): Anchor = - page - .a() - .amend( - cls := "text-white px-3 py-2 rounded-md text-sm font-medium", - cls := Seq( - "bg-indigo-700" -> page.active, - "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active - ), - ariaCurrent := (if page.active then "page" else "false") - ) - - def logoImg: Image = - img( - cls := "h-8 w-8", - src <-- $m.map(_.logo.img), - alt <-- $m.map(_.logo.name) - ) - - def mobileMenuButton = button( - tpe := "button", - cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - aria.controls := "mobile-menu", - aria.expanded <-- mobileMenuOpen.signal, - span(cls := "sr-only", "Open main menu"), - Icons.outline - .menu() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") - ), - Icons.outline - .x() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") - ), - onClick.preventDefault.mapTo( - !mobileMenuOpen.now() - ) --> mobileMenuOpen.writer - ) - - def navBarLeft = - div( - cls := "flex items-center", - div(cls := "flex-shrink-0", logoImg), - div( - desktopOnly, - div( - cls := "ml-10 flex items-baseline space-x-4", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ) - ) - ) - - def navBarRight = - div( - desktopOnly, - div( - cls := "ml-4 flex items-center md:ml-6", - notificationButton, - userProfile - ) - ) - - def navBarMobile = - div( - cls := "-mr-2 flex", - mobileOnly, - mobileMenuButton - ) - - def navBar = - div( - cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex items-center justify-between h-16", - navBarLeft, - navBarRight, - navBarMobile - ) - ) - - def mobileMenu = - div( - mobileOnly, - cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }, - idAttr := "mobile-menu", - div( - cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ), - mobileProfile - ) - - nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala deleted file mode 100644 index 75339df..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object PageHeader: - def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = - header( - cls := "bg-white shadow-sm", - div( - cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", - h1( - cls := "text-lg leading-6 font-semibold text-gray-900", - Breadcrumbs(actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala deleted file mode 100644 index 5096463..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ /dev/null @@ -1,30 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import fiftyforms.ui.components.tailwind.Loading -import io.laminext.syntax.core.* - -object PageLayout: - case class ViewModel( - navigation: NavigationBar.ViewModel, - content: Option[HtmlElement] - ) - def apply(actionBus: Observer[Action])( - $m: Signal[ViewModel], - mods: Modifier[HtmlElement]* - )(using router: Router[Page]): HtmlElement = - val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) - div( - cls := "h-full flex flex-col", - NavigationBar($m.map(_.navigation)), - child.maybe <-- router.$currentPage.map(_.isRoot) - .switch(None, Some(PageHeader(actionBus))), - main( - cls := "flex-grow-1 overflow-y-auto", - mods, - child <-- $maybeContent.map(_.getOrElse(Loading)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala deleted file mode 100644 index 62d820f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.LinkSupport.* -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo - -object PageLink: - type ViewModel = Page - def apply($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = - a(mods($m, actions), child.text <-- $m.map(_.title)) - - def apply(m: ViewModel, actions: Observer[Action])(using - router: Router[Page] - ): Anchor = apply(Val(m), actions) - - def container($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = a(mods($m, actions)) - - def container(m: ViewModel, actions: Observer[Action])(using - Router[Page] - ): Anchor = container(Val(m), actions) - - private def mods($m: Signal[Page], actions: Observer[Action])(using - router: Router[Page] - ): Modifier[Anchor] = - Seq( - href <-- $m.map(router.absoluteUrlForPage).recover { case _ => - Some("invalid url") - }, - composeEvents(onClick.noKeyMod.preventDefault)( - _.sample($m) - .map(NavigateTo.apply) - ) --> actions - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala deleted file mode 100644 index 87f825a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.pages.dashboard.DashboardPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -class DashboardPageConnector(actionBus: Observer[Action])(using - router: Router[Page] -): - def apply: HtmlElement = - AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala deleted file mode 100644 index e9b0f71..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala +++ /dev/null @@ -1,76 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailKriteriaPage -import pages.detail.components.DukazyKriteria -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -object DetailKriteriaPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailKriteriaPageConnector( - state: DetailKriteriaPageConnector.AppState -)( - $page: Signal[Page.DetailKriteria] -)(using Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => - (p.osobniCislo.value, p.parametr.value, p.kriterium.value) - )((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map( - FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) - ) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, s, $s) => - DetailKriteriaPage($s)(state.actionBus.contramap { - case DukazyKriteria.Add => - NavigateTo( - Page.UpravDukazKriteria( - Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), - Page.Titled(s.parametr.id, Some(s.parametr.nazev)), - Page.Titled(s.kriterium.id, Some(s.kriterium.id)) - ) - ) - }) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): DetailKriteriaPage.ViewModel = - DetailKriteriaPage.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala deleted file mode 100644 index 4d8024f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ /dev/null @@ -1,57 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import pages.detail.DetailPage -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import fiftyforms.ui.components.tailwind.Color - -object DetailPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailPageConnector(state: DetailPageConnector.AppState)( - $page: Signal[Page.Detail] -)(using router: Router[Page]): - val $oscChangeSignal = $page.splitOne(_.osobniCislo.value)((osc, _, _) => osc) - val $pageChangeSignal = - $oscChangeSignal.flatMap(osc => - EventStream.fromSeq(Seq(FetchUserDetails(osc), FetchParameters(osc))) - ) - // TODO: filter the value based on the current osc - // OSC change will fetch new data, but still - // - we need to be sure that what we got is really what we ought to display - // - we want to display stale data accordingly (at least with loading indicator) - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - def apply: HtmlElement = - AppPage(state.actionBus)( - $data.combineWithFn($params)(_ zip _) - .map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: List[Parameter] - ): DetailPage.ViewModel = - DetailPage.ViewModel( - o.toDetailOsoby, - p.map( - _.toParametr(param => - PageLink.container(Page.DetailParametru(o, param), state.actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala deleted file mode 100644 index fdaa7f8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailParametruPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink - -object DetailParametruPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailParametruPageConnector( - state: DetailParametruPageConnector.AppState -)( - $page: Signal[Page.DetailParametru] -)(using router: Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => (p.osobniCislo.value, p.parametr.value))((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map(FetchParameter(_, _)) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - } yield (da, pb) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailParametruPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter - ): DetailParametruPage.ViewModel = - DetailParametruPage.ViewModel( - o.toDetailOsoby, - p.toParametr(p => - PageLink.container(Page.DetailParametru(o, p), state.actionBus) - ), - p.criteria.map( - _.toKriterium { c => - PageLink.container( - Page.DetailKriteria(o, p, c), - state.actionBus - ) - } - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala deleted file mode 100644 index 049aa22..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -case class DirectoryPageConnector( - $input: EventStream[List[UserInfo]], - actionBus: Observer[Action] -)(using router: Router[Page]): - val $data = $input.startWithNone - val $actionSignal = EventStream.fromValue(FetchDirectory) - - def apply: HtmlElement = - AppPage(actionBus)( - $data.split(_ => ())((_, _, s) => - pages.directory.DirectoryPage( - s.map( - _.map( - _.toUserRow(u => - PageLink.container( - Page.Detail(Page.Titled(u.personalNumber)), - actionBus - ) - ) - ) - ) - ) - ), - $actionSignal --> actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala deleted file mode 100644 index 88e7450..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ /dev/null @@ -1,68 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii -import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria -import fiftyforms.ui.components.tailwind.Color - -extension (o: UserInfo) - def toDetailOsoby: DetailOsoby.ViewModel = - DetailOsoby.ViewModel( - o.personalNumber, - o.name, - o.email, - o.phone, - o.img, - o.mainFunction.map(f => - DetailOsoby.Funkce.ViewModel(f.name, f.dept, f.ou) - ), - o.userContracts.headOption.map(c => - DetailOsoby.PracovniPomer.ViewModel(c.rel, c.startDate, c.endDate) - ) - ) - -extension (param: Parameter) - def toParametr(container: Parameter => Anchor): DetailParametru.ViewModel = - DetailParametru.ViewModel( - id = param.id, - nazev = param.name, - popis = param.description, - status = "Nesplněno", - statusColor = Color.red, - a = container(param) - ) - -extension (crit: ParameterCriteria) - def toKriterium( - container: ParameterCriteria => Anchor - ): DetailKriteria.ViewModel = - DetailKriteria.ViewModel( - nazev = crit.criteriumText, - kapitola = crit.chapterId, - bod = crit.itemId, - status = "Nesplněno", - statusColor = Color.red, - splneno = false, - dukazy = Nil, - container = container(crit) - ) - -extension (user: UserInfo) - def toUserRow( - container: UserInfo => HtmlElement = (_: UserInfo) => div() - ): UserRow.ViewModel = - UserRow.ViewModel( - osobniCislo = user.personalNumber.toString, - celeJmeno = user.name, - prijmeni = user.surname, - hlavniFunkce = user.mainFunction.map(_.name), - img = user.img, - container = () => container(user) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala deleted file mode 100644 index 7d5b2c1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.dashboard - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.Page - -object DashboardPage: - - def render: HtmlElement = - div("Dashboard page") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala deleted file mode 100644 index 9ef0698..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala +++ /dev/null @@ -1,31 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailKriteriaPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply($m: Signal[ViewModel])( - action: Observer[DukazyKriteria.Action] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - DukazyKriteria($m.map(_.kriterium.dukazy))(action) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala deleted file mode 100644 index 3b316e1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object DetailPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametry: SeznamParametru.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby($m.map(_.osoba)), - SeznamParametru($m.map(_.parametry)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala deleted file mode 100644 index 17224cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala +++ /dev/null @@ -1,24 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailParametruPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriteria: SeznamKriterii.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby.header($m.map(_.osoba)), - DetailParametru($m.map(_.parametr)), - SeznamKriterii($m.map(_.kriteria)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala deleted file mode 100644 index dc912fa..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ /dev/null @@ -1,107 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb -package app -package pages.detail - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.UpravDukazForm -import fiftyforms.services.files.File - -object UpravDukaz: - - type ThisPage = Page.UpravDukazKriteria - type PageKey = (OsobniCislo, String, String) - - trait State { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def availableFiles: EventStream[List[File]] - def actionBus: Observer[Action] - } - - def keyOfPage(page: ThisPage): PageKey = - (page.osobniCislo.value, page.parametr.value, page.kriterium.value) - - def onChangeAction(key: PageKey): Action = - FetchParameterCriteria( - key._1, - key._2, - key._3, - Page.UpravDukazKriteria(_, _, _) - ) - - class Connector(state: State)($page: Signal[ThisPage])(using Router[Page]): - val $paramChangeSignal = $page.splitOne(keyOfPage)((x, _, _) => x) - val $pageChangeSignal = $paramChangeSignal.map(onChangeAction) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.split(_ => ())((_, s, $s) => - PageComponent( - $s.map(buildModel), - state.availableFiles, - state.actionBus.contramap { - case UpravDukazForm.Cancelled => - NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) - case UpravDukazForm.AvailableFilesRequested => - FetchAvailableFiles(s._1.personalNumber) - } - ) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): PageComponent.ViewModel = - import connectors.* - PageComponent.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) - - object PageComponent: - import components.* - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply( - $m: Signal[ViewModel], - availableFilesStream: EventStream[List[File]], - events: Observer[UpravDukazForm.Event] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - UpravDukazForm(availableFilesStream)(events) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala deleted file mode 100644 index ca311b0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.Icons -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Color - -object DetailKriteria: - case class ViewModel( - nazev: String, - kapitola: String, - bod: String, - status: String, - statusColor: Color, - splneno: Boolean, - dukazy: List[DukazKriteria.ViewModel], - container: HtmlElement = div() - ) { - val id = s"${kapitola}${bod}" - } - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h3( - cls := "text-xl leading-6 font-bold text-gray-900", - child.text <-- $m.map(_.nazev) - ), - h3( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - "Kapitola ", - child.text <-- $m.map(_.kapitola), - " bod ", - child <-- $m.map(_.bod) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala deleted file mode 100644 index ae90290..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala +++ /dev/null @@ -1,100 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Avatar - -object DetailOsoby: - - object Funkce: - case class ViewModel( - nazev: String, - stredisko: String, - voj: String - ) - - def render($m: Signal[ViewModel]): HtmlElement = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.nazev), - span( - cls := "hidden md:inline", - " @ ", - child.text <-- $m.map(_.stredisko), - ", ", - child.text <-- $m.map(_.voj) - ) - ) - - object PracovniPomer: - case class ViewModel( - druh: String, - pocatek: LocalDate, - konec: Option[LocalDate] - ) - - def render($m: Signal[ViewModel]): HtmlElement = - import fiftyforms.ui.components.tailwind.CustomAttrs.datetime - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.druh), - " od ", - time( - datetime <-- $m.map(_.pocatek.toString), - child.text <-- $m.map(_.pocatek.toString) - ) - ) - - case class ViewModel( - osobniCislo: OsobniCislo, - jmeno: String, - email: Option[String], - telefon: Option[String], - img: Option[String], - hlavniFunkce: Option[Funkce.ViewModel], - pracovniPomer: Option[PracovniPomer.ViewModel] - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-4", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(16) - ), - div( - h1( - cls := "text-2xl font-bold text-gray-900", - child.text <-- $m.map(_.jmeno) - ), - child.maybe <-- $m.map(_.hlavniFunkce).split(_ => ())((_, _, d) => - Funkce.render(d) - ), - child.maybe <-- $m.map(_.pracovniPomer).split(_ => ())((_, _, d) => - PracovniPomer.render(d) - ) - ) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-5", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(8) - ), - div( - h1( - cls := "text-lg font-medium text-gray-800", - child.text <-- $m.map(_.jmeno) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala deleted file mode 100644 index e11cb62..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color - -object DetailParametru: - case class ViewModel( - id: String, - nazev: String, - popis: String, - status: String, - statusColor: Color, - a: Anchor - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h2( - cls := "text-2xl leading-6 font-medium text-gray-900", - child.text <-- $m.map(_.nazev) - ), - p( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - child.text <-- $m.map(_.popis) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - h2( - cls := "text-lg leading-6 font-medium text-gray-600", - child.text <-- $m.map(_.nazev) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala deleted file mode 100644 index a00ede8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala +++ /dev/null @@ -1,177 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.CustomAttrs - -object DukazKriteria: - case class Osoba(osobniCislo: String, jmeno: String) - case class Udalost(osoba: Osoba, datum: LocalDate) - case class Dokument( - url: String, - nazev: String, - pridal: Udalost, - odebral: Option[Udalost] - ) - case class ViewModel( - dokumenty: List[Dokument], - autorizoval: Option[Udalost], - platiDo: Option[LocalDate], - poznámka: Option[String] - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-lg", - div( - cls := "px-4 py-5 sm:px-6", - h3( - cls := "text-lg leading-6 font-medium text-gray-900", - """Applicant Information""" - ), - p( - cls := "mt-1 max-w-2xl text-sm text-gray-500", - """Personal details and application.""" - ) - ), - div( - cls := "border-t border-gray-200 px-4 py-5 sm:px-6", - dl( - cls := "grid grid-cols-1 gap-x-4 gap-y-8 sm:grid-cols-2", - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Full name""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Margot Foster""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Application for""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Backend Developer""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Email address""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """margotfoster@example.com""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Salary expectation""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """$120,000""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """About""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Fugiat ipsum ipsum deserunt culpa aute sint do nostrud anim incididunt cillum culpa consequat. Excepteur qui ipsum aliquip consequat sint. Sit id mollit nulla mollit nostrud in ea officia proident. Irure nostrud pariatur mollit ad adipisicing reprehenderit deserunt qui eu.""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """Attachments""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - ul( - role := "list", - cls := "border border-gray-200 rounded-md divide-y divide-gray-200", - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """resume_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ), - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """coverletter_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala deleted file mode 100644 index 22f134f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala +++ /dev/null @@ -1,35 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object DukazyKriteria: - sealed trait Action - case object Add extends Action - - type ViewModel = List[DukazKriteria.ViewModel] - - def apply($m: Signal[ViewModel])(actions: Observer[Action]): HtmlElement = - div( - child.maybe <-- $m.map(m => - if (m.isEmpty) then Some(prazdnyDukaz(actions)) - else None - ), - children <-- $m.map(_.zipWithIndex).split(_._2)((_, _, $s) => - DukazKriteria($s.map(_._1)) - ) - ) - - private def prazdnyDukaz(actions: Observer[Action]): HtmlElement = - button( - tpe := "button", - onClick.preventDefault.mapTo(Add) --> actions, - cls := "relative block w-full border-2 border-gray-300 border-dashed rounded-lg p-12 text-center hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - Icons.outline - .`document-add`(12) - .amend(svg.cls := "mx-auto text-gray-400"), - span( - cls := "mt-2 block text-sm font-medium text-gray-900", - "Přidat důkaz" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala deleted file mode 100644 index f628697..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.list.StackedList -import fiftyforms.ui.components.tailwind.list.ListRow -import fiftyforms.ui.components.tailwind.list.RowTag -import fiftyforms.ui.components.tailwind.list.RowNext -import java.time.LocalDate - -object SeznamKriterii: - type ViewModel = List[DetailKriteria.ViewModel] - - private val kritList = new StackedList[DetailKriteria.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - kritList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = - p(cls := "text-sm text-gray-500", s"${i.kapitola}${i.bod}"), - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.container - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala deleted file mode 100644 index 2bfe4bc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.list.{ - StackedList, - ListRow, - RowTag, - PropList, - IconText, - RowNext -} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.LinkSupport.* - -object SeznamParametru: - type ViewModel = List[DetailParametru.ViewModel] - - private val parametrList = new StackedList[DetailParametru.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - parametrList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = emptyNode, - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.a - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala deleted file mode 100644 index f048e9d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ /dev/null @@ -1,101 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.form.* -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement -import fiftyforms.services.files.components.tailwind.FilePicker -import cz.e_bs.cmi.mdr.pdb.frontend.AutorizujDukaz -import cz.e_bs.cmi.mdr.pdb.frontend.DocumentRef -import fiftyforms.services.files.File - -object UpravDukazForm: - sealed trait Event - case object Cancelled extends Event - case object AvailableFilesRequested extends Event - def apply(availableFilesStream: EventStream[List[File]])( - updates: Observer[Event] - ): HtmlElement = - val (filesStream, filesObserver) = - EventStream.withObserver[FilePicker.Event] - val files = Var[List[File]](Nil) - def submitButtons: HtmlElement = - div( - cls := "pt-5", - div( - cls := "flex justify-end", - button( - tpe := "button", - cls := "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zrušit", - onClick.mapTo(Cancelled) --> updates - ), - button( - tpe := "submit", - cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - disabled <-- files.signal.map(_.isEmpty), - "Autorizovat" - ) - ) - ) - - div( - filesStream.collect { case FilePicker.SelectionUpdated(files) => - files.to(List) - } --> files.writer, - filesStream.collect { case FilePicker.AvailableFilesRequested => - AvailableFilesRequested - } --> updates, - cls := "bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6", - Form( - Form.Body( - Form.Section( - FormHeader( - FormHeader.ViewModel( - "Doložení kritéria", - "Sestavte doklady poskytující důkaz kritéria a potvrďte odesláním formuláře. Případné limitace či jiné relevantní údaje vepište do pole Komentář." - ) - ), - FormFields( - FormRow( - "dokumenty", - "Dokumenty", - FilePicker(files.signal, availableFilesStream)(filesObserver) - .amend(idAttr := "dokumenty", cls("max-w-lg")) - ).toHtml, - FormRow( - "platnost", - "Platnost", - input( - idAttr := "platnost", - name := "platnost", - tpe := "date", - autoComplete := "date", - cls := "block max-w-lg w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" - ) - ).toHtml, - FormRow( - "komentar", - "Komentář", - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - textArea( - idAttr := "komentar", - name := "about", - rows := 3, - cls := "max-w-lg shadow-sm block w-full focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border border-gray-300 rounded-md" - ), - p( - cls := "mt-2 text-sm text-gray-500", - "Doplňte prosím potřebné informace související s doložením kritéria, včetně případných limitací." - ) - ) - ).toHtml - ) - ) - ), - submitButtons - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala deleted file mode 100644 index 41033f6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -import components._ - -object DirectoryPage: - - type ViewModel = List[UserRow.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - val (actionsStream, actionObserver) = - EventStream.withObserver[SearchForm.Action] - val $filter = actionsStream - .collect { - case SearchForm.ClearFilter => None - case SearchForm.SetFilter(t) => Some(t) - } - .startWith(None) - val byLetter = for { - d <- $m - f <- $filter - } yield for { - (letter, users) <- d - .filter { user => - f match - case None => true - case Some(t) => user.search.contains(t) - } - .groupBy(_.prijmeni.head) - .to(List) - .sortBy(_._1) - } yield (letter.toString, users.sortBy(_.prijmeni)) - - div( - cls := "h-full max-w-7xl mx-auto order-first flex flex-col flex-shrink-0", - SearchForm(actionObserver), - Directory(byLetter) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala deleted file mode 100644 index 73f915d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -object Directory: - - object Header: - type ViewModel = String - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500", - h3(child.text <-- $m) - ) - - object UserList: - type ViewModel = List[UserRow.ViewModel] - def apply($m: Signal[ViewModel]): HtmlElement = - ul( - role := "list", - cls := "relative z-0 divide-y divide-gray-200", - children <-- $m.split(_.osobniCislo)((_, _, s) => UserRow(s)) - ) - - type ViewModel = List[(String, List[UserRow.ViewModel])] - def apply($m: Signal[ViewModel]): HtmlElement = - nav( - cls := "flex-1 min-h-0 overflow-y-auto", - aria.label := "Directory", - children <-- $m.split(_._1)((_, _, s) => - div( - cls := "relative", - Header(s.map(_._1)), - UserList(s.map(_._2)) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala deleted file mode 100644 index e182910..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala +++ /dev/null @@ -1,54 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object SearchForm: - sealed trait Action - case object SubmitFilter extends Action - sealed trait FilterAction extends Action - case object ClearFilter extends FilterAction - case class SetFilter(value: String) extends FilterAction - - def apply(actions: Observer[Action]): HtmlElement = - div( - cls := "px-6 pt-4 pb-4", - form( - cls := "flex space-x-4", - onSubmit.mapTo(SubmitFilter) --> actions, - div( - cls := "flex-1 min-w-0", - label( - forId := "search", - cls := "sr-only", - "Hledat" - ), - div( - cls := "relative rounded-md shadow-sm", - div( - cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", - Icons.solid.search().amend(svg.cls := "text-gray-400") - ), - input( - tpe := "search", - name := "search", - idAttr := "search", - cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", - placeholder := "Hledat", - composeEvents(onInput.mapToValue.setAsValue.map(SetFilter(_)))( - _.throttle(500) - ) --> actions - ) - ) - ), - button( - tpe := "submit", - cls := "inline-flex justify-center px-3.5 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500", - Icons.solid.filter().amend(svg.cls := "text-gray-400"), - span( - cls := "sr-only", - "Hledat" - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala deleted file mode 100644 index 82f7723..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala +++ /dev/null @@ -1,52 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.airstream.core.Signal -import fiftyforms.ui.components.tailwind.Avatar - -object UserRow: - case class ViewModel( - osobniCislo: String, - celeJmeno: String, - prijmeni: String, - hlavniFunkce: Option[String], - img: Option[String], - container: () => HtmlElement = () => div() - ) { - val search = prijmeni.toLowerCase - } - - def apply($m: Signal[ViewModel]): HtmlElement = - inline def avatarImage = - Avatar($m.map(_.img)).avatarImage(10) - - li( - div( - cls := "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500", - div( - cls := "flex-shrink-0", - child <-- avatarImage - ), - div( - cls := "flex-1 min-w-0", - child <-- $m.map { o => - o.container() - .amend( - cls := "focus:outline-none", - span( - cls := "absolute inset-0", - aria.hidden := true - ), - p( - cls := "text-sm font-medium text-gray-900", - o.celeJmeno - ), - p( - cls := "text-sm text-gray-500 truncate", - o.hlavniFunkce - ) - ) - } - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala deleted file mode 100644 index 467c5f5..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala +++ /dev/null @@ -1,99 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router - -object ErrorPage: - case class ViewModel( - homePage: Page, - errorName: String, - title: String, - subTitle: String - ) - - def apply(m: ViewModel, actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - val ViewModel(homePage, errorName, title, subTitle) = m - div( - cls := "min-h-full pt-16 pb-12 flex flex-col bg-white", - main( - cls := "flex-grow flex flex-col justify-center max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex-shrink-0 flex justify-center", - a( - href := "/", - cls := "inline-flex", - span( - cls := "sr-only", - """Workflow""" - ), - img( - cls := "h-12 w-auto", - src := "https://tailwindui.com/img/logos/workflow-mark.svg?color=indigo&shade=600", - alt := "" - ) - ) - ), - div( - cls := "py-16", - div( - cls := "text-center", - p( - cls := "text-sm font-semibold text-indigo-600 uppercase tracking-wide", - errorName - ), - h1( - cls := "mt-2 text-4xl font-extrabold text-gray-900 tracking-tight sm:text-5xl", - title - ), - p( - cls := "mt-2 text-base text-gray-500", - subTitle - ), - div( - cls := "mt-6", - PageLink - .container(homePage, actionBus) - .amend( - cls := "text-base font-medium text-indigo-600 hover:text-indigo-500", - """Go back home""" - ) - ) - ) - ) - ), - footer( - cls := "flex-shrink-0 max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", - nav( - cls := "flex justify-center space-x-4", - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Contact Support""" - ), - span( - cls := "inline-block border-l border-gray-300", - aria.hidden := true - ), - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Status""" - ), - span( - cls := "inline-block border-l border-gray-300", - aria.hidden := true - ), - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Twitter""" - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala deleted file mode 100644 index bd9baec..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action - -object NotFoundPage: - def apply(homePage: Page, url: String, actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - ErrorPage( - ErrorPage.ViewModel( - homePage, - "404 error", - "Page not found.", - s"Sorry, but page $url doesn't exist." - ), - actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala deleted file mode 100644 index 476fe3c..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action - -object UnhandledErrorPage: - - case class ViewModel( - homePage: Page, - errorName: Option[String], - errorMessage: Option[String] - ) - - def apply(m: ViewModel, actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - ErrorPage( - ErrorPage.ViewModel( - m.homePage, - "Unexpected error occurred", - m.errorName.getOrElse( - "Uh oh!" - ), // TODO: translations, better text than uh oh - m.errorMessage.getOrElse( - "This wasn't supposed to happen! Please try again." - ) - ), - actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala deleted file mode 100644 index 25f059f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala +++ /dev/null @@ -1,6 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.services - -import com.raquo.airstream.core.Observer - -trait DataFetcher[K, A]: - def fetch(id: K, sink: Observer[A]): Unit diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala deleted file mode 100644 index a469830..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala +++ /dev/null @@ -1,120 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package state - -import com.raquo.airstream.core.EventStream -import cz.e_bs.cmi.mdr.pdb.{UserInfo, OsobniCislo} -import com.raquo.airstream.core.Observer -import scala.scalajs.js -import scala.scalajs.js.JSON -import zio.json._ -import com.raquo.airstream.eventbus.EventBus -import com.raquo.airstream.ownership.Owner -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.UserFunction -import cz.e_bs.cmi.mdr.pdb.UserContract -import fiftyforms.services.files.File - -trait AppState - extends connectors.DetailPageConnector.AppState - with connectors.DetailParametruPageConnector.AppState - with connectors.DetailKriteriaPageConnector.AppState - with pages.detail.UpravDukaz.State: - def users: EventStream[List[UserInfo]] - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - -class MockAppState(implicit owner: Owner, router: Router[Page]) - extends AppState: - - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonDecoder[UserFunction] = DeriveJsonDecoder.gen - given JsonDecoder[UserContract] = DeriveJsonDecoder.gen - given JsonDecoder[UserInfo] = DeriveJsonDecoder.gen - - given JsonDecoder[ParameterCriteria] = DeriveJsonDecoder.gen - given JsonDecoder[Parameter] = DeriveJsonDecoder.gen - - private val actions = EventBus[Action]() - private val (parametersStream, pushParameters) = - EventStream.withCallback[List[Parameter]] - private val (usersStream, pushUsers) = - EventStream.withCallback[List[UserInfo]] - private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] - private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] - - private val mockData: List[UserInfo] = - mockUsers - .asInstanceOf[js.Dictionary[js.Object]] - .values - // TODO: is there a more efficient way to parse from JS object directly? - .map { o => - val parsed = JSON.stringify(o).fromJson[UserInfo] - parsed.left.foreach(org.scalajs.dom.console.log(_)) - parsed - } - .collect { case Right(u) => - u - } - .toList - - private val mockParameters: List[Parameter] = - pdbParams - .asInstanceOf[js.Dictionary[js.Object]] - .values - .map(o => JSON.stringify(o).fromJson[Parameter]) - .collect { case Right(p) => p } - .toList - - // TODO: Extract to separate event handler - actions.events.foreach { - case FetchDirectory => pushUsers(mockData) - case FetchUserDetails(osc) => - mockData.find(_.personalNumber == osc).foreach { o => - pushDetails(o) - router.replaceState(Page.Detail(o)) - } - case FetchParameters(osc) => - pushParameters(mockParameters) - case FetchParameter(osc, paramId) => - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(Page.DetailParametru(o, p)) - case FetchParameterCriteria(osc, paramId, critId, page) => - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - c <- p.criteria.find(_.id == critId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(page(o, p, c)) - case NavigateTo(page) => router.pushState(page) - case FetchAvailableFiles(osc) => - pushFiles( - List( - File("https://tc163.cmi.cz/here", "Example file") - ) - ) - } - - override def users: EventStream[List[UserInfo]] = - usersStream.debugWithName("users") - - override def details: EventStream[UserInfo] = - detailsStream.debugWithName("details") - - override def parameters: EventStream[List[Parameter]] = - parametersStream.debugWithName("parameters") - - override def availableFiles: EventStream[List[File]] = - filesStream.debugWithName("available files") - - override def actionBus: Observer[Action] = - actions.writer.debugWithName("actions writer") diff --git a/app/src/main/scala/mdr/pdb/app/Main.scala b/app/src/main/scala/mdr/pdb/app/Main.scala new file mode 100644 index 0000000..081b4ee --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/Main.scala @@ -0,0 +1,103 @@ +package mdr.pdb.app + +import scala.scalajs.js.annotation.JSExportTopLevel +import scala.scalajs.js.annotation.JSExport +import scala.scalajs.js.annotation.JSImport +import scala.scalajs.js +import org.scalajs.dom +import com.raquo.laminar.api.L.{*, given} +import scala.scalajs.js.Date +import com.raquo.waypoint.Router +import com.raquo.waypoint.SplitRender +import mdr.pdb.app.services.DataFetcher +import scala.scalajs.js.JSON +import zio.json._ +import mdr.pdb.UserInfo +import mdr.pdb.app.state.AppState + +@js.native +@JSImport("stylesheets/main.css", JSImport.Namespace) +object Css extends js.Any + +@js.native +@JSImport("data/users.json", JSImport.Default) +object mockUsers extends js.Object + +@js.native +@JSImport("params/pdb-params.json", JSImport.Default) +object pdbParams extends js.Object + +@JSExportTopLevel("app") +object Main: + + @JSExport + def main(args: Array[String]): Unit = + import Routes.given + onLoad { + setupAirstream() + val appContainer = dom.document.querySelector("#app") + val _ = + render( + appContainer, + renderPage(state.MockAppState(using unsafeWindowOwner, router)) + ) + } + + private def onLoad(f: => Unit): Unit = + documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) + + private def setupAirstream()(using router: Router[Page]): Unit = + AirstreamError.registerUnhandledErrorCallback(err => + router.forcePage( + Page.UnhandledError( + Some(err.getClass.getName), // TODO: Fill only in dev mode + Some(err.getMessage) + ) + ) + ) + + def renderPage(state: AppState)(using + router: Router[Page] + ): HtmlElement = + val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) + .collectSignal[Page.Detail]( + connectors + .DetailPageConnector(state)(_) + .apply + ) + .collectSignal[Page.DetailParametru]( + connectors + .DetailParametruPageConnector(state)(_) + .apply + ) + .collectSignal[Page.DetailKriteria]( + connectors + .DetailKriteriaPageConnector(state)(_) + .apply + ) + .collectSignal[Page.UpravDukazKriteria]( + pages.detail.UpravDukaz.Connector(state)(_).apply + ) + .collectStatic(Page.Dashboard)( + connectors.DashboardPageConnector(state.actionBus).apply + ) + .collect[Page.NotFound](pg => + pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) + ) + .collect[Page.UnhandledError](pg => + pages.errors + .UnhandledErrorPage( + pages.errors.UnhandledErrorPage + .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), + state.actionBus + ) + ) + .collectStatic(Page.Directory)( + connectors + .DirectoryPageConnector(state.users, state.actionBus) + .apply + ) + div(cls := "h-full", child <-- pageSplitter.$view) + + // Pull in the stylesheet + val css: Css.type = Css diff --git a/app/src/main/scala/mdr/pdb/app/Routes.scala b/app/src/main/scala/mdr/pdb/app/Routes.scala new file mode 100644 index 0000000..2cc860c --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/Routes.scala @@ -0,0 +1,198 @@ +package mdr.pdb.app + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.* +import org.scalajs.dom +import zio.json.{*, given} +import mdr.pdb.OsobniCislo + +import scala.scalajs.js +import mdr.pdb.UserInfo +import mdr.pdb.Parameter +import mdr.pdb.ParameterCriteria +import mdr.pdb.app.Page.Titled + +// enum is not working with Waypoints' SplitRender collectStatic +sealed abstract class Page( + val id: String, + val title: String, + val parent: Option[Page] +) { + val path: Vector[Page] = + parent match + case None => Vector(this) + case Some(p) => p.path :+ this + + val isRoot: Boolean = parent.isEmpty +} + +object Page: + + case class Titled[V](value: V, title: Option[String] = None): + val show: String = title.getOrElse(value.toString) + + case object Directory extends Page("directory", "Adresář", None) + + case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) + + case class Detail(osobniCislo: Titled[OsobniCislo]) + extends Page("user", osobniCislo.show, Some(Directory)) + + object Detail { + def apply(o: UserInfo): Detail = Detail( + Titled(o.personalNumber, Some(o.name)) + ) + } + + case class DetailParametru( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String] + ) extends Page( + "parameter", + parametr.show, + Some(Detail(osobniCislo)) + ) + + object DetailParametru { + def apply(o: UserInfo, p: Parameter): DetailParametru = + DetailParametru( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)) + ) + } + + case class DetailKriteria( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String], + kriterium: Titled[String] + ) extends Page( + "criteria", + kriterium.show, + Some(DetailParametru(osobniCislo, parametr)) + ) + + object DetailKriteria { + def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = + DetailKriteria( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)), + Titled(k.id, Some(k.id)) + ) + } + + case class UpravDukazKriteria( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String], + kriterium: Titled[String] + ) extends Page( + "addProof", + "Důkaz", + Some(DetailKriteria(osobniCislo, parametr, kriterium)) + ) + + object UpravDukazKriteria { + def apply( + o: UserInfo, + p: Parameter, + k: ParameterCriteria + ): UpravDukazKriteria = + UpravDukazKriteria( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)), + Titled(k.id, Some(k.id)) + ) + } + + case class NotFound(url: String) extends Page("404", "404", Some(Directory)) + + case class UnhandledError( + errorName: Option[String], + errorMessage: Option[String] + ) extends Page("500", "Unexpected error", Some(Directory)) + +object Routes: + given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) + given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) + given [V: JsonEncoder]: JsonEncoder[Titled[V]] = + DeriveJsonEncoder.gen[Titled[V]] + given [V: JsonDecoder]: JsonDecoder[Titled[V]] = + DeriveJsonDecoder.gen[Titled[V]] + given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] + given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] + + val base = + js.`import`.meta.env.BASE_URL + .asInstanceOf[String] + "app" + + val homePage: Page = Page.Directory + + given router: Router[Page] = Router[Page]( + routes = List( + Route.static(homePage, root / endOfSegments, basePath = base), + Route.static( + Page.Dashboard, + root / "dashboard" / endOfSegments, + basePath = base + ), + Route[Page.Detail, String]( + encode = _.osobniCislo.value.toString, + decode = osc => Page.Detail(Titled(OsobniCislo(osc))), + root / "osoba" / segment[String] / endOfSegments, + basePath = base + ), + Route[Page.DetailParametru, (String, String)]( + encode = p => (p.osobniCislo.value.toString, p.parametr.value), + decode = + p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / endOfSegments, + basePath = base + ), + Route[Page.DetailKriteria, (String, String, String)]( + encode = p => + ( + p.osobniCislo.value.toString, + p.parametr.value, + p.kriterium.value.replaceAll("\\.", "--") + ), + decode = p => + Page.DetailKriteria( + Titled(OsobniCislo(p._1)), + Titled(p._2), + Titled(p._3.replaceAll("--", ".")) + ), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / "kriterium" / segment[String] / endOfSegments, + basePath = base + ), + Route[Page.UpravDukazKriteria, (String, String, String)]( + encode = p => + ( + p.osobniCislo.value.toString, + p.parametr.value, + p.kriterium.value.replaceAll("\\.", "--") + ), + decode = p => + Page.UpravDukazKriteria( + Titled(OsobniCislo(p._1)), + Titled(p._2), + Titled(p._3.replaceAll("--", ".")) + ), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / "kriterium" / segment[String] / "edit" / endOfSegments, + basePath = base + ) + ), + serializePage = _.toJson, + deserializePage = _.fromJson[Page] + .fold(s => throw IllegalStateException(s), identity), + getPageTitle = _.title, + routeFallback = url => Page.NotFound(url), + deserializeFallback = _ => Page.Dashboard + )( + $popStateEvent = windowEvents.onPopState, + owner = unsafeWindowOwner + ) diff --git a/app/src/main/scala/mdr/pdb/app/actions.scala b/app/src/main/scala/mdr/pdb/app/actions.scala new file mode 100644 index 0000000..d078b3a --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/actions.scala @@ -0,0 +1,21 @@ +package mdr.pdb.app + +import mdr.pdb.OsobniCislo +import mdr.pdb.UserInfo +import mdr.pdb.Parameter +import mdr.pdb.ParameterCriteria + +sealed trait Action + +case object FetchDirectory extends Action +case class FetchUserDetails(osc: OsobniCislo) extends Action +case class FetchParameters(osc: OsobniCislo) extends Action +case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action +case class FetchParameterCriteria( + osc: OsobniCislo, + paramId: String, + critId: String, + page: (UserInfo, Parameter, ParameterCriteria) => Page +) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action +case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/mdr/pdb/app/components/AppPage.scala new file mode 100644 index 0000000..130c817 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/AppPage.scala @@ -0,0 +1,82 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.Page +import mdr.pdb.{UserProfile, UserInfo, OsobniCislo} +import com.raquo.waypoint.Router +import mdr.pdb.app.Action +import mdr.pdb.app.NavigateTo +import mdr.pdb.UserFunction + +object AppPage: + // TODO: pages by logged in user + val pages: List[Page] = List(Page.Directory, Page.Dashboard) + + import NavigationBar.{Logo, MenuItem} + + val logo = Logo( + "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", + "Workflow" + ) + + // TODO: menu items by user profile + val userMenu = + List( + MenuItem("Your Profile"), + MenuItem("Settings"), + MenuItem("Sign out") + ) + + // TODO: load user profile + val $userProfile = Var( + UserProfile( + "tom", + UserInfo( + OsobniCislo("1031"), + "tom", + "Tom", + "Cook", + None, + None, + Some("tom@example.com"), + Some("+420 222 866 180"), + Some( + UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") + ), + Nil, + None + ) + ) + ) + + val $userInfo = $userProfile.signal.map(_.userInfo) + + type ViewModel = Option[HtmlElement] + def apply( + actionBus: Observer[Action] + )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using + router: Router[Page] + ): HtmlElement = + PageLayout(actionBus)( + $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => + PageLayout.ViewModel( + NavigationBar.ViewModel( + u, + pages.map(p => + NavigationBar.Link( + () => + PageLink( + p, + actionBus + ), + p == cp + ) + ), + userMenu, + logo + ), + c + ) + ), + mods + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala new file mode 100644 index 0000000..9375398 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala @@ -0,0 +1,143 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.tailwind.list.IconText.ViewModel +import fiftyforms.ui.components.tailwind.Icons +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.Page + +import com.raquo.laminar.api.L.{*, given} +import io.laminext.syntax.core.{*, given} +import mdr.pdb.app.Action + +object Breadcrumbs: + + private def slash = + import svg.{*, given} + svg( + cls := "flex-shrink-0 h-5 w-5 text-gray-300", + xmlns := "http://www.w3.org/2000/svg", + fill := "currentColor", + viewBox := "0 0 20 20", + ariaHidden := true, + path( + d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" + ) + ) + + def backIcon = + Icons.solid + .`arrow-narrow-left`() + .amend( + svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" + ) + + object Link: + case class ViewModel( + page: Page, + icon: Option[SvgElement], + extraClasses: String, + text: String, + textClasses: Option[String] = None + ) + + val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" + + def shortHome(p: Page) = ViewModel( + p, + Some(backIcon), + "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", + "Zpět na úvodní stránku", + None + ) + + def fullHome(p: Page) = ViewModel( + p, + Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), + "text-gray-400 hover:text-gray-500", + "Domů", + Some("sr-only") + ) + + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + PageLink + .container($m.map(_.page), actionBus) + .amend( + cls <-- $m.map(_.extraClasses), + child.maybe <-- $m.map(_.icon), + span( + cls <-- $m.map(_.textClasses.getOrElse("")), + child.text <-- $m.map(_.text) + ) + ) + + object FullBreadcrumbs: + type ViewModel = Page + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + ol( + role := "list", + cls := "flex items-center space-x-4", + children <-- $m.map(_.path).split(_.id)((_, _, $p) => + li( + div( + cls := "flex items-center", + child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), + Link( + $p.map(p => + if (p.isRoot) then Link.fullHome(p) + else + Link.ViewModel( + p, + None, + s"ml-4 max-w-xs truncate ${Link.baseClasses}", + p.title + ) + ), + actionBus + ) + ) + ) + ) + ) + + object ShortBreadcrumbs: + type ViewModel = Page + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + Link( + $m.map { p => + val target = p.parent.getOrElse(p) + if target.isRoot then Link.shortHome(target) + else + Link.ViewModel( + target, + Some(backIcon), + "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", + s"Zpět na ${target.title}" + ) + }, + actionBus + ) + + def apply(actionBus: Observer[Action])(using + router: Router[Page] + ): HtmlElement = + nav( + cls := "flex", + aria.label := "Breadcrumb", + div( + cls := "flex sm:hidden", + ShortBreadcrumbs(router.$currentPage, actionBus) + ), + div( + cls := "hidden sm:block", + FullBreadcrumbs(router.$currentPage, actionBus) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala new file mode 100644 index 0000000..4bb8b8d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala @@ -0,0 +1,242 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent +import fiftyforms.ui.components.tailwind.Icons +import fiftyforms.ui.components.tailwind.Avatar +import mdr.pdb.UserInfo +import io.laminext.syntax.core.* + +object NavigationBar: + + case class Logo(img: String, name: String) + case class Link(a: () => Anchor, active: Boolean) + case class MenuItem(title: String) + + case class ViewModel( + userInfo: UserInfo, + pages: List[Link], + userMenu: List[MenuItem], + logo: Logo + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + val $userInfo = $m.map(_.userInfo) + val mobileMenuOpen = Var(false) + + val desktopOnly = cls("hidden md:block") + val mobileOnly = cls("md:hidden") + + inline def avatarImage(size: Int = 8) = + Avatar($userInfo.map(_.img)).avatarImage(size) + + def notificationButton = button( + tpe := "button", + cls := List( + "bg-indigo-600", + "focus:outline-none", + "focus:ring-2", + "focus:ring-offset-2", + "focus:ring-offset-indigo-600", + "focus:ring-white", + "hover:text-white", + "p-1", + "rounded-full", + "text-indigo-200" + ), + span(cls := "sr-only", "View notifications"), + Icons.outline.bell() + ) + + def userProfile: HtmlElement = + val menuOpen = Var(false) + + def menuItem(item: MenuItem, idx: Int): HtmlElement = + a( + href := "#", + cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", + role := "menuitem", + tabIndex := -1, + idAttr := s"user-menu-item-$idx", + item.title + ) + + div( + cls := "ml-3 relative", + div( + button( + tpe := "button", + cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", + idAttr := "user-menu-button", + aria.expanded <-- menuOpen.signal, + aria.hasPopup := true, + span(cls := "sr-only", "Open user menu"), + child <-- avatarImage(), + onClick.preventDefault.mapTo( + !menuOpen.now() + ) --> menuOpen.writer + ) + ), + /* + * */ + div( + cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", + cls <-- menuOpen.signal.map { o => + if (o) "md:block" else "md:hidden" + }, + role := "menu", + aria.orientation := "vertical", + aria.labelledBy := "user-menu-button", + tabIndex := -1, + // : keyboard navigation + children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) + ) + ) + + def mobileProfile = + def menuItem(item: MenuItem): HtmlElement = + a( + href := "#", + cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", + item.title + ) + + div( + cls := "pt-4 pb-3 border-t border-indigo-700", + div( + cls := "flex items-center px-5", + div( + cls := "flex-shrink-0", + child <-- avatarImage(10) + ), + div( + cls := "ml-3", + div( + cls := "text-base font-medium text-white", + child.text <-- $userInfo.map(_.name) + ), + child.maybe <-- $userInfo.map( + _.email.map(e => + div( + cls := "text-sm font-medium text-indigo-300", + e + ) + ) + ) + ), + notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) + ), + div( + cls := "mt-3 px-2 space-y-1", + children <-- $m.map(_.userMenu.map(menuItem)) + ) + ) + + def pageLink(page: Link): Anchor = + page + .a() + .amend( + cls := "text-white px-3 py-2 rounded-md text-sm font-medium", + cls := Seq( + "bg-indigo-700" -> page.active, + "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active + ), + ariaCurrent := (if page.active then "page" else "false") + ) + + def logoImg: Image = + img( + cls := "h-8 w-8", + src <-- $m.map(_.logo.img), + alt <-- $m.map(_.logo.name) + ) + + def mobileMenuButton = button( + tpe := "button", + cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", + aria.controls := "mobile-menu", + aria.expanded <-- mobileMenuOpen.signal, + span(cls := "sr-only", "Open main menu"), + Icons.outline + .menu() + .amend( + svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") + ), + Icons.outline + .x() + .amend( + svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") + ), + onClick.preventDefault.mapTo( + !mobileMenuOpen.now() + ) --> mobileMenuOpen.writer + ) + + def navBarLeft = + div( + cls := "flex items-center", + div(cls := "flex-shrink-0", logoImg), + div( + desktopOnly, + div( + cls := "ml-10 flex items-baseline space-x-4", + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) + ) + ) + ) + + def navBarRight = + div( + desktopOnly, + div( + cls := "ml-4 flex items-center md:ml-6", + notificationButton, + userProfile + ) + ) + + def navBarMobile = + div( + cls := "-mr-2 flex", + mobileOnly, + mobileMenuButton + ) + + def navBar = + div( + cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", + div( + cls := "flex items-center justify-between h-16", + navBarLeft, + navBarRight, + navBarMobile + ) + ) + + def mobileMenu = + div( + mobileOnly, + cls <-- mobileMenuOpen.signal.map { o => + if (o) "block" else "hidden" + }, + idAttr := "mobile-menu", + div( + cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) + ), + mobileProfile + ) + + nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala new file mode 100644 index 0000000..c4cb10d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala @@ -0,0 +1,19 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.Page +import com.raquo.waypoint.Router +import mdr.pdb.app.Action + +object PageHeader: + def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = + header( + cls := "bg-white shadow-sm", + div( + cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", + h1( + cls := "text-lg leading-6 font-semibold text-gray-900", + Breadcrumbs(actionBus) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala new file mode 100644 index 0000000..aae4539 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala @@ -0,0 +1,30 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.Page +import mdr.pdb.app.Action +import fiftyforms.ui.components.tailwind.Loading +import io.laminext.syntax.core.* + +object PageLayout: + case class ViewModel( + navigation: NavigationBar.ViewModel, + content: Option[HtmlElement] + ) + def apply(actionBus: Observer[Action])( + $m: Signal[ViewModel], + mods: Modifier[HtmlElement]* + )(using router: Router[Page]): HtmlElement = + val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) + div( + cls := "h-full flex flex-col", + NavigationBar($m.map(_.navigation)), + child.maybe <-- router.$currentPage.map(_.isRoot) + .switch(None, Some(PageHeader(actionBus))), + main( + cls := "flex-grow-1 overflow-y-auto", + mods, + child <-- $maybeContent.map(_.getOrElse(Loading)) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/mdr/pdb/app/components/PageLink.scala new file mode 100644 index 0000000..e6e7052 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageLink.scala @@ -0,0 +1,40 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.LinkSupport.* +import mdr.pdb.app.Page +import mdr.pdb.app.Action +import com.raquo.waypoint.Router +import mdr.pdb.app.NavigateTo + +object PageLink: + type ViewModel = Page + def apply($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = + a(mods($m, actions), child.text <-- $m.map(_.title)) + + def apply(m: ViewModel, actions: Observer[Action])(using + router: Router[Page] + ): Anchor = apply(Val(m), actions) + + def container($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = a(mods($m, actions)) + + def container(m: ViewModel, actions: Observer[Action])(using + Router[Page] + ): Anchor = container(Val(m), actions) + + private def mods($m: Signal[Page], actions: Observer[Action])(using + router: Router[Page] + ): Modifier[Anchor] = + Seq( + href <-- $m.map(router.absoluteUrlForPage).recover { case _ => + Some("invalid url") + }, + composeEvents(onClick.noKeyMod.preventDefault)( + _.sample($m) + .map(NavigateTo.apply) + ) --> actions + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala new file mode 100644 index 0000000..7682772 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala @@ -0,0 +1,13 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.pages.dashboard.DashboardPage +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage + +class DashboardPageConnector(actionBus: Observer[Action])(using + router: Router[Page] +): + def apply: HtmlElement = + AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala new file mode 100644 index 0000000..76b9133 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala @@ -0,0 +1,76 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.Parameter +import mdr.pdb.UserInfo +import mdr.pdb.app.pages.detail.DetailParametruPage +import pages.detail.DetailKriteriaPage +import pages.detail.components.DukazyKriteria +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.ParameterCriteria + +object DetailKriteriaPageConnector { + trait AppState { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + } +} + +case class DetailKriteriaPageConnector( + state: DetailKriteriaPageConnector.AppState +)( + $page: Signal[Page.DetailKriteria] +)(using Router[Page]): + val $paramChangeSignal = + $page.splitOne(p => + (p.osobniCislo.value, p.parametr.value, p.kriterium.value) + )((x, _, _) => x) + val $pageChangeSignal = + $paramChangeSignal.map( + FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) + ) + + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + val $merged = + $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => + for { + da <- d + pa <- p + pb <- pa.find(_.id == pc._2) + ka <- pb.criteria.find(_.id == pc._3) + } yield (da, pb, ka) + ) + + def apply: HtmlElement = + AppPage(state.actionBus)( + $merged.map(_.map(buildModel)) + .split(_ => ())((_, s, $s) => + DetailKriteriaPage($s)(state.actionBus.contramap { + case DukazyKriteria.Add => + NavigateTo( + Page.UpravDukazKriteria( + Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), + Page.Titled(s.parametr.id, Some(s.parametr.nazev)), + Page.Titled(s.kriterium.id, Some(s.kriterium.id)) + ) + ) + }) + ), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: Parameter, + k: ParameterCriteria + ): DetailKriteriaPage.ViewModel = + DetailKriteriaPage.ViewModel( + o.toDetailOsoby, + p.toParametr(_ => a()), + k.toKriterium(_ => a()) + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DetailPageConnector.scala new file mode 100644 index 0000000..df3a6b7 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DetailPageConnector.scala @@ -0,0 +1,57 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.UserInfo +import pages.detail.DetailPage +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.pages.detail.components.DetailOsoby +import mdr.pdb.Parameter +import mdr.pdb.app.pages.detail.components.SeznamParametru +import fiftyforms.ui.components.tailwind.Color + +object DetailPageConnector { + trait AppState { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + } +} + +case class DetailPageConnector(state: DetailPageConnector.AppState)( + $page: Signal[Page.Detail] +)(using router: Router[Page]): + val $oscChangeSignal = $page.splitOne(_.osobniCislo.value)((osc, _, _) => osc) + val $pageChangeSignal = + $oscChangeSignal.flatMap(osc => + EventStream.fromSeq(Seq(FetchUserDetails(osc), FetchParameters(osc))) + ) + // TODO: filter the value based on the current osc + // OSC change will fetch new data, but still + // - we need to be sure that what we got is really what we ought to display + // - we want to display stale data accordingly (at least with loading indicator) + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + def apply: HtmlElement = + AppPage(state.actionBus)( + $data.combineWithFn($params)(_ zip _) + .map(_.map(buildModel)) + .split(_ => ())((_, _, s) => DetailPage(s)), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: List[Parameter] + ): DetailPage.ViewModel = + DetailPage.ViewModel( + o.toDetailOsoby, + p.map( + _.toParametr(param => + PageLink.container(Page.DetailParametru(o, param), state.actionBus) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DetailParametruPageConnector.scala new file mode 100644 index 0000000..fa7193d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DetailParametruPageConnector.scala @@ -0,0 +1,67 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.Parameter +import mdr.pdb.UserInfo +import mdr.pdb.app.pages.detail.DetailParametruPage +import pages.detail.DetailParametruPage +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.components.PageLink + +object DetailParametruPageConnector { + trait AppState { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + } +} + +case class DetailParametruPageConnector( + state: DetailParametruPageConnector.AppState +)( + $page: Signal[Page.DetailParametru] +)(using router: Router[Page]): + val $paramChangeSignal = + $page.splitOne(p => (p.osobniCislo.value, p.parametr.value))((x, _, _) => x) + val $pageChangeSignal = + $paramChangeSignal.map(FetchParameter(_, _)) + + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + val $merged = + $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => + for { + da <- d + pa <- p + pb <- pa.find(_.id == pc._2) + } yield (da, pb) + ) + + def apply: HtmlElement = + AppPage(state.actionBus)( + $merged.map(_.map(buildModel)) + .split(_ => ())((_, _, s) => DetailParametruPage(s)), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: Parameter + ): DetailParametruPage.ViewModel = + DetailParametruPage.ViewModel( + o.toDetailOsoby, + p.toParametr(p => + PageLink.container(Page.DetailParametru(o, p), state.actionBus) + ), + p.criteria.map( + _.toKriterium { c => + PageLink.container( + Page.DetailKriteria(o, p, c), + state.actionBus + ) + } + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DirectoryPageConnector.scala new file mode 100644 index 0000000..ab2c045 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DirectoryPageConnector.scala @@ -0,0 +1,34 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.UserInfo +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.components.AppPage + +case class DirectoryPageConnector( + $input: EventStream[List[UserInfo]], + actionBus: Observer[Action] +)(using router: Router[Page]): + val $data = $input.startWithNone + val $actionSignal = EventStream.fromValue(FetchDirectory) + + def apply: HtmlElement = + AppPage(actionBus)( + $data.split(_ => ())((_, _, s) => + pages.directory.DirectoryPage( + s.map( + _.map( + _.toUserRow(u => + PageLink.container( + Page.Detail(Page.Titled(u.personalNumber)), + actionBus + ) + ) + ) + ) + ) + ), + $actionSignal --> actionBus + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/mdr/pdb/app/connectors/conversions.scala new file mode 100644 index 0000000..f581796 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/conversions.scala @@ -0,0 +1,68 @@ +package mdr.pdb.app.connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.UserInfo +import mdr.pdb.app.pages.detail.components.DetailOsoby +import mdr.pdb.Parameter +import mdr.pdb.app.pages.detail.components.SeznamParametru +import mdr.pdb.ParameterCriteria +import mdr.pdb.app.pages.detail.components.SeznamKriterii +import mdr.pdb.app.pages.directory.components.UserRow +import mdr.pdb.app.pages.detail.components.DetailParametru +import mdr.pdb.app.pages.detail.components.DetailKriteria +import fiftyforms.ui.components.tailwind.Color + +extension (o: UserInfo) + def toDetailOsoby: DetailOsoby.ViewModel = + DetailOsoby.ViewModel( + o.personalNumber, + o.name, + o.email, + o.phone, + o.img, + o.mainFunction.map(f => + DetailOsoby.Funkce.ViewModel(f.name, f.dept, f.ou) + ), + o.userContracts.headOption.map(c => + DetailOsoby.PracovniPomer.ViewModel(c.rel, c.startDate, c.endDate) + ) + ) + +extension (param: Parameter) + def toParametr(container: Parameter => Anchor): DetailParametru.ViewModel = + DetailParametru.ViewModel( + id = param.id, + nazev = param.name, + popis = param.description, + status = "Nesplněno", + statusColor = Color.red, + a = container(param) + ) + +extension (crit: ParameterCriteria) + def toKriterium( + container: ParameterCriteria => Anchor + ): DetailKriteria.ViewModel = + DetailKriteria.ViewModel( + nazev = crit.criteriumText, + kapitola = crit.chapterId, + bod = crit.itemId, + status = "Nesplněno", + statusColor = Color.red, + splneno = false, + dukazy = Nil, + container = container(crit) + ) + +extension (user: UserInfo) + def toUserRow( + container: UserInfo => HtmlElement = (_: UserInfo) => div() + ): UserRow.ViewModel = + UserRow.ViewModel( + osobniCislo = user.personalNumber.toString, + celeJmeno = user.name, + prijmeni = user.surname, + hlavniFunkce = user.mainFunction.map(_.name), + img = user.img, + container = () => container(user) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/dashboard/DashboardPage.scala b/app/src/main/scala/mdr/pdb/app/pages/dashboard/DashboardPage.scala new file mode 100644 index 0000000..0eb8f7a --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/dashboard/DashboardPage.scala @@ -0,0 +1,11 @@ +package mdr.pdb.app.pages.dashboard + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.Page + +object DashboardPage: + + def render: HtmlElement = + div("Dashboard page") diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala new file mode 100644 index 0000000..ff3a25f --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala @@ -0,0 +1,31 @@ +package mdr.pdb.app.pages.detail + +import com.raquo.laminar.api.L.{*, given} + +import components._ + +object DetailKriteriaPage: + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametr: DetailParametru.ViewModel, + kriterium: DetailKriteria.ViewModel + ) + + def apply($m: Signal[ViewModel])( + action: Observer[DukazyKriteria.Action] + ): HtmlElement = + div( + cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + div( + DetailOsoby.header($m.map(_.osoba)), + DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") + ), + div( + DetailKriteria($m.map(_.kriterium)), + DukazyKriteria($m.map(_.kriterium.dukazy))(action) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/DetailPage.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailPage.scala new file mode 100644 index 0000000..b591cc9 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailPage.scala @@ -0,0 +1,25 @@ +package mdr.pdb.app.pages.detail + +import com.raquo.laminar.api.L.{*, given} + +import components._ +import mdr.pdb.app.Page +import com.raquo.waypoint.Router +import mdr.pdb.app.Action + +object DetailPage: + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametry: SeznamParametru.ViewModel + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + DetailOsoby($m.map(_.osoba)), + SeznamParametru($m.map(_.parametry)) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/DetailParametruPage.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailParametruPage.scala new file mode 100644 index 0000000..3322279 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailParametruPage.scala @@ -0,0 +1,24 @@ +package mdr.pdb.app.pages.detail + +import com.raquo.laminar.api.L.{*, given} + +import components._ + +object DetailParametruPage: + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametr: DetailParametru.ViewModel, + kriteria: SeznamKriterii.ViewModel + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + DetailOsoby.header($m.map(_.osoba)), + DetailParametru($m.map(_.parametr)), + SeznamKriterii($m.map(_.kriteria)) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala new file mode 100644 index 0000000..b58618a --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala @@ -0,0 +1,107 @@ +package mdr.pdb +package app +package pages.detail + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.pages.detail.components.UpravDukazForm +import fiftyforms.services.files.File + +object UpravDukaz: + + type ThisPage = Page.UpravDukazKriteria + type PageKey = (OsobniCislo, String, String) + + trait State { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def availableFiles: EventStream[List[File]] + def actionBus: Observer[Action] + } + + def keyOfPage(page: ThisPage): PageKey = + (page.osobniCislo.value, page.parametr.value, page.kriterium.value) + + def onChangeAction(key: PageKey): Action = + FetchParameterCriteria( + key._1, + key._2, + key._3, + Page.UpravDukazKriteria(_, _, _) + ) + + class Connector(state: State)($page: Signal[ThisPage])(using Router[Page]): + val $paramChangeSignal = $page.splitOne(keyOfPage)((x, _, _) => x) + val $pageChangeSignal = $paramChangeSignal.map(onChangeAction) + + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + val $merged = + $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => + for { + da <- d + pa <- p + pb <- pa.find(_.id == pc._2) + ka <- pb.criteria.find(_.id == pc._3) + } yield (da, pb, ka) + ) + + def apply: HtmlElement = + AppPage(state.actionBus)( + $merged.split(_ => ())((_, s, $s) => + PageComponent( + $s.map(buildModel), + state.availableFiles, + state.actionBus.contramap { + case UpravDukazForm.Cancelled => + NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) + case UpravDukazForm.AvailableFilesRequested => + FetchAvailableFiles(s._1.personalNumber) + } + ) + ), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: Parameter, + k: ParameterCriteria + ): PageComponent.ViewModel = + import connectors.* + PageComponent.ViewModel( + o.toDetailOsoby, + p.toParametr(_ => a()), + k.toKriterium(_ => a()) + ) + + object PageComponent: + import components.* + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametr: DetailParametru.ViewModel, + kriterium: DetailKriteria.ViewModel + ) + + def apply( + $m: Signal[ViewModel], + availableFilesStream: EventStream[List[File]], + events: Observer[UpravDukazForm.Event] + ): HtmlElement = + div( + cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + div( + DetailOsoby.header($m.map(_.osoba)), + DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") + ), + div( + DetailKriteria($m.map(_.kriterium)), + UpravDukazForm(availableFilesStream)(events) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailKriteria.scala new file mode 100644 index 0000000..a3ab634 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailKriteria.scala @@ -0,0 +1,37 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.CustomAttrs +import fiftyforms.ui.components.tailwind.Icons +import java.time.LocalDate +import fiftyforms.ui.components.tailwind.Color + +object DetailKriteria: + case class ViewModel( + nazev: String, + kapitola: String, + bod: String, + status: String, + statusColor: Color, + splneno: Boolean, + dukazy: List[DukazKriteria.ViewModel], + container: HtmlElement = div() + ) { + val id = s"${kapitola}${bod}" + } + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "pb-5 border-b border-gray-200", + h3( + cls := "text-xl leading-6 font-bold text-gray-900", + child.text <-- $m.map(_.nazev) + ), + h3( + cls := "mt-2 max-w-4xl text-sm text-gray-500", + "Kapitola ", + child.text <-- $m.map(_.kapitola), + " bod ", + child <-- $m.map(_.bod) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailOsoby.scala new file mode 100644 index 0000000..0fe2ba5 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailOsoby.scala @@ -0,0 +1,100 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.OsobniCislo + +import java.time.LocalDate +import fiftyforms.ui.components.tailwind.Avatar + +object DetailOsoby: + + object Funkce: + case class ViewModel( + nazev: String, + stredisko: String, + voj: String + ) + + def render($m: Signal[ViewModel]): HtmlElement = + p( + cls := "text-sm font-medium text-gray-500", + child.text <-- $m.map(_.nazev), + span( + cls := "hidden md:inline", + " @ ", + child.text <-- $m.map(_.stredisko), + ", ", + child.text <-- $m.map(_.voj) + ) + ) + + object PracovniPomer: + case class ViewModel( + druh: String, + pocatek: LocalDate, + konec: Option[LocalDate] + ) + + def render($m: Signal[ViewModel]): HtmlElement = + import fiftyforms.ui.components.tailwind.CustomAttrs.datetime + p( + cls := "text-sm font-medium text-gray-500", + child.text <-- $m.map(_.druh), + " od ", + time( + datetime <-- $m.map(_.pocatek.toString), + child.text <-- $m.map(_.pocatek.toString) + ) + ) + + case class ViewModel( + osobniCislo: OsobniCislo, + jmeno: String, + email: Option[String], + telefon: Option[String], + img: Option[String], + hlavniFunkce: Option[Funkce.ViewModel], + pracovniPomer: Option[PracovniPomer.ViewModel] + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "md:flex md:items-center md:justify-between md:space-x-4", + div( + cls := "flex items-start space-x-4", + div( + cls := "flex-shrink-0", + Avatar($m.map(_.img)).avatar(16) + ), + div( + h1( + cls := "text-2xl font-bold text-gray-900", + child.text <-- $m.map(_.jmeno) + ), + child.maybe <-- $m.map(_.hlavniFunkce).split(_ => ())((_, _, d) => + Funkce.render(d) + ), + child.maybe <-- $m.map(_.pracovniPomer).split(_ => ())((_, _, d) => + PracovniPomer.render(d) + ) + ) + ) + ) + + def header($m: Signal[ViewModel]): HtmlElement = + div( + cls := "md:flex md:items-center md:justify-between md:space-x-5", + div( + cls := "flex items-start space-x-4", + div( + cls := "flex-shrink-0", + Avatar($m.map(_.img)).avatar(8) + ), + div( + h1( + cls := "text-lg font-medium text-gray-800", + child.text <-- $m.map(_.jmeno) + ) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailParametru.scala new file mode 100644 index 0000000..6310dde --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailParametru.scala @@ -0,0 +1,34 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.Color + +object DetailParametru: + case class ViewModel( + id: String, + nazev: String, + popis: String, + status: String, + statusColor: Color, + a: Anchor + ) + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "pb-5 border-b border-gray-200", + h2( + cls := "text-2xl leading-6 font-medium text-gray-900", + child.text <-- $m.map(_.nazev) + ), + p( + cls := "mt-2 max-w-4xl text-sm text-gray-500", + child.text <-- $m.map(_.popis) + ) + ) + + def header($m: Signal[ViewModel]): HtmlElement = + div( + h2( + cls := "text-lg leading-6 font-medium text-gray-600", + child.text <-- $m.map(_.nazev) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazKriteria.scala new file mode 100644 index 0000000..c292919 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazKriteria.scala @@ -0,0 +1,177 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import java.time.LocalDate +import fiftyforms.ui.components.tailwind.CustomAttrs + +object DukazKriteria: + case class Osoba(osobniCislo: String, jmeno: String) + case class Udalost(osoba: Osoba, datum: LocalDate) + case class Dokument( + url: String, + nazev: String, + pridal: Udalost, + odebral: Option[Udalost] + ) + case class ViewModel( + dokumenty: List[Dokument], + autorizoval: Option[Udalost], + platiDo: Option[LocalDate], + poznámka: Option[String] + ) + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "bg-white shadow overflow-hidden sm:rounded-lg", + div( + cls := "px-4 py-5 sm:px-6", + h3( + cls := "text-lg leading-6 font-medium text-gray-900", + """Applicant Information""" + ), + p( + cls := "mt-1 max-w-2xl text-sm text-gray-500", + """Personal details and application.""" + ) + ), + div( + cls := "border-t border-gray-200 px-4 py-5 sm:px-6", + dl( + cls := "grid grid-cols-1 gap-x-4 gap-y-8 sm:grid-cols-2", + div( + cls := "sm:col-span-1", + dt( + cls := "text-sm font-medium text-gray-500", + """Full name""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """Margot Foster""" + ) + ), + div( + cls := "sm:col-span-1", + dt( + cls := "text-sm font-medium text-gray-500", + """Application for""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """Backend Developer""" + ) + ), + div( + cls := "sm:col-span-1", + dt( + cls := "text-sm font-medium text-gray-500", + """Email address""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """margotfoster@example.com""" + ) + ), + div( + cls := "sm:col-span-1", + dt( + cls := "text-sm font-medium text-gray-500", + """Salary expectation""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """$120,000""" + ) + ), + div( + cls := "sm:col-span-2", + dt( + cls := "text-sm font-medium text-gray-500", + """About""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """Fugiat ipsum ipsum deserunt culpa aute sint do nostrud anim incididunt cillum culpa consequat. Excepteur qui ipsum aliquip consequat sint. Sit id mollit nulla mollit nostrud in ea officia proident. Irure nostrud pariatur mollit ad adipisicing reprehenderit deserunt qui eu.""" + ) + ), + div( + cls := "sm:col-span-2", + dt( + cls := "text-sm font-medium text-gray-500", + """Attachments""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + ul( + role := "list", + cls := "border border-gray-200 rounded-md divide-y divide-gray-200", + li( + cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", + div( + cls := "w-0 flex-1 flex items-center", { + import svg.* + import CustomAttrs.svg.ariaHidden + svg( + cls := "flex-shrink-0 h-5 w-5 text-gray-400", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + ariaHidden := true, + path( + fillRule := "evenodd", + d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", + clipRule := "evenodd" + ) + ) + }, + span( + cls := "ml-2 flex-1 w-0 truncate", + """resume_back_end_developer.pdf""" + ) + ), + div( + cls := "ml-4 flex-shrink-0", + a( + href := "#", + cls := "font-medium text-indigo-600 hover:text-indigo-500", + """Download""" + ) + ) + ), + li( + cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", + div( + cls := "w-0 flex-1 flex items-center", { + import svg.* + import CustomAttrs.svg.ariaHidden + svg( + cls := "flex-shrink-0 h-5 w-5 text-gray-400", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + ariaHidden := true, + path( + fillRule := "evenodd", + d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", + clipRule := "evenodd" + ) + ) + }, + span( + cls := "ml-2 flex-1 w-0 truncate", + """coverletter_back_end_developer.pdf""" + ) + ), + div( + cls := "ml-4 flex-shrink-0", + a( + href := "#", + cls := "font-medium text-indigo-600 hover:text-indigo-500", + """Download""" + ) + ) + ) + ) + ) + ) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala new file mode 100644 index 0000000..d604860 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala @@ -0,0 +1,35 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.Icons + +object DukazyKriteria: + sealed trait Action + case object Add extends Action + + type ViewModel = List[DukazKriteria.ViewModel] + + def apply($m: Signal[ViewModel])(actions: Observer[Action]): HtmlElement = + div( + child.maybe <-- $m.map(m => + if (m.isEmpty) then Some(prazdnyDukaz(actions)) + else None + ), + children <-- $m.map(_.zipWithIndex).split(_._2)((_, _, $s) => + DukazKriteria($s.map(_._1)) + ) + ) + + private def prazdnyDukaz(actions: Observer[Action]): HtmlElement = + button( + tpe := "button", + onClick.preventDefault.mapTo(Add) --> actions, + cls := "relative block w-full border-2 border-gray-300 border-dashed rounded-lg p-12 text-center hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + Icons.outline + .`document-add`(12) + .amend(svg.cls := "mx-auto text-gray-400"), + span( + cls := "mt-2 block text-sm font-medium text-gray-900", + "Přidat důkaz" + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala new file mode 100644 index 0000000..7e77a7b --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala @@ -0,0 +1,34 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.Color +import fiftyforms.ui.components.tailwind.list.StackedList +import fiftyforms.ui.components.tailwind.list.ListRow +import fiftyforms.ui.components.tailwind.list.RowTag +import fiftyforms.ui.components.tailwind.list.RowNext +import java.time.LocalDate + +object SeznamKriterii: + type ViewModel = List[DetailKriteria.ViewModel] + + private val kritList = new StackedList[DetailKriteria.ViewModel] + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "bg-white shadow overflow-hidden sm:rounded-md", + kritList($m, _.id) { $i => + $i.map { i => + ListRow.ViewModel( + title = i.nazev, + topRight = RowTag.render( + $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) + ), + bottomLeft = + p(cls := "text-sm text-gray-500", s"${i.kapitola}${i.bod}"), + bottomRight = emptyNode, + farRight = RowNext.render, + containerElement = i.container + ) + } + } + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamParametru.scala new file mode 100644 index 0000000..0760b32 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamParametru.scala @@ -0,0 +1,37 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.list.{ + StackedList, + ListRow, + RowTag, + PropList, + IconText, + RowNext +} +import fiftyforms.ui.components.tailwind.Color +import fiftyforms.ui.components.tailwind.LinkSupport.* + +object SeznamParametru: + type ViewModel = List[DetailParametru.ViewModel] + + private val parametrList = new StackedList[DetailParametru.ViewModel] + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "bg-white shadow overflow-hidden sm:rounded-md", + parametrList($m, _.id) { $i => + $i.map { i => + ListRow.ViewModel( + title = i.nazev, + topRight = RowTag.render( + $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) + ), + bottomLeft = emptyNode, + bottomRight = emptyNode, + farRight = RowNext.render, + containerElement = i.a + ) + } + } + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala new file mode 100644 index 0000000..28753c9 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala @@ -0,0 +1,101 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import io.laminext.syntax.core.* +import fiftyforms.ui.components.tailwind.CustomAttrs +import fiftyforms.ui.components.tailwind.form.* +import org.scalajs.dom +import com.raquo.laminar.nodes.ReactiveHtmlElement +import fiftyforms.services.files.components.tailwind.FilePicker +import mdr.pdb.frontend.AutorizujDukaz +import mdr.pdb.frontend.DocumentRef +import fiftyforms.services.files.File + +object UpravDukazForm: + sealed trait Event + case object Cancelled extends Event + case object AvailableFilesRequested extends Event + def apply(availableFilesStream: EventStream[List[File]])( + updates: Observer[Event] + ): HtmlElement = + val (filesStream, filesObserver) = + EventStream.withObserver[FilePicker.Event] + val files = Var[List[File]](Nil) + def submitButtons: HtmlElement = + div( + cls := "pt-5", + div( + cls := "flex justify-end", + button( + tpe := "button", + cls := "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + "Zrušit", + onClick.mapTo(Cancelled) --> updates + ), + button( + tpe := "submit", + cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + disabled <-- files.signal.map(_.isEmpty), + "Autorizovat" + ) + ) + ) + + div( + filesStream.collect { case FilePicker.SelectionUpdated(files) => + files.to(List) + } --> files.writer, + filesStream.collect { case FilePicker.AvailableFilesRequested => + AvailableFilesRequested + } --> updates, + cls := "bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6", + Form( + Form.Body( + Form.Section( + FormHeader( + FormHeader.ViewModel( + "Doložení kritéria", + "Sestavte doklady poskytující důkaz kritéria a potvrďte odesláním formuláře. Případné limitace či jiné relevantní údaje vepište do pole Komentář." + ) + ), + FormFields( + FormRow( + "dokumenty", + "Dokumenty", + FilePicker(files.signal, availableFilesStream)(filesObserver) + .amend(idAttr := "dokumenty", cls("max-w-lg")) + ).toHtml, + FormRow( + "platnost", + "Platnost", + input( + idAttr := "platnost", + name := "platnost", + tpe := "date", + autoComplete := "date", + cls := "block max-w-lg w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" + ) + ).toHtml, + FormRow( + "komentar", + "Komentář", + div( + cls := "mt-1 sm:mt-0 sm:col-span-2", + textArea( + idAttr := "komentar", + name := "about", + rows := 3, + cls := "max-w-lg shadow-sm block w-full focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border border-gray-300 rounded-md" + ), + p( + cls := "mt-2 text-sm text-gray-500", + "Doplňte prosím potřebné informace související s doložením kritéria, včetně případných limitací." + ) + ) + ).toHtml + ) + ) + ), + submitButtons + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/directory/DirectoryPage.scala b/app/src/main/scala/mdr/pdb/app/pages/directory/DirectoryPage.scala new file mode 100644 index 0000000..7a72ca3 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/directory/DirectoryPage.scala @@ -0,0 +1,40 @@ +package mdr.pdb.app.pages.directory + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.UserInfo + +import components._ + +object DirectoryPage: + + type ViewModel = List[UserRow.ViewModel] + + def apply($m: Signal[ViewModel]): HtmlElement = + val (actionsStream, actionObserver) = + EventStream.withObserver[SearchForm.Action] + val $filter = actionsStream + .collect { + case SearchForm.ClearFilter => None + case SearchForm.SetFilter(t) => Some(t) + } + .startWith(None) + val byLetter = for { + d <- $m + f <- $filter + } yield for { + (letter, users) <- d + .filter { user => + f match + case None => true + case Some(t) => user.search.contains(t) + } + .groupBy(_.prijmeni.head) + .to(List) + .sortBy(_._1) + } yield (letter.toString, users.sortBy(_.prijmeni)) + + div( + cls := "h-full max-w-7xl mx-auto order-first flex flex-col flex-shrink-0", + SearchForm(actionObserver), + Directory(byLetter) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/directory/components/Directory.scala b/app/src/main/scala/mdr/pdb/app/pages/directory/components/Directory.scala new file mode 100644 index 0000000..f7d6f99 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/directory/components/Directory.scala @@ -0,0 +1,37 @@ +package mdr.pdb.app.pages.directory.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.UserInfo + +object Directory: + + object Header: + type ViewModel = String + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500", + h3(child.text <-- $m) + ) + + object UserList: + type ViewModel = List[UserRow.ViewModel] + def apply($m: Signal[ViewModel]): HtmlElement = + ul( + role := "list", + cls := "relative z-0 divide-y divide-gray-200", + children <-- $m.split(_.osobniCislo)((_, _, s) => UserRow(s)) + ) + + type ViewModel = List[(String, List[UserRow.ViewModel])] + def apply($m: Signal[ViewModel]): HtmlElement = + nav( + cls := "flex-1 min-h-0 overflow-y-auto", + aria.label := "Directory", + children <-- $m.split(_._1)((_, _, s) => + div( + cls := "relative", + Header(s.map(_._1)), + UserList(s.map(_._2)) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/directory/components/SearchForm.scala b/app/src/main/scala/mdr/pdb/app/pages/directory/components/SearchForm.scala new file mode 100644 index 0000000..fb9ecc4 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/directory/components/SearchForm.scala @@ -0,0 +1,54 @@ +package mdr.pdb.app.pages.directory.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.Icons + +object SearchForm: + sealed trait Action + case object SubmitFilter extends Action + sealed trait FilterAction extends Action + case object ClearFilter extends FilterAction + case class SetFilter(value: String) extends FilterAction + + def apply(actions: Observer[Action]): HtmlElement = + div( + cls := "px-6 pt-4 pb-4", + form( + cls := "flex space-x-4", + onSubmit.mapTo(SubmitFilter) --> actions, + div( + cls := "flex-1 min-w-0", + label( + forId := "search", + cls := "sr-only", + "Hledat" + ), + div( + cls := "relative rounded-md shadow-sm", + div( + cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", + Icons.solid.search().amend(svg.cls := "text-gray-400") + ), + input( + tpe := "search", + name := "search", + idAttr := "search", + cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", + placeholder := "Hledat", + composeEvents(onInput.mapToValue.setAsValue.map(SetFilter(_)))( + _.throttle(500) + ) --> actions + ) + ) + ), + button( + tpe := "submit", + cls := "inline-flex justify-center px-3.5 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500", + Icons.solid.filter().amend(svg.cls := "text-gray-400"), + span( + cls := "sr-only", + "Hledat" + ) + ) + ) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala deleted file mode 100644 index a3b00af..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala +++ /dev/null @@ -1,103 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import scala.scalajs.js.annotation.JSExportTopLevel -import scala.scalajs.js.annotation.JSExport -import scala.scalajs.js.annotation.JSImport -import scala.scalajs.js -import org.scalajs.dom -import com.raquo.laminar.api.L.{*, given} -import scala.scalajs.js.Date -import com.raquo.waypoint.Router -import com.raquo.waypoint.SplitRender -import cz.e_bs.cmi.mdr.pdb.app.services.DataFetcher -import scala.scalajs.js.JSON -import zio.json._ -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.state.AppState - -@js.native -@JSImport("stylesheets/main.css", JSImport.Namespace) -object Css extends js.Any - -@js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native -@JSImport("params/pdb-params.json", JSImport.Default) -object pdbParams extends js.Object - -@JSExportTopLevel("app") -object Main: - - @JSExport - def main(args: Array[String]): Unit = - import Routes.given - onLoad { - setupAirstream() - val appContainer = dom.document.querySelector("#app") - val _ = - render( - appContainer, - renderPage(state.MockAppState(using unsafeWindowOwner, router)) - ) - } - - private def onLoad(f: => Unit): Unit = - documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) - - private def setupAirstream()(using router: Router[Page]): Unit = - AirstreamError.registerUnhandledErrorCallback(err => - router.forcePage( - Page.UnhandledError( - Some(err.getClass.getName), // TODO: Fill only in dev mode - Some(err.getMessage) - ) - ) - ) - - def renderPage(state: AppState)(using - router: Router[Page] - ): HtmlElement = - val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) - .collectSignal[Page.Detail]( - connectors - .DetailPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailParametru]( - connectors - .DetailParametruPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailKriteria]( - connectors - .DetailKriteriaPageConnector(state)(_) - .apply - ) - .collectSignal[Page.UpravDukazKriteria]( - pages.detail.UpravDukaz.Connector(state)(_).apply - ) - .collectStatic(Page.Dashboard)( - connectors.DashboardPageConnector(state.actionBus).apply - ) - .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) - ) - .collect[Page.UnhandledError](pg => - pages.errors - .UnhandledErrorPage( - pages.errors.UnhandledErrorPage - .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), - state.actionBus - ) - ) - .collectStatic(Page.Directory)( - connectors - .DirectoryPageConnector(state.users, state.actionBus) - .apply - ) - div(cls := "h-full", child <-- pageSplitter.$view) - - // Pull in the stylesheet - val css: Css.type = Css diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala deleted file mode 100644 index 3bbc8c0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ /dev/null @@ -1,198 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.* -import org.scalajs.dom -import zio.json.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import scala.scalajs.js -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.Page.Titled - -// enum is not working with Waypoints' SplitRender collectStatic -sealed abstract class Page( - val id: String, - val title: String, - val parent: Option[Page] -) { - val path: Vector[Page] = - parent match - case None => Vector(this) - case Some(p) => p.path :+ this - - val isRoot: Boolean = parent.isEmpty -} - -object Page: - - case class Titled[V](value: V, title: Option[String] = None): - val show: String = title.getOrElse(value.toString) - - case object Directory extends Page("directory", "Adresář", None) - - case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) - - case class Detail(osobniCislo: Titled[OsobniCislo]) - extends Page("user", osobniCislo.show, Some(Directory)) - - object Detail { - def apply(o: UserInfo): Detail = Detail( - Titled(o.personalNumber, Some(o.name)) - ) - } - - case class DetailParametru( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String] - ) extends Page( - "parameter", - parametr.show, - Some(Detail(osobniCislo)) - ) - - object DetailParametru { - def apply(o: UserInfo, p: Parameter): DetailParametru = - DetailParametru( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)) - ) - } - - case class DetailKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "criteria", - kriterium.show, - Some(DetailParametru(osobniCislo, parametr)) - ) - - object DetailKriteria { - def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = - DetailKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class UpravDukazKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "addProof", - "Důkaz", - Some(DetailKriteria(osobniCislo, parametr, kriterium)) - ) - - object UpravDukazKriteria { - def apply( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): UpravDukazKriteria = - UpravDukazKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class NotFound(url: String) extends Page("404", "404", Some(Directory)) - - case class UnhandledError( - errorName: Option[String], - errorMessage: Option[String] - ) extends Page("500", "Unexpected error", Some(Directory)) - -object Routes: - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) - given [V: JsonEncoder]: JsonEncoder[Titled[V]] = - DeriveJsonEncoder.gen[Titled[V]] - given [V: JsonDecoder]: JsonDecoder[Titled[V]] = - DeriveJsonDecoder.gen[Titled[V]] - given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] - given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] - - val base = - js.`import`.meta.env.BASE_URL - .asInstanceOf[String] + "app" - - val homePage: Page = Page.Directory - - given router: Router[Page] = Router[Page]( - routes = List( - Route.static(homePage, root / endOfSegments, basePath = base), - Route.static( - Page.Dashboard, - root / "dashboard" / endOfSegments, - basePath = base - ), - Route[Page.Detail, String]( - encode = _.osobniCislo.value.toString, - decode = osc => Page.Detail(Titled(OsobniCislo(osc))), - root / "osoba" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.DetailParametru, (String, String)]( - encode = p => (p.osobniCislo.value.toString, p.parametr.value), - decode = - p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / endOfSegments, - basePath = base - ), - Route[Page.DetailKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.DetailKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.UpravDukazKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.UpravDukazKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / "edit" / endOfSegments, - basePath = base - ) - ), - serializePage = _.toJson, - deserializePage = _.fromJson[Page] - .fold(s => throw IllegalStateException(s), identity), - getPageTitle = _.title, - routeFallback = url => Page.NotFound(url), - deserializeFallback = _ => Page.Dashboard - )( - $popStateEvent = windowEvents.onPopState, - owner = unsafeWindowOwner - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala deleted file mode 100644 index b24b82b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -sealed trait Action - -case object FetchDirectory extends Action -case class FetchUserDetails(osc: OsobniCislo) extends Action -case class FetchParameters(osc: OsobniCislo) extends Action -case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action -case class FetchParameterCriteria( - osc: OsobniCislo, - paramId: String, - critId: String, - page: (UserInfo, Parameter, ParameterCriteria) => Page -) extends Action -case class FetchAvailableFiles(osc: OsobniCislo) extends Action -case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala deleted file mode 100644 index c637d35..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala +++ /dev/null @@ -1,82 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.{UserProfile, UserInfo, OsobniCislo} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo -import cz.e_bs.cmi.mdr.pdb.UserFunction - -object AppPage: - // TODO: pages by logged in user - val pages: List[Page] = List(Page.Directory, Page.Dashboard) - - import NavigationBar.{Logo, MenuItem} - - val logo = Logo( - "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", - "Workflow" - ) - - // TODO: menu items by user profile - val userMenu = - List( - MenuItem("Your Profile"), - MenuItem("Settings"), - MenuItem("Sign out") - ) - - // TODO: load user profile - val $userProfile = Var( - UserProfile( - "tom", - UserInfo( - OsobniCislo("1031"), - "tom", - "Tom", - "Cook", - None, - None, - Some("tom@example.com"), - Some("+420 222 866 180"), - Some( - UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") - ), - Nil, - None - ) - ) - ) - - val $userInfo = $userProfile.signal.map(_.userInfo) - - type ViewModel = Option[HtmlElement] - def apply( - actionBus: Observer[Action] - )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using - router: Router[Page] - ): HtmlElement = - PageLayout(actionBus)( - $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => - PageLayout.ViewModel( - NavigationBar.ViewModel( - u, - pages.map(p => - NavigationBar.Link( - () => - PageLink( - p, - actionBus - ), - p == cp - ) - ), - userMenu, - logo - ), - c - ) - ), - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala deleted file mode 100644 index bf473c6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ /dev/null @@ -1,143 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden -import fiftyforms.ui.components.tailwind.list.IconText.ViewModel -import fiftyforms.ui.components.tailwind.Icons -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Action - -object Breadcrumbs: - - private def slash = - import svg.{*, given} - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-300", - xmlns := "http://www.w3.org/2000/svg", - fill := "currentColor", - viewBox := "0 0 20 20", - ariaHidden := true, - path( - d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" - ) - ) - - def backIcon = - Icons.solid - .`arrow-narrow-left`() - .amend( - svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" - ) - - object Link: - case class ViewModel( - page: Page, - icon: Option[SvgElement], - extraClasses: String, - text: String, - textClasses: Option[String] = None - ) - - val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" - - def shortHome(p: Page) = ViewModel( - p, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - "Zpět na úvodní stránku", - None - ) - - def fullHome(p: Page) = ViewModel( - p, - Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), - "text-gray-400 hover:text-gray-500", - "Domů", - Some("sr-only") - ) - - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - PageLink - .container($m.map(_.page), actionBus) - .amend( - cls <-- $m.map(_.extraClasses), - child.maybe <-- $m.map(_.icon), - span( - cls <-- $m.map(_.textClasses.getOrElse("")), - child.text <-- $m.map(_.text) - ) - ) - - object FullBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - ol( - role := "list", - cls := "flex items-center space-x-4", - children <-- $m.map(_.path).split(_.id)((_, _, $p) => - li( - div( - cls := "flex items-center", - child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), - Link( - $p.map(p => - if (p.isRoot) then Link.fullHome(p) - else - Link.ViewModel( - p, - None, - s"ml-4 max-w-xs truncate ${Link.baseClasses}", - p.title - ) - ), - actionBus - ) - ) - ) - ) - ) - - object ShortBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - Link( - $m.map { p => - val target = p.parent.getOrElse(p) - if target.isRoot then Link.shortHome(target) - else - Link.ViewModel( - target, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - s"Zpět na ${target.title}" - ) - }, - actionBus - ) - - def apply(actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - nav( - cls := "flex", - aria.label := "Breadcrumb", - div( - cls := "flex sm:hidden", - ShortBreadcrumbs(router.$currentPage, actionBus) - ), - div( - cls := "hidden sm:block", - FullBreadcrumbs(router.$currentPage, actionBus) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala deleted file mode 100644 index 0db2047..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ /dev/null @@ -1,242 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent -import fiftyforms.ui.components.tailwind.Icons -import fiftyforms.ui.components.tailwind.Avatar -import cz.e_bs.cmi.mdr.pdb.UserInfo -import io.laminext.syntax.core.* - -object NavigationBar: - - case class Logo(img: String, name: String) - case class Link(a: () => Anchor, active: Boolean) - case class MenuItem(title: String) - - case class ViewModel( - userInfo: UserInfo, - pages: List[Link], - userMenu: List[MenuItem], - logo: Logo - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - val $userInfo = $m.map(_.userInfo) - val mobileMenuOpen = Var(false) - - val desktopOnly = cls("hidden md:block") - val mobileOnly = cls("md:hidden") - - inline def avatarImage(size: Int = 8) = - Avatar($userInfo.map(_.img)).avatarImage(size) - - def notificationButton = button( - tpe := "button", - cls := List( - "bg-indigo-600", - "focus:outline-none", - "focus:ring-2", - "focus:ring-offset-2", - "focus:ring-offset-indigo-600", - "focus:ring-white", - "hover:text-white", - "p-1", - "rounded-full", - "text-indigo-200" - ), - span(cls := "sr-only", "View notifications"), - Icons.outline.bell() - ) - - def userProfile: HtmlElement = - val menuOpen = Var(false) - - def menuItem(item: MenuItem, idx: Int): HtmlElement = - a( - href := "#", - cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", - role := "menuitem", - tabIndex := -1, - idAttr := s"user-menu-item-$idx", - item.title - ) - - div( - cls := "ml-3 relative", - div( - button( - tpe := "button", - cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - idAttr := "user-menu-button", - aria.expanded <-- menuOpen.signal, - aria.hasPopup := true, - span(cls := "sr-only", "Open user menu"), - child <-- avatarImage(), - onClick.preventDefault.mapTo( - !menuOpen.now() - ) --> menuOpen.writer - ) - ), - /* - * */ - div( - cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", - cls <-- menuOpen.signal.map { o => - if (o) "md:block" else "md:hidden" - }, - role := "menu", - aria.orientation := "vertical", - aria.labelledBy := "user-menu-button", - tabIndex := -1, - // : keyboard navigation - children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) - ) - ) - - def mobileProfile = - def menuItem(item: MenuItem): HtmlElement = - a( - href := "#", - cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", - item.title - ) - - div( - cls := "pt-4 pb-3 border-t border-indigo-700", - div( - cls := "flex items-center px-5", - div( - cls := "flex-shrink-0", - child <-- avatarImage(10) - ), - div( - cls := "ml-3", - div( - cls := "text-base font-medium text-white", - child.text <-- $userInfo.map(_.name) - ), - child.maybe <-- $userInfo.map( - _.email.map(e => - div( - cls := "text-sm font-medium text-indigo-300", - e - ) - ) - ) - ), - notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) - ), - div( - cls := "mt-3 px-2 space-y-1", - children <-- $m.map(_.userMenu.map(menuItem)) - ) - ) - - def pageLink(page: Link): Anchor = - page - .a() - .amend( - cls := "text-white px-3 py-2 rounded-md text-sm font-medium", - cls := Seq( - "bg-indigo-700" -> page.active, - "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active - ), - ariaCurrent := (if page.active then "page" else "false") - ) - - def logoImg: Image = - img( - cls := "h-8 w-8", - src <-- $m.map(_.logo.img), - alt <-- $m.map(_.logo.name) - ) - - def mobileMenuButton = button( - tpe := "button", - cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - aria.controls := "mobile-menu", - aria.expanded <-- mobileMenuOpen.signal, - span(cls := "sr-only", "Open main menu"), - Icons.outline - .menu() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") - ), - Icons.outline - .x() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") - ), - onClick.preventDefault.mapTo( - !mobileMenuOpen.now() - ) --> mobileMenuOpen.writer - ) - - def navBarLeft = - div( - cls := "flex items-center", - div(cls := "flex-shrink-0", logoImg), - div( - desktopOnly, - div( - cls := "ml-10 flex items-baseline space-x-4", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ) - ) - ) - - def navBarRight = - div( - desktopOnly, - div( - cls := "ml-4 flex items-center md:ml-6", - notificationButton, - userProfile - ) - ) - - def navBarMobile = - div( - cls := "-mr-2 flex", - mobileOnly, - mobileMenuButton - ) - - def navBar = - div( - cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex items-center justify-between h-16", - navBarLeft, - navBarRight, - navBarMobile - ) - ) - - def mobileMenu = - div( - mobileOnly, - cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }, - idAttr := "mobile-menu", - div( - cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ), - mobileProfile - ) - - nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala deleted file mode 100644 index 75339df..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object PageHeader: - def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = - header( - cls := "bg-white shadow-sm", - div( - cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", - h1( - cls := "text-lg leading-6 font-semibold text-gray-900", - Breadcrumbs(actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala deleted file mode 100644 index 5096463..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ /dev/null @@ -1,30 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import fiftyforms.ui.components.tailwind.Loading -import io.laminext.syntax.core.* - -object PageLayout: - case class ViewModel( - navigation: NavigationBar.ViewModel, - content: Option[HtmlElement] - ) - def apply(actionBus: Observer[Action])( - $m: Signal[ViewModel], - mods: Modifier[HtmlElement]* - )(using router: Router[Page]): HtmlElement = - val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) - div( - cls := "h-full flex flex-col", - NavigationBar($m.map(_.navigation)), - child.maybe <-- router.$currentPage.map(_.isRoot) - .switch(None, Some(PageHeader(actionBus))), - main( - cls := "flex-grow-1 overflow-y-auto", - mods, - child <-- $maybeContent.map(_.getOrElse(Loading)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala deleted file mode 100644 index 62d820f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.LinkSupport.* -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo - -object PageLink: - type ViewModel = Page - def apply($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = - a(mods($m, actions), child.text <-- $m.map(_.title)) - - def apply(m: ViewModel, actions: Observer[Action])(using - router: Router[Page] - ): Anchor = apply(Val(m), actions) - - def container($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = a(mods($m, actions)) - - def container(m: ViewModel, actions: Observer[Action])(using - Router[Page] - ): Anchor = container(Val(m), actions) - - private def mods($m: Signal[Page], actions: Observer[Action])(using - router: Router[Page] - ): Modifier[Anchor] = - Seq( - href <-- $m.map(router.absoluteUrlForPage).recover { case _ => - Some("invalid url") - }, - composeEvents(onClick.noKeyMod.preventDefault)( - _.sample($m) - .map(NavigateTo.apply) - ) --> actions - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala deleted file mode 100644 index 87f825a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.pages.dashboard.DashboardPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -class DashboardPageConnector(actionBus: Observer[Action])(using - router: Router[Page] -): - def apply: HtmlElement = - AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala deleted file mode 100644 index e9b0f71..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala +++ /dev/null @@ -1,76 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailKriteriaPage -import pages.detail.components.DukazyKriteria -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -object DetailKriteriaPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailKriteriaPageConnector( - state: DetailKriteriaPageConnector.AppState -)( - $page: Signal[Page.DetailKriteria] -)(using Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => - (p.osobniCislo.value, p.parametr.value, p.kriterium.value) - )((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map( - FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) - ) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, s, $s) => - DetailKriteriaPage($s)(state.actionBus.contramap { - case DukazyKriteria.Add => - NavigateTo( - Page.UpravDukazKriteria( - Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), - Page.Titled(s.parametr.id, Some(s.parametr.nazev)), - Page.Titled(s.kriterium.id, Some(s.kriterium.id)) - ) - ) - }) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): DetailKriteriaPage.ViewModel = - DetailKriteriaPage.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala deleted file mode 100644 index 4d8024f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ /dev/null @@ -1,57 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import pages.detail.DetailPage -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import fiftyforms.ui.components.tailwind.Color - -object DetailPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailPageConnector(state: DetailPageConnector.AppState)( - $page: Signal[Page.Detail] -)(using router: Router[Page]): - val $oscChangeSignal = $page.splitOne(_.osobniCislo.value)((osc, _, _) => osc) - val $pageChangeSignal = - $oscChangeSignal.flatMap(osc => - EventStream.fromSeq(Seq(FetchUserDetails(osc), FetchParameters(osc))) - ) - // TODO: filter the value based on the current osc - // OSC change will fetch new data, but still - // - we need to be sure that what we got is really what we ought to display - // - we want to display stale data accordingly (at least with loading indicator) - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - def apply: HtmlElement = - AppPage(state.actionBus)( - $data.combineWithFn($params)(_ zip _) - .map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: List[Parameter] - ): DetailPage.ViewModel = - DetailPage.ViewModel( - o.toDetailOsoby, - p.map( - _.toParametr(param => - PageLink.container(Page.DetailParametru(o, param), state.actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala deleted file mode 100644 index fdaa7f8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailParametruPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink - -object DetailParametruPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailParametruPageConnector( - state: DetailParametruPageConnector.AppState -)( - $page: Signal[Page.DetailParametru] -)(using router: Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => (p.osobniCislo.value, p.parametr.value))((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map(FetchParameter(_, _)) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - } yield (da, pb) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailParametruPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter - ): DetailParametruPage.ViewModel = - DetailParametruPage.ViewModel( - o.toDetailOsoby, - p.toParametr(p => - PageLink.container(Page.DetailParametru(o, p), state.actionBus) - ), - p.criteria.map( - _.toKriterium { c => - PageLink.container( - Page.DetailKriteria(o, p, c), - state.actionBus - ) - } - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala deleted file mode 100644 index 049aa22..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -case class DirectoryPageConnector( - $input: EventStream[List[UserInfo]], - actionBus: Observer[Action] -)(using router: Router[Page]): - val $data = $input.startWithNone - val $actionSignal = EventStream.fromValue(FetchDirectory) - - def apply: HtmlElement = - AppPage(actionBus)( - $data.split(_ => ())((_, _, s) => - pages.directory.DirectoryPage( - s.map( - _.map( - _.toUserRow(u => - PageLink.container( - Page.Detail(Page.Titled(u.personalNumber)), - actionBus - ) - ) - ) - ) - ) - ), - $actionSignal --> actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala deleted file mode 100644 index 88e7450..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ /dev/null @@ -1,68 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii -import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria -import fiftyforms.ui.components.tailwind.Color - -extension (o: UserInfo) - def toDetailOsoby: DetailOsoby.ViewModel = - DetailOsoby.ViewModel( - o.personalNumber, - o.name, - o.email, - o.phone, - o.img, - o.mainFunction.map(f => - DetailOsoby.Funkce.ViewModel(f.name, f.dept, f.ou) - ), - o.userContracts.headOption.map(c => - DetailOsoby.PracovniPomer.ViewModel(c.rel, c.startDate, c.endDate) - ) - ) - -extension (param: Parameter) - def toParametr(container: Parameter => Anchor): DetailParametru.ViewModel = - DetailParametru.ViewModel( - id = param.id, - nazev = param.name, - popis = param.description, - status = "Nesplněno", - statusColor = Color.red, - a = container(param) - ) - -extension (crit: ParameterCriteria) - def toKriterium( - container: ParameterCriteria => Anchor - ): DetailKriteria.ViewModel = - DetailKriteria.ViewModel( - nazev = crit.criteriumText, - kapitola = crit.chapterId, - bod = crit.itemId, - status = "Nesplněno", - statusColor = Color.red, - splneno = false, - dukazy = Nil, - container = container(crit) - ) - -extension (user: UserInfo) - def toUserRow( - container: UserInfo => HtmlElement = (_: UserInfo) => div() - ): UserRow.ViewModel = - UserRow.ViewModel( - osobniCislo = user.personalNumber.toString, - celeJmeno = user.name, - prijmeni = user.surname, - hlavniFunkce = user.mainFunction.map(_.name), - img = user.img, - container = () => container(user) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala deleted file mode 100644 index 7d5b2c1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.dashboard - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.Page - -object DashboardPage: - - def render: HtmlElement = - div("Dashboard page") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala deleted file mode 100644 index 9ef0698..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala +++ /dev/null @@ -1,31 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailKriteriaPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply($m: Signal[ViewModel])( - action: Observer[DukazyKriteria.Action] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - DukazyKriteria($m.map(_.kriterium.dukazy))(action) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala deleted file mode 100644 index 3b316e1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object DetailPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametry: SeznamParametru.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby($m.map(_.osoba)), - SeznamParametru($m.map(_.parametry)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala deleted file mode 100644 index 17224cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala +++ /dev/null @@ -1,24 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailParametruPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriteria: SeznamKriterii.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby.header($m.map(_.osoba)), - DetailParametru($m.map(_.parametr)), - SeznamKriterii($m.map(_.kriteria)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala deleted file mode 100644 index dc912fa..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ /dev/null @@ -1,107 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb -package app -package pages.detail - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.UpravDukazForm -import fiftyforms.services.files.File - -object UpravDukaz: - - type ThisPage = Page.UpravDukazKriteria - type PageKey = (OsobniCislo, String, String) - - trait State { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def availableFiles: EventStream[List[File]] - def actionBus: Observer[Action] - } - - def keyOfPage(page: ThisPage): PageKey = - (page.osobniCislo.value, page.parametr.value, page.kriterium.value) - - def onChangeAction(key: PageKey): Action = - FetchParameterCriteria( - key._1, - key._2, - key._3, - Page.UpravDukazKriteria(_, _, _) - ) - - class Connector(state: State)($page: Signal[ThisPage])(using Router[Page]): - val $paramChangeSignal = $page.splitOne(keyOfPage)((x, _, _) => x) - val $pageChangeSignal = $paramChangeSignal.map(onChangeAction) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.split(_ => ())((_, s, $s) => - PageComponent( - $s.map(buildModel), - state.availableFiles, - state.actionBus.contramap { - case UpravDukazForm.Cancelled => - NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) - case UpravDukazForm.AvailableFilesRequested => - FetchAvailableFiles(s._1.personalNumber) - } - ) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): PageComponent.ViewModel = - import connectors.* - PageComponent.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) - - object PageComponent: - import components.* - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply( - $m: Signal[ViewModel], - availableFilesStream: EventStream[List[File]], - events: Observer[UpravDukazForm.Event] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - UpravDukazForm(availableFilesStream)(events) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala deleted file mode 100644 index ca311b0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.Icons -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Color - -object DetailKriteria: - case class ViewModel( - nazev: String, - kapitola: String, - bod: String, - status: String, - statusColor: Color, - splneno: Boolean, - dukazy: List[DukazKriteria.ViewModel], - container: HtmlElement = div() - ) { - val id = s"${kapitola}${bod}" - } - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h3( - cls := "text-xl leading-6 font-bold text-gray-900", - child.text <-- $m.map(_.nazev) - ), - h3( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - "Kapitola ", - child.text <-- $m.map(_.kapitola), - " bod ", - child <-- $m.map(_.bod) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala deleted file mode 100644 index ae90290..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala +++ /dev/null @@ -1,100 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Avatar - -object DetailOsoby: - - object Funkce: - case class ViewModel( - nazev: String, - stredisko: String, - voj: String - ) - - def render($m: Signal[ViewModel]): HtmlElement = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.nazev), - span( - cls := "hidden md:inline", - " @ ", - child.text <-- $m.map(_.stredisko), - ", ", - child.text <-- $m.map(_.voj) - ) - ) - - object PracovniPomer: - case class ViewModel( - druh: String, - pocatek: LocalDate, - konec: Option[LocalDate] - ) - - def render($m: Signal[ViewModel]): HtmlElement = - import fiftyforms.ui.components.tailwind.CustomAttrs.datetime - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.druh), - " od ", - time( - datetime <-- $m.map(_.pocatek.toString), - child.text <-- $m.map(_.pocatek.toString) - ) - ) - - case class ViewModel( - osobniCislo: OsobniCislo, - jmeno: String, - email: Option[String], - telefon: Option[String], - img: Option[String], - hlavniFunkce: Option[Funkce.ViewModel], - pracovniPomer: Option[PracovniPomer.ViewModel] - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-4", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(16) - ), - div( - h1( - cls := "text-2xl font-bold text-gray-900", - child.text <-- $m.map(_.jmeno) - ), - child.maybe <-- $m.map(_.hlavniFunkce).split(_ => ())((_, _, d) => - Funkce.render(d) - ), - child.maybe <-- $m.map(_.pracovniPomer).split(_ => ())((_, _, d) => - PracovniPomer.render(d) - ) - ) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-5", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(8) - ), - div( - h1( - cls := "text-lg font-medium text-gray-800", - child.text <-- $m.map(_.jmeno) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala deleted file mode 100644 index e11cb62..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color - -object DetailParametru: - case class ViewModel( - id: String, - nazev: String, - popis: String, - status: String, - statusColor: Color, - a: Anchor - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h2( - cls := "text-2xl leading-6 font-medium text-gray-900", - child.text <-- $m.map(_.nazev) - ), - p( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - child.text <-- $m.map(_.popis) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - h2( - cls := "text-lg leading-6 font-medium text-gray-600", - child.text <-- $m.map(_.nazev) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala deleted file mode 100644 index a00ede8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala +++ /dev/null @@ -1,177 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.CustomAttrs - -object DukazKriteria: - case class Osoba(osobniCislo: String, jmeno: String) - case class Udalost(osoba: Osoba, datum: LocalDate) - case class Dokument( - url: String, - nazev: String, - pridal: Udalost, - odebral: Option[Udalost] - ) - case class ViewModel( - dokumenty: List[Dokument], - autorizoval: Option[Udalost], - platiDo: Option[LocalDate], - poznámka: Option[String] - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-lg", - div( - cls := "px-4 py-5 sm:px-6", - h3( - cls := "text-lg leading-6 font-medium text-gray-900", - """Applicant Information""" - ), - p( - cls := "mt-1 max-w-2xl text-sm text-gray-500", - """Personal details and application.""" - ) - ), - div( - cls := "border-t border-gray-200 px-4 py-5 sm:px-6", - dl( - cls := "grid grid-cols-1 gap-x-4 gap-y-8 sm:grid-cols-2", - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Full name""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Margot Foster""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Application for""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Backend Developer""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Email address""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """margotfoster@example.com""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Salary expectation""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """$120,000""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """About""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Fugiat ipsum ipsum deserunt culpa aute sint do nostrud anim incididunt cillum culpa consequat. Excepteur qui ipsum aliquip consequat sint. Sit id mollit nulla mollit nostrud in ea officia proident. Irure nostrud pariatur mollit ad adipisicing reprehenderit deserunt qui eu.""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """Attachments""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - ul( - role := "list", - cls := "border border-gray-200 rounded-md divide-y divide-gray-200", - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """resume_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ), - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """coverletter_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala deleted file mode 100644 index 22f134f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala +++ /dev/null @@ -1,35 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object DukazyKriteria: - sealed trait Action - case object Add extends Action - - type ViewModel = List[DukazKriteria.ViewModel] - - def apply($m: Signal[ViewModel])(actions: Observer[Action]): HtmlElement = - div( - child.maybe <-- $m.map(m => - if (m.isEmpty) then Some(prazdnyDukaz(actions)) - else None - ), - children <-- $m.map(_.zipWithIndex).split(_._2)((_, _, $s) => - DukazKriteria($s.map(_._1)) - ) - ) - - private def prazdnyDukaz(actions: Observer[Action]): HtmlElement = - button( - tpe := "button", - onClick.preventDefault.mapTo(Add) --> actions, - cls := "relative block w-full border-2 border-gray-300 border-dashed rounded-lg p-12 text-center hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - Icons.outline - .`document-add`(12) - .amend(svg.cls := "mx-auto text-gray-400"), - span( - cls := "mt-2 block text-sm font-medium text-gray-900", - "Přidat důkaz" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala deleted file mode 100644 index f628697..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.list.StackedList -import fiftyforms.ui.components.tailwind.list.ListRow -import fiftyforms.ui.components.tailwind.list.RowTag -import fiftyforms.ui.components.tailwind.list.RowNext -import java.time.LocalDate - -object SeznamKriterii: - type ViewModel = List[DetailKriteria.ViewModel] - - private val kritList = new StackedList[DetailKriteria.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - kritList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = - p(cls := "text-sm text-gray-500", s"${i.kapitola}${i.bod}"), - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.container - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala deleted file mode 100644 index 2bfe4bc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.list.{ - StackedList, - ListRow, - RowTag, - PropList, - IconText, - RowNext -} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.LinkSupport.* - -object SeznamParametru: - type ViewModel = List[DetailParametru.ViewModel] - - private val parametrList = new StackedList[DetailParametru.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - parametrList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = emptyNode, - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.a - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala deleted file mode 100644 index f048e9d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ /dev/null @@ -1,101 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.form.* -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement -import fiftyforms.services.files.components.tailwind.FilePicker -import cz.e_bs.cmi.mdr.pdb.frontend.AutorizujDukaz -import cz.e_bs.cmi.mdr.pdb.frontend.DocumentRef -import fiftyforms.services.files.File - -object UpravDukazForm: - sealed trait Event - case object Cancelled extends Event - case object AvailableFilesRequested extends Event - def apply(availableFilesStream: EventStream[List[File]])( - updates: Observer[Event] - ): HtmlElement = - val (filesStream, filesObserver) = - EventStream.withObserver[FilePicker.Event] - val files = Var[List[File]](Nil) - def submitButtons: HtmlElement = - div( - cls := "pt-5", - div( - cls := "flex justify-end", - button( - tpe := "button", - cls := "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zrušit", - onClick.mapTo(Cancelled) --> updates - ), - button( - tpe := "submit", - cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - disabled <-- files.signal.map(_.isEmpty), - "Autorizovat" - ) - ) - ) - - div( - filesStream.collect { case FilePicker.SelectionUpdated(files) => - files.to(List) - } --> files.writer, - filesStream.collect { case FilePicker.AvailableFilesRequested => - AvailableFilesRequested - } --> updates, - cls := "bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6", - Form( - Form.Body( - Form.Section( - FormHeader( - FormHeader.ViewModel( - "Doložení kritéria", - "Sestavte doklady poskytující důkaz kritéria a potvrďte odesláním formuláře. Případné limitace či jiné relevantní údaje vepište do pole Komentář." - ) - ), - FormFields( - FormRow( - "dokumenty", - "Dokumenty", - FilePicker(files.signal, availableFilesStream)(filesObserver) - .amend(idAttr := "dokumenty", cls("max-w-lg")) - ).toHtml, - FormRow( - "platnost", - "Platnost", - input( - idAttr := "platnost", - name := "platnost", - tpe := "date", - autoComplete := "date", - cls := "block max-w-lg w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" - ) - ).toHtml, - FormRow( - "komentar", - "Komentář", - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - textArea( - idAttr := "komentar", - name := "about", - rows := 3, - cls := "max-w-lg shadow-sm block w-full focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border border-gray-300 rounded-md" - ), - p( - cls := "mt-2 text-sm text-gray-500", - "Doplňte prosím potřebné informace související s doložením kritéria, včetně případných limitací." - ) - ) - ).toHtml - ) - ) - ), - submitButtons - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala deleted file mode 100644 index 41033f6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -import components._ - -object DirectoryPage: - - type ViewModel = List[UserRow.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - val (actionsStream, actionObserver) = - EventStream.withObserver[SearchForm.Action] - val $filter = actionsStream - .collect { - case SearchForm.ClearFilter => None - case SearchForm.SetFilter(t) => Some(t) - } - .startWith(None) - val byLetter = for { - d <- $m - f <- $filter - } yield for { - (letter, users) <- d - .filter { user => - f match - case None => true - case Some(t) => user.search.contains(t) - } - .groupBy(_.prijmeni.head) - .to(List) - .sortBy(_._1) - } yield (letter.toString, users.sortBy(_.prijmeni)) - - div( - cls := "h-full max-w-7xl mx-auto order-first flex flex-col flex-shrink-0", - SearchForm(actionObserver), - Directory(byLetter) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala deleted file mode 100644 index 73f915d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -object Directory: - - object Header: - type ViewModel = String - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500", - h3(child.text <-- $m) - ) - - object UserList: - type ViewModel = List[UserRow.ViewModel] - def apply($m: Signal[ViewModel]): HtmlElement = - ul( - role := "list", - cls := "relative z-0 divide-y divide-gray-200", - children <-- $m.split(_.osobniCislo)((_, _, s) => UserRow(s)) - ) - - type ViewModel = List[(String, List[UserRow.ViewModel])] - def apply($m: Signal[ViewModel]): HtmlElement = - nav( - cls := "flex-1 min-h-0 overflow-y-auto", - aria.label := "Directory", - children <-- $m.split(_._1)((_, _, s) => - div( - cls := "relative", - Header(s.map(_._1)), - UserList(s.map(_._2)) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala deleted file mode 100644 index e182910..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala +++ /dev/null @@ -1,54 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object SearchForm: - sealed trait Action - case object SubmitFilter extends Action - sealed trait FilterAction extends Action - case object ClearFilter extends FilterAction - case class SetFilter(value: String) extends FilterAction - - def apply(actions: Observer[Action]): HtmlElement = - div( - cls := "px-6 pt-4 pb-4", - form( - cls := "flex space-x-4", - onSubmit.mapTo(SubmitFilter) --> actions, - div( - cls := "flex-1 min-w-0", - label( - forId := "search", - cls := "sr-only", - "Hledat" - ), - div( - cls := "relative rounded-md shadow-sm", - div( - cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", - Icons.solid.search().amend(svg.cls := "text-gray-400") - ), - input( - tpe := "search", - name := "search", - idAttr := "search", - cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", - placeholder := "Hledat", - composeEvents(onInput.mapToValue.setAsValue.map(SetFilter(_)))( - _.throttle(500) - ) --> actions - ) - ) - ), - button( - tpe := "submit", - cls := "inline-flex justify-center px-3.5 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500", - Icons.solid.filter().amend(svg.cls := "text-gray-400"), - span( - cls := "sr-only", - "Hledat" - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala deleted file mode 100644 index 82f7723..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala +++ /dev/null @@ -1,52 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.airstream.core.Signal -import fiftyforms.ui.components.tailwind.Avatar - -object UserRow: - case class ViewModel( - osobniCislo: String, - celeJmeno: String, - prijmeni: String, - hlavniFunkce: Option[String], - img: Option[String], - container: () => HtmlElement = () => div() - ) { - val search = prijmeni.toLowerCase - } - - def apply($m: Signal[ViewModel]): HtmlElement = - inline def avatarImage = - Avatar($m.map(_.img)).avatarImage(10) - - li( - div( - cls := "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500", - div( - cls := "flex-shrink-0", - child <-- avatarImage - ), - div( - cls := "flex-1 min-w-0", - child <-- $m.map { o => - o.container() - .amend( - cls := "focus:outline-none", - span( - cls := "absolute inset-0", - aria.hidden := true - ), - p( - cls := "text-sm font-medium text-gray-900", - o.celeJmeno - ), - p( - cls := "text-sm text-gray-500 truncate", - o.hlavniFunkce - ) - ) - } - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala deleted file mode 100644 index 467c5f5..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala +++ /dev/null @@ -1,99 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router - -object ErrorPage: - case class ViewModel( - homePage: Page, - errorName: String, - title: String, - subTitle: String - ) - - def apply(m: ViewModel, actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - val ViewModel(homePage, errorName, title, subTitle) = m - div( - cls := "min-h-full pt-16 pb-12 flex flex-col bg-white", - main( - cls := "flex-grow flex flex-col justify-center max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex-shrink-0 flex justify-center", - a( - href := "/", - cls := "inline-flex", - span( - cls := "sr-only", - """Workflow""" - ), - img( - cls := "h-12 w-auto", - src := "https://tailwindui.com/img/logos/workflow-mark.svg?color=indigo&shade=600", - alt := "" - ) - ) - ), - div( - cls := "py-16", - div( - cls := "text-center", - p( - cls := "text-sm font-semibold text-indigo-600 uppercase tracking-wide", - errorName - ), - h1( - cls := "mt-2 text-4xl font-extrabold text-gray-900 tracking-tight sm:text-5xl", - title - ), - p( - cls := "mt-2 text-base text-gray-500", - subTitle - ), - div( - cls := "mt-6", - PageLink - .container(homePage, actionBus) - .amend( - cls := "text-base font-medium text-indigo-600 hover:text-indigo-500", - """Go back home""" - ) - ) - ) - ) - ), - footer( - cls := "flex-shrink-0 max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", - nav( - cls := "flex justify-center space-x-4", - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Contact Support""" - ), - span( - cls := "inline-block border-l border-gray-300", - aria.hidden := true - ), - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Status""" - ), - span( - cls := "inline-block border-l border-gray-300", - aria.hidden := true - ), - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Twitter""" - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala deleted file mode 100644 index bd9baec..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action - -object NotFoundPage: - def apply(homePage: Page, url: String, actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - ErrorPage( - ErrorPage.ViewModel( - homePage, - "404 error", - "Page not found.", - s"Sorry, but page $url doesn't exist." - ), - actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala deleted file mode 100644 index 476fe3c..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action - -object UnhandledErrorPage: - - case class ViewModel( - homePage: Page, - errorName: Option[String], - errorMessage: Option[String] - ) - - def apply(m: ViewModel, actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - ErrorPage( - ErrorPage.ViewModel( - m.homePage, - "Unexpected error occurred", - m.errorName.getOrElse( - "Uh oh!" - ), // TODO: translations, better text than uh oh - m.errorMessage.getOrElse( - "This wasn't supposed to happen! Please try again." - ) - ), - actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala deleted file mode 100644 index 25f059f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala +++ /dev/null @@ -1,6 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.services - -import com.raquo.airstream.core.Observer - -trait DataFetcher[K, A]: - def fetch(id: K, sink: Observer[A]): Unit diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala deleted file mode 100644 index a469830..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala +++ /dev/null @@ -1,120 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package state - -import com.raquo.airstream.core.EventStream -import cz.e_bs.cmi.mdr.pdb.{UserInfo, OsobniCislo} -import com.raquo.airstream.core.Observer -import scala.scalajs.js -import scala.scalajs.js.JSON -import zio.json._ -import com.raquo.airstream.eventbus.EventBus -import com.raquo.airstream.ownership.Owner -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.UserFunction -import cz.e_bs.cmi.mdr.pdb.UserContract -import fiftyforms.services.files.File - -trait AppState - extends connectors.DetailPageConnector.AppState - with connectors.DetailParametruPageConnector.AppState - with connectors.DetailKriteriaPageConnector.AppState - with pages.detail.UpravDukaz.State: - def users: EventStream[List[UserInfo]] - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - -class MockAppState(implicit owner: Owner, router: Router[Page]) - extends AppState: - - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonDecoder[UserFunction] = DeriveJsonDecoder.gen - given JsonDecoder[UserContract] = DeriveJsonDecoder.gen - given JsonDecoder[UserInfo] = DeriveJsonDecoder.gen - - given JsonDecoder[ParameterCriteria] = DeriveJsonDecoder.gen - given JsonDecoder[Parameter] = DeriveJsonDecoder.gen - - private val actions = EventBus[Action]() - private val (parametersStream, pushParameters) = - EventStream.withCallback[List[Parameter]] - private val (usersStream, pushUsers) = - EventStream.withCallback[List[UserInfo]] - private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] - private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] - - private val mockData: List[UserInfo] = - mockUsers - .asInstanceOf[js.Dictionary[js.Object]] - .values - // TODO: is there a more efficient way to parse from JS object directly? - .map { o => - val parsed = JSON.stringify(o).fromJson[UserInfo] - parsed.left.foreach(org.scalajs.dom.console.log(_)) - parsed - } - .collect { case Right(u) => - u - } - .toList - - private val mockParameters: List[Parameter] = - pdbParams - .asInstanceOf[js.Dictionary[js.Object]] - .values - .map(o => JSON.stringify(o).fromJson[Parameter]) - .collect { case Right(p) => p } - .toList - - // TODO: Extract to separate event handler - actions.events.foreach { - case FetchDirectory => pushUsers(mockData) - case FetchUserDetails(osc) => - mockData.find(_.personalNumber == osc).foreach { o => - pushDetails(o) - router.replaceState(Page.Detail(o)) - } - case FetchParameters(osc) => - pushParameters(mockParameters) - case FetchParameter(osc, paramId) => - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(Page.DetailParametru(o, p)) - case FetchParameterCriteria(osc, paramId, critId, page) => - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - c <- p.criteria.find(_.id == critId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(page(o, p, c)) - case NavigateTo(page) => router.pushState(page) - case FetchAvailableFiles(osc) => - pushFiles( - List( - File("https://tc163.cmi.cz/here", "Example file") - ) - ) - } - - override def users: EventStream[List[UserInfo]] = - usersStream.debugWithName("users") - - override def details: EventStream[UserInfo] = - detailsStream.debugWithName("details") - - override def parameters: EventStream[List[Parameter]] = - parametersStream.debugWithName("parameters") - - override def availableFiles: EventStream[List[File]] = - filesStream.debugWithName("available files") - - override def actionBus: Observer[Action] = - actions.writer.debugWithName("actions writer") diff --git a/app/src/main/scala/mdr/pdb/app/Main.scala b/app/src/main/scala/mdr/pdb/app/Main.scala new file mode 100644 index 0000000..081b4ee --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/Main.scala @@ -0,0 +1,103 @@ +package mdr.pdb.app + +import scala.scalajs.js.annotation.JSExportTopLevel +import scala.scalajs.js.annotation.JSExport +import scala.scalajs.js.annotation.JSImport +import scala.scalajs.js +import org.scalajs.dom +import com.raquo.laminar.api.L.{*, given} +import scala.scalajs.js.Date +import com.raquo.waypoint.Router +import com.raquo.waypoint.SplitRender +import mdr.pdb.app.services.DataFetcher +import scala.scalajs.js.JSON +import zio.json._ +import mdr.pdb.UserInfo +import mdr.pdb.app.state.AppState + +@js.native +@JSImport("stylesheets/main.css", JSImport.Namespace) +object Css extends js.Any + +@js.native +@JSImport("data/users.json", JSImport.Default) +object mockUsers extends js.Object + +@js.native +@JSImport("params/pdb-params.json", JSImport.Default) +object pdbParams extends js.Object + +@JSExportTopLevel("app") +object Main: + + @JSExport + def main(args: Array[String]): Unit = + import Routes.given + onLoad { + setupAirstream() + val appContainer = dom.document.querySelector("#app") + val _ = + render( + appContainer, + renderPage(state.MockAppState(using unsafeWindowOwner, router)) + ) + } + + private def onLoad(f: => Unit): Unit = + documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) + + private def setupAirstream()(using router: Router[Page]): Unit = + AirstreamError.registerUnhandledErrorCallback(err => + router.forcePage( + Page.UnhandledError( + Some(err.getClass.getName), // TODO: Fill only in dev mode + Some(err.getMessage) + ) + ) + ) + + def renderPage(state: AppState)(using + router: Router[Page] + ): HtmlElement = + val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) + .collectSignal[Page.Detail]( + connectors + .DetailPageConnector(state)(_) + .apply + ) + .collectSignal[Page.DetailParametru]( + connectors + .DetailParametruPageConnector(state)(_) + .apply + ) + .collectSignal[Page.DetailKriteria]( + connectors + .DetailKriteriaPageConnector(state)(_) + .apply + ) + .collectSignal[Page.UpravDukazKriteria]( + pages.detail.UpravDukaz.Connector(state)(_).apply + ) + .collectStatic(Page.Dashboard)( + connectors.DashboardPageConnector(state.actionBus).apply + ) + .collect[Page.NotFound](pg => + pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) + ) + .collect[Page.UnhandledError](pg => + pages.errors + .UnhandledErrorPage( + pages.errors.UnhandledErrorPage + .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), + state.actionBus + ) + ) + .collectStatic(Page.Directory)( + connectors + .DirectoryPageConnector(state.users, state.actionBus) + .apply + ) + div(cls := "h-full", child <-- pageSplitter.$view) + + // Pull in the stylesheet + val css: Css.type = Css diff --git a/app/src/main/scala/mdr/pdb/app/Routes.scala b/app/src/main/scala/mdr/pdb/app/Routes.scala new file mode 100644 index 0000000..2cc860c --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/Routes.scala @@ -0,0 +1,198 @@ +package mdr.pdb.app + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.* +import org.scalajs.dom +import zio.json.{*, given} +import mdr.pdb.OsobniCislo + +import scala.scalajs.js +import mdr.pdb.UserInfo +import mdr.pdb.Parameter +import mdr.pdb.ParameterCriteria +import mdr.pdb.app.Page.Titled + +// enum is not working with Waypoints' SplitRender collectStatic +sealed abstract class Page( + val id: String, + val title: String, + val parent: Option[Page] +) { + val path: Vector[Page] = + parent match + case None => Vector(this) + case Some(p) => p.path :+ this + + val isRoot: Boolean = parent.isEmpty +} + +object Page: + + case class Titled[V](value: V, title: Option[String] = None): + val show: String = title.getOrElse(value.toString) + + case object Directory extends Page("directory", "Adresář", None) + + case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) + + case class Detail(osobniCislo: Titled[OsobniCislo]) + extends Page("user", osobniCislo.show, Some(Directory)) + + object Detail { + def apply(o: UserInfo): Detail = Detail( + Titled(o.personalNumber, Some(o.name)) + ) + } + + case class DetailParametru( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String] + ) extends Page( + "parameter", + parametr.show, + Some(Detail(osobniCislo)) + ) + + object DetailParametru { + def apply(o: UserInfo, p: Parameter): DetailParametru = + DetailParametru( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)) + ) + } + + case class DetailKriteria( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String], + kriterium: Titled[String] + ) extends Page( + "criteria", + kriterium.show, + Some(DetailParametru(osobniCislo, parametr)) + ) + + object DetailKriteria { + def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = + DetailKriteria( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)), + Titled(k.id, Some(k.id)) + ) + } + + case class UpravDukazKriteria( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String], + kriterium: Titled[String] + ) extends Page( + "addProof", + "Důkaz", + Some(DetailKriteria(osobniCislo, parametr, kriterium)) + ) + + object UpravDukazKriteria { + def apply( + o: UserInfo, + p: Parameter, + k: ParameterCriteria + ): UpravDukazKriteria = + UpravDukazKriteria( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)), + Titled(k.id, Some(k.id)) + ) + } + + case class NotFound(url: String) extends Page("404", "404", Some(Directory)) + + case class UnhandledError( + errorName: Option[String], + errorMessage: Option[String] + ) extends Page("500", "Unexpected error", Some(Directory)) + +object Routes: + given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) + given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) + given [V: JsonEncoder]: JsonEncoder[Titled[V]] = + DeriveJsonEncoder.gen[Titled[V]] + given [V: JsonDecoder]: JsonDecoder[Titled[V]] = + DeriveJsonDecoder.gen[Titled[V]] + given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] + given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] + + val base = + js.`import`.meta.env.BASE_URL + .asInstanceOf[String] + "app" + + val homePage: Page = Page.Directory + + given router: Router[Page] = Router[Page]( + routes = List( + Route.static(homePage, root / endOfSegments, basePath = base), + Route.static( + Page.Dashboard, + root / "dashboard" / endOfSegments, + basePath = base + ), + Route[Page.Detail, String]( + encode = _.osobniCislo.value.toString, + decode = osc => Page.Detail(Titled(OsobniCislo(osc))), + root / "osoba" / segment[String] / endOfSegments, + basePath = base + ), + Route[Page.DetailParametru, (String, String)]( + encode = p => (p.osobniCislo.value.toString, p.parametr.value), + decode = + p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / endOfSegments, + basePath = base + ), + Route[Page.DetailKriteria, (String, String, String)]( + encode = p => + ( + p.osobniCislo.value.toString, + p.parametr.value, + p.kriterium.value.replaceAll("\\.", "--") + ), + decode = p => + Page.DetailKriteria( + Titled(OsobniCislo(p._1)), + Titled(p._2), + Titled(p._3.replaceAll("--", ".")) + ), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / "kriterium" / segment[String] / endOfSegments, + basePath = base + ), + Route[Page.UpravDukazKriteria, (String, String, String)]( + encode = p => + ( + p.osobniCislo.value.toString, + p.parametr.value, + p.kriterium.value.replaceAll("\\.", "--") + ), + decode = p => + Page.UpravDukazKriteria( + Titled(OsobniCislo(p._1)), + Titled(p._2), + Titled(p._3.replaceAll("--", ".")) + ), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / "kriterium" / segment[String] / "edit" / endOfSegments, + basePath = base + ) + ), + serializePage = _.toJson, + deserializePage = _.fromJson[Page] + .fold(s => throw IllegalStateException(s), identity), + getPageTitle = _.title, + routeFallback = url => Page.NotFound(url), + deserializeFallback = _ => Page.Dashboard + )( + $popStateEvent = windowEvents.onPopState, + owner = unsafeWindowOwner + ) diff --git a/app/src/main/scala/mdr/pdb/app/actions.scala b/app/src/main/scala/mdr/pdb/app/actions.scala new file mode 100644 index 0000000..d078b3a --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/actions.scala @@ -0,0 +1,21 @@ +package mdr.pdb.app + +import mdr.pdb.OsobniCislo +import mdr.pdb.UserInfo +import mdr.pdb.Parameter +import mdr.pdb.ParameterCriteria + +sealed trait Action + +case object FetchDirectory extends Action +case class FetchUserDetails(osc: OsobniCislo) extends Action +case class FetchParameters(osc: OsobniCislo) extends Action +case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action +case class FetchParameterCriteria( + osc: OsobniCislo, + paramId: String, + critId: String, + page: (UserInfo, Parameter, ParameterCriteria) => Page +) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action +case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/mdr/pdb/app/components/AppPage.scala new file mode 100644 index 0000000..130c817 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/AppPage.scala @@ -0,0 +1,82 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.Page +import mdr.pdb.{UserProfile, UserInfo, OsobniCislo} +import com.raquo.waypoint.Router +import mdr.pdb.app.Action +import mdr.pdb.app.NavigateTo +import mdr.pdb.UserFunction + +object AppPage: + // TODO: pages by logged in user + val pages: List[Page] = List(Page.Directory, Page.Dashboard) + + import NavigationBar.{Logo, MenuItem} + + val logo = Logo( + "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", + "Workflow" + ) + + // TODO: menu items by user profile + val userMenu = + List( + MenuItem("Your Profile"), + MenuItem("Settings"), + MenuItem("Sign out") + ) + + // TODO: load user profile + val $userProfile = Var( + UserProfile( + "tom", + UserInfo( + OsobniCislo("1031"), + "tom", + "Tom", + "Cook", + None, + None, + Some("tom@example.com"), + Some("+420 222 866 180"), + Some( + UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") + ), + Nil, + None + ) + ) + ) + + val $userInfo = $userProfile.signal.map(_.userInfo) + + type ViewModel = Option[HtmlElement] + def apply( + actionBus: Observer[Action] + )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using + router: Router[Page] + ): HtmlElement = + PageLayout(actionBus)( + $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => + PageLayout.ViewModel( + NavigationBar.ViewModel( + u, + pages.map(p => + NavigationBar.Link( + () => + PageLink( + p, + actionBus + ), + p == cp + ) + ), + userMenu, + logo + ), + c + ) + ), + mods + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala new file mode 100644 index 0000000..9375398 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala @@ -0,0 +1,143 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.tailwind.list.IconText.ViewModel +import fiftyforms.ui.components.tailwind.Icons +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.Page + +import com.raquo.laminar.api.L.{*, given} +import io.laminext.syntax.core.{*, given} +import mdr.pdb.app.Action + +object Breadcrumbs: + + private def slash = + import svg.{*, given} + svg( + cls := "flex-shrink-0 h-5 w-5 text-gray-300", + xmlns := "http://www.w3.org/2000/svg", + fill := "currentColor", + viewBox := "0 0 20 20", + ariaHidden := true, + path( + d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" + ) + ) + + def backIcon = + Icons.solid + .`arrow-narrow-left`() + .amend( + svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" + ) + + object Link: + case class ViewModel( + page: Page, + icon: Option[SvgElement], + extraClasses: String, + text: String, + textClasses: Option[String] = None + ) + + val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" + + def shortHome(p: Page) = ViewModel( + p, + Some(backIcon), + "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", + "Zpět na úvodní stránku", + None + ) + + def fullHome(p: Page) = ViewModel( + p, + Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), + "text-gray-400 hover:text-gray-500", + "Domů", + Some("sr-only") + ) + + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + PageLink + .container($m.map(_.page), actionBus) + .amend( + cls <-- $m.map(_.extraClasses), + child.maybe <-- $m.map(_.icon), + span( + cls <-- $m.map(_.textClasses.getOrElse("")), + child.text <-- $m.map(_.text) + ) + ) + + object FullBreadcrumbs: + type ViewModel = Page + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + ol( + role := "list", + cls := "flex items-center space-x-4", + children <-- $m.map(_.path).split(_.id)((_, _, $p) => + li( + div( + cls := "flex items-center", + child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), + Link( + $p.map(p => + if (p.isRoot) then Link.fullHome(p) + else + Link.ViewModel( + p, + None, + s"ml-4 max-w-xs truncate ${Link.baseClasses}", + p.title + ) + ), + actionBus + ) + ) + ) + ) + ) + + object ShortBreadcrumbs: + type ViewModel = Page + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + Link( + $m.map { p => + val target = p.parent.getOrElse(p) + if target.isRoot then Link.shortHome(target) + else + Link.ViewModel( + target, + Some(backIcon), + "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", + s"Zpět na ${target.title}" + ) + }, + actionBus + ) + + def apply(actionBus: Observer[Action])(using + router: Router[Page] + ): HtmlElement = + nav( + cls := "flex", + aria.label := "Breadcrumb", + div( + cls := "flex sm:hidden", + ShortBreadcrumbs(router.$currentPage, actionBus) + ), + div( + cls := "hidden sm:block", + FullBreadcrumbs(router.$currentPage, actionBus) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala new file mode 100644 index 0000000..4bb8b8d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala @@ -0,0 +1,242 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent +import fiftyforms.ui.components.tailwind.Icons +import fiftyforms.ui.components.tailwind.Avatar +import mdr.pdb.UserInfo +import io.laminext.syntax.core.* + +object NavigationBar: + + case class Logo(img: String, name: String) + case class Link(a: () => Anchor, active: Boolean) + case class MenuItem(title: String) + + case class ViewModel( + userInfo: UserInfo, + pages: List[Link], + userMenu: List[MenuItem], + logo: Logo + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + val $userInfo = $m.map(_.userInfo) + val mobileMenuOpen = Var(false) + + val desktopOnly = cls("hidden md:block") + val mobileOnly = cls("md:hidden") + + inline def avatarImage(size: Int = 8) = + Avatar($userInfo.map(_.img)).avatarImage(size) + + def notificationButton = button( + tpe := "button", + cls := List( + "bg-indigo-600", + "focus:outline-none", + "focus:ring-2", + "focus:ring-offset-2", + "focus:ring-offset-indigo-600", + "focus:ring-white", + "hover:text-white", + "p-1", + "rounded-full", + "text-indigo-200" + ), + span(cls := "sr-only", "View notifications"), + Icons.outline.bell() + ) + + def userProfile: HtmlElement = + val menuOpen = Var(false) + + def menuItem(item: MenuItem, idx: Int): HtmlElement = + a( + href := "#", + cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", + role := "menuitem", + tabIndex := -1, + idAttr := s"user-menu-item-$idx", + item.title + ) + + div( + cls := "ml-3 relative", + div( + button( + tpe := "button", + cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", + idAttr := "user-menu-button", + aria.expanded <-- menuOpen.signal, + aria.hasPopup := true, + span(cls := "sr-only", "Open user menu"), + child <-- avatarImage(), + onClick.preventDefault.mapTo( + !menuOpen.now() + ) --> menuOpen.writer + ) + ), + /* + * */ + div( + cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", + cls <-- menuOpen.signal.map { o => + if (o) "md:block" else "md:hidden" + }, + role := "menu", + aria.orientation := "vertical", + aria.labelledBy := "user-menu-button", + tabIndex := -1, + // : keyboard navigation + children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) + ) + ) + + def mobileProfile = + def menuItem(item: MenuItem): HtmlElement = + a( + href := "#", + cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", + item.title + ) + + div( + cls := "pt-4 pb-3 border-t border-indigo-700", + div( + cls := "flex items-center px-5", + div( + cls := "flex-shrink-0", + child <-- avatarImage(10) + ), + div( + cls := "ml-3", + div( + cls := "text-base font-medium text-white", + child.text <-- $userInfo.map(_.name) + ), + child.maybe <-- $userInfo.map( + _.email.map(e => + div( + cls := "text-sm font-medium text-indigo-300", + e + ) + ) + ) + ), + notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) + ), + div( + cls := "mt-3 px-2 space-y-1", + children <-- $m.map(_.userMenu.map(menuItem)) + ) + ) + + def pageLink(page: Link): Anchor = + page + .a() + .amend( + cls := "text-white px-3 py-2 rounded-md text-sm font-medium", + cls := Seq( + "bg-indigo-700" -> page.active, + "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active + ), + ariaCurrent := (if page.active then "page" else "false") + ) + + def logoImg: Image = + img( + cls := "h-8 w-8", + src <-- $m.map(_.logo.img), + alt <-- $m.map(_.logo.name) + ) + + def mobileMenuButton = button( + tpe := "button", + cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", + aria.controls := "mobile-menu", + aria.expanded <-- mobileMenuOpen.signal, + span(cls := "sr-only", "Open main menu"), + Icons.outline + .menu() + .amend( + svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") + ), + Icons.outline + .x() + .amend( + svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") + ), + onClick.preventDefault.mapTo( + !mobileMenuOpen.now() + ) --> mobileMenuOpen.writer + ) + + def navBarLeft = + div( + cls := "flex items-center", + div(cls := "flex-shrink-0", logoImg), + div( + desktopOnly, + div( + cls := "ml-10 flex items-baseline space-x-4", + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) + ) + ) + ) + + def navBarRight = + div( + desktopOnly, + div( + cls := "ml-4 flex items-center md:ml-6", + notificationButton, + userProfile + ) + ) + + def navBarMobile = + div( + cls := "-mr-2 flex", + mobileOnly, + mobileMenuButton + ) + + def navBar = + div( + cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", + div( + cls := "flex items-center justify-between h-16", + navBarLeft, + navBarRight, + navBarMobile + ) + ) + + def mobileMenu = + div( + mobileOnly, + cls <-- mobileMenuOpen.signal.map { o => + if (o) "block" else "hidden" + }, + idAttr := "mobile-menu", + div( + cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) + ), + mobileProfile + ) + + nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala new file mode 100644 index 0000000..c4cb10d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala @@ -0,0 +1,19 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.Page +import com.raquo.waypoint.Router +import mdr.pdb.app.Action + +object PageHeader: + def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = + header( + cls := "bg-white shadow-sm", + div( + cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", + h1( + cls := "text-lg leading-6 font-semibold text-gray-900", + Breadcrumbs(actionBus) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala new file mode 100644 index 0000000..aae4539 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala @@ -0,0 +1,30 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.Page +import mdr.pdb.app.Action +import fiftyforms.ui.components.tailwind.Loading +import io.laminext.syntax.core.* + +object PageLayout: + case class ViewModel( + navigation: NavigationBar.ViewModel, + content: Option[HtmlElement] + ) + def apply(actionBus: Observer[Action])( + $m: Signal[ViewModel], + mods: Modifier[HtmlElement]* + )(using router: Router[Page]): HtmlElement = + val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) + div( + cls := "h-full flex flex-col", + NavigationBar($m.map(_.navigation)), + child.maybe <-- router.$currentPage.map(_.isRoot) + .switch(None, Some(PageHeader(actionBus))), + main( + cls := "flex-grow-1 overflow-y-auto", + mods, + child <-- $maybeContent.map(_.getOrElse(Loading)) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/mdr/pdb/app/components/PageLink.scala new file mode 100644 index 0000000..e6e7052 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageLink.scala @@ -0,0 +1,40 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.LinkSupport.* +import mdr.pdb.app.Page +import mdr.pdb.app.Action +import com.raquo.waypoint.Router +import mdr.pdb.app.NavigateTo + +object PageLink: + type ViewModel = Page + def apply($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = + a(mods($m, actions), child.text <-- $m.map(_.title)) + + def apply(m: ViewModel, actions: Observer[Action])(using + router: Router[Page] + ): Anchor = apply(Val(m), actions) + + def container($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = a(mods($m, actions)) + + def container(m: ViewModel, actions: Observer[Action])(using + Router[Page] + ): Anchor = container(Val(m), actions) + + private def mods($m: Signal[Page], actions: Observer[Action])(using + router: Router[Page] + ): Modifier[Anchor] = + Seq( + href <-- $m.map(router.absoluteUrlForPage).recover { case _ => + Some("invalid url") + }, + composeEvents(onClick.noKeyMod.preventDefault)( + _.sample($m) + .map(NavigateTo.apply) + ) --> actions + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala new file mode 100644 index 0000000..7682772 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala @@ -0,0 +1,13 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.pages.dashboard.DashboardPage +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage + +class DashboardPageConnector(actionBus: Observer[Action])(using + router: Router[Page] +): + def apply: HtmlElement = + AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala new file mode 100644 index 0000000..76b9133 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala @@ -0,0 +1,76 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.Parameter +import mdr.pdb.UserInfo +import mdr.pdb.app.pages.detail.DetailParametruPage +import pages.detail.DetailKriteriaPage +import pages.detail.components.DukazyKriteria +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.ParameterCriteria + +object DetailKriteriaPageConnector { + trait AppState { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + } +} + +case class DetailKriteriaPageConnector( + state: DetailKriteriaPageConnector.AppState +)( + $page: Signal[Page.DetailKriteria] +)(using Router[Page]): + val $paramChangeSignal = + $page.splitOne(p => + (p.osobniCislo.value, p.parametr.value, p.kriterium.value) + )((x, _, _) => x) + val $pageChangeSignal = + $paramChangeSignal.map( + FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) + ) + + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + val $merged = + $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => + for { + da <- d + pa <- p + pb <- pa.find(_.id == pc._2) + ka <- pb.criteria.find(_.id == pc._3) + } yield (da, pb, ka) + ) + + def apply: HtmlElement = + AppPage(state.actionBus)( + $merged.map(_.map(buildModel)) + .split(_ => ())((_, s, $s) => + DetailKriteriaPage($s)(state.actionBus.contramap { + case DukazyKriteria.Add => + NavigateTo( + Page.UpravDukazKriteria( + Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), + Page.Titled(s.parametr.id, Some(s.parametr.nazev)), + Page.Titled(s.kriterium.id, Some(s.kriterium.id)) + ) + ) + }) + ), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: Parameter, + k: ParameterCriteria + ): DetailKriteriaPage.ViewModel = + DetailKriteriaPage.ViewModel( + o.toDetailOsoby, + p.toParametr(_ => a()), + k.toKriterium(_ => a()) + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DetailPageConnector.scala new file mode 100644 index 0000000..df3a6b7 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DetailPageConnector.scala @@ -0,0 +1,57 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.UserInfo +import pages.detail.DetailPage +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.pages.detail.components.DetailOsoby +import mdr.pdb.Parameter +import mdr.pdb.app.pages.detail.components.SeznamParametru +import fiftyforms.ui.components.tailwind.Color + +object DetailPageConnector { + trait AppState { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + } +} + +case class DetailPageConnector(state: DetailPageConnector.AppState)( + $page: Signal[Page.Detail] +)(using router: Router[Page]): + val $oscChangeSignal = $page.splitOne(_.osobniCislo.value)((osc, _, _) => osc) + val $pageChangeSignal = + $oscChangeSignal.flatMap(osc => + EventStream.fromSeq(Seq(FetchUserDetails(osc), FetchParameters(osc))) + ) + // TODO: filter the value based on the current osc + // OSC change will fetch new data, but still + // - we need to be sure that what we got is really what we ought to display + // - we want to display stale data accordingly (at least with loading indicator) + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + def apply: HtmlElement = + AppPage(state.actionBus)( + $data.combineWithFn($params)(_ zip _) + .map(_.map(buildModel)) + .split(_ => ())((_, _, s) => DetailPage(s)), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: List[Parameter] + ): DetailPage.ViewModel = + DetailPage.ViewModel( + o.toDetailOsoby, + p.map( + _.toParametr(param => + PageLink.container(Page.DetailParametru(o, param), state.actionBus) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DetailParametruPageConnector.scala new file mode 100644 index 0000000..fa7193d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DetailParametruPageConnector.scala @@ -0,0 +1,67 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.Parameter +import mdr.pdb.UserInfo +import mdr.pdb.app.pages.detail.DetailParametruPage +import pages.detail.DetailParametruPage +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.components.PageLink + +object DetailParametruPageConnector { + trait AppState { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + } +} + +case class DetailParametruPageConnector( + state: DetailParametruPageConnector.AppState +)( + $page: Signal[Page.DetailParametru] +)(using router: Router[Page]): + val $paramChangeSignal = + $page.splitOne(p => (p.osobniCislo.value, p.parametr.value))((x, _, _) => x) + val $pageChangeSignal = + $paramChangeSignal.map(FetchParameter(_, _)) + + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + val $merged = + $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => + for { + da <- d + pa <- p + pb <- pa.find(_.id == pc._2) + } yield (da, pb) + ) + + def apply: HtmlElement = + AppPage(state.actionBus)( + $merged.map(_.map(buildModel)) + .split(_ => ())((_, _, s) => DetailParametruPage(s)), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: Parameter + ): DetailParametruPage.ViewModel = + DetailParametruPage.ViewModel( + o.toDetailOsoby, + p.toParametr(p => + PageLink.container(Page.DetailParametru(o, p), state.actionBus) + ), + p.criteria.map( + _.toKriterium { c => + PageLink.container( + Page.DetailKriteria(o, p, c), + state.actionBus + ) + } + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DirectoryPageConnector.scala new file mode 100644 index 0000000..ab2c045 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DirectoryPageConnector.scala @@ -0,0 +1,34 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.UserInfo +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.components.AppPage + +case class DirectoryPageConnector( + $input: EventStream[List[UserInfo]], + actionBus: Observer[Action] +)(using router: Router[Page]): + val $data = $input.startWithNone + val $actionSignal = EventStream.fromValue(FetchDirectory) + + def apply: HtmlElement = + AppPage(actionBus)( + $data.split(_ => ())((_, _, s) => + pages.directory.DirectoryPage( + s.map( + _.map( + _.toUserRow(u => + PageLink.container( + Page.Detail(Page.Titled(u.personalNumber)), + actionBus + ) + ) + ) + ) + ) + ), + $actionSignal --> actionBus + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/mdr/pdb/app/connectors/conversions.scala new file mode 100644 index 0000000..f581796 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/conversions.scala @@ -0,0 +1,68 @@ +package mdr.pdb.app.connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.UserInfo +import mdr.pdb.app.pages.detail.components.DetailOsoby +import mdr.pdb.Parameter +import mdr.pdb.app.pages.detail.components.SeznamParametru +import mdr.pdb.ParameterCriteria +import mdr.pdb.app.pages.detail.components.SeznamKriterii +import mdr.pdb.app.pages.directory.components.UserRow +import mdr.pdb.app.pages.detail.components.DetailParametru +import mdr.pdb.app.pages.detail.components.DetailKriteria +import fiftyforms.ui.components.tailwind.Color + +extension (o: UserInfo) + def toDetailOsoby: DetailOsoby.ViewModel = + DetailOsoby.ViewModel( + o.personalNumber, + o.name, + o.email, + o.phone, + o.img, + o.mainFunction.map(f => + DetailOsoby.Funkce.ViewModel(f.name, f.dept, f.ou) + ), + o.userContracts.headOption.map(c => + DetailOsoby.PracovniPomer.ViewModel(c.rel, c.startDate, c.endDate) + ) + ) + +extension (param: Parameter) + def toParametr(container: Parameter => Anchor): DetailParametru.ViewModel = + DetailParametru.ViewModel( + id = param.id, + nazev = param.name, + popis = param.description, + status = "Nesplněno", + statusColor = Color.red, + a = container(param) + ) + +extension (crit: ParameterCriteria) + def toKriterium( + container: ParameterCriteria => Anchor + ): DetailKriteria.ViewModel = + DetailKriteria.ViewModel( + nazev = crit.criteriumText, + kapitola = crit.chapterId, + bod = crit.itemId, + status = "Nesplněno", + statusColor = Color.red, + splneno = false, + dukazy = Nil, + container = container(crit) + ) + +extension (user: UserInfo) + def toUserRow( + container: UserInfo => HtmlElement = (_: UserInfo) => div() + ): UserRow.ViewModel = + UserRow.ViewModel( + osobniCislo = user.personalNumber.toString, + celeJmeno = user.name, + prijmeni = user.surname, + hlavniFunkce = user.mainFunction.map(_.name), + img = user.img, + container = () => container(user) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/dashboard/DashboardPage.scala b/app/src/main/scala/mdr/pdb/app/pages/dashboard/DashboardPage.scala new file mode 100644 index 0000000..0eb8f7a --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/dashboard/DashboardPage.scala @@ -0,0 +1,11 @@ +package mdr.pdb.app.pages.dashboard + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.Page + +object DashboardPage: + + def render: HtmlElement = + div("Dashboard page") diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala new file mode 100644 index 0000000..ff3a25f --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala @@ -0,0 +1,31 @@ +package mdr.pdb.app.pages.detail + +import com.raquo.laminar.api.L.{*, given} + +import components._ + +object DetailKriteriaPage: + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametr: DetailParametru.ViewModel, + kriterium: DetailKriteria.ViewModel + ) + + def apply($m: Signal[ViewModel])( + action: Observer[DukazyKriteria.Action] + ): HtmlElement = + div( + cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + div( + DetailOsoby.header($m.map(_.osoba)), + DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") + ), + div( + DetailKriteria($m.map(_.kriterium)), + DukazyKriteria($m.map(_.kriterium.dukazy))(action) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/DetailPage.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailPage.scala new file mode 100644 index 0000000..b591cc9 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailPage.scala @@ -0,0 +1,25 @@ +package mdr.pdb.app.pages.detail + +import com.raquo.laminar.api.L.{*, given} + +import components._ +import mdr.pdb.app.Page +import com.raquo.waypoint.Router +import mdr.pdb.app.Action + +object DetailPage: + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametry: SeznamParametru.ViewModel + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + DetailOsoby($m.map(_.osoba)), + SeznamParametru($m.map(_.parametry)) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/DetailParametruPage.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailParametruPage.scala new file mode 100644 index 0000000..3322279 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailParametruPage.scala @@ -0,0 +1,24 @@ +package mdr.pdb.app.pages.detail + +import com.raquo.laminar.api.L.{*, given} + +import components._ + +object DetailParametruPage: + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametr: DetailParametru.ViewModel, + kriteria: SeznamKriterii.ViewModel + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + DetailOsoby.header($m.map(_.osoba)), + DetailParametru($m.map(_.parametr)), + SeznamKriterii($m.map(_.kriteria)) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala new file mode 100644 index 0000000..b58618a --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala @@ -0,0 +1,107 @@ +package mdr.pdb +package app +package pages.detail + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.pages.detail.components.UpravDukazForm +import fiftyforms.services.files.File + +object UpravDukaz: + + type ThisPage = Page.UpravDukazKriteria + type PageKey = (OsobniCislo, String, String) + + trait State { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def availableFiles: EventStream[List[File]] + def actionBus: Observer[Action] + } + + def keyOfPage(page: ThisPage): PageKey = + (page.osobniCislo.value, page.parametr.value, page.kriterium.value) + + def onChangeAction(key: PageKey): Action = + FetchParameterCriteria( + key._1, + key._2, + key._3, + Page.UpravDukazKriteria(_, _, _) + ) + + class Connector(state: State)($page: Signal[ThisPage])(using Router[Page]): + val $paramChangeSignal = $page.splitOne(keyOfPage)((x, _, _) => x) + val $pageChangeSignal = $paramChangeSignal.map(onChangeAction) + + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + val $merged = + $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => + for { + da <- d + pa <- p + pb <- pa.find(_.id == pc._2) + ka <- pb.criteria.find(_.id == pc._3) + } yield (da, pb, ka) + ) + + def apply: HtmlElement = + AppPage(state.actionBus)( + $merged.split(_ => ())((_, s, $s) => + PageComponent( + $s.map(buildModel), + state.availableFiles, + state.actionBus.contramap { + case UpravDukazForm.Cancelled => + NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) + case UpravDukazForm.AvailableFilesRequested => + FetchAvailableFiles(s._1.personalNumber) + } + ) + ), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: Parameter, + k: ParameterCriteria + ): PageComponent.ViewModel = + import connectors.* + PageComponent.ViewModel( + o.toDetailOsoby, + p.toParametr(_ => a()), + k.toKriterium(_ => a()) + ) + + object PageComponent: + import components.* + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametr: DetailParametru.ViewModel, + kriterium: DetailKriteria.ViewModel + ) + + def apply( + $m: Signal[ViewModel], + availableFilesStream: EventStream[List[File]], + events: Observer[UpravDukazForm.Event] + ): HtmlElement = + div( + cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + div( + DetailOsoby.header($m.map(_.osoba)), + DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") + ), + div( + DetailKriteria($m.map(_.kriterium)), + UpravDukazForm(availableFilesStream)(events) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailKriteria.scala new file mode 100644 index 0000000..a3ab634 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailKriteria.scala @@ -0,0 +1,37 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.CustomAttrs +import fiftyforms.ui.components.tailwind.Icons +import java.time.LocalDate +import fiftyforms.ui.components.tailwind.Color + +object DetailKriteria: + case class ViewModel( + nazev: String, + kapitola: String, + bod: String, + status: String, + statusColor: Color, + splneno: Boolean, + dukazy: List[DukazKriteria.ViewModel], + container: HtmlElement = div() + ) { + val id = s"${kapitola}${bod}" + } + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "pb-5 border-b border-gray-200", + h3( + cls := "text-xl leading-6 font-bold text-gray-900", + child.text <-- $m.map(_.nazev) + ), + h3( + cls := "mt-2 max-w-4xl text-sm text-gray-500", + "Kapitola ", + child.text <-- $m.map(_.kapitola), + " bod ", + child <-- $m.map(_.bod) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailOsoby.scala new file mode 100644 index 0000000..0fe2ba5 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailOsoby.scala @@ -0,0 +1,100 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.OsobniCislo + +import java.time.LocalDate +import fiftyforms.ui.components.tailwind.Avatar + +object DetailOsoby: + + object Funkce: + case class ViewModel( + nazev: String, + stredisko: String, + voj: String + ) + + def render($m: Signal[ViewModel]): HtmlElement = + p( + cls := "text-sm font-medium text-gray-500", + child.text <-- $m.map(_.nazev), + span( + cls := "hidden md:inline", + " @ ", + child.text <-- $m.map(_.stredisko), + ", ", + child.text <-- $m.map(_.voj) + ) + ) + + object PracovniPomer: + case class ViewModel( + druh: String, + pocatek: LocalDate, + konec: Option[LocalDate] + ) + + def render($m: Signal[ViewModel]): HtmlElement = + import fiftyforms.ui.components.tailwind.CustomAttrs.datetime + p( + cls := "text-sm font-medium text-gray-500", + child.text <-- $m.map(_.druh), + " od ", + time( + datetime <-- $m.map(_.pocatek.toString), + child.text <-- $m.map(_.pocatek.toString) + ) + ) + + case class ViewModel( + osobniCislo: OsobniCislo, + jmeno: String, + email: Option[String], + telefon: Option[String], + img: Option[String], + hlavniFunkce: Option[Funkce.ViewModel], + pracovniPomer: Option[PracovniPomer.ViewModel] + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "md:flex md:items-center md:justify-between md:space-x-4", + div( + cls := "flex items-start space-x-4", + div( + cls := "flex-shrink-0", + Avatar($m.map(_.img)).avatar(16) + ), + div( + h1( + cls := "text-2xl font-bold text-gray-900", + child.text <-- $m.map(_.jmeno) + ), + child.maybe <-- $m.map(_.hlavniFunkce).split(_ => ())((_, _, d) => + Funkce.render(d) + ), + child.maybe <-- $m.map(_.pracovniPomer).split(_ => ())((_, _, d) => + PracovniPomer.render(d) + ) + ) + ) + ) + + def header($m: Signal[ViewModel]): HtmlElement = + div( + cls := "md:flex md:items-center md:justify-between md:space-x-5", + div( + cls := "flex items-start space-x-4", + div( + cls := "flex-shrink-0", + Avatar($m.map(_.img)).avatar(8) + ), + div( + h1( + cls := "text-lg font-medium text-gray-800", + child.text <-- $m.map(_.jmeno) + ) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailParametru.scala new file mode 100644 index 0000000..6310dde --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailParametru.scala @@ -0,0 +1,34 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.Color + +object DetailParametru: + case class ViewModel( + id: String, + nazev: String, + popis: String, + status: String, + statusColor: Color, + a: Anchor + ) + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "pb-5 border-b border-gray-200", + h2( + cls := "text-2xl leading-6 font-medium text-gray-900", + child.text <-- $m.map(_.nazev) + ), + p( + cls := "mt-2 max-w-4xl text-sm text-gray-500", + child.text <-- $m.map(_.popis) + ) + ) + + def header($m: Signal[ViewModel]): HtmlElement = + div( + h2( + cls := "text-lg leading-6 font-medium text-gray-600", + child.text <-- $m.map(_.nazev) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazKriteria.scala new file mode 100644 index 0000000..c292919 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazKriteria.scala @@ -0,0 +1,177 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import java.time.LocalDate +import fiftyforms.ui.components.tailwind.CustomAttrs + +object DukazKriteria: + case class Osoba(osobniCislo: String, jmeno: String) + case class Udalost(osoba: Osoba, datum: LocalDate) + case class Dokument( + url: String, + nazev: String, + pridal: Udalost, + odebral: Option[Udalost] + ) + case class ViewModel( + dokumenty: List[Dokument], + autorizoval: Option[Udalost], + platiDo: Option[LocalDate], + poznámka: Option[String] + ) + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "bg-white shadow overflow-hidden sm:rounded-lg", + div( + cls := "px-4 py-5 sm:px-6", + h3( + cls := "text-lg leading-6 font-medium text-gray-900", + """Applicant Information""" + ), + p( + cls := "mt-1 max-w-2xl text-sm text-gray-500", + """Personal details and application.""" + ) + ), + div( + cls := "border-t border-gray-200 px-4 py-5 sm:px-6", + dl( + cls := "grid grid-cols-1 gap-x-4 gap-y-8 sm:grid-cols-2", + div( + cls := "sm:col-span-1", + dt( + cls := "text-sm font-medium text-gray-500", + """Full name""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """Margot Foster""" + ) + ), + div( + cls := "sm:col-span-1", + dt( + cls := "text-sm font-medium text-gray-500", + """Application for""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """Backend Developer""" + ) + ), + div( + cls := "sm:col-span-1", + dt( + cls := "text-sm font-medium text-gray-500", + """Email address""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """margotfoster@example.com""" + ) + ), + div( + cls := "sm:col-span-1", + dt( + cls := "text-sm font-medium text-gray-500", + """Salary expectation""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """$120,000""" + ) + ), + div( + cls := "sm:col-span-2", + dt( + cls := "text-sm font-medium text-gray-500", + """About""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """Fugiat ipsum ipsum deserunt culpa aute sint do nostrud anim incididunt cillum culpa consequat. Excepteur qui ipsum aliquip consequat sint. Sit id mollit nulla mollit nostrud in ea officia proident. Irure nostrud pariatur mollit ad adipisicing reprehenderit deserunt qui eu.""" + ) + ), + div( + cls := "sm:col-span-2", + dt( + cls := "text-sm font-medium text-gray-500", + """Attachments""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + ul( + role := "list", + cls := "border border-gray-200 rounded-md divide-y divide-gray-200", + li( + cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", + div( + cls := "w-0 flex-1 flex items-center", { + import svg.* + import CustomAttrs.svg.ariaHidden + svg( + cls := "flex-shrink-0 h-5 w-5 text-gray-400", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + ariaHidden := true, + path( + fillRule := "evenodd", + d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", + clipRule := "evenodd" + ) + ) + }, + span( + cls := "ml-2 flex-1 w-0 truncate", + """resume_back_end_developer.pdf""" + ) + ), + div( + cls := "ml-4 flex-shrink-0", + a( + href := "#", + cls := "font-medium text-indigo-600 hover:text-indigo-500", + """Download""" + ) + ) + ), + li( + cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", + div( + cls := "w-0 flex-1 flex items-center", { + import svg.* + import CustomAttrs.svg.ariaHidden + svg( + cls := "flex-shrink-0 h-5 w-5 text-gray-400", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + ariaHidden := true, + path( + fillRule := "evenodd", + d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", + clipRule := "evenodd" + ) + ) + }, + span( + cls := "ml-2 flex-1 w-0 truncate", + """coverletter_back_end_developer.pdf""" + ) + ), + div( + cls := "ml-4 flex-shrink-0", + a( + href := "#", + cls := "font-medium text-indigo-600 hover:text-indigo-500", + """Download""" + ) + ) + ) + ) + ) + ) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala new file mode 100644 index 0000000..d604860 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala @@ -0,0 +1,35 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.Icons + +object DukazyKriteria: + sealed trait Action + case object Add extends Action + + type ViewModel = List[DukazKriteria.ViewModel] + + def apply($m: Signal[ViewModel])(actions: Observer[Action]): HtmlElement = + div( + child.maybe <-- $m.map(m => + if (m.isEmpty) then Some(prazdnyDukaz(actions)) + else None + ), + children <-- $m.map(_.zipWithIndex).split(_._2)((_, _, $s) => + DukazKriteria($s.map(_._1)) + ) + ) + + private def prazdnyDukaz(actions: Observer[Action]): HtmlElement = + button( + tpe := "button", + onClick.preventDefault.mapTo(Add) --> actions, + cls := "relative block w-full border-2 border-gray-300 border-dashed rounded-lg p-12 text-center hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + Icons.outline + .`document-add`(12) + .amend(svg.cls := "mx-auto text-gray-400"), + span( + cls := "mt-2 block text-sm font-medium text-gray-900", + "Přidat důkaz" + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala new file mode 100644 index 0000000..7e77a7b --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala @@ -0,0 +1,34 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.Color +import fiftyforms.ui.components.tailwind.list.StackedList +import fiftyforms.ui.components.tailwind.list.ListRow +import fiftyforms.ui.components.tailwind.list.RowTag +import fiftyforms.ui.components.tailwind.list.RowNext +import java.time.LocalDate + +object SeznamKriterii: + type ViewModel = List[DetailKriteria.ViewModel] + + private val kritList = new StackedList[DetailKriteria.ViewModel] + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "bg-white shadow overflow-hidden sm:rounded-md", + kritList($m, _.id) { $i => + $i.map { i => + ListRow.ViewModel( + title = i.nazev, + topRight = RowTag.render( + $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) + ), + bottomLeft = + p(cls := "text-sm text-gray-500", s"${i.kapitola}${i.bod}"), + bottomRight = emptyNode, + farRight = RowNext.render, + containerElement = i.container + ) + } + } + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamParametru.scala new file mode 100644 index 0000000..0760b32 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamParametru.scala @@ -0,0 +1,37 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.list.{ + StackedList, + ListRow, + RowTag, + PropList, + IconText, + RowNext +} +import fiftyforms.ui.components.tailwind.Color +import fiftyforms.ui.components.tailwind.LinkSupport.* + +object SeznamParametru: + type ViewModel = List[DetailParametru.ViewModel] + + private val parametrList = new StackedList[DetailParametru.ViewModel] + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "bg-white shadow overflow-hidden sm:rounded-md", + parametrList($m, _.id) { $i => + $i.map { i => + ListRow.ViewModel( + title = i.nazev, + topRight = RowTag.render( + $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) + ), + bottomLeft = emptyNode, + bottomRight = emptyNode, + farRight = RowNext.render, + containerElement = i.a + ) + } + } + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala new file mode 100644 index 0000000..28753c9 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala @@ -0,0 +1,101 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import io.laminext.syntax.core.* +import fiftyforms.ui.components.tailwind.CustomAttrs +import fiftyforms.ui.components.tailwind.form.* +import org.scalajs.dom +import com.raquo.laminar.nodes.ReactiveHtmlElement +import fiftyforms.services.files.components.tailwind.FilePicker +import mdr.pdb.frontend.AutorizujDukaz +import mdr.pdb.frontend.DocumentRef +import fiftyforms.services.files.File + +object UpravDukazForm: + sealed trait Event + case object Cancelled extends Event + case object AvailableFilesRequested extends Event + def apply(availableFilesStream: EventStream[List[File]])( + updates: Observer[Event] + ): HtmlElement = + val (filesStream, filesObserver) = + EventStream.withObserver[FilePicker.Event] + val files = Var[List[File]](Nil) + def submitButtons: HtmlElement = + div( + cls := "pt-5", + div( + cls := "flex justify-end", + button( + tpe := "button", + cls := "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + "Zrušit", + onClick.mapTo(Cancelled) --> updates + ), + button( + tpe := "submit", + cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + disabled <-- files.signal.map(_.isEmpty), + "Autorizovat" + ) + ) + ) + + div( + filesStream.collect { case FilePicker.SelectionUpdated(files) => + files.to(List) + } --> files.writer, + filesStream.collect { case FilePicker.AvailableFilesRequested => + AvailableFilesRequested + } --> updates, + cls := "bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6", + Form( + Form.Body( + Form.Section( + FormHeader( + FormHeader.ViewModel( + "Doložení kritéria", + "Sestavte doklady poskytující důkaz kritéria a potvrďte odesláním formuláře. Případné limitace či jiné relevantní údaje vepište do pole Komentář." + ) + ), + FormFields( + FormRow( + "dokumenty", + "Dokumenty", + FilePicker(files.signal, availableFilesStream)(filesObserver) + .amend(idAttr := "dokumenty", cls("max-w-lg")) + ).toHtml, + FormRow( + "platnost", + "Platnost", + input( + idAttr := "platnost", + name := "platnost", + tpe := "date", + autoComplete := "date", + cls := "block max-w-lg w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" + ) + ).toHtml, + FormRow( + "komentar", + "Komentář", + div( + cls := "mt-1 sm:mt-0 sm:col-span-2", + textArea( + idAttr := "komentar", + name := "about", + rows := 3, + cls := "max-w-lg shadow-sm block w-full focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border border-gray-300 rounded-md" + ), + p( + cls := "mt-2 text-sm text-gray-500", + "Doplňte prosím potřebné informace související s doložením kritéria, včetně případných limitací." + ) + ) + ).toHtml + ) + ) + ), + submitButtons + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/directory/DirectoryPage.scala b/app/src/main/scala/mdr/pdb/app/pages/directory/DirectoryPage.scala new file mode 100644 index 0000000..7a72ca3 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/directory/DirectoryPage.scala @@ -0,0 +1,40 @@ +package mdr.pdb.app.pages.directory + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.UserInfo + +import components._ + +object DirectoryPage: + + type ViewModel = List[UserRow.ViewModel] + + def apply($m: Signal[ViewModel]): HtmlElement = + val (actionsStream, actionObserver) = + EventStream.withObserver[SearchForm.Action] + val $filter = actionsStream + .collect { + case SearchForm.ClearFilter => None + case SearchForm.SetFilter(t) => Some(t) + } + .startWith(None) + val byLetter = for { + d <- $m + f <- $filter + } yield for { + (letter, users) <- d + .filter { user => + f match + case None => true + case Some(t) => user.search.contains(t) + } + .groupBy(_.prijmeni.head) + .to(List) + .sortBy(_._1) + } yield (letter.toString, users.sortBy(_.prijmeni)) + + div( + cls := "h-full max-w-7xl mx-auto order-first flex flex-col flex-shrink-0", + SearchForm(actionObserver), + Directory(byLetter) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/directory/components/Directory.scala b/app/src/main/scala/mdr/pdb/app/pages/directory/components/Directory.scala new file mode 100644 index 0000000..f7d6f99 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/directory/components/Directory.scala @@ -0,0 +1,37 @@ +package mdr.pdb.app.pages.directory.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.UserInfo + +object Directory: + + object Header: + type ViewModel = String + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500", + h3(child.text <-- $m) + ) + + object UserList: + type ViewModel = List[UserRow.ViewModel] + def apply($m: Signal[ViewModel]): HtmlElement = + ul( + role := "list", + cls := "relative z-0 divide-y divide-gray-200", + children <-- $m.split(_.osobniCislo)((_, _, s) => UserRow(s)) + ) + + type ViewModel = List[(String, List[UserRow.ViewModel])] + def apply($m: Signal[ViewModel]): HtmlElement = + nav( + cls := "flex-1 min-h-0 overflow-y-auto", + aria.label := "Directory", + children <-- $m.split(_._1)((_, _, s) => + div( + cls := "relative", + Header(s.map(_._1)), + UserList(s.map(_._2)) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/directory/components/SearchForm.scala b/app/src/main/scala/mdr/pdb/app/pages/directory/components/SearchForm.scala new file mode 100644 index 0000000..fb9ecc4 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/directory/components/SearchForm.scala @@ -0,0 +1,54 @@ +package mdr.pdb.app.pages.directory.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.Icons + +object SearchForm: + sealed trait Action + case object SubmitFilter extends Action + sealed trait FilterAction extends Action + case object ClearFilter extends FilterAction + case class SetFilter(value: String) extends FilterAction + + def apply(actions: Observer[Action]): HtmlElement = + div( + cls := "px-6 pt-4 pb-4", + form( + cls := "flex space-x-4", + onSubmit.mapTo(SubmitFilter) --> actions, + div( + cls := "flex-1 min-w-0", + label( + forId := "search", + cls := "sr-only", + "Hledat" + ), + div( + cls := "relative rounded-md shadow-sm", + div( + cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", + Icons.solid.search().amend(svg.cls := "text-gray-400") + ), + input( + tpe := "search", + name := "search", + idAttr := "search", + cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", + placeholder := "Hledat", + composeEvents(onInput.mapToValue.setAsValue.map(SetFilter(_)))( + _.throttle(500) + ) --> actions + ) + ) + ), + button( + tpe := "submit", + cls := "inline-flex justify-center px-3.5 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500", + Icons.solid.filter().amend(svg.cls := "text-gray-400"), + span( + cls := "sr-only", + "Hledat" + ) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/directory/components/UserRow.scala b/app/src/main/scala/mdr/pdb/app/pages/directory/components/UserRow.scala new file mode 100644 index 0000000..4aa1672 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/directory/components/UserRow.scala @@ -0,0 +1,52 @@ +package mdr.pdb.app.pages.directory.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.airstream.core.Signal +import fiftyforms.ui.components.tailwind.Avatar + +object UserRow: + case class ViewModel( + osobniCislo: String, + celeJmeno: String, + prijmeni: String, + hlavniFunkce: Option[String], + img: Option[String], + container: () => HtmlElement = () => div() + ) { + val search = prijmeni.toLowerCase + } + + def apply($m: Signal[ViewModel]): HtmlElement = + inline def avatarImage = + Avatar($m.map(_.img)).avatarImage(10) + + li( + div( + cls := "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500", + div( + cls := "flex-shrink-0", + child <-- avatarImage + ), + div( + cls := "flex-1 min-w-0", + child <-- $m.map { o => + o.container() + .amend( + cls := "focus:outline-none", + span( + cls := "absolute inset-0", + aria.hidden := true + ), + p( + cls := "text-sm font-medium text-gray-900", + o.celeJmeno + ), + p( + cls := "text-sm text-gray-500 truncate", + o.hlavniFunkce + ) + ) + } + ) + ) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala deleted file mode 100644 index a3b00af..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala +++ /dev/null @@ -1,103 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import scala.scalajs.js.annotation.JSExportTopLevel -import scala.scalajs.js.annotation.JSExport -import scala.scalajs.js.annotation.JSImport -import scala.scalajs.js -import org.scalajs.dom -import com.raquo.laminar.api.L.{*, given} -import scala.scalajs.js.Date -import com.raquo.waypoint.Router -import com.raquo.waypoint.SplitRender -import cz.e_bs.cmi.mdr.pdb.app.services.DataFetcher -import scala.scalajs.js.JSON -import zio.json._ -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.state.AppState - -@js.native -@JSImport("stylesheets/main.css", JSImport.Namespace) -object Css extends js.Any - -@js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native -@JSImport("params/pdb-params.json", JSImport.Default) -object pdbParams extends js.Object - -@JSExportTopLevel("app") -object Main: - - @JSExport - def main(args: Array[String]): Unit = - import Routes.given - onLoad { - setupAirstream() - val appContainer = dom.document.querySelector("#app") - val _ = - render( - appContainer, - renderPage(state.MockAppState(using unsafeWindowOwner, router)) - ) - } - - private def onLoad(f: => Unit): Unit = - documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) - - private def setupAirstream()(using router: Router[Page]): Unit = - AirstreamError.registerUnhandledErrorCallback(err => - router.forcePage( - Page.UnhandledError( - Some(err.getClass.getName), // TODO: Fill only in dev mode - Some(err.getMessage) - ) - ) - ) - - def renderPage(state: AppState)(using - router: Router[Page] - ): HtmlElement = - val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) - .collectSignal[Page.Detail]( - connectors - .DetailPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailParametru]( - connectors - .DetailParametruPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailKriteria]( - connectors - .DetailKriteriaPageConnector(state)(_) - .apply - ) - .collectSignal[Page.UpravDukazKriteria]( - pages.detail.UpravDukaz.Connector(state)(_).apply - ) - .collectStatic(Page.Dashboard)( - connectors.DashboardPageConnector(state.actionBus).apply - ) - .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) - ) - .collect[Page.UnhandledError](pg => - pages.errors - .UnhandledErrorPage( - pages.errors.UnhandledErrorPage - .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), - state.actionBus - ) - ) - .collectStatic(Page.Directory)( - connectors - .DirectoryPageConnector(state.users, state.actionBus) - .apply - ) - div(cls := "h-full", child <-- pageSplitter.$view) - - // Pull in the stylesheet - val css: Css.type = Css diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala deleted file mode 100644 index 3bbc8c0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ /dev/null @@ -1,198 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.* -import org.scalajs.dom -import zio.json.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import scala.scalajs.js -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.Page.Titled - -// enum is not working with Waypoints' SplitRender collectStatic -sealed abstract class Page( - val id: String, - val title: String, - val parent: Option[Page] -) { - val path: Vector[Page] = - parent match - case None => Vector(this) - case Some(p) => p.path :+ this - - val isRoot: Boolean = parent.isEmpty -} - -object Page: - - case class Titled[V](value: V, title: Option[String] = None): - val show: String = title.getOrElse(value.toString) - - case object Directory extends Page("directory", "Adresář", None) - - case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) - - case class Detail(osobniCislo: Titled[OsobniCislo]) - extends Page("user", osobniCislo.show, Some(Directory)) - - object Detail { - def apply(o: UserInfo): Detail = Detail( - Titled(o.personalNumber, Some(o.name)) - ) - } - - case class DetailParametru( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String] - ) extends Page( - "parameter", - parametr.show, - Some(Detail(osobniCislo)) - ) - - object DetailParametru { - def apply(o: UserInfo, p: Parameter): DetailParametru = - DetailParametru( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)) - ) - } - - case class DetailKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "criteria", - kriterium.show, - Some(DetailParametru(osobniCislo, parametr)) - ) - - object DetailKriteria { - def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = - DetailKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class UpravDukazKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "addProof", - "Důkaz", - Some(DetailKriteria(osobniCislo, parametr, kriterium)) - ) - - object UpravDukazKriteria { - def apply( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): UpravDukazKriteria = - UpravDukazKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class NotFound(url: String) extends Page("404", "404", Some(Directory)) - - case class UnhandledError( - errorName: Option[String], - errorMessage: Option[String] - ) extends Page("500", "Unexpected error", Some(Directory)) - -object Routes: - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) - given [V: JsonEncoder]: JsonEncoder[Titled[V]] = - DeriveJsonEncoder.gen[Titled[V]] - given [V: JsonDecoder]: JsonDecoder[Titled[V]] = - DeriveJsonDecoder.gen[Titled[V]] - given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] - given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] - - val base = - js.`import`.meta.env.BASE_URL - .asInstanceOf[String] + "app" - - val homePage: Page = Page.Directory - - given router: Router[Page] = Router[Page]( - routes = List( - Route.static(homePage, root / endOfSegments, basePath = base), - Route.static( - Page.Dashboard, - root / "dashboard" / endOfSegments, - basePath = base - ), - Route[Page.Detail, String]( - encode = _.osobniCislo.value.toString, - decode = osc => Page.Detail(Titled(OsobniCislo(osc))), - root / "osoba" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.DetailParametru, (String, String)]( - encode = p => (p.osobniCislo.value.toString, p.parametr.value), - decode = - p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / endOfSegments, - basePath = base - ), - Route[Page.DetailKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.DetailKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.UpravDukazKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.UpravDukazKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / "edit" / endOfSegments, - basePath = base - ) - ), - serializePage = _.toJson, - deserializePage = _.fromJson[Page] - .fold(s => throw IllegalStateException(s), identity), - getPageTitle = _.title, - routeFallback = url => Page.NotFound(url), - deserializeFallback = _ => Page.Dashboard - )( - $popStateEvent = windowEvents.onPopState, - owner = unsafeWindowOwner - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala deleted file mode 100644 index b24b82b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -sealed trait Action - -case object FetchDirectory extends Action -case class FetchUserDetails(osc: OsobniCislo) extends Action -case class FetchParameters(osc: OsobniCislo) extends Action -case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action -case class FetchParameterCriteria( - osc: OsobniCislo, - paramId: String, - critId: String, - page: (UserInfo, Parameter, ParameterCriteria) => Page -) extends Action -case class FetchAvailableFiles(osc: OsobniCislo) extends Action -case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala deleted file mode 100644 index c637d35..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala +++ /dev/null @@ -1,82 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.{UserProfile, UserInfo, OsobniCislo} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo -import cz.e_bs.cmi.mdr.pdb.UserFunction - -object AppPage: - // TODO: pages by logged in user - val pages: List[Page] = List(Page.Directory, Page.Dashboard) - - import NavigationBar.{Logo, MenuItem} - - val logo = Logo( - "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", - "Workflow" - ) - - // TODO: menu items by user profile - val userMenu = - List( - MenuItem("Your Profile"), - MenuItem("Settings"), - MenuItem("Sign out") - ) - - // TODO: load user profile - val $userProfile = Var( - UserProfile( - "tom", - UserInfo( - OsobniCislo("1031"), - "tom", - "Tom", - "Cook", - None, - None, - Some("tom@example.com"), - Some("+420 222 866 180"), - Some( - UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") - ), - Nil, - None - ) - ) - ) - - val $userInfo = $userProfile.signal.map(_.userInfo) - - type ViewModel = Option[HtmlElement] - def apply( - actionBus: Observer[Action] - )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using - router: Router[Page] - ): HtmlElement = - PageLayout(actionBus)( - $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => - PageLayout.ViewModel( - NavigationBar.ViewModel( - u, - pages.map(p => - NavigationBar.Link( - () => - PageLink( - p, - actionBus - ), - p == cp - ) - ), - userMenu, - logo - ), - c - ) - ), - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala deleted file mode 100644 index bf473c6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ /dev/null @@ -1,143 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden -import fiftyforms.ui.components.tailwind.list.IconText.ViewModel -import fiftyforms.ui.components.tailwind.Icons -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Action - -object Breadcrumbs: - - private def slash = - import svg.{*, given} - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-300", - xmlns := "http://www.w3.org/2000/svg", - fill := "currentColor", - viewBox := "0 0 20 20", - ariaHidden := true, - path( - d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" - ) - ) - - def backIcon = - Icons.solid - .`arrow-narrow-left`() - .amend( - svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" - ) - - object Link: - case class ViewModel( - page: Page, - icon: Option[SvgElement], - extraClasses: String, - text: String, - textClasses: Option[String] = None - ) - - val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" - - def shortHome(p: Page) = ViewModel( - p, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - "Zpět na úvodní stránku", - None - ) - - def fullHome(p: Page) = ViewModel( - p, - Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), - "text-gray-400 hover:text-gray-500", - "Domů", - Some("sr-only") - ) - - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - PageLink - .container($m.map(_.page), actionBus) - .amend( - cls <-- $m.map(_.extraClasses), - child.maybe <-- $m.map(_.icon), - span( - cls <-- $m.map(_.textClasses.getOrElse("")), - child.text <-- $m.map(_.text) - ) - ) - - object FullBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - ol( - role := "list", - cls := "flex items-center space-x-4", - children <-- $m.map(_.path).split(_.id)((_, _, $p) => - li( - div( - cls := "flex items-center", - child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), - Link( - $p.map(p => - if (p.isRoot) then Link.fullHome(p) - else - Link.ViewModel( - p, - None, - s"ml-4 max-w-xs truncate ${Link.baseClasses}", - p.title - ) - ), - actionBus - ) - ) - ) - ) - ) - - object ShortBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - Link( - $m.map { p => - val target = p.parent.getOrElse(p) - if target.isRoot then Link.shortHome(target) - else - Link.ViewModel( - target, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - s"Zpět na ${target.title}" - ) - }, - actionBus - ) - - def apply(actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - nav( - cls := "flex", - aria.label := "Breadcrumb", - div( - cls := "flex sm:hidden", - ShortBreadcrumbs(router.$currentPage, actionBus) - ), - div( - cls := "hidden sm:block", - FullBreadcrumbs(router.$currentPage, actionBus) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala deleted file mode 100644 index 0db2047..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ /dev/null @@ -1,242 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent -import fiftyforms.ui.components.tailwind.Icons -import fiftyforms.ui.components.tailwind.Avatar -import cz.e_bs.cmi.mdr.pdb.UserInfo -import io.laminext.syntax.core.* - -object NavigationBar: - - case class Logo(img: String, name: String) - case class Link(a: () => Anchor, active: Boolean) - case class MenuItem(title: String) - - case class ViewModel( - userInfo: UserInfo, - pages: List[Link], - userMenu: List[MenuItem], - logo: Logo - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - val $userInfo = $m.map(_.userInfo) - val mobileMenuOpen = Var(false) - - val desktopOnly = cls("hidden md:block") - val mobileOnly = cls("md:hidden") - - inline def avatarImage(size: Int = 8) = - Avatar($userInfo.map(_.img)).avatarImage(size) - - def notificationButton = button( - tpe := "button", - cls := List( - "bg-indigo-600", - "focus:outline-none", - "focus:ring-2", - "focus:ring-offset-2", - "focus:ring-offset-indigo-600", - "focus:ring-white", - "hover:text-white", - "p-1", - "rounded-full", - "text-indigo-200" - ), - span(cls := "sr-only", "View notifications"), - Icons.outline.bell() - ) - - def userProfile: HtmlElement = - val menuOpen = Var(false) - - def menuItem(item: MenuItem, idx: Int): HtmlElement = - a( - href := "#", - cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", - role := "menuitem", - tabIndex := -1, - idAttr := s"user-menu-item-$idx", - item.title - ) - - div( - cls := "ml-3 relative", - div( - button( - tpe := "button", - cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - idAttr := "user-menu-button", - aria.expanded <-- menuOpen.signal, - aria.hasPopup := true, - span(cls := "sr-only", "Open user menu"), - child <-- avatarImage(), - onClick.preventDefault.mapTo( - !menuOpen.now() - ) --> menuOpen.writer - ) - ), - /* - * */ - div( - cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", - cls <-- menuOpen.signal.map { o => - if (o) "md:block" else "md:hidden" - }, - role := "menu", - aria.orientation := "vertical", - aria.labelledBy := "user-menu-button", - tabIndex := -1, - // : keyboard navigation - children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) - ) - ) - - def mobileProfile = - def menuItem(item: MenuItem): HtmlElement = - a( - href := "#", - cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", - item.title - ) - - div( - cls := "pt-4 pb-3 border-t border-indigo-700", - div( - cls := "flex items-center px-5", - div( - cls := "flex-shrink-0", - child <-- avatarImage(10) - ), - div( - cls := "ml-3", - div( - cls := "text-base font-medium text-white", - child.text <-- $userInfo.map(_.name) - ), - child.maybe <-- $userInfo.map( - _.email.map(e => - div( - cls := "text-sm font-medium text-indigo-300", - e - ) - ) - ) - ), - notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) - ), - div( - cls := "mt-3 px-2 space-y-1", - children <-- $m.map(_.userMenu.map(menuItem)) - ) - ) - - def pageLink(page: Link): Anchor = - page - .a() - .amend( - cls := "text-white px-3 py-2 rounded-md text-sm font-medium", - cls := Seq( - "bg-indigo-700" -> page.active, - "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active - ), - ariaCurrent := (if page.active then "page" else "false") - ) - - def logoImg: Image = - img( - cls := "h-8 w-8", - src <-- $m.map(_.logo.img), - alt <-- $m.map(_.logo.name) - ) - - def mobileMenuButton = button( - tpe := "button", - cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - aria.controls := "mobile-menu", - aria.expanded <-- mobileMenuOpen.signal, - span(cls := "sr-only", "Open main menu"), - Icons.outline - .menu() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") - ), - Icons.outline - .x() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") - ), - onClick.preventDefault.mapTo( - !mobileMenuOpen.now() - ) --> mobileMenuOpen.writer - ) - - def navBarLeft = - div( - cls := "flex items-center", - div(cls := "flex-shrink-0", logoImg), - div( - desktopOnly, - div( - cls := "ml-10 flex items-baseline space-x-4", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ) - ) - ) - - def navBarRight = - div( - desktopOnly, - div( - cls := "ml-4 flex items-center md:ml-6", - notificationButton, - userProfile - ) - ) - - def navBarMobile = - div( - cls := "-mr-2 flex", - mobileOnly, - mobileMenuButton - ) - - def navBar = - div( - cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex items-center justify-between h-16", - navBarLeft, - navBarRight, - navBarMobile - ) - ) - - def mobileMenu = - div( - mobileOnly, - cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }, - idAttr := "mobile-menu", - div( - cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ), - mobileProfile - ) - - nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala deleted file mode 100644 index 75339df..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object PageHeader: - def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = - header( - cls := "bg-white shadow-sm", - div( - cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", - h1( - cls := "text-lg leading-6 font-semibold text-gray-900", - Breadcrumbs(actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala deleted file mode 100644 index 5096463..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ /dev/null @@ -1,30 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import fiftyforms.ui.components.tailwind.Loading -import io.laminext.syntax.core.* - -object PageLayout: - case class ViewModel( - navigation: NavigationBar.ViewModel, - content: Option[HtmlElement] - ) - def apply(actionBus: Observer[Action])( - $m: Signal[ViewModel], - mods: Modifier[HtmlElement]* - )(using router: Router[Page]): HtmlElement = - val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) - div( - cls := "h-full flex flex-col", - NavigationBar($m.map(_.navigation)), - child.maybe <-- router.$currentPage.map(_.isRoot) - .switch(None, Some(PageHeader(actionBus))), - main( - cls := "flex-grow-1 overflow-y-auto", - mods, - child <-- $maybeContent.map(_.getOrElse(Loading)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala deleted file mode 100644 index 62d820f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.LinkSupport.* -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo - -object PageLink: - type ViewModel = Page - def apply($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = - a(mods($m, actions), child.text <-- $m.map(_.title)) - - def apply(m: ViewModel, actions: Observer[Action])(using - router: Router[Page] - ): Anchor = apply(Val(m), actions) - - def container($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = a(mods($m, actions)) - - def container(m: ViewModel, actions: Observer[Action])(using - Router[Page] - ): Anchor = container(Val(m), actions) - - private def mods($m: Signal[Page], actions: Observer[Action])(using - router: Router[Page] - ): Modifier[Anchor] = - Seq( - href <-- $m.map(router.absoluteUrlForPage).recover { case _ => - Some("invalid url") - }, - composeEvents(onClick.noKeyMod.preventDefault)( - _.sample($m) - .map(NavigateTo.apply) - ) --> actions - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala deleted file mode 100644 index 87f825a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.pages.dashboard.DashboardPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -class DashboardPageConnector(actionBus: Observer[Action])(using - router: Router[Page] -): - def apply: HtmlElement = - AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala deleted file mode 100644 index e9b0f71..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala +++ /dev/null @@ -1,76 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailKriteriaPage -import pages.detail.components.DukazyKriteria -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -object DetailKriteriaPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailKriteriaPageConnector( - state: DetailKriteriaPageConnector.AppState -)( - $page: Signal[Page.DetailKriteria] -)(using Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => - (p.osobniCislo.value, p.parametr.value, p.kriterium.value) - )((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map( - FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) - ) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, s, $s) => - DetailKriteriaPage($s)(state.actionBus.contramap { - case DukazyKriteria.Add => - NavigateTo( - Page.UpravDukazKriteria( - Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), - Page.Titled(s.parametr.id, Some(s.parametr.nazev)), - Page.Titled(s.kriterium.id, Some(s.kriterium.id)) - ) - ) - }) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): DetailKriteriaPage.ViewModel = - DetailKriteriaPage.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala deleted file mode 100644 index 4d8024f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ /dev/null @@ -1,57 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import pages.detail.DetailPage -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import fiftyforms.ui.components.tailwind.Color - -object DetailPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailPageConnector(state: DetailPageConnector.AppState)( - $page: Signal[Page.Detail] -)(using router: Router[Page]): - val $oscChangeSignal = $page.splitOne(_.osobniCislo.value)((osc, _, _) => osc) - val $pageChangeSignal = - $oscChangeSignal.flatMap(osc => - EventStream.fromSeq(Seq(FetchUserDetails(osc), FetchParameters(osc))) - ) - // TODO: filter the value based on the current osc - // OSC change will fetch new data, but still - // - we need to be sure that what we got is really what we ought to display - // - we want to display stale data accordingly (at least with loading indicator) - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - def apply: HtmlElement = - AppPage(state.actionBus)( - $data.combineWithFn($params)(_ zip _) - .map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: List[Parameter] - ): DetailPage.ViewModel = - DetailPage.ViewModel( - o.toDetailOsoby, - p.map( - _.toParametr(param => - PageLink.container(Page.DetailParametru(o, param), state.actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala deleted file mode 100644 index fdaa7f8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailParametruPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink - -object DetailParametruPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailParametruPageConnector( - state: DetailParametruPageConnector.AppState -)( - $page: Signal[Page.DetailParametru] -)(using router: Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => (p.osobniCislo.value, p.parametr.value))((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map(FetchParameter(_, _)) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - } yield (da, pb) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailParametruPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter - ): DetailParametruPage.ViewModel = - DetailParametruPage.ViewModel( - o.toDetailOsoby, - p.toParametr(p => - PageLink.container(Page.DetailParametru(o, p), state.actionBus) - ), - p.criteria.map( - _.toKriterium { c => - PageLink.container( - Page.DetailKriteria(o, p, c), - state.actionBus - ) - } - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala deleted file mode 100644 index 049aa22..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -case class DirectoryPageConnector( - $input: EventStream[List[UserInfo]], - actionBus: Observer[Action] -)(using router: Router[Page]): - val $data = $input.startWithNone - val $actionSignal = EventStream.fromValue(FetchDirectory) - - def apply: HtmlElement = - AppPage(actionBus)( - $data.split(_ => ())((_, _, s) => - pages.directory.DirectoryPage( - s.map( - _.map( - _.toUserRow(u => - PageLink.container( - Page.Detail(Page.Titled(u.personalNumber)), - actionBus - ) - ) - ) - ) - ) - ), - $actionSignal --> actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala deleted file mode 100644 index 88e7450..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ /dev/null @@ -1,68 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii -import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria -import fiftyforms.ui.components.tailwind.Color - -extension (o: UserInfo) - def toDetailOsoby: DetailOsoby.ViewModel = - DetailOsoby.ViewModel( - o.personalNumber, - o.name, - o.email, - o.phone, - o.img, - o.mainFunction.map(f => - DetailOsoby.Funkce.ViewModel(f.name, f.dept, f.ou) - ), - o.userContracts.headOption.map(c => - DetailOsoby.PracovniPomer.ViewModel(c.rel, c.startDate, c.endDate) - ) - ) - -extension (param: Parameter) - def toParametr(container: Parameter => Anchor): DetailParametru.ViewModel = - DetailParametru.ViewModel( - id = param.id, - nazev = param.name, - popis = param.description, - status = "Nesplněno", - statusColor = Color.red, - a = container(param) - ) - -extension (crit: ParameterCriteria) - def toKriterium( - container: ParameterCriteria => Anchor - ): DetailKriteria.ViewModel = - DetailKriteria.ViewModel( - nazev = crit.criteriumText, - kapitola = crit.chapterId, - bod = crit.itemId, - status = "Nesplněno", - statusColor = Color.red, - splneno = false, - dukazy = Nil, - container = container(crit) - ) - -extension (user: UserInfo) - def toUserRow( - container: UserInfo => HtmlElement = (_: UserInfo) => div() - ): UserRow.ViewModel = - UserRow.ViewModel( - osobniCislo = user.personalNumber.toString, - celeJmeno = user.name, - prijmeni = user.surname, - hlavniFunkce = user.mainFunction.map(_.name), - img = user.img, - container = () => container(user) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala deleted file mode 100644 index 7d5b2c1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.dashboard - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.Page - -object DashboardPage: - - def render: HtmlElement = - div("Dashboard page") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala deleted file mode 100644 index 9ef0698..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala +++ /dev/null @@ -1,31 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailKriteriaPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply($m: Signal[ViewModel])( - action: Observer[DukazyKriteria.Action] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - DukazyKriteria($m.map(_.kriterium.dukazy))(action) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala deleted file mode 100644 index 3b316e1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object DetailPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametry: SeznamParametru.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby($m.map(_.osoba)), - SeznamParametru($m.map(_.parametry)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala deleted file mode 100644 index 17224cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala +++ /dev/null @@ -1,24 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailParametruPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriteria: SeznamKriterii.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby.header($m.map(_.osoba)), - DetailParametru($m.map(_.parametr)), - SeznamKriterii($m.map(_.kriteria)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala deleted file mode 100644 index dc912fa..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ /dev/null @@ -1,107 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb -package app -package pages.detail - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.UpravDukazForm -import fiftyforms.services.files.File - -object UpravDukaz: - - type ThisPage = Page.UpravDukazKriteria - type PageKey = (OsobniCislo, String, String) - - trait State { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def availableFiles: EventStream[List[File]] - def actionBus: Observer[Action] - } - - def keyOfPage(page: ThisPage): PageKey = - (page.osobniCislo.value, page.parametr.value, page.kriterium.value) - - def onChangeAction(key: PageKey): Action = - FetchParameterCriteria( - key._1, - key._2, - key._3, - Page.UpravDukazKriteria(_, _, _) - ) - - class Connector(state: State)($page: Signal[ThisPage])(using Router[Page]): - val $paramChangeSignal = $page.splitOne(keyOfPage)((x, _, _) => x) - val $pageChangeSignal = $paramChangeSignal.map(onChangeAction) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.split(_ => ())((_, s, $s) => - PageComponent( - $s.map(buildModel), - state.availableFiles, - state.actionBus.contramap { - case UpravDukazForm.Cancelled => - NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) - case UpravDukazForm.AvailableFilesRequested => - FetchAvailableFiles(s._1.personalNumber) - } - ) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): PageComponent.ViewModel = - import connectors.* - PageComponent.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) - - object PageComponent: - import components.* - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply( - $m: Signal[ViewModel], - availableFilesStream: EventStream[List[File]], - events: Observer[UpravDukazForm.Event] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - UpravDukazForm(availableFilesStream)(events) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala deleted file mode 100644 index ca311b0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.Icons -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Color - -object DetailKriteria: - case class ViewModel( - nazev: String, - kapitola: String, - bod: String, - status: String, - statusColor: Color, - splneno: Boolean, - dukazy: List[DukazKriteria.ViewModel], - container: HtmlElement = div() - ) { - val id = s"${kapitola}${bod}" - } - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h3( - cls := "text-xl leading-6 font-bold text-gray-900", - child.text <-- $m.map(_.nazev) - ), - h3( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - "Kapitola ", - child.text <-- $m.map(_.kapitola), - " bod ", - child <-- $m.map(_.bod) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala deleted file mode 100644 index ae90290..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala +++ /dev/null @@ -1,100 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Avatar - -object DetailOsoby: - - object Funkce: - case class ViewModel( - nazev: String, - stredisko: String, - voj: String - ) - - def render($m: Signal[ViewModel]): HtmlElement = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.nazev), - span( - cls := "hidden md:inline", - " @ ", - child.text <-- $m.map(_.stredisko), - ", ", - child.text <-- $m.map(_.voj) - ) - ) - - object PracovniPomer: - case class ViewModel( - druh: String, - pocatek: LocalDate, - konec: Option[LocalDate] - ) - - def render($m: Signal[ViewModel]): HtmlElement = - import fiftyforms.ui.components.tailwind.CustomAttrs.datetime - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.druh), - " od ", - time( - datetime <-- $m.map(_.pocatek.toString), - child.text <-- $m.map(_.pocatek.toString) - ) - ) - - case class ViewModel( - osobniCislo: OsobniCislo, - jmeno: String, - email: Option[String], - telefon: Option[String], - img: Option[String], - hlavniFunkce: Option[Funkce.ViewModel], - pracovniPomer: Option[PracovniPomer.ViewModel] - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-4", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(16) - ), - div( - h1( - cls := "text-2xl font-bold text-gray-900", - child.text <-- $m.map(_.jmeno) - ), - child.maybe <-- $m.map(_.hlavniFunkce).split(_ => ())((_, _, d) => - Funkce.render(d) - ), - child.maybe <-- $m.map(_.pracovniPomer).split(_ => ())((_, _, d) => - PracovniPomer.render(d) - ) - ) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-5", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(8) - ), - div( - h1( - cls := "text-lg font-medium text-gray-800", - child.text <-- $m.map(_.jmeno) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala deleted file mode 100644 index e11cb62..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color - -object DetailParametru: - case class ViewModel( - id: String, - nazev: String, - popis: String, - status: String, - statusColor: Color, - a: Anchor - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h2( - cls := "text-2xl leading-6 font-medium text-gray-900", - child.text <-- $m.map(_.nazev) - ), - p( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - child.text <-- $m.map(_.popis) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - h2( - cls := "text-lg leading-6 font-medium text-gray-600", - child.text <-- $m.map(_.nazev) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala deleted file mode 100644 index a00ede8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala +++ /dev/null @@ -1,177 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.CustomAttrs - -object DukazKriteria: - case class Osoba(osobniCislo: String, jmeno: String) - case class Udalost(osoba: Osoba, datum: LocalDate) - case class Dokument( - url: String, - nazev: String, - pridal: Udalost, - odebral: Option[Udalost] - ) - case class ViewModel( - dokumenty: List[Dokument], - autorizoval: Option[Udalost], - platiDo: Option[LocalDate], - poznámka: Option[String] - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-lg", - div( - cls := "px-4 py-5 sm:px-6", - h3( - cls := "text-lg leading-6 font-medium text-gray-900", - """Applicant Information""" - ), - p( - cls := "mt-1 max-w-2xl text-sm text-gray-500", - """Personal details and application.""" - ) - ), - div( - cls := "border-t border-gray-200 px-4 py-5 sm:px-6", - dl( - cls := "grid grid-cols-1 gap-x-4 gap-y-8 sm:grid-cols-2", - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Full name""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Margot Foster""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Application for""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Backend Developer""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Email address""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """margotfoster@example.com""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Salary expectation""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """$120,000""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """About""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Fugiat ipsum ipsum deserunt culpa aute sint do nostrud anim incididunt cillum culpa consequat. Excepteur qui ipsum aliquip consequat sint. Sit id mollit nulla mollit nostrud in ea officia proident. Irure nostrud pariatur mollit ad adipisicing reprehenderit deserunt qui eu.""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """Attachments""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - ul( - role := "list", - cls := "border border-gray-200 rounded-md divide-y divide-gray-200", - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """resume_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ), - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """coverletter_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala deleted file mode 100644 index 22f134f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala +++ /dev/null @@ -1,35 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object DukazyKriteria: - sealed trait Action - case object Add extends Action - - type ViewModel = List[DukazKriteria.ViewModel] - - def apply($m: Signal[ViewModel])(actions: Observer[Action]): HtmlElement = - div( - child.maybe <-- $m.map(m => - if (m.isEmpty) then Some(prazdnyDukaz(actions)) - else None - ), - children <-- $m.map(_.zipWithIndex).split(_._2)((_, _, $s) => - DukazKriteria($s.map(_._1)) - ) - ) - - private def prazdnyDukaz(actions: Observer[Action]): HtmlElement = - button( - tpe := "button", - onClick.preventDefault.mapTo(Add) --> actions, - cls := "relative block w-full border-2 border-gray-300 border-dashed rounded-lg p-12 text-center hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - Icons.outline - .`document-add`(12) - .amend(svg.cls := "mx-auto text-gray-400"), - span( - cls := "mt-2 block text-sm font-medium text-gray-900", - "Přidat důkaz" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala deleted file mode 100644 index f628697..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.list.StackedList -import fiftyforms.ui.components.tailwind.list.ListRow -import fiftyforms.ui.components.tailwind.list.RowTag -import fiftyforms.ui.components.tailwind.list.RowNext -import java.time.LocalDate - -object SeznamKriterii: - type ViewModel = List[DetailKriteria.ViewModel] - - private val kritList = new StackedList[DetailKriteria.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - kritList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = - p(cls := "text-sm text-gray-500", s"${i.kapitola}${i.bod}"), - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.container - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala deleted file mode 100644 index 2bfe4bc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.list.{ - StackedList, - ListRow, - RowTag, - PropList, - IconText, - RowNext -} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.LinkSupport.* - -object SeznamParametru: - type ViewModel = List[DetailParametru.ViewModel] - - private val parametrList = new StackedList[DetailParametru.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - parametrList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = emptyNode, - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.a - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala deleted file mode 100644 index f048e9d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ /dev/null @@ -1,101 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.form.* -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement -import fiftyforms.services.files.components.tailwind.FilePicker -import cz.e_bs.cmi.mdr.pdb.frontend.AutorizujDukaz -import cz.e_bs.cmi.mdr.pdb.frontend.DocumentRef -import fiftyforms.services.files.File - -object UpravDukazForm: - sealed trait Event - case object Cancelled extends Event - case object AvailableFilesRequested extends Event - def apply(availableFilesStream: EventStream[List[File]])( - updates: Observer[Event] - ): HtmlElement = - val (filesStream, filesObserver) = - EventStream.withObserver[FilePicker.Event] - val files = Var[List[File]](Nil) - def submitButtons: HtmlElement = - div( - cls := "pt-5", - div( - cls := "flex justify-end", - button( - tpe := "button", - cls := "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zrušit", - onClick.mapTo(Cancelled) --> updates - ), - button( - tpe := "submit", - cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - disabled <-- files.signal.map(_.isEmpty), - "Autorizovat" - ) - ) - ) - - div( - filesStream.collect { case FilePicker.SelectionUpdated(files) => - files.to(List) - } --> files.writer, - filesStream.collect { case FilePicker.AvailableFilesRequested => - AvailableFilesRequested - } --> updates, - cls := "bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6", - Form( - Form.Body( - Form.Section( - FormHeader( - FormHeader.ViewModel( - "Doložení kritéria", - "Sestavte doklady poskytující důkaz kritéria a potvrďte odesláním formuláře. Případné limitace či jiné relevantní údaje vepište do pole Komentář." - ) - ), - FormFields( - FormRow( - "dokumenty", - "Dokumenty", - FilePicker(files.signal, availableFilesStream)(filesObserver) - .amend(idAttr := "dokumenty", cls("max-w-lg")) - ).toHtml, - FormRow( - "platnost", - "Platnost", - input( - idAttr := "platnost", - name := "platnost", - tpe := "date", - autoComplete := "date", - cls := "block max-w-lg w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" - ) - ).toHtml, - FormRow( - "komentar", - "Komentář", - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - textArea( - idAttr := "komentar", - name := "about", - rows := 3, - cls := "max-w-lg shadow-sm block w-full focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border border-gray-300 rounded-md" - ), - p( - cls := "mt-2 text-sm text-gray-500", - "Doplňte prosím potřebné informace související s doložením kritéria, včetně případných limitací." - ) - ) - ).toHtml - ) - ) - ), - submitButtons - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala deleted file mode 100644 index 41033f6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -import components._ - -object DirectoryPage: - - type ViewModel = List[UserRow.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - val (actionsStream, actionObserver) = - EventStream.withObserver[SearchForm.Action] - val $filter = actionsStream - .collect { - case SearchForm.ClearFilter => None - case SearchForm.SetFilter(t) => Some(t) - } - .startWith(None) - val byLetter = for { - d <- $m - f <- $filter - } yield for { - (letter, users) <- d - .filter { user => - f match - case None => true - case Some(t) => user.search.contains(t) - } - .groupBy(_.prijmeni.head) - .to(List) - .sortBy(_._1) - } yield (letter.toString, users.sortBy(_.prijmeni)) - - div( - cls := "h-full max-w-7xl mx-auto order-first flex flex-col flex-shrink-0", - SearchForm(actionObserver), - Directory(byLetter) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala deleted file mode 100644 index 73f915d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -object Directory: - - object Header: - type ViewModel = String - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500", - h3(child.text <-- $m) - ) - - object UserList: - type ViewModel = List[UserRow.ViewModel] - def apply($m: Signal[ViewModel]): HtmlElement = - ul( - role := "list", - cls := "relative z-0 divide-y divide-gray-200", - children <-- $m.split(_.osobniCislo)((_, _, s) => UserRow(s)) - ) - - type ViewModel = List[(String, List[UserRow.ViewModel])] - def apply($m: Signal[ViewModel]): HtmlElement = - nav( - cls := "flex-1 min-h-0 overflow-y-auto", - aria.label := "Directory", - children <-- $m.split(_._1)((_, _, s) => - div( - cls := "relative", - Header(s.map(_._1)), - UserList(s.map(_._2)) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala deleted file mode 100644 index e182910..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala +++ /dev/null @@ -1,54 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object SearchForm: - sealed trait Action - case object SubmitFilter extends Action - sealed trait FilterAction extends Action - case object ClearFilter extends FilterAction - case class SetFilter(value: String) extends FilterAction - - def apply(actions: Observer[Action]): HtmlElement = - div( - cls := "px-6 pt-4 pb-4", - form( - cls := "flex space-x-4", - onSubmit.mapTo(SubmitFilter) --> actions, - div( - cls := "flex-1 min-w-0", - label( - forId := "search", - cls := "sr-only", - "Hledat" - ), - div( - cls := "relative rounded-md shadow-sm", - div( - cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", - Icons.solid.search().amend(svg.cls := "text-gray-400") - ), - input( - tpe := "search", - name := "search", - idAttr := "search", - cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", - placeholder := "Hledat", - composeEvents(onInput.mapToValue.setAsValue.map(SetFilter(_)))( - _.throttle(500) - ) --> actions - ) - ) - ), - button( - tpe := "submit", - cls := "inline-flex justify-center px-3.5 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500", - Icons.solid.filter().amend(svg.cls := "text-gray-400"), - span( - cls := "sr-only", - "Hledat" - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala deleted file mode 100644 index 82f7723..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala +++ /dev/null @@ -1,52 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.airstream.core.Signal -import fiftyforms.ui.components.tailwind.Avatar - -object UserRow: - case class ViewModel( - osobniCislo: String, - celeJmeno: String, - prijmeni: String, - hlavniFunkce: Option[String], - img: Option[String], - container: () => HtmlElement = () => div() - ) { - val search = prijmeni.toLowerCase - } - - def apply($m: Signal[ViewModel]): HtmlElement = - inline def avatarImage = - Avatar($m.map(_.img)).avatarImage(10) - - li( - div( - cls := "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500", - div( - cls := "flex-shrink-0", - child <-- avatarImage - ), - div( - cls := "flex-1 min-w-0", - child <-- $m.map { o => - o.container() - .amend( - cls := "focus:outline-none", - span( - cls := "absolute inset-0", - aria.hidden := true - ), - p( - cls := "text-sm font-medium text-gray-900", - o.celeJmeno - ), - p( - cls := "text-sm text-gray-500 truncate", - o.hlavniFunkce - ) - ) - } - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala deleted file mode 100644 index 467c5f5..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala +++ /dev/null @@ -1,99 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router - -object ErrorPage: - case class ViewModel( - homePage: Page, - errorName: String, - title: String, - subTitle: String - ) - - def apply(m: ViewModel, actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - val ViewModel(homePage, errorName, title, subTitle) = m - div( - cls := "min-h-full pt-16 pb-12 flex flex-col bg-white", - main( - cls := "flex-grow flex flex-col justify-center max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex-shrink-0 flex justify-center", - a( - href := "/", - cls := "inline-flex", - span( - cls := "sr-only", - """Workflow""" - ), - img( - cls := "h-12 w-auto", - src := "https://tailwindui.com/img/logos/workflow-mark.svg?color=indigo&shade=600", - alt := "" - ) - ) - ), - div( - cls := "py-16", - div( - cls := "text-center", - p( - cls := "text-sm font-semibold text-indigo-600 uppercase tracking-wide", - errorName - ), - h1( - cls := "mt-2 text-4xl font-extrabold text-gray-900 tracking-tight sm:text-5xl", - title - ), - p( - cls := "mt-2 text-base text-gray-500", - subTitle - ), - div( - cls := "mt-6", - PageLink - .container(homePage, actionBus) - .amend( - cls := "text-base font-medium text-indigo-600 hover:text-indigo-500", - """Go back home""" - ) - ) - ) - ) - ), - footer( - cls := "flex-shrink-0 max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", - nav( - cls := "flex justify-center space-x-4", - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Contact Support""" - ), - span( - cls := "inline-block border-l border-gray-300", - aria.hidden := true - ), - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Status""" - ), - span( - cls := "inline-block border-l border-gray-300", - aria.hidden := true - ), - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Twitter""" - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala deleted file mode 100644 index bd9baec..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action - -object NotFoundPage: - def apply(homePage: Page, url: String, actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - ErrorPage( - ErrorPage.ViewModel( - homePage, - "404 error", - "Page not found.", - s"Sorry, but page $url doesn't exist." - ), - actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala deleted file mode 100644 index 476fe3c..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action - -object UnhandledErrorPage: - - case class ViewModel( - homePage: Page, - errorName: Option[String], - errorMessage: Option[String] - ) - - def apply(m: ViewModel, actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - ErrorPage( - ErrorPage.ViewModel( - m.homePage, - "Unexpected error occurred", - m.errorName.getOrElse( - "Uh oh!" - ), // TODO: translations, better text than uh oh - m.errorMessage.getOrElse( - "This wasn't supposed to happen! Please try again." - ) - ), - actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala deleted file mode 100644 index 25f059f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala +++ /dev/null @@ -1,6 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.services - -import com.raquo.airstream.core.Observer - -trait DataFetcher[K, A]: - def fetch(id: K, sink: Observer[A]): Unit diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala deleted file mode 100644 index a469830..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala +++ /dev/null @@ -1,120 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package state - -import com.raquo.airstream.core.EventStream -import cz.e_bs.cmi.mdr.pdb.{UserInfo, OsobniCislo} -import com.raquo.airstream.core.Observer -import scala.scalajs.js -import scala.scalajs.js.JSON -import zio.json._ -import com.raquo.airstream.eventbus.EventBus -import com.raquo.airstream.ownership.Owner -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.UserFunction -import cz.e_bs.cmi.mdr.pdb.UserContract -import fiftyforms.services.files.File - -trait AppState - extends connectors.DetailPageConnector.AppState - with connectors.DetailParametruPageConnector.AppState - with connectors.DetailKriteriaPageConnector.AppState - with pages.detail.UpravDukaz.State: - def users: EventStream[List[UserInfo]] - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - -class MockAppState(implicit owner: Owner, router: Router[Page]) - extends AppState: - - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonDecoder[UserFunction] = DeriveJsonDecoder.gen - given JsonDecoder[UserContract] = DeriveJsonDecoder.gen - given JsonDecoder[UserInfo] = DeriveJsonDecoder.gen - - given JsonDecoder[ParameterCriteria] = DeriveJsonDecoder.gen - given JsonDecoder[Parameter] = DeriveJsonDecoder.gen - - private val actions = EventBus[Action]() - private val (parametersStream, pushParameters) = - EventStream.withCallback[List[Parameter]] - private val (usersStream, pushUsers) = - EventStream.withCallback[List[UserInfo]] - private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] - private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] - - private val mockData: List[UserInfo] = - mockUsers - .asInstanceOf[js.Dictionary[js.Object]] - .values - // TODO: is there a more efficient way to parse from JS object directly? - .map { o => - val parsed = JSON.stringify(o).fromJson[UserInfo] - parsed.left.foreach(org.scalajs.dom.console.log(_)) - parsed - } - .collect { case Right(u) => - u - } - .toList - - private val mockParameters: List[Parameter] = - pdbParams - .asInstanceOf[js.Dictionary[js.Object]] - .values - .map(o => JSON.stringify(o).fromJson[Parameter]) - .collect { case Right(p) => p } - .toList - - // TODO: Extract to separate event handler - actions.events.foreach { - case FetchDirectory => pushUsers(mockData) - case FetchUserDetails(osc) => - mockData.find(_.personalNumber == osc).foreach { o => - pushDetails(o) - router.replaceState(Page.Detail(o)) - } - case FetchParameters(osc) => - pushParameters(mockParameters) - case FetchParameter(osc, paramId) => - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(Page.DetailParametru(o, p)) - case FetchParameterCriteria(osc, paramId, critId, page) => - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - c <- p.criteria.find(_.id == critId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(page(o, p, c)) - case NavigateTo(page) => router.pushState(page) - case FetchAvailableFiles(osc) => - pushFiles( - List( - File("https://tc163.cmi.cz/here", "Example file") - ) - ) - } - - override def users: EventStream[List[UserInfo]] = - usersStream.debugWithName("users") - - override def details: EventStream[UserInfo] = - detailsStream.debugWithName("details") - - override def parameters: EventStream[List[Parameter]] = - parametersStream.debugWithName("parameters") - - override def availableFiles: EventStream[List[File]] = - filesStream.debugWithName("available files") - - override def actionBus: Observer[Action] = - actions.writer.debugWithName("actions writer") diff --git a/app/src/main/scala/mdr/pdb/app/Main.scala b/app/src/main/scala/mdr/pdb/app/Main.scala new file mode 100644 index 0000000..081b4ee --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/Main.scala @@ -0,0 +1,103 @@ +package mdr.pdb.app + +import scala.scalajs.js.annotation.JSExportTopLevel +import scala.scalajs.js.annotation.JSExport +import scala.scalajs.js.annotation.JSImport +import scala.scalajs.js +import org.scalajs.dom +import com.raquo.laminar.api.L.{*, given} +import scala.scalajs.js.Date +import com.raquo.waypoint.Router +import com.raquo.waypoint.SplitRender +import mdr.pdb.app.services.DataFetcher +import scala.scalajs.js.JSON +import zio.json._ +import mdr.pdb.UserInfo +import mdr.pdb.app.state.AppState + +@js.native +@JSImport("stylesheets/main.css", JSImport.Namespace) +object Css extends js.Any + +@js.native +@JSImport("data/users.json", JSImport.Default) +object mockUsers extends js.Object + +@js.native +@JSImport("params/pdb-params.json", JSImport.Default) +object pdbParams extends js.Object + +@JSExportTopLevel("app") +object Main: + + @JSExport + def main(args: Array[String]): Unit = + import Routes.given + onLoad { + setupAirstream() + val appContainer = dom.document.querySelector("#app") + val _ = + render( + appContainer, + renderPage(state.MockAppState(using unsafeWindowOwner, router)) + ) + } + + private def onLoad(f: => Unit): Unit = + documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) + + private def setupAirstream()(using router: Router[Page]): Unit = + AirstreamError.registerUnhandledErrorCallback(err => + router.forcePage( + Page.UnhandledError( + Some(err.getClass.getName), // TODO: Fill only in dev mode + Some(err.getMessage) + ) + ) + ) + + def renderPage(state: AppState)(using + router: Router[Page] + ): HtmlElement = + val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) + .collectSignal[Page.Detail]( + connectors + .DetailPageConnector(state)(_) + .apply + ) + .collectSignal[Page.DetailParametru]( + connectors + .DetailParametruPageConnector(state)(_) + .apply + ) + .collectSignal[Page.DetailKriteria]( + connectors + .DetailKriteriaPageConnector(state)(_) + .apply + ) + .collectSignal[Page.UpravDukazKriteria]( + pages.detail.UpravDukaz.Connector(state)(_).apply + ) + .collectStatic(Page.Dashboard)( + connectors.DashboardPageConnector(state.actionBus).apply + ) + .collect[Page.NotFound](pg => + pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) + ) + .collect[Page.UnhandledError](pg => + pages.errors + .UnhandledErrorPage( + pages.errors.UnhandledErrorPage + .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), + state.actionBus + ) + ) + .collectStatic(Page.Directory)( + connectors + .DirectoryPageConnector(state.users, state.actionBus) + .apply + ) + div(cls := "h-full", child <-- pageSplitter.$view) + + // Pull in the stylesheet + val css: Css.type = Css diff --git a/app/src/main/scala/mdr/pdb/app/Routes.scala b/app/src/main/scala/mdr/pdb/app/Routes.scala new file mode 100644 index 0000000..2cc860c --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/Routes.scala @@ -0,0 +1,198 @@ +package mdr.pdb.app + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.* +import org.scalajs.dom +import zio.json.{*, given} +import mdr.pdb.OsobniCislo + +import scala.scalajs.js +import mdr.pdb.UserInfo +import mdr.pdb.Parameter +import mdr.pdb.ParameterCriteria +import mdr.pdb.app.Page.Titled + +// enum is not working with Waypoints' SplitRender collectStatic +sealed abstract class Page( + val id: String, + val title: String, + val parent: Option[Page] +) { + val path: Vector[Page] = + parent match + case None => Vector(this) + case Some(p) => p.path :+ this + + val isRoot: Boolean = parent.isEmpty +} + +object Page: + + case class Titled[V](value: V, title: Option[String] = None): + val show: String = title.getOrElse(value.toString) + + case object Directory extends Page("directory", "Adresář", None) + + case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) + + case class Detail(osobniCislo: Titled[OsobniCislo]) + extends Page("user", osobniCislo.show, Some(Directory)) + + object Detail { + def apply(o: UserInfo): Detail = Detail( + Titled(o.personalNumber, Some(o.name)) + ) + } + + case class DetailParametru( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String] + ) extends Page( + "parameter", + parametr.show, + Some(Detail(osobniCislo)) + ) + + object DetailParametru { + def apply(o: UserInfo, p: Parameter): DetailParametru = + DetailParametru( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)) + ) + } + + case class DetailKriteria( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String], + kriterium: Titled[String] + ) extends Page( + "criteria", + kriterium.show, + Some(DetailParametru(osobniCislo, parametr)) + ) + + object DetailKriteria { + def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = + DetailKriteria( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)), + Titled(k.id, Some(k.id)) + ) + } + + case class UpravDukazKriteria( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String], + kriterium: Titled[String] + ) extends Page( + "addProof", + "Důkaz", + Some(DetailKriteria(osobniCislo, parametr, kriterium)) + ) + + object UpravDukazKriteria { + def apply( + o: UserInfo, + p: Parameter, + k: ParameterCriteria + ): UpravDukazKriteria = + UpravDukazKriteria( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)), + Titled(k.id, Some(k.id)) + ) + } + + case class NotFound(url: String) extends Page("404", "404", Some(Directory)) + + case class UnhandledError( + errorName: Option[String], + errorMessage: Option[String] + ) extends Page("500", "Unexpected error", Some(Directory)) + +object Routes: + given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) + given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) + given [V: JsonEncoder]: JsonEncoder[Titled[V]] = + DeriveJsonEncoder.gen[Titled[V]] + given [V: JsonDecoder]: JsonDecoder[Titled[V]] = + DeriveJsonDecoder.gen[Titled[V]] + given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] + given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] + + val base = + js.`import`.meta.env.BASE_URL + .asInstanceOf[String] + "app" + + val homePage: Page = Page.Directory + + given router: Router[Page] = Router[Page]( + routes = List( + Route.static(homePage, root / endOfSegments, basePath = base), + Route.static( + Page.Dashboard, + root / "dashboard" / endOfSegments, + basePath = base + ), + Route[Page.Detail, String]( + encode = _.osobniCislo.value.toString, + decode = osc => Page.Detail(Titled(OsobniCislo(osc))), + root / "osoba" / segment[String] / endOfSegments, + basePath = base + ), + Route[Page.DetailParametru, (String, String)]( + encode = p => (p.osobniCislo.value.toString, p.parametr.value), + decode = + p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / endOfSegments, + basePath = base + ), + Route[Page.DetailKriteria, (String, String, String)]( + encode = p => + ( + p.osobniCislo.value.toString, + p.parametr.value, + p.kriterium.value.replaceAll("\\.", "--") + ), + decode = p => + Page.DetailKriteria( + Titled(OsobniCislo(p._1)), + Titled(p._2), + Titled(p._3.replaceAll("--", ".")) + ), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / "kriterium" / segment[String] / endOfSegments, + basePath = base + ), + Route[Page.UpravDukazKriteria, (String, String, String)]( + encode = p => + ( + p.osobniCislo.value.toString, + p.parametr.value, + p.kriterium.value.replaceAll("\\.", "--") + ), + decode = p => + Page.UpravDukazKriteria( + Titled(OsobniCislo(p._1)), + Titled(p._2), + Titled(p._3.replaceAll("--", ".")) + ), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / "kriterium" / segment[String] / "edit" / endOfSegments, + basePath = base + ) + ), + serializePage = _.toJson, + deserializePage = _.fromJson[Page] + .fold(s => throw IllegalStateException(s), identity), + getPageTitle = _.title, + routeFallback = url => Page.NotFound(url), + deserializeFallback = _ => Page.Dashboard + )( + $popStateEvent = windowEvents.onPopState, + owner = unsafeWindowOwner + ) diff --git a/app/src/main/scala/mdr/pdb/app/actions.scala b/app/src/main/scala/mdr/pdb/app/actions.scala new file mode 100644 index 0000000..d078b3a --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/actions.scala @@ -0,0 +1,21 @@ +package mdr.pdb.app + +import mdr.pdb.OsobniCislo +import mdr.pdb.UserInfo +import mdr.pdb.Parameter +import mdr.pdb.ParameterCriteria + +sealed trait Action + +case object FetchDirectory extends Action +case class FetchUserDetails(osc: OsobniCislo) extends Action +case class FetchParameters(osc: OsobniCislo) extends Action +case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action +case class FetchParameterCriteria( + osc: OsobniCislo, + paramId: String, + critId: String, + page: (UserInfo, Parameter, ParameterCriteria) => Page +) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action +case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/mdr/pdb/app/components/AppPage.scala new file mode 100644 index 0000000..130c817 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/AppPage.scala @@ -0,0 +1,82 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.Page +import mdr.pdb.{UserProfile, UserInfo, OsobniCislo} +import com.raquo.waypoint.Router +import mdr.pdb.app.Action +import mdr.pdb.app.NavigateTo +import mdr.pdb.UserFunction + +object AppPage: + // TODO: pages by logged in user + val pages: List[Page] = List(Page.Directory, Page.Dashboard) + + import NavigationBar.{Logo, MenuItem} + + val logo = Logo( + "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", + "Workflow" + ) + + // TODO: menu items by user profile + val userMenu = + List( + MenuItem("Your Profile"), + MenuItem("Settings"), + MenuItem("Sign out") + ) + + // TODO: load user profile + val $userProfile = Var( + UserProfile( + "tom", + UserInfo( + OsobniCislo("1031"), + "tom", + "Tom", + "Cook", + None, + None, + Some("tom@example.com"), + Some("+420 222 866 180"), + Some( + UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") + ), + Nil, + None + ) + ) + ) + + val $userInfo = $userProfile.signal.map(_.userInfo) + + type ViewModel = Option[HtmlElement] + def apply( + actionBus: Observer[Action] + )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using + router: Router[Page] + ): HtmlElement = + PageLayout(actionBus)( + $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => + PageLayout.ViewModel( + NavigationBar.ViewModel( + u, + pages.map(p => + NavigationBar.Link( + () => + PageLink( + p, + actionBus + ), + p == cp + ) + ), + userMenu, + logo + ), + c + ) + ), + mods + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala new file mode 100644 index 0000000..9375398 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala @@ -0,0 +1,143 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.tailwind.list.IconText.ViewModel +import fiftyforms.ui.components.tailwind.Icons +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.Page + +import com.raquo.laminar.api.L.{*, given} +import io.laminext.syntax.core.{*, given} +import mdr.pdb.app.Action + +object Breadcrumbs: + + private def slash = + import svg.{*, given} + svg( + cls := "flex-shrink-0 h-5 w-5 text-gray-300", + xmlns := "http://www.w3.org/2000/svg", + fill := "currentColor", + viewBox := "0 0 20 20", + ariaHidden := true, + path( + d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" + ) + ) + + def backIcon = + Icons.solid + .`arrow-narrow-left`() + .amend( + svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" + ) + + object Link: + case class ViewModel( + page: Page, + icon: Option[SvgElement], + extraClasses: String, + text: String, + textClasses: Option[String] = None + ) + + val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" + + def shortHome(p: Page) = ViewModel( + p, + Some(backIcon), + "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", + "Zpět na úvodní stránku", + None + ) + + def fullHome(p: Page) = ViewModel( + p, + Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), + "text-gray-400 hover:text-gray-500", + "Domů", + Some("sr-only") + ) + + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + PageLink + .container($m.map(_.page), actionBus) + .amend( + cls <-- $m.map(_.extraClasses), + child.maybe <-- $m.map(_.icon), + span( + cls <-- $m.map(_.textClasses.getOrElse("")), + child.text <-- $m.map(_.text) + ) + ) + + object FullBreadcrumbs: + type ViewModel = Page + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + ol( + role := "list", + cls := "flex items-center space-x-4", + children <-- $m.map(_.path).split(_.id)((_, _, $p) => + li( + div( + cls := "flex items-center", + child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), + Link( + $p.map(p => + if (p.isRoot) then Link.fullHome(p) + else + Link.ViewModel( + p, + None, + s"ml-4 max-w-xs truncate ${Link.baseClasses}", + p.title + ) + ), + actionBus + ) + ) + ) + ) + ) + + object ShortBreadcrumbs: + type ViewModel = Page + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + Link( + $m.map { p => + val target = p.parent.getOrElse(p) + if target.isRoot then Link.shortHome(target) + else + Link.ViewModel( + target, + Some(backIcon), + "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", + s"Zpět na ${target.title}" + ) + }, + actionBus + ) + + def apply(actionBus: Observer[Action])(using + router: Router[Page] + ): HtmlElement = + nav( + cls := "flex", + aria.label := "Breadcrumb", + div( + cls := "flex sm:hidden", + ShortBreadcrumbs(router.$currentPage, actionBus) + ), + div( + cls := "hidden sm:block", + FullBreadcrumbs(router.$currentPage, actionBus) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala new file mode 100644 index 0000000..4bb8b8d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala @@ -0,0 +1,242 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent +import fiftyforms.ui.components.tailwind.Icons +import fiftyforms.ui.components.tailwind.Avatar +import mdr.pdb.UserInfo +import io.laminext.syntax.core.* + +object NavigationBar: + + case class Logo(img: String, name: String) + case class Link(a: () => Anchor, active: Boolean) + case class MenuItem(title: String) + + case class ViewModel( + userInfo: UserInfo, + pages: List[Link], + userMenu: List[MenuItem], + logo: Logo + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + val $userInfo = $m.map(_.userInfo) + val mobileMenuOpen = Var(false) + + val desktopOnly = cls("hidden md:block") + val mobileOnly = cls("md:hidden") + + inline def avatarImage(size: Int = 8) = + Avatar($userInfo.map(_.img)).avatarImage(size) + + def notificationButton = button( + tpe := "button", + cls := List( + "bg-indigo-600", + "focus:outline-none", + "focus:ring-2", + "focus:ring-offset-2", + "focus:ring-offset-indigo-600", + "focus:ring-white", + "hover:text-white", + "p-1", + "rounded-full", + "text-indigo-200" + ), + span(cls := "sr-only", "View notifications"), + Icons.outline.bell() + ) + + def userProfile: HtmlElement = + val menuOpen = Var(false) + + def menuItem(item: MenuItem, idx: Int): HtmlElement = + a( + href := "#", + cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", + role := "menuitem", + tabIndex := -1, + idAttr := s"user-menu-item-$idx", + item.title + ) + + div( + cls := "ml-3 relative", + div( + button( + tpe := "button", + cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", + idAttr := "user-menu-button", + aria.expanded <-- menuOpen.signal, + aria.hasPopup := true, + span(cls := "sr-only", "Open user menu"), + child <-- avatarImage(), + onClick.preventDefault.mapTo( + !menuOpen.now() + ) --> menuOpen.writer + ) + ), + /* + * */ + div( + cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", + cls <-- menuOpen.signal.map { o => + if (o) "md:block" else "md:hidden" + }, + role := "menu", + aria.orientation := "vertical", + aria.labelledBy := "user-menu-button", + tabIndex := -1, + // : keyboard navigation + children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) + ) + ) + + def mobileProfile = + def menuItem(item: MenuItem): HtmlElement = + a( + href := "#", + cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", + item.title + ) + + div( + cls := "pt-4 pb-3 border-t border-indigo-700", + div( + cls := "flex items-center px-5", + div( + cls := "flex-shrink-0", + child <-- avatarImage(10) + ), + div( + cls := "ml-3", + div( + cls := "text-base font-medium text-white", + child.text <-- $userInfo.map(_.name) + ), + child.maybe <-- $userInfo.map( + _.email.map(e => + div( + cls := "text-sm font-medium text-indigo-300", + e + ) + ) + ) + ), + notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) + ), + div( + cls := "mt-3 px-2 space-y-1", + children <-- $m.map(_.userMenu.map(menuItem)) + ) + ) + + def pageLink(page: Link): Anchor = + page + .a() + .amend( + cls := "text-white px-3 py-2 rounded-md text-sm font-medium", + cls := Seq( + "bg-indigo-700" -> page.active, + "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active + ), + ariaCurrent := (if page.active then "page" else "false") + ) + + def logoImg: Image = + img( + cls := "h-8 w-8", + src <-- $m.map(_.logo.img), + alt <-- $m.map(_.logo.name) + ) + + def mobileMenuButton = button( + tpe := "button", + cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", + aria.controls := "mobile-menu", + aria.expanded <-- mobileMenuOpen.signal, + span(cls := "sr-only", "Open main menu"), + Icons.outline + .menu() + .amend( + svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") + ), + Icons.outline + .x() + .amend( + svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") + ), + onClick.preventDefault.mapTo( + !mobileMenuOpen.now() + ) --> mobileMenuOpen.writer + ) + + def navBarLeft = + div( + cls := "flex items-center", + div(cls := "flex-shrink-0", logoImg), + div( + desktopOnly, + div( + cls := "ml-10 flex items-baseline space-x-4", + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) + ) + ) + ) + + def navBarRight = + div( + desktopOnly, + div( + cls := "ml-4 flex items-center md:ml-6", + notificationButton, + userProfile + ) + ) + + def navBarMobile = + div( + cls := "-mr-2 flex", + mobileOnly, + mobileMenuButton + ) + + def navBar = + div( + cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", + div( + cls := "flex items-center justify-between h-16", + navBarLeft, + navBarRight, + navBarMobile + ) + ) + + def mobileMenu = + div( + mobileOnly, + cls <-- mobileMenuOpen.signal.map { o => + if (o) "block" else "hidden" + }, + idAttr := "mobile-menu", + div( + cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) + ), + mobileProfile + ) + + nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala new file mode 100644 index 0000000..c4cb10d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala @@ -0,0 +1,19 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.Page +import com.raquo.waypoint.Router +import mdr.pdb.app.Action + +object PageHeader: + def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = + header( + cls := "bg-white shadow-sm", + div( + cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", + h1( + cls := "text-lg leading-6 font-semibold text-gray-900", + Breadcrumbs(actionBus) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala new file mode 100644 index 0000000..aae4539 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala @@ -0,0 +1,30 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.Page +import mdr.pdb.app.Action +import fiftyforms.ui.components.tailwind.Loading +import io.laminext.syntax.core.* + +object PageLayout: + case class ViewModel( + navigation: NavigationBar.ViewModel, + content: Option[HtmlElement] + ) + def apply(actionBus: Observer[Action])( + $m: Signal[ViewModel], + mods: Modifier[HtmlElement]* + )(using router: Router[Page]): HtmlElement = + val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) + div( + cls := "h-full flex flex-col", + NavigationBar($m.map(_.navigation)), + child.maybe <-- router.$currentPage.map(_.isRoot) + .switch(None, Some(PageHeader(actionBus))), + main( + cls := "flex-grow-1 overflow-y-auto", + mods, + child <-- $maybeContent.map(_.getOrElse(Loading)) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/mdr/pdb/app/components/PageLink.scala new file mode 100644 index 0000000..e6e7052 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageLink.scala @@ -0,0 +1,40 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.LinkSupport.* +import mdr.pdb.app.Page +import mdr.pdb.app.Action +import com.raquo.waypoint.Router +import mdr.pdb.app.NavigateTo + +object PageLink: + type ViewModel = Page + def apply($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = + a(mods($m, actions), child.text <-- $m.map(_.title)) + + def apply(m: ViewModel, actions: Observer[Action])(using + router: Router[Page] + ): Anchor = apply(Val(m), actions) + + def container($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = a(mods($m, actions)) + + def container(m: ViewModel, actions: Observer[Action])(using + Router[Page] + ): Anchor = container(Val(m), actions) + + private def mods($m: Signal[Page], actions: Observer[Action])(using + router: Router[Page] + ): Modifier[Anchor] = + Seq( + href <-- $m.map(router.absoluteUrlForPage).recover { case _ => + Some("invalid url") + }, + composeEvents(onClick.noKeyMod.preventDefault)( + _.sample($m) + .map(NavigateTo.apply) + ) --> actions + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala new file mode 100644 index 0000000..7682772 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala @@ -0,0 +1,13 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.pages.dashboard.DashboardPage +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage + +class DashboardPageConnector(actionBus: Observer[Action])(using + router: Router[Page] +): + def apply: HtmlElement = + AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala new file mode 100644 index 0000000..76b9133 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala @@ -0,0 +1,76 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.Parameter +import mdr.pdb.UserInfo +import mdr.pdb.app.pages.detail.DetailParametruPage +import pages.detail.DetailKriteriaPage +import pages.detail.components.DukazyKriteria +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.ParameterCriteria + +object DetailKriteriaPageConnector { + trait AppState { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + } +} + +case class DetailKriteriaPageConnector( + state: DetailKriteriaPageConnector.AppState +)( + $page: Signal[Page.DetailKriteria] +)(using Router[Page]): + val $paramChangeSignal = + $page.splitOne(p => + (p.osobniCislo.value, p.parametr.value, p.kriterium.value) + )((x, _, _) => x) + val $pageChangeSignal = + $paramChangeSignal.map( + FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) + ) + + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + val $merged = + $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => + for { + da <- d + pa <- p + pb <- pa.find(_.id == pc._2) + ka <- pb.criteria.find(_.id == pc._3) + } yield (da, pb, ka) + ) + + def apply: HtmlElement = + AppPage(state.actionBus)( + $merged.map(_.map(buildModel)) + .split(_ => ())((_, s, $s) => + DetailKriteriaPage($s)(state.actionBus.contramap { + case DukazyKriteria.Add => + NavigateTo( + Page.UpravDukazKriteria( + Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), + Page.Titled(s.parametr.id, Some(s.parametr.nazev)), + Page.Titled(s.kriterium.id, Some(s.kriterium.id)) + ) + ) + }) + ), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: Parameter, + k: ParameterCriteria + ): DetailKriteriaPage.ViewModel = + DetailKriteriaPage.ViewModel( + o.toDetailOsoby, + p.toParametr(_ => a()), + k.toKriterium(_ => a()) + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DetailPageConnector.scala new file mode 100644 index 0000000..df3a6b7 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DetailPageConnector.scala @@ -0,0 +1,57 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.UserInfo +import pages.detail.DetailPage +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.pages.detail.components.DetailOsoby +import mdr.pdb.Parameter +import mdr.pdb.app.pages.detail.components.SeznamParametru +import fiftyforms.ui.components.tailwind.Color + +object DetailPageConnector { + trait AppState { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + } +} + +case class DetailPageConnector(state: DetailPageConnector.AppState)( + $page: Signal[Page.Detail] +)(using router: Router[Page]): + val $oscChangeSignal = $page.splitOne(_.osobniCislo.value)((osc, _, _) => osc) + val $pageChangeSignal = + $oscChangeSignal.flatMap(osc => + EventStream.fromSeq(Seq(FetchUserDetails(osc), FetchParameters(osc))) + ) + // TODO: filter the value based on the current osc + // OSC change will fetch new data, but still + // - we need to be sure that what we got is really what we ought to display + // - we want to display stale data accordingly (at least with loading indicator) + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + def apply: HtmlElement = + AppPage(state.actionBus)( + $data.combineWithFn($params)(_ zip _) + .map(_.map(buildModel)) + .split(_ => ())((_, _, s) => DetailPage(s)), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: List[Parameter] + ): DetailPage.ViewModel = + DetailPage.ViewModel( + o.toDetailOsoby, + p.map( + _.toParametr(param => + PageLink.container(Page.DetailParametru(o, param), state.actionBus) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DetailParametruPageConnector.scala new file mode 100644 index 0000000..fa7193d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DetailParametruPageConnector.scala @@ -0,0 +1,67 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.Parameter +import mdr.pdb.UserInfo +import mdr.pdb.app.pages.detail.DetailParametruPage +import pages.detail.DetailParametruPage +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.components.PageLink + +object DetailParametruPageConnector { + trait AppState { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + } +} + +case class DetailParametruPageConnector( + state: DetailParametruPageConnector.AppState +)( + $page: Signal[Page.DetailParametru] +)(using router: Router[Page]): + val $paramChangeSignal = + $page.splitOne(p => (p.osobniCislo.value, p.parametr.value))((x, _, _) => x) + val $pageChangeSignal = + $paramChangeSignal.map(FetchParameter(_, _)) + + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + val $merged = + $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => + for { + da <- d + pa <- p + pb <- pa.find(_.id == pc._2) + } yield (da, pb) + ) + + def apply: HtmlElement = + AppPage(state.actionBus)( + $merged.map(_.map(buildModel)) + .split(_ => ())((_, _, s) => DetailParametruPage(s)), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: Parameter + ): DetailParametruPage.ViewModel = + DetailParametruPage.ViewModel( + o.toDetailOsoby, + p.toParametr(p => + PageLink.container(Page.DetailParametru(o, p), state.actionBus) + ), + p.criteria.map( + _.toKriterium { c => + PageLink.container( + Page.DetailKriteria(o, p, c), + state.actionBus + ) + } + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DirectoryPageConnector.scala new file mode 100644 index 0000000..ab2c045 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DirectoryPageConnector.scala @@ -0,0 +1,34 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.UserInfo +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.components.AppPage + +case class DirectoryPageConnector( + $input: EventStream[List[UserInfo]], + actionBus: Observer[Action] +)(using router: Router[Page]): + val $data = $input.startWithNone + val $actionSignal = EventStream.fromValue(FetchDirectory) + + def apply: HtmlElement = + AppPage(actionBus)( + $data.split(_ => ())((_, _, s) => + pages.directory.DirectoryPage( + s.map( + _.map( + _.toUserRow(u => + PageLink.container( + Page.Detail(Page.Titled(u.personalNumber)), + actionBus + ) + ) + ) + ) + ) + ), + $actionSignal --> actionBus + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/mdr/pdb/app/connectors/conversions.scala new file mode 100644 index 0000000..f581796 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/conversions.scala @@ -0,0 +1,68 @@ +package mdr.pdb.app.connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.UserInfo +import mdr.pdb.app.pages.detail.components.DetailOsoby +import mdr.pdb.Parameter +import mdr.pdb.app.pages.detail.components.SeznamParametru +import mdr.pdb.ParameterCriteria +import mdr.pdb.app.pages.detail.components.SeznamKriterii +import mdr.pdb.app.pages.directory.components.UserRow +import mdr.pdb.app.pages.detail.components.DetailParametru +import mdr.pdb.app.pages.detail.components.DetailKriteria +import fiftyforms.ui.components.tailwind.Color + +extension (o: UserInfo) + def toDetailOsoby: DetailOsoby.ViewModel = + DetailOsoby.ViewModel( + o.personalNumber, + o.name, + o.email, + o.phone, + o.img, + o.mainFunction.map(f => + DetailOsoby.Funkce.ViewModel(f.name, f.dept, f.ou) + ), + o.userContracts.headOption.map(c => + DetailOsoby.PracovniPomer.ViewModel(c.rel, c.startDate, c.endDate) + ) + ) + +extension (param: Parameter) + def toParametr(container: Parameter => Anchor): DetailParametru.ViewModel = + DetailParametru.ViewModel( + id = param.id, + nazev = param.name, + popis = param.description, + status = "Nesplněno", + statusColor = Color.red, + a = container(param) + ) + +extension (crit: ParameterCriteria) + def toKriterium( + container: ParameterCriteria => Anchor + ): DetailKriteria.ViewModel = + DetailKriteria.ViewModel( + nazev = crit.criteriumText, + kapitola = crit.chapterId, + bod = crit.itemId, + status = "Nesplněno", + statusColor = Color.red, + splneno = false, + dukazy = Nil, + container = container(crit) + ) + +extension (user: UserInfo) + def toUserRow( + container: UserInfo => HtmlElement = (_: UserInfo) => div() + ): UserRow.ViewModel = + UserRow.ViewModel( + osobniCislo = user.personalNumber.toString, + celeJmeno = user.name, + prijmeni = user.surname, + hlavniFunkce = user.mainFunction.map(_.name), + img = user.img, + container = () => container(user) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/dashboard/DashboardPage.scala b/app/src/main/scala/mdr/pdb/app/pages/dashboard/DashboardPage.scala new file mode 100644 index 0000000..0eb8f7a --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/dashboard/DashboardPage.scala @@ -0,0 +1,11 @@ +package mdr.pdb.app.pages.dashboard + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.Page + +object DashboardPage: + + def render: HtmlElement = + div("Dashboard page") diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala new file mode 100644 index 0000000..ff3a25f --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala @@ -0,0 +1,31 @@ +package mdr.pdb.app.pages.detail + +import com.raquo.laminar.api.L.{*, given} + +import components._ + +object DetailKriteriaPage: + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametr: DetailParametru.ViewModel, + kriterium: DetailKriteria.ViewModel + ) + + def apply($m: Signal[ViewModel])( + action: Observer[DukazyKriteria.Action] + ): HtmlElement = + div( + cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + div( + DetailOsoby.header($m.map(_.osoba)), + DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") + ), + div( + DetailKriteria($m.map(_.kriterium)), + DukazyKriteria($m.map(_.kriterium.dukazy))(action) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/DetailPage.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailPage.scala new file mode 100644 index 0000000..b591cc9 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailPage.scala @@ -0,0 +1,25 @@ +package mdr.pdb.app.pages.detail + +import com.raquo.laminar.api.L.{*, given} + +import components._ +import mdr.pdb.app.Page +import com.raquo.waypoint.Router +import mdr.pdb.app.Action + +object DetailPage: + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametry: SeznamParametru.ViewModel + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + DetailOsoby($m.map(_.osoba)), + SeznamParametru($m.map(_.parametry)) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/DetailParametruPage.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailParametruPage.scala new file mode 100644 index 0000000..3322279 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailParametruPage.scala @@ -0,0 +1,24 @@ +package mdr.pdb.app.pages.detail + +import com.raquo.laminar.api.L.{*, given} + +import components._ + +object DetailParametruPage: + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametr: DetailParametru.ViewModel, + kriteria: SeznamKriterii.ViewModel + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + DetailOsoby.header($m.map(_.osoba)), + DetailParametru($m.map(_.parametr)), + SeznamKriterii($m.map(_.kriteria)) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala new file mode 100644 index 0000000..b58618a --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala @@ -0,0 +1,107 @@ +package mdr.pdb +package app +package pages.detail + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.pages.detail.components.UpravDukazForm +import fiftyforms.services.files.File + +object UpravDukaz: + + type ThisPage = Page.UpravDukazKriteria + type PageKey = (OsobniCislo, String, String) + + trait State { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def availableFiles: EventStream[List[File]] + def actionBus: Observer[Action] + } + + def keyOfPage(page: ThisPage): PageKey = + (page.osobniCislo.value, page.parametr.value, page.kriterium.value) + + def onChangeAction(key: PageKey): Action = + FetchParameterCriteria( + key._1, + key._2, + key._3, + Page.UpravDukazKriteria(_, _, _) + ) + + class Connector(state: State)($page: Signal[ThisPage])(using Router[Page]): + val $paramChangeSignal = $page.splitOne(keyOfPage)((x, _, _) => x) + val $pageChangeSignal = $paramChangeSignal.map(onChangeAction) + + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + val $merged = + $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => + for { + da <- d + pa <- p + pb <- pa.find(_.id == pc._2) + ka <- pb.criteria.find(_.id == pc._3) + } yield (da, pb, ka) + ) + + def apply: HtmlElement = + AppPage(state.actionBus)( + $merged.split(_ => ())((_, s, $s) => + PageComponent( + $s.map(buildModel), + state.availableFiles, + state.actionBus.contramap { + case UpravDukazForm.Cancelled => + NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) + case UpravDukazForm.AvailableFilesRequested => + FetchAvailableFiles(s._1.personalNumber) + } + ) + ), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: Parameter, + k: ParameterCriteria + ): PageComponent.ViewModel = + import connectors.* + PageComponent.ViewModel( + o.toDetailOsoby, + p.toParametr(_ => a()), + k.toKriterium(_ => a()) + ) + + object PageComponent: + import components.* + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametr: DetailParametru.ViewModel, + kriterium: DetailKriteria.ViewModel + ) + + def apply( + $m: Signal[ViewModel], + availableFilesStream: EventStream[List[File]], + events: Observer[UpravDukazForm.Event] + ): HtmlElement = + div( + cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + div( + DetailOsoby.header($m.map(_.osoba)), + DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") + ), + div( + DetailKriteria($m.map(_.kriterium)), + UpravDukazForm(availableFilesStream)(events) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailKriteria.scala new file mode 100644 index 0000000..a3ab634 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailKriteria.scala @@ -0,0 +1,37 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.CustomAttrs +import fiftyforms.ui.components.tailwind.Icons +import java.time.LocalDate +import fiftyforms.ui.components.tailwind.Color + +object DetailKriteria: + case class ViewModel( + nazev: String, + kapitola: String, + bod: String, + status: String, + statusColor: Color, + splneno: Boolean, + dukazy: List[DukazKriteria.ViewModel], + container: HtmlElement = div() + ) { + val id = s"${kapitola}${bod}" + } + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "pb-5 border-b border-gray-200", + h3( + cls := "text-xl leading-6 font-bold text-gray-900", + child.text <-- $m.map(_.nazev) + ), + h3( + cls := "mt-2 max-w-4xl text-sm text-gray-500", + "Kapitola ", + child.text <-- $m.map(_.kapitola), + " bod ", + child <-- $m.map(_.bod) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailOsoby.scala new file mode 100644 index 0000000..0fe2ba5 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailOsoby.scala @@ -0,0 +1,100 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.OsobniCislo + +import java.time.LocalDate +import fiftyforms.ui.components.tailwind.Avatar + +object DetailOsoby: + + object Funkce: + case class ViewModel( + nazev: String, + stredisko: String, + voj: String + ) + + def render($m: Signal[ViewModel]): HtmlElement = + p( + cls := "text-sm font-medium text-gray-500", + child.text <-- $m.map(_.nazev), + span( + cls := "hidden md:inline", + " @ ", + child.text <-- $m.map(_.stredisko), + ", ", + child.text <-- $m.map(_.voj) + ) + ) + + object PracovniPomer: + case class ViewModel( + druh: String, + pocatek: LocalDate, + konec: Option[LocalDate] + ) + + def render($m: Signal[ViewModel]): HtmlElement = + import fiftyforms.ui.components.tailwind.CustomAttrs.datetime + p( + cls := "text-sm font-medium text-gray-500", + child.text <-- $m.map(_.druh), + " od ", + time( + datetime <-- $m.map(_.pocatek.toString), + child.text <-- $m.map(_.pocatek.toString) + ) + ) + + case class ViewModel( + osobniCislo: OsobniCislo, + jmeno: String, + email: Option[String], + telefon: Option[String], + img: Option[String], + hlavniFunkce: Option[Funkce.ViewModel], + pracovniPomer: Option[PracovniPomer.ViewModel] + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "md:flex md:items-center md:justify-between md:space-x-4", + div( + cls := "flex items-start space-x-4", + div( + cls := "flex-shrink-0", + Avatar($m.map(_.img)).avatar(16) + ), + div( + h1( + cls := "text-2xl font-bold text-gray-900", + child.text <-- $m.map(_.jmeno) + ), + child.maybe <-- $m.map(_.hlavniFunkce).split(_ => ())((_, _, d) => + Funkce.render(d) + ), + child.maybe <-- $m.map(_.pracovniPomer).split(_ => ())((_, _, d) => + PracovniPomer.render(d) + ) + ) + ) + ) + + def header($m: Signal[ViewModel]): HtmlElement = + div( + cls := "md:flex md:items-center md:justify-between md:space-x-5", + div( + cls := "flex items-start space-x-4", + div( + cls := "flex-shrink-0", + Avatar($m.map(_.img)).avatar(8) + ), + div( + h1( + cls := "text-lg font-medium text-gray-800", + child.text <-- $m.map(_.jmeno) + ) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailParametru.scala new file mode 100644 index 0000000..6310dde --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailParametru.scala @@ -0,0 +1,34 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.Color + +object DetailParametru: + case class ViewModel( + id: String, + nazev: String, + popis: String, + status: String, + statusColor: Color, + a: Anchor + ) + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "pb-5 border-b border-gray-200", + h2( + cls := "text-2xl leading-6 font-medium text-gray-900", + child.text <-- $m.map(_.nazev) + ), + p( + cls := "mt-2 max-w-4xl text-sm text-gray-500", + child.text <-- $m.map(_.popis) + ) + ) + + def header($m: Signal[ViewModel]): HtmlElement = + div( + h2( + cls := "text-lg leading-6 font-medium text-gray-600", + child.text <-- $m.map(_.nazev) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazKriteria.scala new file mode 100644 index 0000000..c292919 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazKriteria.scala @@ -0,0 +1,177 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import java.time.LocalDate +import fiftyforms.ui.components.tailwind.CustomAttrs + +object DukazKriteria: + case class Osoba(osobniCislo: String, jmeno: String) + case class Udalost(osoba: Osoba, datum: LocalDate) + case class Dokument( + url: String, + nazev: String, + pridal: Udalost, + odebral: Option[Udalost] + ) + case class ViewModel( + dokumenty: List[Dokument], + autorizoval: Option[Udalost], + platiDo: Option[LocalDate], + poznámka: Option[String] + ) + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "bg-white shadow overflow-hidden sm:rounded-lg", + div( + cls := "px-4 py-5 sm:px-6", + h3( + cls := "text-lg leading-6 font-medium text-gray-900", + """Applicant Information""" + ), + p( + cls := "mt-1 max-w-2xl text-sm text-gray-500", + """Personal details and application.""" + ) + ), + div( + cls := "border-t border-gray-200 px-4 py-5 sm:px-6", + dl( + cls := "grid grid-cols-1 gap-x-4 gap-y-8 sm:grid-cols-2", + div( + cls := "sm:col-span-1", + dt( + cls := "text-sm font-medium text-gray-500", + """Full name""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """Margot Foster""" + ) + ), + div( + cls := "sm:col-span-1", + dt( + cls := "text-sm font-medium text-gray-500", + """Application for""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """Backend Developer""" + ) + ), + div( + cls := "sm:col-span-1", + dt( + cls := "text-sm font-medium text-gray-500", + """Email address""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """margotfoster@example.com""" + ) + ), + div( + cls := "sm:col-span-1", + dt( + cls := "text-sm font-medium text-gray-500", + """Salary expectation""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """$120,000""" + ) + ), + div( + cls := "sm:col-span-2", + dt( + cls := "text-sm font-medium text-gray-500", + """About""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """Fugiat ipsum ipsum deserunt culpa aute sint do nostrud anim incididunt cillum culpa consequat. Excepteur qui ipsum aliquip consequat sint. Sit id mollit nulla mollit nostrud in ea officia proident. Irure nostrud pariatur mollit ad adipisicing reprehenderit deserunt qui eu.""" + ) + ), + div( + cls := "sm:col-span-2", + dt( + cls := "text-sm font-medium text-gray-500", + """Attachments""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + ul( + role := "list", + cls := "border border-gray-200 rounded-md divide-y divide-gray-200", + li( + cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", + div( + cls := "w-0 flex-1 flex items-center", { + import svg.* + import CustomAttrs.svg.ariaHidden + svg( + cls := "flex-shrink-0 h-5 w-5 text-gray-400", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + ariaHidden := true, + path( + fillRule := "evenodd", + d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", + clipRule := "evenodd" + ) + ) + }, + span( + cls := "ml-2 flex-1 w-0 truncate", + """resume_back_end_developer.pdf""" + ) + ), + div( + cls := "ml-4 flex-shrink-0", + a( + href := "#", + cls := "font-medium text-indigo-600 hover:text-indigo-500", + """Download""" + ) + ) + ), + li( + cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", + div( + cls := "w-0 flex-1 flex items-center", { + import svg.* + import CustomAttrs.svg.ariaHidden + svg( + cls := "flex-shrink-0 h-5 w-5 text-gray-400", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + ariaHidden := true, + path( + fillRule := "evenodd", + d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", + clipRule := "evenodd" + ) + ) + }, + span( + cls := "ml-2 flex-1 w-0 truncate", + """coverletter_back_end_developer.pdf""" + ) + ), + div( + cls := "ml-4 flex-shrink-0", + a( + href := "#", + cls := "font-medium text-indigo-600 hover:text-indigo-500", + """Download""" + ) + ) + ) + ) + ) + ) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala new file mode 100644 index 0000000..d604860 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala @@ -0,0 +1,35 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.Icons + +object DukazyKriteria: + sealed trait Action + case object Add extends Action + + type ViewModel = List[DukazKriteria.ViewModel] + + def apply($m: Signal[ViewModel])(actions: Observer[Action]): HtmlElement = + div( + child.maybe <-- $m.map(m => + if (m.isEmpty) then Some(prazdnyDukaz(actions)) + else None + ), + children <-- $m.map(_.zipWithIndex).split(_._2)((_, _, $s) => + DukazKriteria($s.map(_._1)) + ) + ) + + private def prazdnyDukaz(actions: Observer[Action]): HtmlElement = + button( + tpe := "button", + onClick.preventDefault.mapTo(Add) --> actions, + cls := "relative block w-full border-2 border-gray-300 border-dashed rounded-lg p-12 text-center hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + Icons.outline + .`document-add`(12) + .amend(svg.cls := "mx-auto text-gray-400"), + span( + cls := "mt-2 block text-sm font-medium text-gray-900", + "Přidat důkaz" + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala new file mode 100644 index 0000000..7e77a7b --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala @@ -0,0 +1,34 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.Color +import fiftyforms.ui.components.tailwind.list.StackedList +import fiftyforms.ui.components.tailwind.list.ListRow +import fiftyforms.ui.components.tailwind.list.RowTag +import fiftyforms.ui.components.tailwind.list.RowNext +import java.time.LocalDate + +object SeznamKriterii: + type ViewModel = List[DetailKriteria.ViewModel] + + private val kritList = new StackedList[DetailKriteria.ViewModel] + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "bg-white shadow overflow-hidden sm:rounded-md", + kritList($m, _.id) { $i => + $i.map { i => + ListRow.ViewModel( + title = i.nazev, + topRight = RowTag.render( + $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) + ), + bottomLeft = + p(cls := "text-sm text-gray-500", s"${i.kapitola}${i.bod}"), + bottomRight = emptyNode, + farRight = RowNext.render, + containerElement = i.container + ) + } + } + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamParametru.scala new file mode 100644 index 0000000..0760b32 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamParametru.scala @@ -0,0 +1,37 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.list.{ + StackedList, + ListRow, + RowTag, + PropList, + IconText, + RowNext +} +import fiftyforms.ui.components.tailwind.Color +import fiftyforms.ui.components.tailwind.LinkSupport.* + +object SeznamParametru: + type ViewModel = List[DetailParametru.ViewModel] + + private val parametrList = new StackedList[DetailParametru.ViewModel] + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "bg-white shadow overflow-hidden sm:rounded-md", + parametrList($m, _.id) { $i => + $i.map { i => + ListRow.ViewModel( + title = i.nazev, + topRight = RowTag.render( + $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) + ), + bottomLeft = emptyNode, + bottomRight = emptyNode, + farRight = RowNext.render, + containerElement = i.a + ) + } + } + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala new file mode 100644 index 0000000..28753c9 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala @@ -0,0 +1,101 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import io.laminext.syntax.core.* +import fiftyforms.ui.components.tailwind.CustomAttrs +import fiftyforms.ui.components.tailwind.form.* +import org.scalajs.dom +import com.raquo.laminar.nodes.ReactiveHtmlElement +import fiftyforms.services.files.components.tailwind.FilePicker +import mdr.pdb.frontend.AutorizujDukaz +import mdr.pdb.frontend.DocumentRef +import fiftyforms.services.files.File + +object UpravDukazForm: + sealed trait Event + case object Cancelled extends Event + case object AvailableFilesRequested extends Event + def apply(availableFilesStream: EventStream[List[File]])( + updates: Observer[Event] + ): HtmlElement = + val (filesStream, filesObserver) = + EventStream.withObserver[FilePicker.Event] + val files = Var[List[File]](Nil) + def submitButtons: HtmlElement = + div( + cls := "pt-5", + div( + cls := "flex justify-end", + button( + tpe := "button", + cls := "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + "Zrušit", + onClick.mapTo(Cancelled) --> updates + ), + button( + tpe := "submit", + cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + disabled <-- files.signal.map(_.isEmpty), + "Autorizovat" + ) + ) + ) + + div( + filesStream.collect { case FilePicker.SelectionUpdated(files) => + files.to(List) + } --> files.writer, + filesStream.collect { case FilePicker.AvailableFilesRequested => + AvailableFilesRequested + } --> updates, + cls := "bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6", + Form( + Form.Body( + Form.Section( + FormHeader( + FormHeader.ViewModel( + "Doložení kritéria", + "Sestavte doklady poskytující důkaz kritéria a potvrďte odesláním formuláře. Případné limitace či jiné relevantní údaje vepište do pole Komentář." + ) + ), + FormFields( + FormRow( + "dokumenty", + "Dokumenty", + FilePicker(files.signal, availableFilesStream)(filesObserver) + .amend(idAttr := "dokumenty", cls("max-w-lg")) + ).toHtml, + FormRow( + "platnost", + "Platnost", + input( + idAttr := "platnost", + name := "platnost", + tpe := "date", + autoComplete := "date", + cls := "block max-w-lg w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" + ) + ).toHtml, + FormRow( + "komentar", + "Komentář", + div( + cls := "mt-1 sm:mt-0 sm:col-span-2", + textArea( + idAttr := "komentar", + name := "about", + rows := 3, + cls := "max-w-lg shadow-sm block w-full focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border border-gray-300 rounded-md" + ), + p( + cls := "mt-2 text-sm text-gray-500", + "Doplňte prosím potřebné informace související s doložením kritéria, včetně případných limitací." + ) + ) + ).toHtml + ) + ) + ), + submitButtons + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/directory/DirectoryPage.scala b/app/src/main/scala/mdr/pdb/app/pages/directory/DirectoryPage.scala new file mode 100644 index 0000000..7a72ca3 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/directory/DirectoryPage.scala @@ -0,0 +1,40 @@ +package mdr.pdb.app.pages.directory + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.UserInfo + +import components._ + +object DirectoryPage: + + type ViewModel = List[UserRow.ViewModel] + + def apply($m: Signal[ViewModel]): HtmlElement = + val (actionsStream, actionObserver) = + EventStream.withObserver[SearchForm.Action] + val $filter = actionsStream + .collect { + case SearchForm.ClearFilter => None + case SearchForm.SetFilter(t) => Some(t) + } + .startWith(None) + val byLetter = for { + d <- $m + f <- $filter + } yield for { + (letter, users) <- d + .filter { user => + f match + case None => true + case Some(t) => user.search.contains(t) + } + .groupBy(_.prijmeni.head) + .to(List) + .sortBy(_._1) + } yield (letter.toString, users.sortBy(_.prijmeni)) + + div( + cls := "h-full max-w-7xl mx-auto order-first flex flex-col flex-shrink-0", + SearchForm(actionObserver), + Directory(byLetter) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/directory/components/Directory.scala b/app/src/main/scala/mdr/pdb/app/pages/directory/components/Directory.scala new file mode 100644 index 0000000..f7d6f99 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/directory/components/Directory.scala @@ -0,0 +1,37 @@ +package mdr.pdb.app.pages.directory.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.UserInfo + +object Directory: + + object Header: + type ViewModel = String + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500", + h3(child.text <-- $m) + ) + + object UserList: + type ViewModel = List[UserRow.ViewModel] + def apply($m: Signal[ViewModel]): HtmlElement = + ul( + role := "list", + cls := "relative z-0 divide-y divide-gray-200", + children <-- $m.split(_.osobniCislo)((_, _, s) => UserRow(s)) + ) + + type ViewModel = List[(String, List[UserRow.ViewModel])] + def apply($m: Signal[ViewModel]): HtmlElement = + nav( + cls := "flex-1 min-h-0 overflow-y-auto", + aria.label := "Directory", + children <-- $m.split(_._1)((_, _, s) => + div( + cls := "relative", + Header(s.map(_._1)), + UserList(s.map(_._2)) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/directory/components/SearchForm.scala b/app/src/main/scala/mdr/pdb/app/pages/directory/components/SearchForm.scala new file mode 100644 index 0000000..fb9ecc4 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/directory/components/SearchForm.scala @@ -0,0 +1,54 @@ +package mdr.pdb.app.pages.directory.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.Icons + +object SearchForm: + sealed trait Action + case object SubmitFilter extends Action + sealed trait FilterAction extends Action + case object ClearFilter extends FilterAction + case class SetFilter(value: String) extends FilterAction + + def apply(actions: Observer[Action]): HtmlElement = + div( + cls := "px-6 pt-4 pb-4", + form( + cls := "flex space-x-4", + onSubmit.mapTo(SubmitFilter) --> actions, + div( + cls := "flex-1 min-w-0", + label( + forId := "search", + cls := "sr-only", + "Hledat" + ), + div( + cls := "relative rounded-md shadow-sm", + div( + cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", + Icons.solid.search().amend(svg.cls := "text-gray-400") + ), + input( + tpe := "search", + name := "search", + idAttr := "search", + cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", + placeholder := "Hledat", + composeEvents(onInput.mapToValue.setAsValue.map(SetFilter(_)))( + _.throttle(500) + ) --> actions + ) + ) + ), + button( + tpe := "submit", + cls := "inline-flex justify-center px-3.5 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500", + Icons.solid.filter().amend(svg.cls := "text-gray-400"), + span( + cls := "sr-only", + "Hledat" + ) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/directory/components/UserRow.scala b/app/src/main/scala/mdr/pdb/app/pages/directory/components/UserRow.scala new file mode 100644 index 0000000..4aa1672 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/directory/components/UserRow.scala @@ -0,0 +1,52 @@ +package mdr.pdb.app.pages.directory.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.airstream.core.Signal +import fiftyforms.ui.components.tailwind.Avatar + +object UserRow: + case class ViewModel( + osobniCislo: String, + celeJmeno: String, + prijmeni: String, + hlavniFunkce: Option[String], + img: Option[String], + container: () => HtmlElement = () => div() + ) { + val search = prijmeni.toLowerCase + } + + def apply($m: Signal[ViewModel]): HtmlElement = + inline def avatarImage = + Avatar($m.map(_.img)).avatarImage(10) + + li( + div( + cls := "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500", + div( + cls := "flex-shrink-0", + child <-- avatarImage + ), + div( + cls := "flex-1 min-w-0", + child <-- $m.map { o => + o.container() + .amend( + cls := "focus:outline-none", + span( + cls := "absolute inset-0", + aria.hidden := true + ), + p( + cls := "text-sm font-medium text-gray-900", + o.celeJmeno + ), + p( + cls := "text-sm text-gray-500 truncate", + o.hlavniFunkce + ) + ) + } + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/errors/ErrorPage.scala b/app/src/main/scala/mdr/pdb/app/pages/errors/ErrorPage.scala new file mode 100644 index 0000000..6b195ef --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/errors/ErrorPage.scala @@ -0,0 +1,99 @@ +package mdr.pdb.app.pages.errors + +import com.raquo.domtypes.generic.codecs.StringAsIsCodec +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.Page +import mdr.pdb.app.Action +import com.raquo.waypoint.Router + +object ErrorPage: + case class ViewModel( + homePage: Page, + errorName: String, + title: String, + subTitle: String + ) + + def apply(m: ViewModel, actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + val ViewModel(homePage, errorName, title, subTitle) = m + div( + cls := "min-h-full pt-16 pb-12 flex flex-col bg-white", + main( + cls := "flex-grow flex flex-col justify-center max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", + div( + cls := "flex-shrink-0 flex justify-center", + a( + href := "/", + cls := "inline-flex", + span( + cls := "sr-only", + """Workflow""" + ), + img( + cls := "h-12 w-auto", + src := "https://tailwindui.com/img/logos/workflow-mark.svg?color=indigo&shade=600", + alt := "" + ) + ) + ), + div( + cls := "py-16", + div( + cls := "text-center", + p( + cls := "text-sm font-semibold text-indigo-600 uppercase tracking-wide", + errorName + ), + h1( + cls := "mt-2 text-4xl font-extrabold text-gray-900 tracking-tight sm:text-5xl", + title + ), + p( + cls := "mt-2 text-base text-gray-500", + subTitle + ), + div( + cls := "mt-6", + PageLink + .container(homePage, actionBus) + .amend( + cls := "text-base font-medium text-indigo-600 hover:text-indigo-500", + """Go back home""" + ) + ) + ) + ) + ), + footer( + cls := "flex-shrink-0 max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", + nav( + cls := "flex justify-center space-x-4", + a( + href := "#", + cls := "text-sm font-medium text-gray-500 hover:text-gray-600", + """Contact Support""" + ), + span( + cls := "inline-block border-l border-gray-300", + aria.hidden := true + ), + a( + href := "#", + cls := "text-sm font-medium text-gray-500 hover:text-gray-600", + """Status""" + ), + span( + cls := "inline-block border-l border-gray-300", + aria.hidden := true + ), + a( + href := "#", + cls := "text-sm font-medium text-gray-500 hover:text-gray-600", + """Twitter""" + ) + ) + ) + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala deleted file mode 100644 index a3b00af..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala +++ /dev/null @@ -1,103 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import scala.scalajs.js.annotation.JSExportTopLevel -import scala.scalajs.js.annotation.JSExport -import scala.scalajs.js.annotation.JSImport -import scala.scalajs.js -import org.scalajs.dom -import com.raquo.laminar.api.L.{*, given} -import scala.scalajs.js.Date -import com.raquo.waypoint.Router -import com.raquo.waypoint.SplitRender -import cz.e_bs.cmi.mdr.pdb.app.services.DataFetcher -import scala.scalajs.js.JSON -import zio.json._ -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.state.AppState - -@js.native -@JSImport("stylesheets/main.css", JSImport.Namespace) -object Css extends js.Any - -@js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native -@JSImport("params/pdb-params.json", JSImport.Default) -object pdbParams extends js.Object - -@JSExportTopLevel("app") -object Main: - - @JSExport - def main(args: Array[String]): Unit = - import Routes.given - onLoad { - setupAirstream() - val appContainer = dom.document.querySelector("#app") - val _ = - render( - appContainer, - renderPage(state.MockAppState(using unsafeWindowOwner, router)) - ) - } - - private def onLoad(f: => Unit): Unit = - documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) - - private def setupAirstream()(using router: Router[Page]): Unit = - AirstreamError.registerUnhandledErrorCallback(err => - router.forcePage( - Page.UnhandledError( - Some(err.getClass.getName), // TODO: Fill only in dev mode - Some(err.getMessage) - ) - ) - ) - - def renderPage(state: AppState)(using - router: Router[Page] - ): HtmlElement = - val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) - .collectSignal[Page.Detail]( - connectors - .DetailPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailParametru]( - connectors - .DetailParametruPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailKriteria]( - connectors - .DetailKriteriaPageConnector(state)(_) - .apply - ) - .collectSignal[Page.UpravDukazKriteria]( - pages.detail.UpravDukaz.Connector(state)(_).apply - ) - .collectStatic(Page.Dashboard)( - connectors.DashboardPageConnector(state.actionBus).apply - ) - .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) - ) - .collect[Page.UnhandledError](pg => - pages.errors - .UnhandledErrorPage( - pages.errors.UnhandledErrorPage - .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), - state.actionBus - ) - ) - .collectStatic(Page.Directory)( - connectors - .DirectoryPageConnector(state.users, state.actionBus) - .apply - ) - div(cls := "h-full", child <-- pageSplitter.$view) - - // Pull in the stylesheet - val css: Css.type = Css diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala deleted file mode 100644 index 3bbc8c0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ /dev/null @@ -1,198 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.* -import org.scalajs.dom -import zio.json.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import scala.scalajs.js -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.Page.Titled - -// enum is not working with Waypoints' SplitRender collectStatic -sealed abstract class Page( - val id: String, - val title: String, - val parent: Option[Page] -) { - val path: Vector[Page] = - parent match - case None => Vector(this) - case Some(p) => p.path :+ this - - val isRoot: Boolean = parent.isEmpty -} - -object Page: - - case class Titled[V](value: V, title: Option[String] = None): - val show: String = title.getOrElse(value.toString) - - case object Directory extends Page("directory", "Adresář", None) - - case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) - - case class Detail(osobniCislo: Titled[OsobniCislo]) - extends Page("user", osobniCislo.show, Some(Directory)) - - object Detail { - def apply(o: UserInfo): Detail = Detail( - Titled(o.personalNumber, Some(o.name)) - ) - } - - case class DetailParametru( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String] - ) extends Page( - "parameter", - parametr.show, - Some(Detail(osobniCislo)) - ) - - object DetailParametru { - def apply(o: UserInfo, p: Parameter): DetailParametru = - DetailParametru( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)) - ) - } - - case class DetailKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "criteria", - kriterium.show, - Some(DetailParametru(osobniCislo, parametr)) - ) - - object DetailKriteria { - def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = - DetailKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class UpravDukazKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "addProof", - "Důkaz", - Some(DetailKriteria(osobniCislo, parametr, kriterium)) - ) - - object UpravDukazKriteria { - def apply( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): UpravDukazKriteria = - UpravDukazKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class NotFound(url: String) extends Page("404", "404", Some(Directory)) - - case class UnhandledError( - errorName: Option[String], - errorMessage: Option[String] - ) extends Page("500", "Unexpected error", Some(Directory)) - -object Routes: - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) - given [V: JsonEncoder]: JsonEncoder[Titled[V]] = - DeriveJsonEncoder.gen[Titled[V]] - given [V: JsonDecoder]: JsonDecoder[Titled[V]] = - DeriveJsonDecoder.gen[Titled[V]] - given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] - given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] - - val base = - js.`import`.meta.env.BASE_URL - .asInstanceOf[String] + "app" - - val homePage: Page = Page.Directory - - given router: Router[Page] = Router[Page]( - routes = List( - Route.static(homePage, root / endOfSegments, basePath = base), - Route.static( - Page.Dashboard, - root / "dashboard" / endOfSegments, - basePath = base - ), - Route[Page.Detail, String]( - encode = _.osobniCislo.value.toString, - decode = osc => Page.Detail(Titled(OsobniCislo(osc))), - root / "osoba" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.DetailParametru, (String, String)]( - encode = p => (p.osobniCislo.value.toString, p.parametr.value), - decode = - p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / endOfSegments, - basePath = base - ), - Route[Page.DetailKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.DetailKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.UpravDukazKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.UpravDukazKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / "edit" / endOfSegments, - basePath = base - ) - ), - serializePage = _.toJson, - deserializePage = _.fromJson[Page] - .fold(s => throw IllegalStateException(s), identity), - getPageTitle = _.title, - routeFallback = url => Page.NotFound(url), - deserializeFallback = _ => Page.Dashboard - )( - $popStateEvent = windowEvents.onPopState, - owner = unsafeWindowOwner - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala deleted file mode 100644 index b24b82b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -sealed trait Action - -case object FetchDirectory extends Action -case class FetchUserDetails(osc: OsobniCislo) extends Action -case class FetchParameters(osc: OsobniCislo) extends Action -case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action -case class FetchParameterCriteria( - osc: OsobniCislo, - paramId: String, - critId: String, - page: (UserInfo, Parameter, ParameterCriteria) => Page -) extends Action -case class FetchAvailableFiles(osc: OsobniCislo) extends Action -case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala deleted file mode 100644 index c637d35..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala +++ /dev/null @@ -1,82 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.{UserProfile, UserInfo, OsobniCislo} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo -import cz.e_bs.cmi.mdr.pdb.UserFunction - -object AppPage: - // TODO: pages by logged in user - val pages: List[Page] = List(Page.Directory, Page.Dashboard) - - import NavigationBar.{Logo, MenuItem} - - val logo = Logo( - "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", - "Workflow" - ) - - // TODO: menu items by user profile - val userMenu = - List( - MenuItem("Your Profile"), - MenuItem("Settings"), - MenuItem("Sign out") - ) - - // TODO: load user profile - val $userProfile = Var( - UserProfile( - "tom", - UserInfo( - OsobniCislo("1031"), - "tom", - "Tom", - "Cook", - None, - None, - Some("tom@example.com"), - Some("+420 222 866 180"), - Some( - UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") - ), - Nil, - None - ) - ) - ) - - val $userInfo = $userProfile.signal.map(_.userInfo) - - type ViewModel = Option[HtmlElement] - def apply( - actionBus: Observer[Action] - )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using - router: Router[Page] - ): HtmlElement = - PageLayout(actionBus)( - $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => - PageLayout.ViewModel( - NavigationBar.ViewModel( - u, - pages.map(p => - NavigationBar.Link( - () => - PageLink( - p, - actionBus - ), - p == cp - ) - ), - userMenu, - logo - ), - c - ) - ), - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala deleted file mode 100644 index bf473c6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ /dev/null @@ -1,143 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden -import fiftyforms.ui.components.tailwind.list.IconText.ViewModel -import fiftyforms.ui.components.tailwind.Icons -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Action - -object Breadcrumbs: - - private def slash = - import svg.{*, given} - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-300", - xmlns := "http://www.w3.org/2000/svg", - fill := "currentColor", - viewBox := "0 0 20 20", - ariaHidden := true, - path( - d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" - ) - ) - - def backIcon = - Icons.solid - .`arrow-narrow-left`() - .amend( - svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" - ) - - object Link: - case class ViewModel( - page: Page, - icon: Option[SvgElement], - extraClasses: String, - text: String, - textClasses: Option[String] = None - ) - - val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" - - def shortHome(p: Page) = ViewModel( - p, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - "Zpět na úvodní stránku", - None - ) - - def fullHome(p: Page) = ViewModel( - p, - Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), - "text-gray-400 hover:text-gray-500", - "Domů", - Some("sr-only") - ) - - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - PageLink - .container($m.map(_.page), actionBus) - .amend( - cls <-- $m.map(_.extraClasses), - child.maybe <-- $m.map(_.icon), - span( - cls <-- $m.map(_.textClasses.getOrElse("")), - child.text <-- $m.map(_.text) - ) - ) - - object FullBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - ol( - role := "list", - cls := "flex items-center space-x-4", - children <-- $m.map(_.path).split(_.id)((_, _, $p) => - li( - div( - cls := "flex items-center", - child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), - Link( - $p.map(p => - if (p.isRoot) then Link.fullHome(p) - else - Link.ViewModel( - p, - None, - s"ml-4 max-w-xs truncate ${Link.baseClasses}", - p.title - ) - ), - actionBus - ) - ) - ) - ) - ) - - object ShortBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - Link( - $m.map { p => - val target = p.parent.getOrElse(p) - if target.isRoot then Link.shortHome(target) - else - Link.ViewModel( - target, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - s"Zpět na ${target.title}" - ) - }, - actionBus - ) - - def apply(actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - nav( - cls := "flex", - aria.label := "Breadcrumb", - div( - cls := "flex sm:hidden", - ShortBreadcrumbs(router.$currentPage, actionBus) - ), - div( - cls := "hidden sm:block", - FullBreadcrumbs(router.$currentPage, actionBus) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala deleted file mode 100644 index 0db2047..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ /dev/null @@ -1,242 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent -import fiftyforms.ui.components.tailwind.Icons -import fiftyforms.ui.components.tailwind.Avatar -import cz.e_bs.cmi.mdr.pdb.UserInfo -import io.laminext.syntax.core.* - -object NavigationBar: - - case class Logo(img: String, name: String) - case class Link(a: () => Anchor, active: Boolean) - case class MenuItem(title: String) - - case class ViewModel( - userInfo: UserInfo, - pages: List[Link], - userMenu: List[MenuItem], - logo: Logo - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - val $userInfo = $m.map(_.userInfo) - val mobileMenuOpen = Var(false) - - val desktopOnly = cls("hidden md:block") - val mobileOnly = cls("md:hidden") - - inline def avatarImage(size: Int = 8) = - Avatar($userInfo.map(_.img)).avatarImage(size) - - def notificationButton = button( - tpe := "button", - cls := List( - "bg-indigo-600", - "focus:outline-none", - "focus:ring-2", - "focus:ring-offset-2", - "focus:ring-offset-indigo-600", - "focus:ring-white", - "hover:text-white", - "p-1", - "rounded-full", - "text-indigo-200" - ), - span(cls := "sr-only", "View notifications"), - Icons.outline.bell() - ) - - def userProfile: HtmlElement = - val menuOpen = Var(false) - - def menuItem(item: MenuItem, idx: Int): HtmlElement = - a( - href := "#", - cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", - role := "menuitem", - tabIndex := -1, - idAttr := s"user-menu-item-$idx", - item.title - ) - - div( - cls := "ml-3 relative", - div( - button( - tpe := "button", - cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - idAttr := "user-menu-button", - aria.expanded <-- menuOpen.signal, - aria.hasPopup := true, - span(cls := "sr-only", "Open user menu"), - child <-- avatarImage(), - onClick.preventDefault.mapTo( - !menuOpen.now() - ) --> menuOpen.writer - ) - ), - /* - * */ - div( - cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", - cls <-- menuOpen.signal.map { o => - if (o) "md:block" else "md:hidden" - }, - role := "menu", - aria.orientation := "vertical", - aria.labelledBy := "user-menu-button", - tabIndex := -1, - // : keyboard navigation - children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) - ) - ) - - def mobileProfile = - def menuItem(item: MenuItem): HtmlElement = - a( - href := "#", - cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", - item.title - ) - - div( - cls := "pt-4 pb-3 border-t border-indigo-700", - div( - cls := "flex items-center px-5", - div( - cls := "flex-shrink-0", - child <-- avatarImage(10) - ), - div( - cls := "ml-3", - div( - cls := "text-base font-medium text-white", - child.text <-- $userInfo.map(_.name) - ), - child.maybe <-- $userInfo.map( - _.email.map(e => - div( - cls := "text-sm font-medium text-indigo-300", - e - ) - ) - ) - ), - notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) - ), - div( - cls := "mt-3 px-2 space-y-1", - children <-- $m.map(_.userMenu.map(menuItem)) - ) - ) - - def pageLink(page: Link): Anchor = - page - .a() - .amend( - cls := "text-white px-3 py-2 rounded-md text-sm font-medium", - cls := Seq( - "bg-indigo-700" -> page.active, - "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active - ), - ariaCurrent := (if page.active then "page" else "false") - ) - - def logoImg: Image = - img( - cls := "h-8 w-8", - src <-- $m.map(_.logo.img), - alt <-- $m.map(_.logo.name) - ) - - def mobileMenuButton = button( - tpe := "button", - cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - aria.controls := "mobile-menu", - aria.expanded <-- mobileMenuOpen.signal, - span(cls := "sr-only", "Open main menu"), - Icons.outline - .menu() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") - ), - Icons.outline - .x() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") - ), - onClick.preventDefault.mapTo( - !mobileMenuOpen.now() - ) --> mobileMenuOpen.writer - ) - - def navBarLeft = - div( - cls := "flex items-center", - div(cls := "flex-shrink-0", logoImg), - div( - desktopOnly, - div( - cls := "ml-10 flex items-baseline space-x-4", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ) - ) - ) - - def navBarRight = - div( - desktopOnly, - div( - cls := "ml-4 flex items-center md:ml-6", - notificationButton, - userProfile - ) - ) - - def navBarMobile = - div( - cls := "-mr-2 flex", - mobileOnly, - mobileMenuButton - ) - - def navBar = - div( - cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex items-center justify-between h-16", - navBarLeft, - navBarRight, - navBarMobile - ) - ) - - def mobileMenu = - div( - mobileOnly, - cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }, - idAttr := "mobile-menu", - div( - cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ), - mobileProfile - ) - - nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala deleted file mode 100644 index 75339df..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object PageHeader: - def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = - header( - cls := "bg-white shadow-sm", - div( - cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", - h1( - cls := "text-lg leading-6 font-semibold text-gray-900", - Breadcrumbs(actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala deleted file mode 100644 index 5096463..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ /dev/null @@ -1,30 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import fiftyforms.ui.components.tailwind.Loading -import io.laminext.syntax.core.* - -object PageLayout: - case class ViewModel( - navigation: NavigationBar.ViewModel, - content: Option[HtmlElement] - ) - def apply(actionBus: Observer[Action])( - $m: Signal[ViewModel], - mods: Modifier[HtmlElement]* - )(using router: Router[Page]): HtmlElement = - val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) - div( - cls := "h-full flex flex-col", - NavigationBar($m.map(_.navigation)), - child.maybe <-- router.$currentPage.map(_.isRoot) - .switch(None, Some(PageHeader(actionBus))), - main( - cls := "flex-grow-1 overflow-y-auto", - mods, - child <-- $maybeContent.map(_.getOrElse(Loading)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala deleted file mode 100644 index 62d820f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.LinkSupport.* -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo - -object PageLink: - type ViewModel = Page - def apply($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = - a(mods($m, actions), child.text <-- $m.map(_.title)) - - def apply(m: ViewModel, actions: Observer[Action])(using - router: Router[Page] - ): Anchor = apply(Val(m), actions) - - def container($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = a(mods($m, actions)) - - def container(m: ViewModel, actions: Observer[Action])(using - Router[Page] - ): Anchor = container(Val(m), actions) - - private def mods($m: Signal[Page], actions: Observer[Action])(using - router: Router[Page] - ): Modifier[Anchor] = - Seq( - href <-- $m.map(router.absoluteUrlForPage).recover { case _ => - Some("invalid url") - }, - composeEvents(onClick.noKeyMod.preventDefault)( - _.sample($m) - .map(NavigateTo.apply) - ) --> actions - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala deleted file mode 100644 index 87f825a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.pages.dashboard.DashboardPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -class DashboardPageConnector(actionBus: Observer[Action])(using - router: Router[Page] -): - def apply: HtmlElement = - AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala deleted file mode 100644 index e9b0f71..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala +++ /dev/null @@ -1,76 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailKriteriaPage -import pages.detail.components.DukazyKriteria -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -object DetailKriteriaPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailKriteriaPageConnector( - state: DetailKriteriaPageConnector.AppState -)( - $page: Signal[Page.DetailKriteria] -)(using Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => - (p.osobniCislo.value, p.parametr.value, p.kriterium.value) - )((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map( - FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) - ) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, s, $s) => - DetailKriteriaPage($s)(state.actionBus.contramap { - case DukazyKriteria.Add => - NavigateTo( - Page.UpravDukazKriteria( - Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), - Page.Titled(s.parametr.id, Some(s.parametr.nazev)), - Page.Titled(s.kriterium.id, Some(s.kriterium.id)) - ) - ) - }) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): DetailKriteriaPage.ViewModel = - DetailKriteriaPage.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala deleted file mode 100644 index 4d8024f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ /dev/null @@ -1,57 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import pages.detail.DetailPage -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import fiftyforms.ui.components.tailwind.Color - -object DetailPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailPageConnector(state: DetailPageConnector.AppState)( - $page: Signal[Page.Detail] -)(using router: Router[Page]): - val $oscChangeSignal = $page.splitOne(_.osobniCislo.value)((osc, _, _) => osc) - val $pageChangeSignal = - $oscChangeSignal.flatMap(osc => - EventStream.fromSeq(Seq(FetchUserDetails(osc), FetchParameters(osc))) - ) - // TODO: filter the value based on the current osc - // OSC change will fetch new data, but still - // - we need to be sure that what we got is really what we ought to display - // - we want to display stale data accordingly (at least with loading indicator) - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - def apply: HtmlElement = - AppPage(state.actionBus)( - $data.combineWithFn($params)(_ zip _) - .map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: List[Parameter] - ): DetailPage.ViewModel = - DetailPage.ViewModel( - o.toDetailOsoby, - p.map( - _.toParametr(param => - PageLink.container(Page.DetailParametru(o, param), state.actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala deleted file mode 100644 index fdaa7f8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailParametruPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink - -object DetailParametruPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailParametruPageConnector( - state: DetailParametruPageConnector.AppState -)( - $page: Signal[Page.DetailParametru] -)(using router: Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => (p.osobniCislo.value, p.parametr.value))((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map(FetchParameter(_, _)) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - } yield (da, pb) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailParametruPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter - ): DetailParametruPage.ViewModel = - DetailParametruPage.ViewModel( - o.toDetailOsoby, - p.toParametr(p => - PageLink.container(Page.DetailParametru(o, p), state.actionBus) - ), - p.criteria.map( - _.toKriterium { c => - PageLink.container( - Page.DetailKriteria(o, p, c), - state.actionBus - ) - } - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala deleted file mode 100644 index 049aa22..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -case class DirectoryPageConnector( - $input: EventStream[List[UserInfo]], - actionBus: Observer[Action] -)(using router: Router[Page]): - val $data = $input.startWithNone - val $actionSignal = EventStream.fromValue(FetchDirectory) - - def apply: HtmlElement = - AppPage(actionBus)( - $data.split(_ => ())((_, _, s) => - pages.directory.DirectoryPage( - s.map( - _.map( - _.toUserRow(u => - PageLink.container( - Page.Detail(Page.Titled(u.personalNumber)), - actionBus - ) - ) - ) - ) - ) - ), - $actionSignal --> actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala deleted file mode 100644 index 88e7450..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ /dev/null @@ -1,68 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii -import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria -import fiftyforms.ui.components.tailwind.Color - -extension (o: UserInfo) - def toDetailOsoby: DetailOsoby.ViewModel = - DetailOsoby.ViewModel( - o.personalNumber, - o.name, - o.email, - o.phone, - o.img, - o.mainFunction.map(f => - DetailOsoby.Funkce.ViewModel(f.name, f.dept, f.ou) - ), - o.userContracts.headOption.map(c => - DetailOsoby.PracovniPomer.ViewModel(c.rel, c.startDate, c.endDate) - ) - ) - -extension (param: Parameter) - def toParametr(container: Parameter => Anchor): DetailParametru.ViewModel = - DetailParametru.ViewModel( - id = param.id, - nazev = param.name, - popis = param.description, - status = "Nesplněno", - statusColor = Color.red, - a = container(param) - ) - -extension (crit: ParameterCriteria) - def toKriterium( - container: ParameterCriteria => Anchor - ): DetailKriteria.ViewModel = - DetailKriteria.ViewModel( - nazev = crit.criteriumText, - kapitola = crit.chapterId, - bod = crit.itemId, - status = "Nesplněno", - statusColor = Color.red, - splneno = false, - dukazy = Nil, - container = container(crit) - ) - -extension (user: UserInfo) - def toUserRow( - container: UserInfo => HtmlElement = (_: UserInfo) => div() - ): UserRow.ViewModel = - UserRow.ViewModel( - osobniCislo = user.personalNumber.toString, - celeJmeno = user.name, - prijmeni = user.surname, - hlavniFunkce = user.mainFunction.map(_.name), - img = user.img, - container = () => container(user) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala deleted file mode 100644 index 7d5b2c1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.dashboard - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.Page - -object DashboardPage: - - def render: HtmlElement = - div("Dashboard page") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala deleted file mode 100644 index 9ef0698..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala +++ /dev/null @@ -1,31 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailKriteriaPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply($m: Signal[ViewModel])( - action: Observer[DukazyKriteria.Action] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - DukazyKriteria($m.map(_.kriterium.dukazy))(action) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala deleted file mode 100644 index 3b316e1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object DetailPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametry: SeznamParametru.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby($m.map(_.osoba)), - SeznamParametru($m.map(_.parametry)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala deleted file mode 100644 index 17224cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala +++ /dev/null @@ -1,24 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailParametruPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriteria: SeznamKriterii.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby.header($m.map(_.osoba)), - DetailParametru($m.map(_.parametr)), - SeznamKriterii($m.map(_.kriteria)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala deleted file mode 100644 index dc912fa..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ /dev/null @@ -1,107 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb -package app -package pages.detail - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.UpravDukazForm -import fiftyforms.services.files.File - -object UpravDukaz: - - type ThisPage = Page.UpravDukazKriteria - type PageKey = (OsobniCislo, String, String) - - trait State { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def availableFiles: EventStream[List[File]] - def actionBus: Observer[Action] - } - - def keyOfPage(page: ThisPage): PageKey = - (page.osobniCislo.value, page.parametr.value, page.kriterium.value) - - def onChangeAction(key: PageKey): Action = - FetchParameterCriteria( - key._1, - key._2, - key._3, - Page.UpravDukazKriteria(_, _, _) - ) - - class Connector(state: State)($page: Signal[ThisPage])(using Router[Page]): - val $paramChangeSignal = $page.splitOne(keyOfPage)((x, _, _) => x) - val $pageChangeSignal = $paramChangeSignal.map(onChangeAction) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.split(_ => ())((_, s, $s) => - PageComponent( - $s.map(buildModel), - state.availableFiles, - state.actionBus.contramap { - case UpravDukazForm.Cancelled => - NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) - case UpravDukazForm.AvailableFilesRequested => - FetchAvailableFiles(s._1.personalNumber) - } - ) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): PageComponent.ViewModel = - import connectors.* - PageComponent.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) - - object PageComponent: - import components.* - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply( - $m: Signal[ViewModel], - availableFilesStream: EventStream[List[File]], - events: Observer[UpravDukazForm.Event] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - UpravDukazForm(availableFilesStream)(events) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala deleted file mode 100644 index ca311b0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.Icons -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Color - -object DetailKriteria: - case class ViewModel( - nazev: String, - kapitola: String, - bod: String, - status: String, - statusColor: Color, - splneno: Boolean, - dukazy: List[DukazKriteria.ViewModel], - container: HtmlElement = div() - ) { - val id = s"${kapitola}${bod}" - } - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h3( - cls := "text-xl leading-6 font-bold text-gray-900", - child.text <-- $m.map(_.nazev) - ), - h3( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - "Kapitola ", - child.text <-- $m.map(_.kapitola), - " bod ", - child <-- $m.map(_.bod) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala deleted file mode 100644 index ae90290..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala +++ /dev/null @@ -1,100 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Avatar - -object DetailOsoby: - - object Funkce: - case class ViewModel( - nazev: String, - stredisko: String, - voj: String - ) - - def render($m: Signal[ViewModel]): HtmlElement = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.nazev), - span( - cls := "hidden md:inline", - " @ ", - child.text <-- $m.map(_.stredisko), - ", ", - child.text <-- $m.map(_.voj) - ) - ) - - object PracovniPomer: - case class ViewModel( - druh: String, - pocatek: LocalDate, - konec: Option[LocalDate] - ) - - def render($m: Signal[ViewModel]): HtmlElement = - import fiftyforms.ui.components.tailwind.CustomAttrs.datetime - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.druh), - " od ", - time( - datetime <-- $m.map(_.pocatek.toString), - child.text <-- $m.map(_.pocatek.toString) - ) - ) - - case class ViewModel( - osobniCislo: OsobniCislo, - jmeno: String, - email: Option[String], - telefon: Option[String], - img: Option[String], - hlavniFunkce: Option[Funkce.ViewModel], - pracovniPomer: Option[PracovniPomer.ViewModel] - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-4", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(16) - ), - div( - h1( - cls := "text-2xl font-bold text-gray-900", - child.text <-- $m.map(_.jmeno) - ), - child.maybe <-- $m.map(_.hlavniFunkce).split(_ => ())((_, _, d) => - Funkce.render(d) - ), - child.maybe <-- $m.map(_.pracovniPomer).split(_ => ())((_, _, d) => - PracovniPomer.render(d) - ) - ) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-5", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(8) - ), - div( - h1( - cls := "text-lg font-medium text-gray-800", - child.text <-- $m.map(_.jmeno) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala deleted file mode 100644 index e11cb62..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color - -object DetailParametru: - case class ViewModel( - id: String, - nazev: String, - popis: String, - status: String, - statusColor: Color, - a: Anchor - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h2( - cls := "text-2xl leading-6 font-medium text-gray-900", - child.text <-- $m.map(_.nazev) - ), - p( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - child.text <-- $m.map(_.popis) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - h2( - cls := "text-lg leading-6 font-medium text-gray-600", - child.text <-- $m.map(_.nazev) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala deleted file mode 100644 index a00ede8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala +++ /dev/null @@ -1,177 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.CustomAttrs - -object DukazKriteria: - case class Osoba(osobniCislo: String, jmeno: String) - case class Udalost(osoba: Osoba, datum: LocalDate) - case class Dokument( - url: String, - nazev: String, - pridal: Udalost, - odebral: Option[Udalost] - ) - case class ViewModel( - dokumenty: List[Dokument], - autorizoval: Option[Udalost], - platiDo: Option[LocalDate], - poznámka: Option[String] - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-lg", - div( - cls := "px-4 py-5 sm:px-6", - h3( - cls := "text-lg leading-6 font-medium text-gray-900", - """Applicant Information""" - ), - p( - cls := "mt-1 max-w-2xl text-sm text-gray-500", - """Personal details and application.""" - ) - ), - div( - cls := "border-t border-gray-200 px-4 py-5 sm:px-6", - dl( - cls := "grid grid-cols-1 gap-x-4 gap-y-8 sm:grid-cols-2", - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Full name""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Margot Foster""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Application for""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Backend Developer""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Email address""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """margotfoster@example.com""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Salary expectation""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """$120,000""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """About""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Fugiat ipsum ipsum deserunt culpa aute sint do nostrud anim incididunt cillum culpa consequat. Excepteur qui ipsum aliquip consequat sint. Sit id mollit nulla mollit nostrud in ea officia proident. Irure nostrud pariatur mollit ad adipisicing reprehenderit deserunt qui eu.""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """Attachments""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - ul( - role := "list", - cls := "border border-gray-200 rounded-md divide-y divide-gray-200", - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """resume_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ), - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """coverletter_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala deleted file mode 100644 index 22f134f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala +++ /dev/null @@ -1,35 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object DukazyKriteria: - sealed trait Action - case object Add extends Action - - type ViewModel = List[DukazKriteria.ViewModel] - - def apply($m: Signal[ViewModel])(actions: Observer[Action]): HtmlElement = - div( - child.maybe <-- $m.map(m => - if (m.isEmpty) then Some(prazdnyDukaz(actions)) - else None - ), - children <-- $m.map(_.zipWithIndex).split(_._2)((_, _, $s) => - DukazKriteria($s.map(_._1)) - ) - ) - - private def prazdnyDukaz(actions: Observer[Action]): HtmlElement = - button( - tpe := "button", - onClick.preventDefault.mapTo(Add) --> actions, - cls := "relative block w-full border-2 border-gray-300 border-dashed rounded-lg p-12 text-center hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - Icons.outline - .`document-add`(12) - .amend(svg.cls := "mx-auto text-gray-400"), - span( - cls := "mt-2 block text-sm font-medium text-gray-900", - "Přidat důkaz" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala deleted file mode 100644 index f628697..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.list.StackedList -import fiftyforms.ui.components.tailwind.list.ListRow -import fiftyforms.ui.components.tailwind.list.RowTag -import fiftyforms.ui.components.tailwind.list.RowNext -import java.time.LocalDate - -object SeznamKriterii: - type ViewModel = List[DetailKriteria.ViewModel] - - private val kritList = new StackedList[DetailKriteria.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - kritList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = - p(cls := "text-sm text-gray-500", s"${i.kapitola}${i.bod}"), - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.container - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala deleted file mode 100644 index 2bfe4bc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.list.{ - StackedList, - ListRow, - RowTag, - PropList, - IconText, - RowNext -} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.LinkSupport.* - -object SeznamParametru: - type ViewModel = List[DetailParametru.ViewModel] - - private val parametrList = new StackedList[DetailParametru.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - parametrList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = emptyNode, - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.a - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala deleted file mode 100644 index f048e9d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ /dev/null @@ -1,101 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.form.* -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement -import fiftyforms.services.files.components.tailwind.FilePicker -import cz.e_bs.cmi.mdr.pdb.frontend.AutorizujDukaz -import cz.e_bs.cmi.mdr.pdb.frontend.DocumentRef -import fiftyforms.services.files.File - -object UpravDukazForm: - sealed trait Event - case object Cancelled extends Event - case object AvailableFilesRequested extends Event - def apply(availableFilesStream: EventStream[List[File]])( - updates: Observer[Event] - ): HtmlElement = - val (filesStream, filesObserver) = - EventStream.withObserver[FilePicker.Event] - val files = Var[List[File]](Nil) - def submitButtons: HtmlElement = - div( - cls := "pt-5", - div( - cls := "flex justify-end", - button( - tpe := "button", - cls := "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zrušit", - onClick.mapTo(Cancelled) --> updates - ), - button( - tpe := "submit", - cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - disabled <-- files.signal.map(_.isEmpty), - "Autorizovat" - ) - ) - ) - - div( - filesStream.collect { case FilePicker.SelectionUpdated(files) => - files.to(List) - } --> files.writer, - filesStream.collect { case FilePicker.AvailableFilesRequested => - AvailableFilesRequested - } --> updates, - cls := "bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6", - Form( - Form.Body( - Form.Section( - FormHeader( - FormHeader.ViewModel( - "Doložení kritéria", - "Sestavte doklady poskytující důkaz kritéria a potvrďte odesláním formuláře. Případné limitace či jiné relevantní údaje vepište do pole Komentář." - ) - ), - FormFields( - FormRow( - "dokumenty", - "Dokumenty", - FilePicker(files.signal, availableFilesStream)(filesObserver) - .amend(idAttr := "dokumenty", cls("max-w-lg")) - ).toHtml, - FormRow( - "platnost", - "Platnost", - input( - idAttr := "platnost", - name := "platnost", - tpe := "date", - autoComplete := "date", - cls := "block max-w-lg w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" - ) - ).toHtml, - FormRow( - "komentar", - "Komentář", - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - textArea( - idAttr := "komentar", - name := "about", - rows := 3, - cls := "max-w-lg shadow-sm block w-full focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border border-gray-300 rounded-md" - ), - p( - cls := "mt-2 text-sm text-gray-500", - "Doplňte prosím potřebné informace související s doložením kritéria, včetně případných limitací." - ) - ) - ).toHtml - ) - ) - ), - submitButtons - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala deleted file mode 100644 index 41033f6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -import components._ - -object DirectoryPage: - - type ViewModel = List[UserRow.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - val (actionsStream, actionObserver) = - EventStream.withObserver[SearchForm.Action] - val $filter = actionsStream - .collect { - case SearchForm.ClearFilter => None - case SearchForm.SetFilter(t) => Some(t) - } - .startWith(None) - val byLetter = for { - d <- $m - f <- $filter - } yield for { - (letter, users) <- d - .filter { user => - f match - case None => true - case Some(t) => user.search.contains(t) - } - .groupBy(_.prijmeni.head) - .to(List) - .sortBy(_._1) - } yield (letter.toString, users.sortBy(_.prijmeni)) - - div( - cls := "h-full max-w-7xl mx-auto order-first flex flex-col flex-shrink-0", - SearchForm(actionObserver), - Directory(byLetter) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala deleted file mode 100644 index 73f915d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -object Directory: - - object Header: - type ViewModel = String - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500", - h3(child.text <-- $m) - ) - - object UserList: - type ViewModel = List[UserRow.ViewModel] - def apply($m: Signal[ViewModel]): HtmlElement = - ul( - role := "list", - cls := "relative z-0 divide-y divide-gray-200", - children <-- $m.split(_.osobniCislo)((_, _, s) => UserRow(s)) - ) - - type ViewModel = List[(String, List[UserRow.ViewModel])] - def apply($m: Signal[ViewModel]): HtmlElement = - nav( - cls := "flex-1 min-h-0 overflow-y-auto", - aria.label := "Directory", - children <-- $m.split(_._1)((_, _, s) => - div( - cls := "relative", - Header(s.map(_._1)), - UserList(s.map(_._2)) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala deleted file mode 100644 index e182910..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala +++ /dev/null @@ -1,54 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object SearchForm: - sealed trait Action - case object SubmitFilter extends Action - sealed trait FilterAction extends Action - case object ClearFilter extends FilterAction - case class SetFilter(value: String) extends FilterAction - - def apply(actions: Observer[Action]): HtmlElement = - div( - cls := "px-6 pt-4 pb-4", - form( - cls := "flex space-x-4", - onSubmit.mapTo(SubmitFilter) --> actions, - div( - cls := "flex-1 min-w-0", - label( - forId := "search", - cls := "sr-only", - "Hledat" - ), - div( - cls := "relative rounded-md shadow-sm", - div( - cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", - Icons.solid.search().amend(svg.cls := "text-gray-400") - ), - input( - tpe := "search", - name := "search", - idAttr := "search", - cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", - placeholder := "Hledat", - composeEvents(onInput.mapToValue.setAsValue.map(SetFilter(_)))( - _.throttle(500) - ) --> actions - ) - ) - ), - button( - tpe := "submit", - cls := "inline-flex justify-center px-3.5 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500", - Icons.solid.filter().amend(svg.cls := "text-gray-400"), - span( - cls := "sr-only", - "Hledat" - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala deleted file mode 100644 index 82f7723..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala +++ /dev/null @@ -1,52 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.airstream.core.Signal -import fiftyforms.ui.components.tailwind.Avatar - -object UserRow: - case class ViewModel( - osobniCislo: String, - celeJmeno: String, - prijmeni: String, - hlavniFunkce: Option[String], - img: Option[String], - container: () => HtmlElement = () => div() - ) { - val search = prijmeni.toLowerCase - } - - def apply($m: Signal[ViewModel]): HtmlElement = - inline def avatarImage = - Avatar($m.map(_.img)).avatarImage(10) - - li( - div( - cls := "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500", - div( - cls := "flex-shrink-0", - child <-- avatarImage - ), - div( - cls := "flex-1 min-w-0", - child <-- $m.map { o => - o.container() - .amend( - cls := "focus:outline-none", - span( - cls := "absolute inset-0", - aria.hidden := true - ), - p( - cls := "text-sm font-medium text-gray-900", - o.celeJmeno - ), - p( - cls := "text-sm text-gray-500 truncate", - o.hlavniFunkce - ) - ) - } - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala deleted file mode 100644 index 467c5f5..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala +++ /dev/null @@ -1,99 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router - -object ErrorPage: - case class ViewModel( - homePage: Page, - errorName: String, - title: String, - subTitle: String - ) - - def apply(m: ViewModel, actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - val ViewModel(homePage, errorName, title, subTitle) = m - div( - cls := "min-h-full pt-16 pb-12 flex flex-col bg-white", - main( - cls := "flex-grow flex flex-col justify-center max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex-shrink-0 flex justify-center", - a( - href := "/", - cls := "inline-flex", - span( - cls := "sr-only", - """Workflow""" - ), - img( - cls := "h-12 w-auto", - src := "https://tailwindui.com/img/logos/workflow-mark.svg?color=indigo&shade=600", - alt := "" - ) - ) - ), - div( - cls := "py-16", - div( - cls := "text-center", - p( - cls := "text-sm font-semibold text-indigo-600 uppercase tracking-wide", - errorName - ), - h1( - cls := "mt-2 text-4xl font-extrabold text-gray-900 tracking-tight sm:text-5xl", - title - ), - p( - cls := "mt-2 text-base text-gray-500", - subTitle - ), - div( - cls := "mt-6", - PageLink - .container(homePage, actionBus) - .amend( - cls := "text-base font-medium text-indigo-600 hover:text-indigo-500", - """Go back home""" - ) - ) - ) - ) - ), - footer( - cls := "flex-shrink-0 max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", - nav( - cls := "flex justify-center space-x-4", - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Contact Support""" - ), - span( - cls := "inline-block border-l border-gray-300", - aria.hidden := true - ), - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Status""" - ), - span( - cls := "inline-block border-l border-gray-300", - aria.hidden := true - ), - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Twitter""" - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala deleted file mode 100644 index bd9baec..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action - -object NotFoundPage: - def apply(homePage: Page, url: String, actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - ErrorPage( - ErrorPage.ViewModel( - homePage, - "404 error", - "Page not found.", - s"Sorry, but page $url doesn't exist." - ), - actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala deleted file mode 100644 index 476fe3c..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action - -object UnhandledErrorPage: - - case class ViewModel( - homePage: Page, - errorName: Option[String], - errorMessage: Option[String] - ) - - def apply(m: ViewModel, actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - ErrorPage( - ErrorPage.ViewModel( - m.homePage, - "Unexpected error occurred", - m.errorName.getOrElse( - "Uh oh!" - ), // TODO: translations, better text than uh oh - m.errorMessage.getOrElse( - "This wasn't supposed to happen! Please try again." - ) - ), - actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala deleted file mode 100644 index 25f059f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala +++ /dev/null @@ -1,6 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.services - -import com.raquo.airstream.core.Observer - -trait DataFetcher[K, A]: - def fetch(id: K, sink: Observer[A]): Unit diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala deleted file mode 100644 index a469830..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala +++ /dev/null @@ -1,120 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package state - -import com.raquo.airstream.core.EventStream -import cz.e_bs.cmi.mdr.pdb.{UserInfo, OsobniCislo} -import com.raquo.airstream.core.Observer -import scala.scalajs.js -import scala.scalajs.js.JSON -import zio.json._ -import com.raquo.airstream.eventbus.EventBus -import com.raquo.airstream.ownership.Owner -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.UserFunction -import cz.e_bs.cmi.mdr.pdb.UserContract -import fiftyforms.services.files.File - -trait AppState - extends connectors.DetailPageConnector.AppState - with connectors.DetailParametruPageConnector.AppState - with connectors.DetailKriteriaPageConnector.AppState - with pages.detail.UpravDukaz.State: - def users: EventStream[List[UserInfo]] - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - -class MockAppState(implicit owner: Owner, router: Router[Page]) - extends AppState: - - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonDecoder[UserFunction] = DeriveJsonDecoder.gen - given JsonDecoder[UserContract] = DeriveJsonDecoder.gen - given JsonDecoder[UserInfo] = DeriveJsonDecoder.gen - - given JsonDecoder[ParameterCriteria] = DeriveJsonDecoder.gen - given JsonDecoder[Parameter] = DeriveJsonDecoder.gen - - private val actions = EventBus[Action]() - private val (parametersStream, pushParameters) = - EventStream.withCallback[List[Parameter]] - private val (usersStream, pushUsers) = - EventStream.withCallback[List[UserInfo]] - private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] - private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] - - private val mockData: List[UserInfo] = - mockUsers - .asInstanceOf[js.Dictionary[js.Object]] - .values - // TODO: is there a more efficient way to parse from JS object directly? - .map { o => - val parsed = JSON.stringify(o).fromJson[UserInfo] - parsed.left.foreach(org.scalajs.dom.console.log(_)) - parsed - } - .collect { case Right(u) => - u - } - .toList - - private val mockParameters: List[Parameter] = - pdbParams - .asInstanceOf[js.Dictionary[js.Object]] - .values - .map(o => JSON.stringify(o).fromJson[Parameter]) - .collect { case Right(p) => p } - .toList - - // TODO: Extract to separate event handler - actions.events.foreach { - case FetchDirectory => pushUsers(mockData) - case FetchUserDetails(osc) => - mockData.find(_.personalNumber == osc).foreach { o => - pushDetails(o) - router.replaceState(Page.Detail(o)) - } - case FetchParameters(osc) => - pushParameters(mockParameters) - case FetchParameter(osc, paramId) => - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(Page.DetailParametru(o, p)) - case FetchParameterCriteria(osc, paramId, critId, page) => - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - c <- p.criteria.find(_.id == critId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(page(o, p, c)) - case NavigateTo(page) => router.pushState(page) - case FetchAvailableFiles(osc) => - pushFiles( - List( - File("https://tc163.cmi.cz/here", "Example file") - ) - ) - } - - override def users: EventStream[List[UserInfo]] = - usersStream.debugWithName("users") - - override def details: EventStream[UserInfo] = - detailsStream.debugWithName("details") - - override def parameters: EventStream[List[Parameter]] = - parametersStream.debugWithName("parameters") - - override def availableFiles: EventStream[List[File]] = - filesStream.debugWithName("available files") - - override def actionBus: Observer[Action] = - actions.writer.debugWithName("actions writer") diff --git a/app/src/main/scala/mdr/pdb/app/Main.scala b/app/src/main/scala/mdr/pdb/app/Main.scala new file mode 100644 index 0000000..081b4ee --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/Main.scala @@ -0,0 +1,103 @@ +package mdr.pdb.app + +import scala.scalajs.js.annotation.JSExportTopLevel +import scala.scalajs.js.annotation.JSExport +import scala.scalajs.js.annotation.JSImport +import scala.scalajs.js +import org.scalajs.dom +import com.raquo.laminar.api.L.{*, given} +import scala.scalajs.js.Date +import com.raquo.waypoint.Router +import com.raquo.waypoint.SplitRender +import mdr.pdb.app.services.DataFetcher +import scala.scalajs.js.JSON +import zio.json._ +import mdr.pdb.UserInfo +import mdr.pdb.app.state.AppState + +@js.native +@JSImport("stylesheets/main.css", JSImport.Namespace) +object Css extends js.Any + +@js.native +@JSImport("data/users.json", JSImport.Default) +object mockUsers extends js.Object + +@js.native +@JSImport("params/pdb-params.json", JSImport.Default) +object pdbParams extends js.Object + +@JSExportTopLevel("app") +object Main: + + @JSExport + def main(args: Array[String]): Unit = + import Routes.given + onLoad { + setupAirstream() + val appContainer = dom.document.querySelector("#app") + val _ = + render( + appContainer, + renderPage(state.MockAppState(using unsafeWindowOwner, router)) + ) + } + + private def onLoad(f: => Unit): Unit = + documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) + + private def setupAirstream()(using router: Router[Page]): Unit = + AirstreamError.registerUnhandledErrorCallback(err => + router.forcePage( + Page.UnhandledError( + Some(err.getClass.getName), // TODO: Fill only in dev mode + Some(err.getMessage) + ) + ) + ) + + def renderPage(state: AppState)(using + router: Router[Page] + ): HtmlElement = + val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) + .collectSignal[Page.Detail]( + connectors + .DetailPageConnector(state)(_) + .apply + ) + .collectSignal[Page.DetailParametru]( + connectors + .DetailParametruPageConnector(state)(_) + .apply + ) + .collectSignal[Page.DetailKriteria]( + connectors + .DetailKriteriaPageConnector(state)(_) + .apply + ) + .collectSignal[Page.UpravDukazKriteria]( + pages.detail.UpravDukaz.Connector(state)(_).apply + ) + .collectStatic(Page.Dashboard)( + connectors.DashboardPageConnector(state.actionBus).apply + ) + .collect[Page.NotFound](pg => + pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) + ) + .collect[Page.UnhandledError](pg => + pages.errors + .UnhandledErrorPage( + pages.errors.UnhandledErrorPage + .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), + state.actionBus + ) + ) + .collectStatic(Page.Directory)( + connectors + .DirectoryPageConnector(state.users, state.actionBus) + .apply + ) + div(cls := "h-full", child <-- pageSplitter.$view) + + // Pull in the stylesheet + val css: Css.type = Css diff --git a/app/src/main/scala/mdr/pdb/app/Routes.scala b/app/src/main/scala/mdr/pdb/app/Routes.scala new file mode 100644 index 0000000..2cc860c --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/Routes.scala @@ -0,0 +1,198 @@ +package mdr.pdb.app + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.* +import org.scalajs.dom +import zio.json.{*, given} +import mdr.pdb.OsobniCislo + +import scala.scalajs.js +import mdr.pdb.UserInfo +import mdr.pdb.Parameter +import mdr.pdb.ParameterCriteria +import mdr.pdb.app.Page.Titled + +// enum is not working with Waypoints' SplitRender collectStatic +sealed abstract class Page( + val id: String, + val title: String, + val parent: Option[Page] +) { + val path: Vector[Page] = + parent match + case None => Vector(this) + case Some(p) => p.path :+ this + + val isRoot: Boolean = parent.isEmpty +} + +object Page: + + case class Titled[V](value: V, title: Option[String] = None): + val show: String = title.getOrElse(value.toString) + + case object Directory extends Page("directory", "Adresář", None) + + case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) + + case class Detail(osobniCislo: Titled[OsobniCislo]) + extends Page("user", osobniCislo.show, Some(Directory)) + + object Detail { + def apply(o: UserInfo): Detail = Detail( + Titled(o.personalNumber, Some(o.name)) + ) + } + + case class DetailParametru( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String] + ) extends Page( + "parameter", + parametr.show, + Some(Detail(osobniCislo)) + ) + + object DetailParametru { + def apply(o: UserInfo, p: Parameter): DetailParametru = + DetailParametru( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)) + ) + } + + case class DetailKriteria( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String], + kriterium: Titled[String] + ) extends Page( + "criteria", + kriterium.show, + Some(DetailParametru(osobniCislo, parametr)) + ) + + object DetailKriteria { + def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = + DetailKriteria( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)), + Titled(k.id, Some(k.id)) + ) + } + + case class UpravDukazKriteria( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String], + kriterium: Titled[String] + ) extends Page( + "addProof", + "Důkaz", + Some(DetailKriteria(osobniCislo, parametr, kriterium)) + ) + + object UpravDukazKriteria { + def apply( + o: UserInfo, + p: Parameter, + k: ParameterCriteria + ): UpravDukazKriteria = + UpravDukazKriteria( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)), + Titled(k.id, Some(k.id)) + ) + } + + case class NotFound(url: String) extends Page("404", "404", Some(Directory)) + + case class UnhandledError( + errorName: Option[String], + errorMessage: Option[String] + ) extends Page("500", "Unexpected error", Some(Directory)) + +object Routes: + given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) + given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) + given [V: JsonEncoder]: JsonEncoder[Titled[V]] = + DeriveJsonEncoder.gen[Titled[V]] + given [V: JsonDecoder]: JsonDecoder[Titled[V]] = + DeriveJsonDecoder.gen[Titled[V]] + given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] + given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] + + val base = + js.`import`.meta.env.BASE_URL + .asInstanceOf[String] + "app" + + val homePage: Page = Page.Directory + + given router: Router[Page] = Router[Page]( + routes = List( + Route.static(homePage, root / endOfSegments, basePath = base), + Route.static( + Page.Dashboard, + root / "dashboard" / endOfSegments, + basePath = base + ), + Route[Page.Detail, String]( + encode = _.osobniCislo.value.toString, + decode = osc => Page.Detail(Titled(OsobniCislo(osc))), + root / "osoba" / segment[String] / endOfSegments, + basePath = base + ), + Route[Page.DetailParametru, (String, String)]( + encode = p => (p.osobniCislo.value.toString, p.parametr.value), + decode = + p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / endOfSegments, + basePath = base + ), + Route[Page.DetailKriteria, (String, String, String)]( + encode = p => + ( + p.osobniCislo.value.toString, + p.parametr.value, + p.kriterium.value.replaceAll("\\.", "--") + ), + decode = p => + Page.DetailKriteria( + Titled(OsobniCislo(p._1)), + Titled(p._2), + Titled(p._3.replaceAll("--", ".")) + ), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / "kriterium" / segment[String] / endOfSegments, + basePath = base + ), + Route[Page.UpravDukazKriteria, (String, String, String)]( + encode = p => + ( + p.osobniCislo.value.toString, + p.parametr.value, + p.kriterium.value.replaceAll("\\.", "--") + ), + decode = p => + Page.UpravDukazKriteria( + Titled(OsobniCislo(p._1)), + Titled(p._2), + Titled(p._3.replaceAll("--", ".")) + ), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / "kriterium" / segment[String] / "edit" / endOfSegments, + basePath = base + ) + ), + serializePage = _.toJson, + deserializePage = _.fromJson[Page] + .fold(s => throw IllegalStateException(s), identity), + getPageTitle = _.title, + routeFallback = url => Page.NotFound(url), + deserializeFallback = _ => Page.Dashboard + )( + $popStateEvent = windowEvents.onPopState, + owner = unsafeWindowOwner + ) diff --git a/app/src/main/scala/mdr/pdb/app/actions.scala b/app/src/main/scala/mdr/pdb/app/actions.scala new file mode 100644 index 0000000..d078b3a --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/actions.scala @@ -0,0 +1,21 @@ +package mdr.pdb.app + +import mdr.pdb.OsobniCislo +import mdr.pdb.UserInfo +import mdr.pdb.Parameter +import mdr.pdb.ParameterCriteria + +sealed trait Action + +case object FetchDirectory extends Action +case class FetchUserDetails(osc: OsobniCislo) extends Action +case class FetchParameters(osc: OsobniCislo) extends Action +case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action +case class FetchParameterCriteria( + osc: OsobniCislo, + paramId: String, + critId: String, + page: (UserInfo, Parameter, ParameterCriteria) => Page +) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action +case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/mdr/pdb/app/components/AppPage.scala new file mode 100644 index 0000000..130c817 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/AppPage.scala @@ -0,0 +1,82 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.Page +import mdr.pdb.{UserProfile, UserInfo, OsobniCislo} +import com.raquo.waypoint.Router +import mdr.pdb.app.Action +import mdr.pdb.app.NavigateTo +import mdr.pdb.UserFunction + +object AppPage: + // TODO: pages by logged in user + val pages: List[Page] = List(Page.Directory, Page.Dashboard) + + import NavigationBar.{Logo, MenuItem} + + val logo = Logo( + "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", + "Workflow" + ) + + // TODO: menu items by user profile + val userMenu = + List( + MenuItem("Your Profile"), + MenuItem("Settings"), + MenuItem("Sign out") + ) + + // TODO: load user profile + val $userProfile = Var( + UserProfile( + "tom", + UserInfo( + OsobniCislo("1031"), + "tom", + "Tom", + "Cook", + None, + None, + Some("tom@example.com"), + Some("+420 222 866 180"), + Some( + UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") + ), + Nil, + None + ) + ) + ) + + val $userInfo = $userProfile.signal.map(_.userInfo) + + type ViewModel = Option[HtmlElement] + def apply( + actionBus: Observer[Action] + )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using + router: Router[Page] + ): HtmlElement = + PageLayout(actionBus)( + $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => + PageLayout.ViewModel( + NavigationBar.ViewModel( + u, + pages.map(p => + NavigationBar.Link( + () => + PageLink( + p, + actionBus + ), + p == cp + ) + ), + userMenu, + logo + ), + c + ) + ), + mods + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala new file mode 100644 index 0000000..9375398 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala @@ -0,0 +1,143 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.tailwind.list.IconText.ViewModel +import fiftyforms.ui.components.tailwind.Icons +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.Page + +import com.raquo.laminar.api.L.{*, given} +import io.laminext.syntax.core.{*, given} +import mdr.pdb.app.Action + +object Breadcrumbs: + + private def slash = + import svg.{*, given} + svg( + cls := "flex-shrink-0 h-5 w-5 text-gray-300", + xmlns := "http://www.w3.org/2000/svg", + fill := "currentColor", + viewBox := "0 0 20 20", + ariaHidden := true, + path( + d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" + ) + ) + + def backIcon = + Icons.solid + .`arrow-narrow-left`() + .amend( + svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" + ) + + object Link: + case class ViewModel( + page: Page, + icon: Option[SvgElement], + extraClasses: String, + text: String, + textClasses: Option[String] = None + ) + + val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" + + def shortHome(p: Page) = ViewModel( + p, + Some(backIcon), + "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", + "Zpět na úvodní stránku", + None + ) + + def fullHome(p: Page) = ViewModel( + p, + Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), + "text-gray-400 hover:text-gray-500", + "Domů", + Some("sr-only") + ) + + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + PageLink + .container($m.map(_.page), actionBus) + .amend( + cls <-- $m.map(_.extraClasses), + child.maybe <-- $m.map(_.icon), + span( + cls <-- $m.map(_.textClasses.getOrElse("")), + child.text <-- $m.map(_.text) + ) + ) + + object FullBreadcrumbs: + type ViewModel = Page + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + ol( + role := "list", + cls := "flex items-center space-x-4", + children <-- $m.map(_.path).split(_.id)((_, _, $p) => + li( + div( + cls := "flex items-center", + child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), + Link( + $p.map(p => + if (p.isRoot) then Link.fullHome(p) + else + Link.ViewModel( + p, + None, + s"ml-4 max-w-xs truncate ${Link.baseClasses}", + p.title + ) + ), + actionBus + ) + ) + ) + ) + ) + + object ShortBreadcrumbs: + type ViewModel = Page + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + Link( + $m.map { p => + val target = p.parent.getOrElse(p) + if target.isRoot then Link.shortHome(target) + else + Link.ViewModel( + target, + Some(backIcon), + "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", + s"Zpět na ${target.title}" + ) + }, + actionBus + ) + + def apply(actionBus: Observer[Action])(using + router: Router[Page] + ): HtmlElement = + nav( + cls := "flex", + aria.label := "Breadcrumb", + div( + cls := "flex sm:hidden", + ShortBreadcrumbs(router.$currentPage, actionBus) + ), + div( + cls := "hidden sm:block", + FullBreadcrumbs(router.$currentPage, actionBus) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala new file mode 100644 index 0000000..4bb8b8d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala @@ -0,0 +1,242 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent +import fiftyforms.ui.components.tailwind.Icons +import fiftyforms.ui.components.tailwind.Avatar +import mdr.pdb.UserInfo +import io.laminext.syntax.core.* + +object NavigationBar: + + case class Logo(img: String, name: String) + case class Link(a: () => Anchor, active: Boolean) + case class MenuItem(title: String) + + case class ViewModel( + userInfo: UserInfo, + pages: List[Link], + userMenu: List[MenuItem], + logo: Logo + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + val $userInfo = $m.map(_.userInfo) + val mobileMenuOpen = Var(false) + + val desktopOnly = cls("hidden md:block") + val mobileOnly = cls("md:hidden") + + inline def avatarImage(size: Int = 8) = + Avatar($userInfo.map(_.img)).avatarImage(size) + + def notificationButton = button( + tpe := "button", + cls := List( + "bg-indigo-600", + "focus:outline-none", + "focus:ring-2", + "focus:ring-offset-2", + "focus:ring-offset-indigo-600", + "focus:ring-white", + "hover:text-white", + "p-1", + "rounded-full", + "text-indigo-200" + ), + span(cls := "sr-only", "View notifications"), + Icons.outline.bell() + ) + + def userProfile: HtmlElement = + val menuOpen = Var(false) + + def menuItem(item: MenuItem, idx: Int): HtmlElement = + a( + href := "#", + cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", + role := "menuitem", + tabIndex := -1, + idAttr := s"user-menu-item-$idx", + item.title + ) + + div( + cls := "ml-3 relative", + div( + button( + tpe := "button", + cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", + idAttr := "user-menu-button", + aria.expanded <-- menuOpen.signal, + aria.hasPopup := true, + span(cls := "sr-only", "Open user menu"), + child <-- avatarImage(), + onClick.preventDefault.mapTo( + !menuOpen.now() + ) --> menuOpen.writer + ) + ), + /* + * */ + div( + cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", + cls <-- menuOpen.signal.map { o => + if (o) "md:block" else "md:hidden" + }, + role := "menu", + aria.orientation := "vertical", + aria.labelledBy := "user-menu-button", + tabIndex := -1, + // : keyboard navigation + children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) + ) + ) + + def mobileProfile = + def menuItem(item: MenuItem): HtmlElement = + a( + href := "#", + cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", + item.title + ) + + div( + cls := "pt-4 pb-3 border-t border-indigo-700", + div( + cls := "flex items-center px-5", + div( + cls := "flex-shrink-0", + child <-- avatarImage(10) + ), + div( + cls := "ml-3", + div( + cls := "text-base font-medium text-white", + child.text <-- $userInfo.map(_.name) + ), + child.maybe <-- $userInfo.map( + _.email.map(e => + div( + cls := "text-sm font-medium text-indigo-300", + e + ) + ) + ) + ), + notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) + ), + div( + cls := "mt-3 px-2 space-y-1", + children <-- $m.map(_.userMenu.map(menuItem)) + ) + ) + + def pageLink(page: Link): Anchor = + page + .a() + .amend( + cls := "text-white px-3 py-2 rounded-md text-sm font-medium", + cls := Seq( + "bg-indigo-700" -> page.active, + "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active + ), + ariaCurrent := (if page.active then "page" else "false") + ) + + def logoImg: Image = + img( + cls := "h-8 w-8", + src <-- $m.map(_.logo.img), + alt <-- $m.map(_.logo.name) + ) + + def mobileMenuButton = button( + tpe := "button", + cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", + aria.controls := "mobile-menu", + aria.expanded <-- mobileMenuOpen.signal, + span(cls := "sr-only", "Open main menu"), + Icons.outline + .menu() + .amend( + svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") + ), + Icons.outline + .x() + .amend( + svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") + ), + onClick.preventDefault.mapTo( + !mobileMenuOpen.now() + ) --> mobileMenuOpen.writer + ) + + def navBarLeft = + div( + cls := "flex items-center", + div(cls := "flex-shrink-0", logoImg), + div( + desktopOnly, + div( + cls := "ml-10 flex items-baseline space-x-4", + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) + ) + ) + ) + + def navBarRight = + div( + desktopOnly, + div( + cls := "ml-4 flex items-center md:ml-6", + notificationButton, + userProfile + ) + ) + + def navBarMobile = + div( + cls := "-mr-2 flex", + mobileOnly, + mobileMenuButton + ) + + def navBar = + div( + cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", + div( + cls := "flex items-center justify-between h-16", + navBarLeft, + navBarRight, + navBarMobile + ) + ) + + def mobileMenu = + div( + mobileOnly, + cls <-- mobileMenuOpen.signal.map { o => + if (o) "block" else "hidden" + }, + idAttr := "mobile-menu", + div( + cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) + ), + mobileProfile + ) + + nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala new file mode 100644 index 0000000..c4cb10d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala @@ -0,0 +1,19 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.Page +import com.raquo.waypoint.Router +import mdr.pdb.app.Action + +object PageHeader: + def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = + header( + cls := "bg-white shadow-sm", + div( + cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", + h1( + cls := "text-lg leading-6 font-semibold text-gray-900", + Breadcrumbs(actionBus) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala new file mode 100644 index 0000000..aae4539 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala @@ -0,0 +1,30 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.Page +import mdr.pdb.app.Action +import fiftyforms.ui.components.tailwind.Loading +import io.laminext.syntax.core.* + +object PageLayout: + case class ViewModel( + navigation: NavigationBar.ViewModel, + content: Option[HtmlElement] + ) + def apply(actionBus: Observer[Action])( + $m: Signal[ViewModel], + mods: Modifier[HtmlElement]* + )(using router: Router[Page]): HtmlElement = + val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) + div( + cls := "h-full flex flex-col", + NavigationBar($m.map(_.navigation)), + child.maybe <-- router.$currentPage.map(_.isRoot) + .switch(None, Some(PageHeader(actionBus))), + main( + cls := "flex-grow-1 overflow-y-auto", + mods, + child <-- $maybeContent.map(_.getOrElse(Loading)) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/mdr/pdb/app/components/PageLink.scala new file mode 100644 index 0000000..e6e7052 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageLink.scala @@ -0,0 +1,40 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.LinkSupport.* +import mdr.pdb.app.Page +import mdr.pdb.app.Action +import com.raquo.waypoint.Router +import mdr.pdb.app.NavigateTo + +object PageLink: + type ViewModel = Page + def apply($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = + a(mods($m, actions), child.text <-- $m.map(_.title)) + + def apply(m: ViewModel, actions: Observer[Action])(using + router: Router[Page] + ): Anchor = apply(Val(m), actions) + + def container($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = a(mods($m, actions)) + + def container(m: ViewModel, actions: Observer[Action])(using + Router[Page] + ): Anchor = container(Val(m), actions) + + private def mods($m: Signal[Page], actions: Observer[Action])(using + router: Router[Page] + ): Modifier[Anchor] = + Seq( + href <-- $m.map(router.absoluteUrlForPage).recover { case _ => + Some("invalid url") + }, + composeEvents(onClick.noKeyMod.preventDefault)( + _.sample($m) + .map(NavigateTo.apply) + ) --> actions + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala new file mode 100644 index 0000000..7682772 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala @@ -0,0 +1,13 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.pages.dashboard.DashboardPage +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage + +class DashboardPageConnector(actionBus: Observer[Action])(using + router: Router[Page] +): + def apply: HtmlElement = + AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala new file mode 100644 index 0000000..76b9133 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala @@ -0,0 +1,76 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.Parameter +import mdr.pdb.UserInfo +import mdr.pdb.app.pages.detail.DetailParametruPage +import pages.detail.DetailKriteriaPage +import pages.detail.components.DukazyKriteria +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.ParameterCriteria + +object DetailKriteriaPageConnector { + trait AppState { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + } +} + +case class DetailKriteriaPageConnector( + state: DetailKriteriaPageConnector.AppState +)( + $page: Signal[Page.DetailKriteria] +)(using Router[Page]): + val $paramChangeSignal = + $page.splitOne(p => + (p.osobniCislo.value, p.parametr.value, p.kriterium.value) + )((x, _, _) => x) + val $pageChangeSignal = + $paramChangeSignal.map( + FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) + ) + + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + val $merged = + $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => + for { + da <- d + pa <- p + pb <- pa.find(_.id == pc._2) + ka <- pb.criteria.find(_.id == pc._3) + } yield (da, pb, ka) + ) + + def apply: HtmlElement = + AppPage(state.actionBus)( + $merged.map(_.map(buildModel)) + .split(_ => ())((_, s, $s) => + DetailKriteriaPage($s)(state.actionBus.contramap { + case DukazyKriteria.Add => + NavigateTo( + Page.UpravDukazKriteria( + Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), + Page.Titled(s.parametr.id, Some(s.parametr.nazev)), + Page.Titled(s.kriterium.id, Some(s.kriterium.id)) + ) + ) + }) + ), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: Parameter, + k: ParameterCriteria + ): DetailKriteriaPage.ViewModel = + DetailKriteriaPage.ViewModel( + o.toDetailOsoby, + p.toParametr(_ => a()), + k.toKriterium(_ => a()) + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DetailPageConnector.scala new file mode 100644 index 0000000..df3a6b7 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DetailPageConnector.scala @@ -0,0 +1,57 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.UserInfo +import pages.detail.DetailPage +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.pages.detail.components.DetailOsoby +import mdr.pdb.Parameter +import mdr.pdb.app.pages.detail.components.SeznamParametru +import fiftyforms.ui.components.tailwind.Color + +object DetailPageConnector { + trait AppState { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + } +} + +case class DetailPageConnector(state: DetailPageConnector.AppState)( + $page: Signal[Page.Detail] +)(using router: Router[Page]): + val $oscChangeSignal = $page.splitOne(_.osobniCislo.value)((osc, _, _) => osc) + val $pageChangeSignal = + $oscChangeSignal.flatMap(osc => + EventStream.fromSeq(Seq(FetchUserDetails(osc), FetchParameters(osc))) + ) + // TODO: filter the value based on the current osc + // OSC change will fetch new data, but still + // - we need to be sure that what we got is really what we ought to display + // - we want to display stale data accordingly (at least with loading indicator) + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + def apply: HtmlElement = + AppPage(state.actionBus)( + $data.combineWithFn($params)(_ zip _) + .map(_.map(buildModel)) + .split(_ => ())((_, _, s) => DetailPage(s)), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: List[Parameter] + ): DetailPage.ViewModel = + DetailPage.ViewModel( + o.toDetailOsoby, + p.map( + _.toParametr(param => + PageLink.container(Page.DetailParametru(o, param), state.actionBus) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DetailParametruPageConnector.scala new file mode 100644 index 0000000..fa7193d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DetailParametruPageConnector.scala @@ -0,0 +1,67 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.Parameter +import mdr.pdb.UserInfo +import mdr.pdb.app.pages.detail.DetailParametruPage +import pages.detail.DetailParametruPage +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.components.PageLink + +object DetailParametruPageConnector { + trait AppState { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + } +} + +case class DetailParametruPageConnector( + state: DetailParametruPageConnector.AppState +)( + $page: Signal[Page.DetailParametru] +)(using router: Router[Page]): + val $paramChangeSignal = + $page.splitOne(p => (p.osobniCislo.value, p.parametr.value))((x, _, _) => x) + val $pageChangeSignal = + $paramChangeSignal.map(FetchParameter(_, _)) + + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + val $merged = + $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => + for { + da <- d + pa <- p + pb <- pa.find(_.id == pc._2) + } yield (da, pb) + ) + + def apply: HtmlElement = + AppPage(state.actionBus)( + $merged.map(_.map(buildModel)) + .split(_ => ())((_, _, s) => DetailParametruPage(s)), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: Parameter + ): DetailParametruPage.ViewModel = + DetailParametruPage.ViewModel( + o.toDetailOsoby, + p.toParametr(p => + PageLink.container(Page.DetailParametru(o, p), state.actionBus) + ), + p.criteria.map( + _.toKriterium { c => + PageLink.container( + Page.DetailKriteria(o, p, c), + state.actionBus + ) + } + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DirectoryPageConnector.scala new file mode 100644 index 0000000..ab2c045 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DirectoryPageConnector.scala @@ -0,0 +1,34 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.UserInfo +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.components.AppPage + +case class DirectoryPageConnector( + $input: EventStream[List[UserInfo]], + actionBus: Observer[Action] +)(using router: Router[Page]): + val $data = $input.startWithNone + val $actionSignal = EventStream.fromValue(FetchDirectory) + + def apply: HtmlElement = + AppPage(actionBus)( + $data.split(_ => ())((_, _, s) => + pages.directory.DirectoryPage( + s.map( + _.map( + _.toUserRow(u => + PageLink.container( + Page.Detail(Page.Titled(u.personalNumber)), + actionBus + ) + ) + ) + ) + ) + ), + $actionSignal --> actionBus + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/mdr/pdb/app/connectors/conversions.scala new file mode 100644 index 0000000..f581796 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/conversions.scala @@ -0,0 +1,68 @@ +package mdr.pdb.app.connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.UserInfo +import mdr.pdb.app.pages.detail.components.DetailOsoby +import mdr.pdb.Parameter +import mdr.pdb.app.pages.detail.components.SeznamParametru +import mdr.pdb.ParameterCriteria +import mdr.pdb.app.pages.detail.components.SeznamKriterii +import mdr.pdb.app.pages.directory.components.UserRow +import mdr.pdb.app.pages.detail.components.DetailParametru +import mdr.pdb.app.pages.detail.components.DetailKriteria +import fiftyforms.ui.components.tailwind.Color + +extension (o: UserInfo) + def toDetailOsoby: DetailOsoby.ViewModel = + DetailOsoby.ViewModel( + o.personalNumber, + o.name, + o.email, + o.phone, + o.img, + o.mainFunction.map(f => + DetailOsoby.Funkce.ViewModel(f.name, f.dept, f.ou) + ), + o.userContracts.headOption.map(c => + DetailOsoby.PracovniPomer.ViewModel(c.rel, c.startDate, c.endDate) + ) + ) + +extension (param: Parameter) + def toParametr(container: Parameter => Anchor): DetailParametru.ViewModel = + DetailParametru.ViewModel( + id = param.id, + nazev = param.name, + popis = param.description, + status = "Nesplněno", + statusColor = Color.red, + a = container(param) + ) + +extension (crit: ParameterCriteria) + def toKriterium( + container: ParameterCriteria => Anchor + ): DetailKriteria.ViewModel = + DetailKriteria.ViewModel( + nazev = crit.criteriumText, + kapitola = crit.chapterId, + bod = crit.itemId, + status = "Nesplněno", + statusColor = Color.red, + splneno = false, + dukazy = Nil, + container = container(crit) + ) + +extension (user: UserInfo) + def toUserRow( + container: UserInfo => HtmlElement = (_: UserInfo) => div() + ): UserRow.ViewModel = + UserRow.ViewModel( + osobniCislo = user.personalNumber.toString, + celeJmeno = user.name, + prijmeni = user.surname, + hlavniFunkce = user.mainFunction.map(_.name), + img = user.img, + container = () => container(user) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/dashboard/DashboardPage.scala b/app/src/main/scala/mdr/pdb/app/pages/dashboard/DashboardPage.scala new file mode 100644 index 0000000..0eb8f7a --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/dashboard/DashboardPage.scala @@ -0,0 +1,11 @@ +package mdr.pdb.app.pages.dashboard + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.Page + +object DashboardPage: + + def render: HtmlElement = + div("Dashboard page") diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala new file mode 100644 index 0000000..ff3a25f --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala @@ -0,0 +1,31 @@ +package mdr.pdb.app.pages.detail + +import com.raquo.laminar.api.L.{*, given} + +import components._ + +object DetailKriteriaPage: + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametr: DetailParametru.ViewModel, + kriterium: DetailKriteria.ViewModel + ) + + def apply($m: Signal[ViewModel])( + action: Observer[DukazyKriteria.Action] + ): HtmlElement = + div( + cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + div( + DetailOsoby.header($m.map(_.osoba)), + DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") + ), + div( + DetailKriteria($m.map(_.kriterium)), + DukazyKriteria($m.map(_.kriterium.dukazy))(action) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/DetailPage.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailPage.scala new file mode 100644 index 0000000..b591cc9 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailPage.scala @@ -0,0 +1,25 @@ +package mdr.pdb.app.pages.detail + +import com.raquo.laminar.api.L.{*, given} + +import components._ +import mdr.pdb.app.Page +import com.raquo.waypoint.Router +import mdr.pdb.app.Action + +object DetailPage: + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametry: SeznamParametru.ViewModel + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + DetailOsoby($m.map(_.osoba)), + SeznamParametru($m.map(_.parametry)) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/DetailParametruPage.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailParametruPage.scala new file mode 100644 index 0000000..3322279 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailParametruPage.scala @@ -0,0 +1,24 @@ +package mdr.pdb.app.pages.detail + +import com.raquo.laminar.api.L.{*, given} + +import components._ + +object DetailParametruPage: + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametr: DetailParametru.ViewModel, + kriteria: SeznamKriterii.ViewModel + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + DetailOsoby.header($m.map(_.osoba)), + DetailParametru($m.map(_.parametr)), + SeznamKriterii($m.map(_.kriteria)) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala new file mode 100644 index 0000000..b58618a --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala @@ -0,0 +1,107 @@ +package mdr.pdb +package app +package pages.detail + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.pages.detail.components.UpravDukazForm +import fiftyforms.services.files.File + +object UpravDukaz: + + type ThisPage = Page.UpravDukazKriteria + type PageKey = (OsobniCislo, String, String) + + trait State { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def availableFiles: EventStream[List[File]] + def actionBus: Observer[Action] + } + + def keyOfPage(page: ThisPage): PageKey = + (page.osobniCislo.value, page.parametr.value, page.kriterium.value) + + def onChangeAction(key: PageKey): Action = + FetchParameterCriteria( + key._1, + key._2, + key._3, + Page.UpravDukazKriteria(_, _, _) + ) + + class Connector(state: State)($page: Signal[ThisPage])(using Router[Page]): + val $paramChangeSignal = $page.splitOne(keyOfPage)((x, _, _) => x) + val $pageChangeSignal = $paramChangeSignal.map(onChangeAction) + + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + val $merged = + $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => + for { + da <- d + pa <- p + pb <- pa.find(_.id == pc._2) + ka <- pb.criteria.find(_.id == pc._3) + } yield (da, pb, ka) + ) + + def apply: HtmlElement = + AppPage(state.actionBus)( + $merged.split(_ => ())((_, s, $s) => + PageComponent( + $s.map(buildModel), + state.availableFiles, + state.actionBus.contramap { + case UpravDukazForm.Cancelled => + NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) + case UpravDukazForm.AvailableFilesRequested => + FetchAvailableFiles(s._1.personalNumber) + } + ) + ), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: Parameter, + k: ParameterCriteria + ): PageComponent.ViewModel = + import connectors.* + PageComponent.ViewModel( + o.toDetailOsoby, + p.toParametr(_ => a()), + k.toKriterium(_ => a()) + ) + + object PageComponent: + import components.* + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametr: DetailParametru.ViewModel, + kriterium: DetailKriteria.ViewModel + ) + + def apply( + $m: Signal[ViewModel], + availableFilesStream: EventStream[List[File]], + events: Observer[UpravDukazForm.Event] + ): HtmlElement = + div( + cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + div( + DetailOsoby.header($m.map(_.osoba)), + DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") + ), + div( + DetailKriteria($m.map(_.kriterium)), + UpravDukazForm(availableFilesStream)(events) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailKriteria.scala new file mode 100644 index 0000000..a3ab634 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailKriteria.scala @@ -0,0 +1,37 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.CustomAttrs +import fiftyforms.ui.components.tailwind.Icons +import java.time.LocalDate +import fiftyforms.ui.components.tailwind.Color + +object DetailKriteria: + case class ViewModel( + nazev: String, + kapitola: String, + bod: String, + status: String, + statusColor: Color, + splneno: Boolean, + dukazy: List[DukazKriteria.ViewModel], + container: HtmlElement = div() + ) { + val id = s"${kapitola}${bod}" + } + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "pb-5 border-b border-gray-200", + h3( + cls := "text-xl leading-6 font-bold text-gray-900", + child.text <-- $m.map(_.nazev) + ), + h3( + cls := "mt-2 max-w-4xl text-sm text-gray-500", + "Kapitola ", + child.text <-- $m.map(_.kapitola), + " bod ", + child <-- $m.map(_.bod) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailOsoby.scala new file mode 100644 index 0000000..0fe2ba5 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailOsoby.scala @@ -0,0 +1,100 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.OsobniCislo + +import java.time.LocalDate +import fiftyforms.ui.components.tailwind.Avatar + +object DetailOsoby: + + object Funkce: + case class ViewModel( + nazev: String, + stredisko: String, + voj: String + ) + + def render($m: Signal[ViewModel]): HtmlElement = + p( + cls := "text-sm font-medium text-gray-500", + child.text <-- $m.map(_.nazev), + span( + cls := "hidden md:inline", + " @ ", + child.text <-- $m.map(_.stredisko), + ", ", + child.text <-- $m.map(_.voj) + ) + ) + + object PracovniPomer: + case class ViewModel( + druh: String, + pocatek: LocalDate, + konec: Option[LocalDate] + ) + + def render($m: Signal[ViewModel]): HtmlElement = + import fiftyforms.ui.components.tailwind.CustomAttrs.datetime + p( + cls := "text-sm font-medium text-gray-500", + child.text <-- $m.map(_.druh), + " od ", + time( + datetime <-- $m.map(_.pocatek.toString), + child.text <-- $m.map(_.pocatek.toString) + ) + ) + + case class ViewModel( + osobniCislo: OsobniCislo, + jmeno: String, + email: Option[String], + telefon: Option[String], + img: Option[String], + hlavniFunkce: Option[Funkce.ViewModel], + pracovniPomer: Option[PracovniPomer.ViewModel] + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "md:flex md:items-center md:justify-between md:space-x-4", + div( + cls := "flex items-start space-x-4", + div( + cls := "flex-shrink-0", + Avatar($m.map(_.img)).avatar(16) + ), + div( + h1( + cls := "text-2xl font-bold text-gray-900", + child.text <-- $m.map(_.jmeno) + ), + child.maybe <-- $m.map(_.hlavniFunkce).split(_ => ())((_, _, d) => + Funkce.render(d) + ), + child.maybe <-- $m.map(_.pracovniPomer).split(_ => ())((_, _, d) => + PracovniPomer.render(d) + ) + ) + ) + ) + + def header($m: Signal[ViewModel]): HtmlElement = + div( + cls := "md:flex md:items-center md:justify-between md:space-x-5", + div( + cls := "flex items-start space-x-4", + div( + cls := "flex-shrink-0", + Avatar($m.map(_.img)).avatar(8) + ), + div( + h1( + cls := "text-lg font-medium text-gray-800", + child.text <-- $m.map(_.jmeno) + ) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailParametru.scala new file mode 100644 index 0000000..6310dde --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailParametru.scala @@ -0,0 +1,34 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.Color + +object DetailParametru: + case class ViewModel( + id: String, + nazev: String, + popis: String, + status: String, + statusColor: Color, + a: Anchor + ) + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "pb-5 border-b border-gray-200", + h2( + cls := "text-2xl leading-6 font-medium text-gray-900", + child.text <-- $m.map(_.nazev) + ), + p( + cls := "mt-2 max-w-4xl text-sm text-gray-500", + child.text <-- $m.map(_.popis) + ) + ) + + def header($m: Signal[ViewModel]): HtmlElement = + div( + h2( + cls := "text-lg leading-6 font-medium text-gray-600", + child.text <-- $m.map(_.nazev) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazKriteria.scala new file mode 100644 index 0000000..c292919 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazKriteria.scala @@ -0,0 +1,177 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import java.time.LocalDate +import fiftyforms.ui.components.tailwind.CustomAttrs + +object DukazKriteria: + case class Osoba(osobniCislo: String, jmeno: String) + case class Udalost(osoba: Osoba, datum: LocalDate) + case class Dokument( + url: String, + nazev: String, + pridal: Udalost, + odebral: Option[Udalost] + ) + case class ViewModel( + dokumenty: List[Dokument], + autorizoval: Option[Udalost], + platiDo: Option[LocalDate], + poznámka: Option[String] + ) + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "bg-white shadow overflow-hidden sm:rounded-lg", + div( + cls := "px-4 py-5 sm:px-6", + h3( + cls := "text-lg leading-6 font-medium text-gray-900", + """Applicant Information""" + ), + p( + cls := "mt-1 max-w-2xl text-sm text-gray-500", + """Personal details and application.""" + ) + ), + div( + cls := "border-t border-gray-200 px-4 py-5 sm:px-6", + dl( + cls := "grid grid-cols-1 gap-x-4 gap-y-8 sm:grid-cols-2", + div( + cls := "sm:col-span-1", + dt( + cls := "text-sm font-medium text-gray-500", + """Full name""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """Margot Foster""" + ) + ), + div( + cls := "sm:col-span-1", + dt( + cls := "text-sm font-medium text-gray-500", + """Application for""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """Backend Developer""" + ) + ), + div( + cls := "sm:col-span-1", + dt( + cls := "text-sm font-medium text-gray-500", + """Email address""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """margotfoster@example.com""" + ) + ), + div( + cls := "sm:col-span-1", + dt( + cls := "text-sm font-medium text-gray-500", + """Salary expectation""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """$120,000""" + ) + ), + div( + cls := "sm:col-span-2", + dt( + cls := "text-sm font-medium text-gray-500", + """About""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """Fugiat ipsum ipsum deserunt culpa aute sint do nostrud anim incididunt cillum culpa consequat. Excepteur qui ipsum aliquip consequat sint. Sit id mollit nulla mollit nostrud in ea officia proident. Irure nostrud pariatur mollit ad adipisicing reprehenderit deserunt qui eu.""" + ) + ), + div( + cls := "sm:col-span-2", + dt( + cls := "text-sm font-medium text-gray-500", + """Attachments""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + ul( + role := "list", + cls := "border border-gray-200 rounded-md divide-y divide-gray-200", + li( + cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", + div( + cls := "w-0 flex-1 flex items-center", { + import svg.* + import CustomAttrs.svg.ariaHidden + svg( + cls := "flex-shrink-0 h-5 w-5 text-gray-400", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + ariaHidden := true, + path( + fillRule := "evenodd", + d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", + clipRule := "evenodd" + ) + ) + }, + span( + cls := "ml-2 flex-1 w-0 truncate", + """resume_back_end_developer.pdf""" + ) + ), + div( + cls := "ml-4 flex-shrink-0", + a( + href := "#", + cls := "font-medium text-indigo-600 hover:text-indigo-500", + """Download""" + ) + ) + ), + li( + cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", + div( + cls := "w-0 flex-1 flex items-center", { + import svg.* + import CustomAttrs.svg.ariaHidden + svg( + cls := "flex-shrink-0 h-5 w-5 text-gray-400", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + ariaHidden := true, + path( + fillRule := "evenodd", + d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", + clipRule := "evenodd" + ) + ) + }, + span( + cls := "ml-2 flex-1 w-0 truncate", + """coverletter_back_end_developer.pdf""" + ) + ), + div( + cls := "ml-4 flex-shrink-0", + a( + href := "#", + cls := "font-medium text-indigo-600 hover:text-indigo-500", + """Download""" + ) + ) + ) + ) + ) + ) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala new file mode 100644 index 0000000..d604860 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala @@ -0,0 +1,35 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.Icons + +object DukazyKriteria: + sealed trait Action + case object Add extends Action + + type ViewModel = List[DukazKriteria.ViewModel] + + def apply($m: Signal[ViewModel])(actions: Observer[Action]): HtmlElement = + div( + child.maybe <-- $m.map(m => + if (m.isEmpty) then Some(prazdnyDukaz(actions)) + else None + ), + children <-- $m.map(_.zipWithIndex).split(_._2)((_, _, $s) => + DukazKriteria($s.map(_._1)) + ) + ) + + private def prazdnyDukaz(actions: Observer[Action]): HtmlElement = + button( + tpe := "button", + onClick.preventDefault.mapTo(Add) --> actions, + cls := "relative block w-full border-2 border-gray-300 border-dashed rounded-lg p-12 text-center hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + Icons.outline + .`document-add`(12) + .amend(svg.cls := "mx-auto text-gray-400"), + span( + cls := "mt-2 block text-sm font-medium text-gray-900", + "Přidat důkaz" + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala new file mode 100644 index 0000000..7e77a7b --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala @@ -0,0 +1,34 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.Color +import fiftyforms.ui.components.tailwind.list.StackedList +import fiftyforms.ui.components.tailwind.list.ListRow +import fiftyforms.ui.components.tailwind.list.RowTag +import fiftyforms.ui.components.tailwind.list.RowNext +import java.time.LocalDate + +object SeznamKriterii: + type ViewModel = List[DetailKriteria.ViewModel] + + private val kritList = new StackedList[DetailKriteria.ViewModel] + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "bg-white shadow overflow-hidden sm:rounded-md", + kritList($m, _.id) { $i => + $i.map { i => + ListRow.ViewModel( + title = i.nazev, + topRight = RowTag.render( + $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) + ), + bottomLeft = + p(cls := "text-sm text-gray-500", s"${i.kapitola}${i.bod}"), + bottomRight = emptyNode, + farRight = RowNext.render, + containerElement = i.container + ) + } + } + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamParametru.scala new file mode 100644 index 0000000..0760b32 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamParametru.scala @@ -0,0 +1,37 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.list.{ + StackedList, + ListRow, + RowTag, + PropList, + IconText, + RowNext +} +import fiftyforms.ui.components.tailwind.Color +import fiftyforms.ui.components.tailwind.LinkSupport.* + +object SeznamParametru: + type ViewModel = List[DetailParametru.ViewModel] + + private val parametrList = new StackedList[DetailParametru.ViewModel] + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "bg-white shadow overflow-hidden sm:rounded-md", + parametrList($m, _.id) { $i => + $i.map { i => + ListRow.ViewModel( + title = i.nazev, + topRight = RowTag.render( + $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) + ), + bottomLeft = emptyNode, + bottomRight = emptyNode, + farRight = RowNext.render, + containerElement = i.a + ) + } + } + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala new file mode 100644 index 0000000..28753c9 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala @@ -0,0 +1,101 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import io.laminext.syntax.core.* +import fiftyforms.ui.components.tailwind.CustomAttrs +import fiftyforms.ui.components.tailwind.form.* +import org.scalajs.dom +import com.raquo.laminar.nodes.ReactiveHtmlElement +import fiftyforms.services.files.components.tailwind.FilePicker +import mdr.pdb.frontend.AutorizujDukaz +import mdr.pdb.frontend.DocumentRef +import fiftyforms.services.files.File + +object UpravDukazForm: + sealed trait Event + case object Cancelled extends Event + case object AvailableFilesRequested extends Event + def apply(availableFilesStream: EventStream[List[File]])( + updates: Observer[Event] + ): HtmlElement = + val (filesStream, filesObserver) = + EventStream.withObserver[FilePicker.Event] + val files = Var[List[File]](Nil) + def submitButtons: HtmlElement = + div( + cls := "pt-5", + div( + cls := "flex justify-end", + button( + tpe := "button", + cls := "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + "Zrušit", + onClick.mapTo(Cancelled) --> updates + ), + button( + tpe := "submit", + cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + disabled <-- files.signal.map(_.isEmpty), + "Autorizovat" + ) + ) + ) + + div( + filesStream.collect { case FilePicker.SelectionUpdated(files) => + files.to(List) + } --> files.writer, + filesStream.collect { case FilePicker.AvailableFilesRequested => + AvailableFilesRequested + } --> updates, + cls := "bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6", + Form( + Form.Body( + Form.Section( + FormHeader( + FormHeader.ViewModel( + "Doložení kritéria", + "Sestavte doklady poskytující důkaz kritéria a potvrďte odesláním formuláře. Případné limitace či jiné relevantní údaje vepište do pole Komentář." + ) + ), + FormFields( + FormRow( + "dokumenty", + "Dokumenty", + FilePicker(files.signal, availableFilesStream)(filesObserver) + .amend(idAttr := "dokumenty", cls("max-w-lg")) + ).toHtml, + FormRow( + "platnost", + "Platnost", + input( + idAttr := "platnost", + name := "platnost", + tpe := "date", + autoComplete := "date", + cls := "block max-w-lg w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" + ) + ).toHtml, + FormRow( + "komentar", + "Komentář", + div( + cls := "mt-1 sm:mt-0 sm:col-span-2", + textArea( + idAttr := "komentar", + name := "about", + rows := 3, + cls := "max-w-lg shadow-sm block w-full focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border border-gray-300 rounded-md" + ), + p( + cls := "mt-2 text-sm text-gray-500", + "Doplňte prosím potřebné informace související s doložením kritéria, včetně případných limitací." + ) + ) + ).toHtml + ) + ) + ), + submitButtons + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/directory/DirectoryPage.scala b/app/src/main/scala/mdr/pdb/app/pages/directory/DirectoryPage.scala new file mode 100644 index 0000000..7a72ca3 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/directory/DirectoryPage.scala @@ -0,0 +1,40 @@ +package mdr.pdb.app.pages.directory + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.UserInfo + +import components._ + +object DirectoryPage: + + type ViewModel = List[UserRow.ViewModel] + + def apply($m: Signal[ViewModel]): HtmlElement = + val (actionsStream, actionObserver) = + EventStream.withObserver[SearchForm.Action] + val $filter = actionsStream + .collect { + case SearchForm.ClearFilter => None + case SearchForm.SetFilter(t) => Some(t) + } + .startWith(None) + val byLetter = for { + d <- $m + f <- $filter + } yield for { + (letter, users) <- d + .filter { user => + f match + case None => true + case Some(t) => user.search.contains(t) + } + .groupBy(_.prijmeni.head) + .to(List) + .sortBy(_._1) + } yield (letter.toString, users.sortBy(_.prijmeni)) + + div( + cls := "h-full max-w-7xl mx-auto order-first flex flex-col flex-shrink-0", + SearchForm(actionObserver), + Directory(byLetter) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/directory/components/Directory.scala b/app/src/main/scala/mdr/pdb/app/pages/directory/components/Directory.scala new file mode 100644 index 0000000..f7d6f99 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/directory/components/Directory.scala @@ -0,0 +1,37 @@ +package mdr.pdb.app.pages.directory.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.UserInfo + +object Directory: + + object Header: + type ViewModel = String + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500", + h3(child.text <-- $m) + ) + + object UserList: + type ViewModel = List[UserRow.ViewModel] + def apply($m: Signal[ViewModel]): HtmlElement = + ul( + role := "list", + cls := "relative z-0 divide-y divide-gray-200", + children <-- $m.split(_.osobniCislo)((_, _, s) => UserRow(s)) + ) + + type ViewModel = List[(String, List[UserRow.ViewModel])] + def apply($m: Signal[ViewModel]): HtmlElement = + nav( + cls := "flex-1 min-h-0 overflow-y-auto", + aria.label := "Directory", + children <-- $m.split(_._1)((_, _, s) => + div( + cls := "relative", + Header(s.map(_._1)), + UserList(s.map(_._2)) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/directory/components/SearchForm.scala b/app/src/main/scala/mdr/pdb/app/pages/directory/components/SearchForm.scala new file mode 100644 index 0000000..fb9ecc4 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/directory/components/SearchForm.scala @@ -0,0 +1,54 @@ +package mdr.pdb.app.pages.directory.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.Icons + +object SearchForm: + sealed trait Action + case object SubmitFilter extends Action + sealed trait FilterAction extends Action + case object ClearFilter extends FilterAction + case class SetFilter(value: String) extends FilterAction + + def apply(actions: Observer[Action]): HtmlElement = + div( + cls := "px-6 pt-4 pb-4", + form( + cls := "flex space-x-4", + onSubmit.mapTo(SubmitFilter) --> actions, + div( + cls := "flex-1 min-w-0", + label( + forId := "search", + cls := "sr-only", + "Hledat" + ), + div( + cls := "relative rounded-md shadow-sm", + div( + cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", + Icons.solid.search().amend(svg.cls := "text-gray-400") + ), + input( + tpe := "search", + name := "search", + idAttr := "search", + cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", + placeholder := "Hledat", + composeEvents(onInput.mapToValue.setAsValue.map(SetFilter(_)))( + _.throttle(500) + ) --> actions + ) + ) + ), + button( + tpe := "submit", + cls := "inline-flex justify-center px-3.5 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500", + Icons.solid.filter().amend(svg.cls := "text-gray-400"), + span( + cls := "sr-only", + "Hledat" + ) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/directory/components/UserRow.scala b/app/src/main/scala/mdr/pdb/app/pages/directory/components/UserRow.scala new file mode 100644 index 0000000..4aa1672 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/directory/components/UserRow.scala @@ -0,0 +1,52 @@ +package mdr.pdb.app.pages.directory.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.airstream.core.Signal +import fiftyforms.ui.components.tailwind.Avatar + +object UserRow: + case class ViewModel( + osobniCislo: String, + celeJmeno: String, + prijmeni: String, + hlavniFunkce: Option[String], + img: Option[String], + container: () => HtmlElement = () => div() + ) { + val search = prijmeni.toLowerCase + } + + def apply($m: Signal[ViewModel]): HtmlElement = + inline def avatarImage = + Avatar($m.map(_.img)).avatarImage(10) + + li( + div( + cls := "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500", + div( + cls := "flex-shrink-0", + child <-- avatarImage + ), + div( + cls := "flex-1 min-w-0", + child <-- $m.map { o => + o.container() + .amend( + cls := "focus:outline-none", + span( + cls := "absolute inset-0", + aria.hidden := true + ), + p( + cls := "text-sm font-medium text-gray-900", + o.celeJmeno + ), + p( + cls := "text-sm text-gray-500 truncate", + o.hlavniFunkce + ) + ) + } + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/errors/ErrorPage.scala b/app/src/main/scala/mdr/pdb/app/pages/errors/ErrorPage.scala new file mode 100644 index 0000000..6b195ef --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/errors/ErrorPage.scala @@ -0,0 +1,99 @@ +package mdr.pdb.app.pages.errors + +import com.raquo.domtypes.generic.codecs.StringAsIsCodec +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.Page +import mdr.pdb.app.Action +import com.raquo.waypoint.Router + +object ErrorPage: + case class ViewModel( + homePage: Page, + errorName: String, + title: String, + subTitle: String + ) + + def apply(m: ViewModel, actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + val ViewModel(homePage, errorName, title, subTitle) = m + div( + cls := "min-h-full pt-16 pb-12 flex flex-col bg-white", + main( + cls := "flex-grow flex flex-col justify-center max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", + div( + cls := "flex-shrink-0 flex justify-center", + a( + href := "/", + cls := "inline-flex", + span( + cls := "sr-only", + """Workflow""" + ), + img( + cls := "h-12 w-auto", + src := "https://tailwindui.com/img/logos/workflow-mark.svg?color=indigo&shade=600", + alt := "" + ) + ) + ), + div( + cls := "py-16", + div( + cls := "text-center", + p( + cls := "text-sm font-semibold text-indigo-600 uppercase tracking-wide", + errorName + ), + h1( + cls := "mt-2 text-4xl font-extrabold text-gray-900 tracking-tight sm:text-5xl", + title + ), + p( + cls := "mt-2 text-base text-gray-500", + subTitle + ), + div( + cls := "mt-6", + PageLink + .container(homePage, actionBus) + .amend( + cls := "text-base font-medium text-indigo-600 hover:text-indigo-500", + """Go back home""" + ) + ) + ) + ) + ), + footer( + cls := "flex-shrink-0 max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", + nav( + cls := "flex justify-center space-x-4", + a( + href := "#", + cls := "text-sm font-medium text-gray-500 hover:text-gray-600", + """Contact Support""" + ), + span( + cls := "inline-block border-l border-gray-300", + aria.hidden := true + ), + a( + href := "#", + cls := "text-sm font-medium text-gray-500 hover:text-gray-600", + """Status""" + ), + span( + cls := "inline-block border-l border-gray-300", + aria.hidden := true + ), + a( + href := "#", + cls := "text-sm font-medium text-gray-500 hover:text-gray-600", + """Twitter""" + ) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/errors/NotFoundPage.scala b/app/src/main/scala/mdr/pdb/app/pages/errors/NotFoundPage.scala new file mode 100644 index 0000000..97760e1 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/errors/NotFoundPage.scala @@ -0,0 +1,21 @@ +package mdr.pdb.app.pages.errors + +import com.raquo.domtypes.generic.codecs.StringAsIsCodec +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.Page +import mdr.pdb.app.Action + +object NotFoundPage: + def apply(homePage: Page, url: String, actionBus: Observer[Action])(using + router: Router[Page] + ): HtmlElement = + ErrorPage( + ErrorPage.ViewModel( + homePage, + "404 error", + "Page not found.", + s"Sorry, but page $url doesn't exist." + ), + actionBus + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala deleted file mode 100644 index a3b00af..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala +++ /dev/null @@ -1,103 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import scala.scalajs.js.annotation.JSExportTopLevel -import scala.scalajs.js.annotation.JSExport -import scala.scalajs.js.annotation.JSImport -import scala.scalajs.js -import org.scalajs.dom -import com.raquo.laminar.api.L.{*, given} -import scala.scalajs.js.Date -import com.raquo.waypoint.Router -import com.raquo.waypoint.SplitRender -import cz.e_bs.cmi.mdr.pdb.app.services.DataFetcher -import scala.scalajs.js.JSON -import zio.json._ -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.state.AppState - -@js.native -@JSImport("stylesheets/main.css", JSImport.Namespace) -object Css extends js.Any - -@js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native -@JSImport("params/pdb-params.json", JSImport.Default) -object pdbParams extends js.Object - -@JSExportTopLevel("app") -object Main: - - @JSExport - def main(args: Array[String]): Unit = - import Routes.given - onLoad { - setupAirstream() - val appContainer = dom.document.querySelector("#app") - val _ = - render( - appContainer, - renderPage(state.MockAppState(using unsafeWindowOwner, router)) - ) - } - - private def onLoad(f: => Unit): Unit = - documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) - - private def setupAirstream()(using router: Router[Page]): Unit = - AirstreamError.registerUnhandledErrorCallback(err => - router.forcePage( - Page.UnhandledError( - Some(err.getClass.getName), // TODO: Fill only in dev mode - Some(err.getMessage) - ) - ) - ) - - def renderPage(state: AppState)(using - router: Router[Page] - ): HtmlElement = - val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) - .collectSignal[Page.Detail]( - connectors - .DetailPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailParametru]( - connectors - .DetailParametruPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailKriteria]( - connectors - .DetailKriteriaPageConnector(state)(_) - .apply - ) - .collectSignal[Page.UpravDukazKriteria]( - pages.detail.UpravDukaz.Connector(state)(_).apply - ) - .collectStatic(Page.Dashboard)( - connectors.DashboardPageConnector(state.actionBus).apply - ) - .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) - ) - .collect[Page.UnhandledError](pg => - pages.errors - .UnhandledErrorPage( - pages.errors.UnhandledErrorPage - .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), - state.actionBus - ) - ) - .collectStatic(Page.Directory)( - connectors - .DirectoryPageConnector(state.users, state.actionBus) - .apply - ) - div(cls := "h-full", child <-- pageSplitter.$view) - - // Pull in the stylesheet - val css: Css.type = Css diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala deleted file mode 100644 index 3bbc8c0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ /dev/null @@ -1,198 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.* -import org.scalajs.dom -import zio.json.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import scala.scalajs.js -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.Page.Titled - -// enum is not working with Waypoints' SplitRender collectStatic -sealed abstract class Page( - val id: String, - val title: String, - val parent: Option[Page] -) { - val path: Vector[Page] = - parent match - case None => Vector(this) - case Some(p) => p.path :+ this - - val isRoot: Boolean = parent.isEmpty -} - -object Page: - - case class Titled[V](value: V, title: Option[String] = None): - val show: String = title.getOrElse(value.toString) - - case object Directory extends Page("directory", "Adresář", None) - - case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) - - case class Detail(osobniCislo: Titled[OsobniCislo]) - extends Page("user", osobniCislo.show, Some(Directory)) - - object Detail { - def apply(o: UserInfo): Detail = Detail( - Titled(o.personalNumber, Some(o.name)) - ) - } - - case class DetailParametru( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String] - ) extends Page( - "parameter", - parametr.show, - Some(Detail(osobniCislo)) - ) - - object DetailParametru { - def apply(o: UserInfo, p: Parameter): DetailParametru = - DetailParametru( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)) - ) - } - - case class DetailKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "criteria", - kriterium.show, - Some(DetailParametru(osobniCislo, parametr)) - ) - - object DetailKriteria { - def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = - DetailKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class UpravDukazKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "addProof", - "Důkaz", - Some(DetailKriteria(osobniCislo, parametr, kriterium)) - ) - - object UpravDukazKriteria { - def apply( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): UpravDukazKriteria = - UpravDukazKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class NotFound(url: String) extends Page("404", "404", Some(Directory)) - - case class UnhandledError( - errorName: Option[String], - errorMessage: Option[String] - ) extends Page("500", "Unexpected error", Some(Directory)) - -object Routes: - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) - given [V: JsonEncoder]: JsonEncoder[Titled[V]] = - DeriveJsonEncoder.gen[Titled[V]] - given [V: JsonDecoder]: JsonDecoder[Titled[V]] = - DeriveJsonDecoder.gen[Titled[V]] - given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] - given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] - - val base = - js.`import`.meta.env.BASE_URL - .asInstanceOf[String] + "app" - - val homePage: Page = Page.Directory - - given router: Router[Page] = Router[Page]( - routes = List( - Route.static(homePage, root / endOfSegments, basePath = base), - Route.static( - Page.Dashboard, - root / "dashboard" / endOfSegments, - basePath = base - ), - Route[Page.Detail, String]( - encode = _.osobniCislo.value.toString, - decode = osc => Page.Detail(Titled(OsobniCislo(osc))), - root / "osoba" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.DetailParametru, (String, String)]( - encode = p => (p.osobniCislo.value.toString, p.parametr.value), - decode = - p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / endOfSegments, - basePath = base - ), - Route[Page.DetailKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.DetailKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.UpravDukazKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.UpravDukazKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / "edit" / endOfSegments, - basePath = base - ) - ), - serializePage = _.toJson, - deserializePage = _.fromJson[Page] - .fold(s => throw IllegalStateException(s), identity), - getPageTitle = _.title, - routeFallback = url => Page.NotFound(url), - deserializeFallback = _ => Page.Dashboard - )( - $popStateEvent = windowEvents.onPopState, - owner = unsafeWindowOwner - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala deleted file mode 100644 index b24b82b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -sealed trait Action - -case object FetchDirectory extends Action -case class FetchUserDetails(osc: OsobniCislo) extends Action -case class FetchParameters(osc: OsobniCislo) extends Action -case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action -case class FetchParameterCriteria( - osc: OsobniCislo, - paramId: String, - critId: String, - page: (UserInfo, Parameter, ParameterCriteria) => Page -) extends Action -case class FetchAvailableFiles(osc: OsobniCislo) extends Action -case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala deleted file mode 100644 index c637d35..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala +++ /dev/null @@ -1,82 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.{UserProfile, UserInfo, OsobniCislo} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo -import cz.e_bs.cmi.mdr.pdb.UserFunction - -object AppPage: - // TODO: pages by logged in user - val pages: List[Page] = List(Page.Directory, Page.Dashboard) - - import NavigationBar.{Logo, MenuItem} - - val logo = Logo( - "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", - "Workflow" - ) - - // TODO: menu items by user profile - val userMenu = - List( - MenuItem("Your Profile"), - MenuItem("Settings"), - MenuItem("Sign out") - ) - - // TODO: load user profile - val $userProfile = Var( - UserProfile( - "tom", - UserInfo( - OsobniCislo("1031"), - "tom", - "Tom", - "Cook", - None, - None, - Some("tom@example.com"), - Some("+420 222 866 180"), - Some( - UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") - ), - Nil, - None - ) - ) - ) - - val $userInfo = $userProfile.signal.map(_.userInfo) - - type ViewModel = Option[HtmlElement] - def apply( - actionBus: Observer[Action] - )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using - router: Router[Page] - ): HtmlElement = - PageLayout(actionBus)( - $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => - PageLayout.ViewModel( - NavigationBar.ViewModel( - u, - pages.map(p => - NavigationBar.Link( - () => - PageLink( - p, - actionBus - ), - p == cp - ) - ), - userMenu, - logo - ), - c - ) - ), - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala deleted file mode 100644 index bf473c6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ /dev/null @@ -1,143 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden -import fiftyforms.ui.components.tailwind.list.IconText.ViewModel -import fiftyforms.ui.components.tailwind.Icons -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Action - -object Breadcrumbs: - - private def slash = - import svg.{*, given} - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-300", - xmlns := "http://www.w3.org/2000/svg", - fill := "currentColor", - viewBox := "0 0 20 20", - ariaHidden := true, - path( - d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" - ) - ) - - def backIcon = - Icons.solid - .`arrow-narrow-left`() - .amend( - svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" - ) - - object Link: - case class ViewModel( - page: Page, - icon: Option[SvgElement], - extraClasses: String, - text: String, - textClasses: Option[String] = None - ) - - val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" - - def shortHome(p: Page) = ViewModel( - p, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - "Zpět na úvodní stránku", - None - ) - - def fullHome(p: Page) = ViewModel( - p, - Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), - "text-gray-400 hover:text-gray-500", - "Domů", - Some("sr-only") - ) - - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - PageLink - .container($m.map(_.page), actionBus) - .amend( - cls <-- $m.map(_.extraClasses), - child.maybe <-- $m.map(_.icon), - span( - cls <-- $m.map(_.textClasses.getOrElse("")), - child.text <-- $m.map(_.text) - ) - ) - - object FullBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - ol( - role := "list", - cls := "flex items-center space-x-4", - children <-- $m.map(_.path).split(_.id)((_, _, $p) => - li( - div( - cls := "flex items-center", - child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), - Link( - $p.map(p => - if (p.isRoot) then Link.fullHome(p) - else - Link.ViewModel( - p, - None, - s"ml-4 max-w-xs truncate ${Link.baseClasses}", - p.title - ) - ), - actionBus - ) - ) - ) - ) - ) - - object ShortBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - Link( - $m.map { p => - val target = p.parent.getOrElse(p) - if target.isRoot then Link.shortHome(target) - else - Link.ViewModel( - target, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - s"Zpět na ${target.title}" - ) - }, - actionBus - ) - - def apply(actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - nav( - cls := "flex", - aria.label := "Breadcrumb", - div( - cls := "flex sm:hidden", - ShortBreadcrumbs(router.$currentPage, actionBus) - ), - div( - cls := "hidden sm:block", - FullBreadcrumbs(router.$currentPage, actionBus) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala deleted file mode 100644 index 0db2047..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ /dev/null @@ -1,242 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent -import fiftyforms.ui.components.tailwind.Icons -import fiftyforms.ui.components.tailwind.Avatar -import cz.e_bs.cmi.mdr.pdb.UserInfo -import io.laminext.syntax.core.* - -object NavigationBar: - - case class Logo(img: String, name: String) - case class Link(a: () => Anchor, active: Boolean) - case class MenuItem(title: String) - - case class ViewModel( - userInfo: UserInfo, - pages: List[Link], - userMenu: List[MenuItem], - logo: Logo - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - val $userInfo = $m.map(_.userInfo) - val mobileMenuOpen = Var(false) - - val desktopOnly = cls("hidden md:block") - val mobileOnly = cls("md:hidden") - - inline def avatarImage(size: Int = 8) = - Avatar($userInfo.map(_.img)).avatarImage(size) - - def notificationButton = button( - tpe := "button", - cls := List( - "bg-indigo-600", - "focus:outline-none", - "focus:ring-2", - "focus:ring-offset-2", - "focus:ring-offset-indigo-600", - "focus:ring-white", - "hover:text-white", - "p-1", - "rounded-full", - "text-indigo-200" - ), - span(cls := "sr-only", "View notifications"), - Icons.outline.bell() - ) - - def userProfile: HtmlElement = - val menuOpen = Var(false) - - def menuItem(item: MenuItem, idx: Int): HtmlElement = - a( - href := "#", - cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", - role := "menuitem", - tabIndex := -1, - idAttr := s"user-menu-item-$idx", - item.title - ) - - div( - cls := "ml-3 relative", - div( - button( - tpe := "button", - cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - idAttr := "user-menu-button", - aria.expanded <-- menuOpen.signal, - aria.hasPopup := true, - span(cls := "sr-only", "Open user menu"), - child <-- avatarImage(), - onClick.preventDefault.mapTo( - !menuOpen.now() - ) --> menuOpen.writer - ) - ), - /* - * */ - div( - cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", - cls <-- menuOpen.signal.map { o => - if (o) "md:block" else "md:hidden" - }, - role := "menu", - aria.orientation := "vertical", - aria.labelledBy := "user-menu-button", - tabIndex := -1, - // : keyboard navigation - children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) - ) - ) - - def mobileProfile = - def menuItem(item: MenuItem): HtmlElement = - a( - href := "#", - cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", - item.title - ) - - div( - cls := "pt-4 pb-3 border-t border-indigo-700", - div( - cls := "flex items-center px-5", - div( - cls := "flex-shrink-0", - child <-- avatarImage(10) - ), - div( - cls := "ml-3", - div( - cls := "text-base font-medium text-white", - child.text <-- $userInfo.map(_.name) - ), - child.maybe <-- $userInfo.map( - _.email.map(e => - div( - cls := "text-sm font-medium text-indigo-300", - e - ) - ) - ) - ), - notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) - ), - div( - cls := "mt-3 px-2 space-y-1", - children <-- $m.map(_.userMenu.map(menuItem)) - ) - ) - - def pageLink(page: Link): Anchor = - page - .a() - .amend( - cls := "text-white px-3 py-2 rounded-md text-sm font-medium", - cls := Seq( - "bg-indigo-700" -> page.active, - "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active - ), - ariaCurrent := (if page.active then "page" else "false") - ) - - def logoImg: Image = - img( - cls := "h-8 w-8", - src <-- $m.map(_.logo.img), - alt <-- $m.map(_.logo.name) - ) - - def mobileMenuButton = button( - tpe := "button", - cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - aria.controls := "mobile-menu", - aria.expanded <-- mobileMenuOpen.signal, - span(cls := "sr-only", "Open main menu"), - Icons.outline - .menu() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") - ), - Icons.outline - .x() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") - ), - onClick.preventDefault.mapTo( - !mobileMenuOpen.now() - ) --> mobileMenuOpen.writer - ) - - def navBarLeft = - div( - cls := "flex items-center", - div(cls := "flex-shrink-0", logoImg), - div( - desktopOnly, - div( - cls := "ml-10 flex items-baseline space-x-4", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ) - ) - ) - - def navBarRight = - div( - desktopOnly, - div( - cls := "ml-4 flex items-center md:ml-6", - notificationButton, - userProfile - ) - ) - - def navBarMobile = - div( - cls := "-mr-2 flex", - mobileOnly, - mobileMenuButton - ) - - def navBar = - div( - cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex items-center justify-between h-16", - navBarLeft, - navBarRight, - navBarMobile - ) - ) - - def mobileMenu = - div( - mobileOnly, - cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }, - idAttr := "mobile-menu", - div( - cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ), - mobileProfile - ) - - nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala deleted file mode 100644 index 75339df..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object PageHeader: - def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = - header( - cls := "bg-white shadow-sm", - div( - cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", - h1( - cls := "text-lg leading-6 font-semibold text-gray-900", - Breadcrumbs(actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala deleted file mode 100644 index 5096463..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ /dev/null @@ -1,30 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import fiftyforms.ui.components.tailwind.Loading -import io.laminext.syntax.core.* - -object PageLayout: - case class ViewModel( - navigation: NavigationBar.ViewModel, - content: Option[HtmlElement] - ) - def apply(actionBus: Observer[Action])( - $m: Signal[ViewModel], - mods: Modifier[HtmlElement]* - )(using router: Router[Page]): HtmlElement = - val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) - div( - cls := "h-full flex flex-col", - NavigationBar($m.map(_.navigation)), - child.maybe <-- router.$currentPage.map(_.isRoot) - .switch(None, Some(PageHeader(actionBus))), - main( - cls := "flex-grow-1 overflow-y-auto", - mods, - child <-- $maybeContent.map(_.getOrElse(Loading)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala deleted file mode 100644 index 62d820f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.LinkSupport.* -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo - -object PageLink: - type ViewModel = Page - def apply($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = - a(mods($m, actions), child.text <-- $m.map(_.title)) - - def apply(m: ViewModel, actions: Observer[Action])(using - router: Router[Page] - ): Anchor = apply(Val(m), actions) - - def container($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = a(mods($m, actions)) - - def container(m: ViewModel, actions: Observer[Action])(using - Router[Page] - ): Anchor = container(Val(m), actions) - - private def mods($m: Signal[Page], actions: Observer[Action])(using - router: Router[Page] - ): Modifier[Anchor] = - Seq( - href <-- $m.map(router.absoluteUrlForPage).recover { case _ => - Some("invalid url") - }, - composeEvents(onClick.noKeyMod.preventDefault)( - _.sample($m) - .map(NavigateTo.apply) - ) --> actions - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala deleted file mode 100644 index 87f825a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.pages.dashboard.DashboardPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -class DashboardPageConnector(actionBus: Observer[Action])(using - router: Router[Page] -): - def apply: HtmlElement = - AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala deleted file mode 100644 index e9b0f71..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala +++ /dev/null @@ -1,76 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailKriteriaPage -import pages.detail.components.DukazyKriteria -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -object DetailKriteriaPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailKriteriaPageConnector( - state: DetailKriteriaPageConnector.AppState -)( - $page: Signal[Page.DetailKriteria] -)(using Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => - (p.osobniCislo.value, p.parametr.value, p.kriterium.value) - )((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map( - FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) - ) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, s, $s) => - DetailKriteriaPage($s)(state.actionBus.contramap { - case DukazyKriteria.Add => - NavigateTo( - Page.UpravDukazKriteria( - Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), - Page.Titled(s.parametr.id, Some(s.parametr.nazev)), - Page.Titled(s.kriterium.id, Some(s.kriterium.id)) - ) - ) - }) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): DetailKriteriaPage.ViewModel = - DetailKriteriaPage.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala deleted file mode 100644 index 4d8024f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ /dev/null @@ -1,57 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import pages.detail.DetailPage -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import fiftyforms.ui.components.tailwind.Color - -object DetailPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailPageConnector(state: DetailPageConnector.AppState)( - $page: Signal[Page.Detail] -)(using router: Router[Page]): - val $oscChangeSignal = $page.splitOne(_.osobniCislo.value)((osc, _, _) => osc) - val $pageChangeSignal = - $oscChangeSignal.flatMap(osc => - EventStream.fromSeq(Seq(FetchUserDetails(osc), FetchParameters(osc))) - ) - // TODO: filter the value based on the current osc - // OSC change will fetch new data, but still - // - we need to be sure that what we got is really what we ought to display - // - we want to display stale data accordingly (at least with loading indicator) - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - def apply: HtmlElement = - AppPage(state.actionBus)( - $data.combineWithFn($params)(_ zip _) - .map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: List[Parameter] - ): DetailPage.ViewModel = - DetailPage.ViewModel( - o.toDetailOsoby, - p.map( - _.toParametr(param => - PageLink.container(Page.DetailParametru(o, param), state.actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala deleted file mode 100644 index fdaa7f8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailParametruPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink - -object DetailParametruPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailParametruPageConnector( - state: DetailParametruPageConnector.AppState -)( - $page: Signal[Page.DetailParametru] -)(using router: Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => (p.osobniCislo.value, p.parametr.value))((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map(FetchParameter(_, _)) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - } yield (da, pb) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailParametruPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter - ): DetailParametruPage.ViewModel = - DetailParametruPage.ViewModel( - o.toDetailOsoby, - p.toParametr(p => - PageLink.container(Page.DetailParametru(o, p), state.actionBus) - ), - p.criteria.map( - _.toKriterium { c => - PageLink.container( - Page.DetailKriteria(o, p, c), - state.actionBus - ) - } - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala deleted file mode 100644 index 049aa22..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -case class DirectoryPageConnector( - $input: EventStream[List[UserInfo]], - actionBus: Observer[Action] -)(using router: Router[Page]): - val $data = $input.startWithNone - val $actionSignal = EventStream.fromValue(FetchDirectory) - - def apply: HtmlElement = - AppPage(actionBus)( - $data.split(_ => ())((_, _, s) => - pages.directory.DirectoryPage( - s.map( - _.map( - _.toUserRow(u => - PageLink.container( - Page.Detail(Page.Titled(u.personalNumber)), - actionBus - ) - ) - ) - ) - ) - ), - $actionSignal --> actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala deleted file mode 100644 index 88e7450..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ /dev/null @@ -1,68 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii -import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria -import fiftyforms.ui.components.tailwind.Color - -extension (o: UserInfo) - def toDetailOsoby: DetailOsoby.ViewModel = - DetailOsoby.ViewModel( - o.personalNumber, - o.name, - o.email, - o.phone, - o.img, - o.mainFunction.map(f => - DetailOsoby.Funkce.ViewModel(f.name, f.dept, f.ou) - ), - o.userContracts.headOption.map(c => - DetailOsoby.PracovniPomer.ViewModel(c.rel, c.startDate, c.endDate) - ) - ) - -extension (param: Parameter) - def toParametr(container: Parameter => Anchor): DetailParametru.ViewModel = - DetailParametru.ViewModel( - id = param.id, - nazev = param.name, - popis = param.description, - status = "Nesplněno", - statusColor = Color.red, - a = container(param) - ) - -extension (crit: ParameterCriteria) - def toKriterium( - container: ParameterCriteria => Anchor - ): DetailKriteria.ViewModel = - DetailKriteria.ViewModel( - nazev = crit.criteriumText, - kapitola = crit.chapterId, - bod = crit.itemId, - status = "Nesplněno", - statusColor = Color.red, - splneno = false, - dukazy = Nil, - container = container(crit) - ) - -extension (user: UserInfo) - def toUserRow( - container: UserInfo => HtmlElement = (_: UserInfo) => div() - ): UserRow.ViewModel = - UserRow.ViewModel( - osobniCislo = user.personalNumber.toString, - celeJmeno = user.name, - prijmeni = user.surname, - hlavniFunkce = user.mainFunction.map(_.name), - img = user.img, - container = () => container(user) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala deleted file mode 100644 index 7d5b2c1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.dashboard - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.Page - -object DashboardPage: - - def render: HtmlElement = - div("Dashboard page") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala deleted file mode 100644 index 9ef0698..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala +++ /dev/null @@ -1,31 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailKriteriaPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply($m: Signal[ViewModel])( - action: Observer[DukazyKriteria.Action] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - DukazyKriteria($m.map(_.kriterium.dukazy))(action) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala deleted file mode 100644 index 3b316e1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object DetailPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametry: SeznamParametru.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby($m.map(_.osoba)), - SeznamParametru($m.map(_.parametry)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala deleted file mode 100644 index 17224cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala +++ /dev/null @@ -1,24 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailParametruPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriteria: SeznamKriterii.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby.header($m.map(_.osoba)), - DetailParametru($m.map(_.parametr)), - SeznamKriterii($m.map(_.kriteria)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala deleted file mode 100644 index dc912fa..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ /dev/null @@ -1,107 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb -package app -package pages.detail - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.UpravDukazForm -import fiftyforms.services.files.File - -object UpravDukaz: - - type ThisPage = Page.UpravDukazKriteria - type PageKey = (OsobniCislo, String, String) - - trait State { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def availableFiles: EventStream[List[File]] - def actionBus: Observer[Action] - } - - def keyOfPage(page: ThisPage): PageKey = - (page.osobniCislo.value, page.parametr.value, page.kriterium.value) - - def onChangeAction(key: PageKey): Action = - FetchParameterCriteria( - key._1, - key._2, - key._3, - Page.UpravDukazKriteria(_, _, _) - ) - - class Connector(state: State)($page: Signal[ThisPage])(using Router[Page]): - val $paramChangeSignal = $page.splitOne(keyOfPage)((x, _, _) => x) - val $pageChangeSignal = $paramChangeSignal.map(onChangeAction) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.split(_ => ())((_, s, $s) => - PageComponent( - $s.map(buildModel), - state.availableFiles, - state.actionBus.contramap { - case UpravDukazForm.Cancelled => - NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) - case UpravDukazForm.AvailableFilesRequested => - FetchAvailableFiles(s._1.personalNumber) - } - ) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): PageComponent.ViewModel = - import connectors.* - PageComponent.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) - - object PageComponent: - import components.* - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply( - $m: Signal[ViewModel], - availableFilesStream: EventStream[List[File]], - events: Observer[UpravDukazForm.Event] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - UpravDukazForm(availableFilesStream)(events) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala deleted file mode 100644 index ca311b0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.Icons -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Color - -object DetailKriteria: - case class ViewModel( - nazev: String, - kapitola: String, - bod: String, - status: String, - statusColor: Color, - splneno: Boolean, - dukazy: List[DukazKriteria.ViewModel], - container: HtmlElement = div() - ) { - val id = s"${kapitola}${bod}" - } - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h3( - cls := "text-xl leading-6 font-bold text-gray-900", - child.text <-- $m.map(_.nazev) - ), - h3( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - "Kapitola ", - child.text <-- $m.map(_.kapitola), - " bod ", - child <-- $m.map(_.bod) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala deleted file mode 100644 index ae90290..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala +++ /dev/null @@ -1,100 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Avatar - -object DetailOsoby: - - object Funkce: - case class ViewModel( - nazev: String, - stredisko: String, - voj: String - ) - - def render($m: Signal[ViewModel]): HtmlElement = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.nazev), - span( - cls := "hidden md:inline", - " @ ", - child.text <-- $m.map(_.stredisko), - ", ", - child.text <-- $m.map(_.voj) - ) - ) - - object PracovniPomer: - case class ViewModel( - druh: String, - pocatek: LocalDate, - konec: Option[LocalDate] - ) - - def render($m: Signal[ViewModel]): HtmlElement = - import fiftyforms.ui.components.tailwind.CustomAttrs.datetime - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.druh), - " od ", - time( - datetime <-- $m.map(_.pocatek.toString), - child.text <-- $m.map(_.pocatek.toString) - ) - ) - - case class ViewModel( - osobniCislo: OsobniCislo, - jmeno: String, - email: Option[String], - telefon: Option[String], - img: Option[String], - hlavniFunkce: Option[Funkce.ViewModel], - pracovniPomer: Option[PracovniPomer.ViewModel] - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-4", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(16) - ), - div( - h1( - cls := "text-2xl font-bold text-gray-900", - child.text <-- $m.map(_.jmeno) - ), - child.maybe <-- $m.map(_.hlavniFunkce).split(_ => ())((_, _, d) => - Funkce.render(d) - ), - child.maybe <-- $m.map(_.pracovniPomer).split(_ => ())((_, _, d) => - PracovniPomer.render(d) - ) - ) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-5", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(8) - ), - div( - h1( - cls := "text-lg font-medium text-gray-800", - child.text <-- $m.map(_.jmeno) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala deleted file mode 100644 index e11cb62..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color - -object DetailParametru: - case class ViewModel( - id: String, - nazev: String, - popis: String, - status: String, - statusColor: Color, - a: Anchor - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h2( - cls := "text-2xl leading-6 font-medium text-gray-900", - child.text <-- $m.map(_.nazev) - ), - p( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - child.text <-- $m.map(_.popis) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - h2( - cls := "text-lg leading-6 font-medium text-gray-600", - child.text <-- $m.map(_.nazev) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala deleted file mode 100644 index a00ede8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala +++ /dev/null @@ -1,177 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.CustomAttrs - -object DukazKriteria: - case class Osoba(osobniCislo: String, jmeno: String) - case class Udalost(osoba: Osoba, datum: LocalDate) - case class Dokument( - url: String, - nazev: String, - pridal: Udalost, - odebral: Option[Udalost] - ) - case class ViewModel( - dokumenty: List[Dokument], - autorizoval: Option[Udalost], - platiDo: Option[LocalDate], - poznámka: Option[String] - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-lg", - div( - cls := "px-4 py-5 sm:px-6", - h3( - cls := "text-lg leading-6 font-medium text-gray-900", - """Applicant Information""" - ), - p( - cls := "mt-1 max-w-2xl text-sm text-gray-500", - """Personal details and application.""" - ) - ), - div( - cls := "border-t border-gray-200 px-4 py-5 sm:px-6", - dl( - cls := "grid grid-cols-1 gap-x-4 gap-y-8 sm:grid-cols-2", - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Full name""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Margot Foster""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Application for""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Backend Developer""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Email address""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """margotfoster@example.com""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Salary expectation""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """$120,000""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """About""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Fugiat ipsum ipsum deserunt culpa aute sint do nostrud anim incididunt cillum culpa consequat. Excepteur qui ipsum aliquip consequat sint. Sit id mollit nulla mollit nostrud in ea officia proident. Irure nostrud pariatur mollit ad adipisicing reprehenderit deserunt qui eu.""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """Attachments""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - ul( - role := "list", - cls := "border border-gray-200 rounded-md divide-y divide-gray-200", - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """resume_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ), - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """coverletter_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala deleted file mode 100644 index 22f134f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala +++ /dev/null @@ -1,35 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object DukazyKriteria: - sealed trait Action - case object Add extends Action - - type ViewModel = List[DukazKriteria.ViewModel] - - def apply($m: Signal[ViewModel])(actions: Observer[Action]): HtmlElement = - div( - child.maybe <-- $m.map(m => - if (m.isEmpty) then Some(prazdnyDukaz(actions)) - else None - ), - children <-- $m.map(_.zipWithIndex).split(_._2)((_, _, $s) => - DukazKriteria($s.map(_._1)) - ) - ) - - private def prazdnyDukaz(actions: Observer[Action]): HtmlElement = - button( - tpe := "button", - onClick.preventDefault.mapTo(Add) --> actions, - cls := "relative block w-full border-2 border-gray-300 border-dashed rounded-lg p-12 text-center hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - Icons.outline - .`document-add`(12) - .amend(svg.cls := "mx-auto text-gray-400"), - span( - cls := "mt-2 block text-sm font-medium text-gray-900", - "Přidat důkaz" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala deleted file mode 100644 index f628697..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.list.StackedList -import fiftyforms.ui.components.tailwind.list.ListRow -import fiftyforms.ui.components.tailwind.list.RowTag -import fiftyforms.ui.components.tailwind.list.RowNext -import java.time.LocalDate - -object SeznamKriterii: - type ViewModel = List[DetailKriteria.ViewModel] - - private val kritList = new StackedList[DetailKriteria.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - kritList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = - p(cls := "text-sm text-gray-500", s"${i.kapitola}${i.bod}"), - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.container - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala deleted file mode 100644 index 2bfe4bc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.list.{ - StackedList, - ListRow, - RowTag, - PropList, - IconText, - RowNext -} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.LinkSupport.* - -object SeznamParametru: - type ViewModel = List[DetailParametru.ViewModel] - - private val parametrList = new StackedList[DetailParametru.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - parametrList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = emptyNode, - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.a - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala deleted file mode 100644 index f048e9d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ /dev/null @@ -1,101 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.form.* -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement -import fiftyforms.services.files.components.tailwind.FilePicker -import cz.e_bs.cmi.mdr.pdb.frontend.AutorizujDukaz -import cz.e_bs.cmi.mdr.pdb.frontend.DocumentRef -import fiftyforms.services.files.File - -object UpravDukazForm: - sealed trait Event - case object Cancelled extends Event - case object AvailableFilesRequested extends Event - def apply(availableFilesStream: EventStream[List[File]])( - updates: Observer[Event] - ): HtmlElement = - val (filesStream, filesObserver) = - EventStream.withObserver[FilePicker.Event] - val files = Var[List[File]](Nil) - def submitButtons: HtmlElement = - div( - cls := "pt-5", - div( - cls := "flex justify-end", - button( - tpe := "button", - cls := "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zrušit", - onClick.mapTo(Cancelled) --> updates - ), - button( - tpe := "submit", - cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - disabled <-- files.signal.map(_.isEmpty), - "Autorizovat" - ) - ) - ) - - div( - filesStream.collect { case FilePicker.SelectionUpdated(files) => - files.to(List) - } --> files.writer, - filesStream.collect { case FilePicker.AvailableFilesRequested => - AvailableFilesRequested - } --> updates, - cls := "bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6", - Form( - Form.Body( - Form.Section( - FormHeader( - FormHeader.ViewModel( - "Doložení kritéria", - "Sestavte doklady poskytující důkaz kritéria a potvrďte odesláním formuláře. Případné limitace či jiné relevantní údaje vepište do pole Komentář." - ) - ), - FormFields( - FormRow( - "dokumenty", - "Dokumenty", - FilePicker(files.signal, availableFilesStream)(filesObserver) - .amend(idAttr := "dokumenty", cls("max-w-lg")) - ).toHtml, - FormRow( - "platnost", - "Platnost", - input( - idAttr := "platnost", - name := "platnost", - tpe := "date", - autoComplete := "date", - cls := "block max-w-lg w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" - ) - ).toHtml, - FormRow( - "komentar", - "Komentář", - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - textArea( - idAttr := "komentar", - name := "about", - rows := 3, - cls := "max-w-lg shadow-sm block w-full focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border border-gray-300 rounded-md" - ), - p( - cls := "mt-2 text-sm text-gray-500", - "Doplňte prosím potřebné informace související s doložením kritéria, včetně případných limitací." - ) - ) - ).toHtml - ) - ) - ), - submitButtons - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala deleted file mode 100644 index 41033f6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -import components._ - -object DirectoryPage: - - type ViewModel = List[UserRow.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - val (actionsStream, actionObserver) = - EventStream.withObserver[SearchForm.Action] - val $filter = actionsStream - .collect { - case SearchForm.ClearFilter => None - case SearchForm.SetFilter(t) => Some(t) - } - .startWith(None) - val byLetter = for { - d <- $m - f <- $filter - } yield for { - (letter, users) <- d - .filter { user => - f match - case None => true - case Some(t) => user.search.contains(t) - } - .groupBy(_.prijmeni.head) - .to(List) - .sortBy(_._1) - } yield (letter.toString, users.sortBy(_.prijmeni)) - - div( - cls := "h-full max-w-7xl mx-auto order-first flex flex-col flex-shrink-0", - SearchForm(actionObserver), - Directory(byLetter) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala deleted file mode 100644 index 73f915d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -object Directory: - - object Header: - type ViewModel = String - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500", - h3(child.text <-- $m) - ) - - object UserList: - type ViewModel = List[UserRow.ViewModel] - def apply($m: Signal[ViewModel]): HtmlElement = - ul( - role := "list", - cls := "relative z-0 divide-y divide-gray-200", - children <-- $m.split(_.osobniCislo)((_, _, s) => UserRow(s)) - ) - - type ViewModel = List[(String, List[UserRow.ViewModel])] - def apply($m: Signal[ViewModel]): HtmlElement = - nav( - cls := "flex-1 min-h-0 overflow-y-auto", - aria.label := "Directory", - children <-- $m.split(_._1)((_, _, s) => - div( - cls := "relative", - Header(s.map(_._1)), - UserList(s.map(_._2)) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala deleted file mode 100644 index e182910..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala +++ /dev/null @@ -1,54 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object SearchForm: - sealed trait Action - case object SubmitFilter extends Action - sealed trait FilterAction extends Action - case object ClearFilter extends FilterAction - case class SetFilter(value: String) extends FilterAction - - def apply(actions: Observer[Action]): HtmlElement = - div( - cls := "px-6 pt-4 pb-4", - form( - cls := "flex space-x-4", - onSubmit.mapTo(SubmitFilter) --> actions, - div( - cls := "flex-1 min-w-0", - label( - forId := "search", - cls := "sr-only", - "Hledat" - ), - div( - cls := "relative rounded-md shadow-sm", - div( - cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", - Icons.solid.search().amend(svg.cls := "text-gray-400") - ), - input( - tpe := "search", - name := "search", - idAttr := "search", - cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", - placeholder := "Hledat", - composeEvents(onInput.mapToValue.setAsValue.map(SetFilter(_)))( - _.throttle(500) - ) --> actions - ) - ) - ), - button( - tpe := "submit", - cls := "inline-flex justify-center px-3.5 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500", - Icons.solid.filter().amend(svg.cls := "text-gray-400"), - span( - cls := "sr-only", - "Hledat" - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala deleted file mode 100644 index 82f7723..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala +++ /dev/null @@ -1,52 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.airstream.core.Signal -import fiftyforms.ui.components.tailwind.Avatar - -object UserRow: - case class ViewModel( - osobniCislo: String, - celeJmeno: String, - prijmeni: String, - hlavniFunkce: Option[String], - img: Option[String], - container: () => HtmlElement = () => div() - ) { - val search = prijmeni.toLowerCase - } - - def apply($m: Signal[ViewModel]): HtmlElement = - inline def avatarImage = - Avatar($m.map(_.img)).avatarImage(10) - - li( - div( - cls := "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500", - div( - cls := "flex-shrink-0", - child <-- avatarImage - ), - div( - cls := "flex-1 min-w-0", - child <-- $m.map { o => - o.container() - .amend( - cls := "focus:outline-none", - span( - cls := "absolute inset-0", - aria.hidden := true - ), - p( - cls := "text-sm font-medium text-gray-900", - o.celeJmeno - ), - p( - cls := "text-sm text-gray-500 truncate", - o.hlavniFunkce - ) - ) - } - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala deleted file mode 100644 index 467c5f5..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala +++ /dev/null @@ -1,99 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router - -object ErrorPage: - case class ViewModel( - homePage: Page, - errorName: String, - title: String, - subTitle: String - ) - - def apply(m: ViewModel, actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - val ViewModel(homePage, errorName, title, subTitle) = m - div( - cls := "min-h-full pt-16 pb-12 flex flex-col bg-white", - main( - cls := "flex-grow flex flex-col justify-center max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex-shrink-0 flex justify-center", - a( - href := "/", - cls := "inline-flex", - span( - cls := "sr-only", - """Workflow""" - ), - img( - cls := "h-12 w-auto", - src := "https://tailwindui.com/img/logos/workflow-mark.svg?color=indigo&shade=600", - alt := "" - ) - ) - ), - div( - cls := "py-16", - div( - cls := "text-center", - p( - cls := "text-sm font-semibold text-indigo-600 uppercase tracking-wide", - errorName - ), - h1( - cls := "mt-2 text-4xl font-extrabold text-gray-900 tracking-tight sm:text-5xl", - title - ), - p( - cls := "mt-2 text-base text-gray-500", - subTitle - ), - div( - cls := "mt-6", - PageLink - .container(homePage, actionBus) - .amend( - cls := "text-base font-medium text-indigo-600 hover:text-indigo-500", - """Go back home""" - ) - ) - ) - ) - ), - footer( - cls := "flex-shrink-0 max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", - nav( - cls := "flex justify-center space-x-4", - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Contact Support""" - ), - span( - cls := "inline-block border-l border-gray-300", - aria.hidden := true - ), - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Status""" - ), - span( - cls := "inline-block border-l border-gray-300", - aria.hidden := true - ), - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Twitter""" - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala deleted file mode 100644 index bd9baec..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action - -object NotFoundPage: - def apply(homePage: Page, url: String, actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - ErrorPage( - ErrorPage.ViewModel( - homePage, - "404 error", - "Page not found.", - s"Sorry, but page $url doesn't exist." - ), - actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala deleted file mode 100644 index 476fe3c..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action - -object UnhandledErrorPage: - - case class ViewModel( - homePage: Page, - errorName: Option[String], - errorMessage: Option[String] - ) - - def apply(m: ViewModel, actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - ErrorPage( - ErrorPage.ViewModel( - m.homePage, - "Unexpected error occurred", - m.errorName.getOrElse( - "Uh oh!" - ), // TODO: translations, better text than uh oh - m.errorMessage.getOrElse( - "This wasn't supposed to happen! Please try again." - ) - ), - actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala deleted file mode 100644 index 25f059f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala +++ /dev/null @@ -1,6 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.services - -import com.raquo.airstream.core.Observer - -trait DataFetcher[K, A]: - def fetch(id: K, sink: Observer[A]): Unit diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala deleted file mode 100644 index a469830..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala +++ /dev/null @@ -1,120 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package state - -import com.raquo.airstream.core.EventStream -import cz.e_bs.cmi.mdr.pdb.{UserInfo, OsobniCislo} -import com.raquo.airstream.core.Observer -import scala.scalajs.js -import scala.scalajs.js.JSON -import zio.json._ -import com.raquo.airstream.eventbus.EventBus -import com.raquo.airstream.ownership.Owner -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.UserFunction -import cz.e_bs.cmi.mdr.pdb.UserContract -import fiftyforms.services.files.File - -trait AppState - extends connectors.DetailPageConnector.AppState - with connectors.DetailParametruPageConnector.AppState - with connectors.DetailKriteriaPageConnector.AppState - with pages.detail.UpravDukaz.State: - def users: EventStream[List[UserInfo]] - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - -class MockAppState(implicit owner: Owner, router: Router[Page]) - extends AppState: - - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonDecoder[UserFunction] = DeriveJsonDecoder.gen - given JsonDecoder[UserContract] = DeriveJsonDecoder.gen - given JsonDecoder[UserInfo] = DeriveJsonDecoder.gen - - given JsonDecoder[ParameterCriteria] = DeriveJsonDecoder.gen - given JsonDecoder[Parameter] = DeriveJsonDecoder.gen - - private val actions = EventBus[Action]() - private val (parametersStream, pushParameters) = - EventStream.withCallback[List[Parameter]] - private val (usersStream, pushUsers) = - EventStream.withCallback[List[UserInfo]] - private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] - private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] - - private val mockData: List[UserInfo] = - mockUsers - .asInstanceOf[js.Dictionary[js.Object]] - .values - // TODO: is there a more efficient way to parse from JS object directly? - .map { o => - val parsed = JSON.stringify(o).fromJson[UserInfo] - parsed.left.foreach(org.scalajs.dom.console.log(_)) - parsed - } - .collect { case Right(u) => - u - } - .toList - - private val mockParameters: List[Parameter] = - pdbParams - .asInstanceOf[js.Dictionary[js.Object]] - .values - .map(o => JSON.stringify(o).fromJson[Parameter]) - .collect { case Right(p) => p } - .toList - - // TODO: Extract to separate event handler - actions.events.foreach { - case FetchDirectory => pushUsers(mockData) - case FetchUserDetails(osc) => - mockData.find(_.personalNumber == osc).foreach { o => - pushDetails(o) - router.replaceState(Page.Detail(o)) - } - case FetchParameters(osc) => - pushParameters(mockParameters) - case FetchParameter(osc, paramId) => - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(Page.DetailParametru(o, p)) - case FetchParameterCriteria(osc, paramId, critId, page) => - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - c <- p.criteria.find(_.id == critId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(page(o, p, c)) - case NavigateTo(page) => router.pushState(page) - case FetchAvailableFiles(osc) => - pushFiles( - List( - File("https://tc163.cmi.cz/here", "Example file") - ) - ) - } - - override def users: EventStream[List[UserInfo]] = - usersStream.debugWithName("users") - - override def details: EventStream[UserInfo] = - detailsStream.debugWithName("details") - - override def parameters: EventStream[List[Parameter]] = - parametersStream.debugWithName("parameters") - - override def availableFiles: EventStream[List[File]] = - filesStream.debugWithName("available files") - - override def actionBus: Observer[Action] = - actions.writer.debugWithName("actions writer") diff --git a/app/src/main/scala/mdr/pdb/app/Main.scala b/app/src/main/scala/mdr/pdb/app/Main.scala new file mode 100644 index 0000000..081b4ee --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/Main.scala @@ -0,0 +1,103 @@ +package mdr.pdb.app + +import scala.scalajs.js.annotation.JSExportTopLevel +import scala.scalajs.js.annotation.JSExport +import scala.scalajs.js.annotation.JSImport +import scala.scalajs.js +import org.scalajs.dom +import com.raquo.laminar.api.L.{*, given} +import scala.scalajs.js.Date +import com.raquo.waypoint.Router +import com.raquo.waypoint.SplitRender +import mdr.pdb.app.services.DataFetcher +import scala.scalajs.js.JSON +import zio.json._ +import mdr.pdb.UserInfo +import mdr.pdb.app.state.AppState + +@js.native +@JSImport("stylesheets/main.css", JSImport.Namespace) +object Css extends js.Any + +@js.native +@JSImport("data/users.json", JSImport.Default) +object mockUsers extends js.Object + +@js.native +@JSImport("params/pdb-params.json", JSImport.Default) +object pdbParams extends js.Object + +@JSExportTopLevel("app") +object Main: + + @JSExport + def main(args: Array[String]): Unit = + import Routes.given + onLoad { + setupAirstream() + val appContainer = dom.document.querySelector("#app") + val _ = + render( + appContainer, + renderPage(state.MockAppState(using unsafeWindowOwner, router)) + ) + } + + private def onLoad(f: => Unit): Unit = + documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) + + private def setupAirstream()(using router: Router[Page]): Unit = + AirstreamError.registerUnhandledErrorCallback(err => + router.forcePage( + Page.UnhandledError( + Some(err.getClass.getName), // TODO: Fill only in dev mode + Some(err.getMessage) + ) + ) + ) + + def renderPage(state: AppState)(using + router: Router[Page] + ): HtmlElement = + val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) + .collectSignal[Page.Detail]( + connectors + .DetailPageConnector(state)(_) + .apply + ) + .collectSignal[Page.DetailParametru]( + connectors + .DetailParametruPageConnector(state)(_) + .apply + ) + .collectSignal[Page.DetailKriteria]( + connectors + .DetailKriteriaPageConnector(state)(_) + .apply + ) + .collectSignal[Page.UpravDukazKriteria]( + pages.detail.UpravDukaz.Connector(state)(_).apply + ) + .collectStatic(Page.Dashboard)( + connectors.DashboardPageConnector(state.actionBus).apply + ) + .collect[Page.NotFound](pg => + pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) + ) + .collect[Page.UnhandledError](pg => + pages.errors + .UnhandledErrorPage( + pages.errors.UnhandledErrorPage + .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), + state.actionBus + ) + ) + .collectStatic(Page.Directory)( + connectors + .DirectoryPageConnector(state.users, state.actionBus) + .apply + ) + div(cls := "h-full", child <-- pageSplitter.$view) + + // Pull in the stylesheet + val css: Css.type = Css diff --git a/app/src/main/scala/mdr/pdb/app/Routes.scala b/app/src/main/scala/mdr/pdb/app/Routes.scala new file mode 100644 index 0000000..2cc860c --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/Routes.scala @@ -0,0 +1,198 @@ +package mdr.pdb.app + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.* +import org.scalajs.dom +import zio.json.{*, given} +import mdr.pdb.OsobniCislo + +import scala.scalajs.js +import mdr.pdb.UserInfo +import mdr.pdb.Parameter +import mdr.pdb.ParameterCriteria +import mdr.pdb.app.Page.Titled + +// enum is not working with Waypoints' SplitRender collectStatic +sealed abstract class Page( + val id: String, + val title: String, + val parent: Option[Page] +) { + val path: Vector[Page] = + parent match + case None => Vector(this) + case Some(p) => p.path :+ this + + val isRoot: Boolean = parent.isEmpty +} + +object Page: + + case class Titled[V](value: V, title: Option[String] = None): + val show: String = title.getOrElse(value.toString) + + case object Directory extends Page("directory", "Adresář", None) + + case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) + + case class Detail(osobniCislo: Titled[OsobniCislo]) + extends Page("user", osobniCislo.show, Some(Directory)) + + object Detail { + def apply(o: UserInfo): Detail = Detail( + Titled(o.personalNumber, Some(o.name)) + ) + } + + case class DetailParametru( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String] + ) extends Page( + "parameter", + parametr.show, + Some(Detail(osobniCislo)) + ) + + object DetailParametru { + def apply(o: UserInfo, p: Parameter): DetailParametru = + DetailParametru( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)) + ) + } + + case class DetailKriteria( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String], + kriterium: Titled[String] + ) extends Page( + "criteria", + kriterium.show, + Some(DetailParametru(osobniCislo, parametr)) + ) + + object DetailKriteria { + def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = + DetailKriteria( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)), + Titled(k.id, Some(k.id)) + ) + } + + case class UpravDukazKriteria( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String], + kriterium: Titled[String] + ) extends Page( + "addProof", + "Důkaz", + Some(DetailKriteria(osobniCislo, parametr, kriterium)) + ) + + object UpravDukazKriteria { + def apply( + o: UserInfo, + p: Parameter, + k: ParameterCriteria + ): UpravDukazKriteria = + UpravDukazKriteria( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)), + Titled(k.id, Some(k.id)) + ) + } + + case class NotFound(url: String) extends Page("404", "404", Some(Directory)) + + case class UnhandledError( + errorName: Option[String], + errorMessage: Option[String] + ) extends Page("500", "Unexpected error", Some(Directory)) + +object Routes: + given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) + given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) + given [V: JsonEncoder]: JsonEncoder[Titled[V]] = + DeriveJsonEncoder.gen[Titled[V]] + given [V: JsonDecoder]: JsonDecoder[Titled[V]] = + DeriveJsonDecoder.gen[Titled[V]] + given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] + given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] + + val base = + js.`import`.meta.env.BASE_URL + .asInstanceOf[String] + "app" + + val homePage: Page = Page.Directory + + given router: Router[Page] = Router[Page]( + routes = List( + Route.static(homePage, root / endOfSegments, basePath = base), + Route.static( + Page.Dashboard, + root / "dashboard" / endOfSegments, + basePath = base + ), + Route[Page.Detail, String]( + encode = _.osobniCislo.value.toString, + decode = osc => Page.Detail(Titled(OsobniCislo(osc))), + root / "osoba" / segment[String] / endOfSegments, + basePath = base + ), + Route[Page.DetailParametru, (String, String)]( + encode = p => (p.osobniCislo.value.toString, p.parametr.value), + decode = + p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / endOfSegments, + basePath = base + ), + Route[Page.DetailKriteria, (String, String, String)]( + encode = p => + ( + p.osobniCislo.value.toString, + p.parametr.value, + p.kriterium.value.replaceAll("\\.", "--") + ), + decode = p => + Page.DetailKriteria( + Titled(OsobniCislo(p._1)), + Titled(p._2), + Titled(p._3.replaceAll("--", ".")) + ), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / "kriterium" / segment[String] / endOfSegments, + basePath = base + ), + Route[Page.UpravDukazKriteria, (String, String, String)]( + encode = p => + ( + p.osobniCislo.value.toString, + p.parametr.value, + p.kriterium.value.replaceAll("\\.", "--") + ), + decode = p => + Page.UpravDukazKriteria( + Titled(OsobniCislo(p._1)), + Titled(p._2), + Titled(p._3.replaceAll("--", ".")) + ), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / "kriterium" / segment[String] / "edit" / endOfSegments, + basePath = base + ) + ), + serializePage = _.toJson, + deserializePage = _.fromJson[Page] + .fold(s => throw IllegalStateException(s), identity), + getPageTitle = _.title, + routeFallback = url => Page.NotFound(url), + deserializeFallback = _ => Page.Dashboard + )( + $popStateEvent = windowEvents.onPopState, + owner = unsafeWindowOwner + ) diff --git a/app/src/main/scala/mdr/pdb/app/actions.scala b/app/src/main/scala/mdr/pdb/app/actions.scala new file mode 100644 index 0000000..d078b3a --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/actions.scala @@ -0,0 +1,21 @@ +package mdr.pdb.app + +import mdr.pdb.OsobniCislo +import mdr.pdb.UserInfo +import mdr.pdb.Parameter +import mdr.pdb.ParameterCriteria + +sealed trait Action + +case object FetchDirectory extends Action +case class FetchUserDetails(osc: OsobniCislo) extends Action +case class FetchParameters(osc: OsobniCislo) extends Action +case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action +case class FetchParameterCriteria( + osc: OsobniCislo, + paramId: String, + critId: String, + page: (UserInfo, Parameter, ParameterCriteria) => Page +) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action +case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/mdr/pdb/app/components/AppPage.scala new file mode 100644 index 0000000..130c817 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/AppPage.scala @@ -0,0 +1,82 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.Page +import mdr.pdb.{UserProfile, UserInfo, OsobniCislo} +import com.raquo.waypoint.Router +import mdr.pdb.app.Action +import mdr.pdb.app.NavigateTo +import mdr.pdb.UserFunction + +object AppPage: + // TODO: pages by logged in user + val pages: List[Page] = List(Page.Directory, Page.Dashboard) + + import NavigationBar.{Logo, MenuItem} + + val logo = Logo( + "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", + "Workflow" + ) + + // TODO: menu items by user profile + val userMenu = + List( + MenuItem("Your Profile"), + MenuItem("Settings"), + MenuItem("Sign out") + ) + + // TODO: load user profile + val $userProfile = Var( + UserProfile( + "tom", + UserInfo( + OsobniCislo("1031"), + "tom", + "Tom", + "Cook", + None, + None, + Some("tom@example.com"), + Some("+420 222 866 180"), + Some( + UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") + ), + Nil, + None + ) + ) + ) + + val $userInfo = $userProfile.signal.map(_.userInfo) + + type ViewModel = Option[HtmlElement] + def apply( + actionBus: Observer[Action] + )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using + router: Router[Page] + ): HtmlElement = + PageLayout(actionBus)( + $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => + PageLayout.ViewModel( + NavigationBar.ViewModel( + u, + pages.map(p => + NavigationBar.Link( + () => + PageLink( + p, + actionBus + ), + p == cp + ) + ), + userMenu, + logo + ), + c + ) + ), + mods + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala new file mode 100644 index 0000000..9375398 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala @@ -0,0 +1,143 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.tailwind.list.IconText.ViewModel +import fiftyforms.ui.components.tailwind.Icons +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.Page + +import com.raquo.laminar.api.L.{*, given} +import io.laminext.syntax.core.{*, given} +import mdr.pdb.app.Action + +object Breadcrumbs: + + private def slash = + import svg.{*, given} + svg( + cls := "flex-shrink-0 h-5 w-5 text-gray-300", + xmlns := "http://www.w3.org/2000/svg", + fill := "currentColor", + viewBox := "0 0 20 20", + ariaHidden := true, + path( + d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" + ) + ) + + def backIcon = + Icons.solid + .`arrow-narrow-left`() + .amend( + svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" + ) + + object Link: + case class ViewModel( + page: Page, + icon: Option[SvgElement], + extraClasses: String, + text: String, + textClasses: Option[String] = None + ) + + val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" + + def shortHome(p: Page) = ViewModel( + p, + Some(backIcon), + "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", + "Zpět na úvodní stránku", + None + ) + + def fullHome(p: Page) = ViewModel( + p, + Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), + "text-gray-400 hover:text-gray-500", + "Domů", + Some("sr-only") + ) + + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + PageLink + .container($m.map(_.page), actionBus) + .amend( + cls <-- $m.map(_.extraClasses), + child.maybe <-- $m.map(_.icon), + span( + cls <-- $m.map(_.textClasses.getOrElse("")), + child.text <-- $m.map(_.text) + ) + ) + + object FullBreadcrumbs: + type ViewModel = Page + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + ol( + role := "list", + cls := "flex items-center space-x-4", + children <-- $m.map(_.path).split(_.id)((_, _, $p) => + li( + div( + cls := "flex items-center", + child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), + Link( + $p.map(p => + if (p.isRoot) then Link.fullHome(p) + else + Link.ViewModel( + p, + None, + s"ml-4 max-w-xs truncate ${Link.baseClasses}", + p.title + ) + ), + actionBus + ) + ) + ) + ) + ) + + object ShortBreadcrumbs: + type ViewModel = Page + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + Link( + $m.map { p => + val target = p.parent.getOrElse(p) + if target.isRoot then Link.shortHome(target) + else + Link.ViewModel( + target, + Some(backIcon), + "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", + s"Zpět na ${target.title}" + ) + }, + actionBus + ) + + def apply(actionBus: Observer[Action])(using + router: Router[Page] + ): HtmlElement = + nav( + cls := "flex", + aria.label := "Breadcrumb", + div( + cls := "flex sm:hidden", + ShortBreadcrumbs(router.$currentPage, actionBus) + ), + div( + cls := "hidden sm:block", + FullBreadcrumbs(router.$currentPage, actionBus) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala new file mode 100644 index 0000000..4bb8b8d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala @@ -0,0 +1,242 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent +import fiftyforms.ui.components.tailwind.Icons +import fiftyforms.ui.components.tailwind.Avatar +import mdr.pdb.UserInfo +import io.laminext.syntax.core.* + +object NavigationBar: + + case class Logo(img: String, name: String) + case class Link(a: () => Anchor, active: Boolean) + case class MenuItem(title: String) + + case class ViewModel( + userInfo: UserInfo, + pages: List[Link], + userMenu: List[MenuItem], + logo: Logo + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + val $userInfo = $m.map(_.userInfo) + val mobileMenuOpen = Var(false) + + val desktopOnly = cls("hidden md:block") + val mobileOnly = cls("md:hidden") + + inline def avatarImage(size: Int = 8) = + Avatar($userInfo.map(_.img)).avatarImage(size) + + def notificationButton = button( + tpe := "button", + cls := List( + "bg-indigo-600", + "focus:outline-none", + "focus:ring-2", + "focus:ring-offset-2", + "focus:ring-offset-indigo-600", + "focus:ring-white", + "hover:text-white", + "p-1", + "rounded-full", + "text-indigo-200" + ), + span(cls := "sr-only", "View notifications"), + Icons.outline.bell() + ) + + def userProfile: HtmlElement = + val menuOpen = Var(false) + + def menuItem(item: MenuItem, idx: Int): HtmlElement = + a( + href := "#", + cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", + role := "menuitem", + tabIndex := -1, + idAttr := s"user-menu-item-$idx", + item.title + ) + + div( + cls := "ml-3 relative", + div( + button( + tpe := "button", + cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", + idAttr := "user-menu-button", + aria.expanded <-- menuOpen.signal, + aria.hasPopup := true, + span(cls := "sr-only", "Open user menu"), + child <-- avatarImage(), + onClick.preventDefault.mapTo( + !menuOpen.now() + ) --> menuOpen.writer + ) + ), + /* + * */ + div( + cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", + cls <-- menuOpen.signal.map { o => + if (o) "md:block" else "md:hidden" + }, + role := "menu", + aria.orientation := "vertical", + aria.labelledBy := "user-menu-button", + tabIndex := -1, + // : keyboard navigation + children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) + ) + ) + + def mobileProfile = + def menuItem(item: MenuItem): HtmlElement = + a( + href := "#", + cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", + item.title + ) + + div( + cls := "pt-4 pb-3 border-t border-indigo-700", + div( + cls := "flex items-center px-5", + div( + cls := "flex-shrink-0", + child <-- avatarImage(10) + ), + div( + cls := "ml-3", + div( + cls := "text-base font-medium text-white", + child.text <-- $userInfo.map(_.name) + ), + child.maybe <-- $userInfo.map( + _.email.map(e => + div( + cls := "text-sm font-medium text-indigo-300", + e + ) + ) + ) + ), + notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) + ), + div( + cls := "mt-3 px-2 space-y-1", + children <-- $m.map(_.userMenu.map(menuItem)) + ) + ) + + def pageLink(page: Link): Anchor = + page + .a() + .amend( + cls := "text-white px-3 py-2 rounded-md text-sm font-medium", + cls := Seq( + "bg-indigo-700" -> page.active, + "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active + ), + ariaCurrent := (if page.active then "page" else "false") + ) + + def logoImg: Image = + img( + cls := "h-8 w-8", + src <-- $m.map(_.logo.img), + alt <-- $m.map(_.logo.name) + ) + + def mobileMenuButton = button( + tpe := "button", + cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", + aria.controls := "mobile-menu", + aria.expanded <-- mobileMenuOpen.signal, + span(cls := "sr-only", "Open main menu"), + Icons.outline + .menu() + .amend( + svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") + ), + Icons.outline + .x() + .amend( + svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") + ), + onClick.preventDefault.mapTo( + !mobileMenuOpen.now() + ) --> mobileMenuOpen.writer + ) + + def navBarLeft = + div( + cls := "flex items-center", + div(cls := "flex-shrink-0", logoImg), + div( + desktopOnly, + div( + cls := "ml-10 flex items-baseline space-x-4", + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) + ) + ) + ) + + def navBarRight = + div( + desktopOnly, + div( + cls := "ml-4 flex items-center md:ml-6", + notificationButton, + userProfile + ) + ) + + def navBarMobile = + div( + cls := "-mr-2 flex", + mobileOnly, + mobileMenuButton + ) + + def navBar = + div( + cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", + div( + cls := "flex items-center justify-between h-16", + navBarLeft, + navBarRight, + navBarMobile + ) + ) + + def mobileMenu = + div( + mobileOnly, + cls <-- mobileMenuOpen.signal.map { o => + if (o) "block" else "hidden" + }, + idAttr := "mobile-menu", + div( + cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) + ), + mobileProfile + ) + + nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala new file mode 100644 index 0000000..c4cb10d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala @@ -0,0 +1,19 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.Page +import com.raquo.waypoint.Router +import mdr.pdb.app.Action + +object PageHeader: + def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = + header( + cls := "bg-white shadow-sm", + div( + cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", + h1( + cls := "text-lg leading-6 font-semibold text-gray-900", + Breadcrumbs(actionBus) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala new file mode 100644 index 0000000..aae4539 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala @@ -0,0 +1,30 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.Page +import mdr.pdb.app.Action +import fiftyforms.ui.components.tailwind.Loading +import io.laminext.syntax.core.* + +object PageLayout: + case class ViewModel( + navigation: NavigationBar.ViewModel, + content: Option[HtmlElement] + ) + def apply(actionBus: Observer[Action])( + $m: Signal[ViewModel], + mods: Modifier[HtmlElement]* + )(using router: Router[Page]): HtmlElement = + val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) + div( + cls := "h-full flex flex-col", + NavigationBar($m.map(_.navigation)), + child.maybe <-- router.$currentPage.map(_.isRoot) + .switch(None, Some(PageHeader(actionBus))), + main( + cls := "flex-grow-1 overflow-y-auto", + mods, + child <-- $maybeContent.map(_.getOrElse(Loading)) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/mdr/pdb/app/components/PageLink.scala new file mode 100644 index 0000000..e6e7052 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageLink.scala @@ -0,0 +1,40 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.LinkSupport.* +import mdr.pdb.app.Page +import mdr.pdb.app.Action +import com.raquo.waypoint.Router +import mdr.pdb.app.NavigateTo + +object PageLink: + type ViewModel = Page + def apply($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = + a(mods($m, actions), child.text <-- $m.map(_.title)) + + def apply(m: ViewModel, actions: Observer[Action])(using + router: Router[Page] + ): Anchor = apply(Val(m), actions) + + def container($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = a(mods($m, actions)) + + def container(m: ViewModel, actions: Observer[Action])(using + Router[Page] + ): Anchor = container(Val(m), actions) + + private def mods($m: Signal[Page], actions: Observer[Action])(using + router: Router[Page] + ): Modifier[Anchor] = + Seq( + href <-- $m.map(router.absoluteUrlForPage).recover { case _ => + Some("invalid url") + }, + composeEvents(onClick.noKeyMod.preventDefault)( + _.sample($m) + .map(NavigateTo.apply) + ) --> actions + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala new file mode 100644 index 0000000..7682772 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala @@ -0,0 +1,13 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.pages.dashboard.DashboardPage +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage + +class DashboardPageConnector(actionBus: Observer[Action])(using + router: Router[Page] +): + def apply: HtmlElement = + AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala new file mode 100644 index 0000000..76b9133 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala @@ -0,0 +1,76 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.Parameter +import mdr.pdb.UserInfo +import mdr.pdb.app.pages.detail.DetailParametruPage +import pages.detail.DetailKriteriaPage +import pages.detail.components.DukazyKriteria +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.ParameterCriteria + +object DetailKriteriaPageConnector { + trait AppState { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + } +} + +case class DetailKriteriaPageConnector( + state: DetailKriteriaPageConnector.AppState +)( + $page: Signal[Page.DetailKriteria] +)(using Router[Page]): + val $paramChangeSignal = + $page.splitOne(p => + (p.osobniCislo.value, p.parametr.value, p.kriterium.value) + )((x, _, _) => x) + val $pageChangeSignal = + $paramChangeSignal.map( + FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) + ) + + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + val $merged = + $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => + for { + da <- d + pa <- p + pb <- pa.find(_.id == pc._2) + ka <- pb.criteria.find(_.id == pc._3) + } yield (da, pb, ka) + ) + + def apply: HtmlElement = + AppPage(state.actionBus)( + $merged.map(_.map(buildModel)) + .split(_ => ())((_, s, $s) => + DetailKriteriaPage($s)(state.actionBus.contramap { + case DukazyKriteria.Add => + NavigateTo( + Page.UpravDukazKriteria( + Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), + Page.Titled(s.parametr.id, Some(s.parametr.nazev)), + Page.Titled(s.kriterium.id, Some(s.kriterium.id)) + ) + ) + }) + ), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: Parameter, + k: ParameterCriteria + ): DetailKriteriaPage.ViewModel = + DetailKriteriaPage.ViewModel( + o.toDetailOsoby, + p.toParametr(_ => a()), + k.toKriterium(_ => a()) + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DetailPageConnector.scala new file mode 100644 index 0000000..df3a6b7 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DetailPageConnector.scala @@ -0,0 +1,57 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.UserInfo +import pages.detail.DetailPage +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.pages.detail.components.DetailOsoby +import mdr.pdb.Parameter +import mdr.pdb.app.pages.detail.components.SeznamParametru +import fiftyforms.ui.components.tailwind.Color + +object DetailPageConnector { + trait AppState { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + } +} + +case class DetailPageConnector(state: DetailPageConnector.AppState)( + $page: Signal[Page.Detail] +)(using router: Router[Page]): + val $oscChangeSignal = $page.splitOne(_.osobniCislo.value)((osc, _, _) => osc) + val $pageChangeSignal = + $oscChangeSignal.flatMap(osc => + EventStream.fromSeq(Seq(FetchUserDetails(osc), FetchParameters(osc))) + ) + // TODO: filter the value based on the current osc + // OSC change will fetch new data, but still + // - we need to be sure that what we got is really what we ought to display + // - we want to display stale data accordingly (at least with loading indicator) + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + def apply: HtmlElement = + AppPage(state.actionBus)( + $data.combineWithFn($params)(_ zip _) + .map(_.map(buildModel)) + .split(_ => ())((_, _, s) => DetailPage(s)), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: List[Parameter] + ): DetailPage.ViewModel = + DetailPage.ViewModel( + o.toDetailOsoby, + p.map( + _.toParametr(param => + PageLink.container(Page.DetailParametru(o, param), state.actionBus) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DetailParametruPageConnector.scala new file mode 100644 index 0000000..fa7193d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DetailParametruPageConnector.scala @@ -0,0 +1,67 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.Parameter +import mdr.pdb.UserInfo +import mdr.pdb.app.pages.detail.DetailParametruPage +import pages.detail.DetailParametruPage +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.components.PageLink + +object DetailParametruPageConnector { + trait AppState { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + } +} + +case class DetailParametruPageConnector( + state: DetailParametruPageConnector.AppState +)( + $page: Signal[Page.DetailParametru] +)(using router: Router[Page]): + val $paramChangeSignal = + $page.splitOne(p => (p.osobniCislo.value, p.parametr.value))((x, _, _) => x) + val $pageChangeSignal = + $paramChangeSignal.map(FetchParameter(_, _)) + + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + val $merged = + $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => + for { + da <- d + pa <- p + pb <- pa.find(_.id == pc._2) + } yield (da, pb) + ) + + def apply: HtmlElement = + AppPage(state.actionBus)( + $merged.map(_.map(buildModel)) + .split(_ => ())((_, _, s) => DetailParametruPage(s)), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: Parameter + ): DetailParametruPage.ViewModel = + DetailParametruPage.ViewModel( + o.toDetailOsoby, + p.toParametr(p => + PageLink.container(Page.DetailParametru(o, p), state.actionBus) + ), + p.criteria.map( + _.toKriterium { c => + PageLink.container( + Page.DetailKriteria(o, p, c), + state.actionBus + ) + } + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DirectoryPageConnector.scala new file mode 100644 index 0000000..ab2c045 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DirectoryPageConnector.scala @@ -0,0 +1,34 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.UserInfo +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.components.AppPage + +case class DirectoryPageConnector( + $input: EventStream[List[UserInfo]], + actionBus: Observer[Action] +)(using router: Router[Page]): + val $data = $input.startWithNone + val $actionSignal = EventStream.fromValue(FetchDirectory) + + def apply: HtmlElement = + AppPage(actionBus)( + $data.split(_ => ())((_, _, s) => + pages.directory.DirectoryPage( + s.map( + _.map( + _.toUserRow(u => + PageLink.container( + Page.Detail(Page.Titled(u.personalNumber)), + actionBus + ) + ) + ) + ) + ) + ), + $actionSignal --> actionBus + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/mdr/pdb/app/connectors/conversions.scala new file mode 100644 index 0000000..f581796 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/conversions.scala @@ -0,0 +1,68 @@ +package mdr.pdb.app.connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.UserInfo +import mdr.pdb.app.pages.detail.components.DetailOsoby +import mdr.pdb.Parameter +import mdr.pdb.app.pages.detail.components.SeznamParametru +import mdr.pdb.ParameterCriteria +import mdr.pdb.app.pages.detail.components.SeznamKriterii +import mdr.pdb.app.pages.directory.components.UserRow +import mdr.pdb.app.pages.detail.components.DetailParametru +import mdr.pdb.app.pages.detail.components.DetailKriteria +import fiftyforms.ui.components.tailwind.Color + +extension (o: UserInfo) + def toDetailOsoby: DetailOsoby.ViewModel = + DetailOsoby.ViewModel( + o.personalNumber, + o.name, + o.email, + o.phone, + o.img, + o.mainFunction.map(f => + DetailOsoby.Funkce.ViewModel(f.name, f.dept, f.ou) + ), + o.userContracts.headOption.map(c => + DetailOsoby.PracovniPomer.ViewModel(c.rel, c.startDate, c.endDate) + ) + ) + +extension (param: Parameter) + def toParametr(container: Parameter => Anchor): DetailParametru.ViewModel = + DetailParametru.ViewModel( + id = param.id, + nazev = param.name, + popis = param.description, + status = "Nesplněno", + statusColor = Color.red, + a = container(param) + ) + +extension (crit: ParameterCriteria) + def toKriterium( + container: ParameterCriteria => Anchor + ): DetailKriteria.ViewModel = + DetailKriteria.ViewModel( + nazev = crit.criteriumText, + kapitola = crit.chapterId, + bod = crit.itemId, + status = "Nesplněno", + statusColor = Color.red, + splneno = false, + dukazy = Nil, + container = container(crit) + ) + +extension (user: UserInfo) + def toUserRow( + container: UserInfo => HtmlElement = (_: UserInfo) => div() + ): UserRow.ViewModel = + UserRow.ViewModel( + osobniCislo = user.personalNumber.toString, + celeJmeno = user.name, + prijmeni = user.surname, + hlavniFunkce = user.mainFunction.map(_.name), + img = user.img, + container = () => container(user) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/dashboard/DashboardPage.scala b/app/src/main/scala/mdr/pdb/app/pages/dashboard/DashboardPage.scala new file mode 100644 index 0000000..0eb8f7a --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/dashboard/DashboardPage.scala @@ -0,0 +1,11 @@ +package mdr.pdb.app.pages.dashboard + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.Page + +object DashboardPage: + + def render: HtmlElement = + div("Dashboard page") diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala new file mode 100644 index 0000000..ff3a25f --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala @@ -0,0 +1,31 @@ +package mdr.pdb.app.pages.detail + +import com.raquo.laminar.api.L.{*, given} + +import components._ + +object DetailKriteriaPage: + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametr: DetailParametru.ViewModel, + kriterium: DetailKriteria.ViewModel + ) + + def apply($m: Signal[ViewModel])( + action: Observer[DukazyKriteria.Action] + ): HtmlElement = + div( + cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + div( + DetailOsoby.header($m.map(_.osoba)), + DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") + ), + div( + DetailKriteria($m.map(_.kriterium)), + DukazyKriteria($m.map(_.kriterium.dukazy))(action) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/DetailPage.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailPage.scala new file mode 100644 index 0000000..b591cc9 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailPage.scala @@ -0,0 +1,25 @@ +package mdr.pdb.app.pages.detail + +import com.raquo.laminar.api.L.{*, given} + +import components._ +import mdr.pdb.app.Page +import com.raquo.waypoint.Router +import mdr.pdb.app.Action + +object DetailPage: + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametry: SeznamParametru.ViewModel + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + DetailOsoby($m.map(_.osoba)), + SeznamParametru($m.map(_.parametry)) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/DetailParametruPage.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailParametruPage.scala new file mode 100644 index 0000000..3322279 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailParametruPage.scala @@ -0,0 +1,24 @@ +package mdr.pdb.app.pages.detail + +import com.raquo.laminar.api.L.{*, given} + +import components._ + +object DetailParametruPage: + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametr: DetailParametru.ViewModel, + kriteria: SeznamKriterii.ViewModel + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + DetailOsoby.header($m.map(_.osoba)), + DetailParametru($m.map(_.parametr)), + SeznamKriterii($m.map(_.kriteria)) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala new file mode 100644 index 0000000..b58618a --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala @@ -0,0 +1,107 @@ +package mdr.pdb +package app +package pages.detail + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.pages.detail.components.UpravDukazForm +import fiftyforms.services.files.File + +object UpravDukaz: + + type ThisPage = Page.UpravDukazKriteria + type PageKey = (OsobniCislo, String, String) + + trait State { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def availableFiles: EventStream[List[File]] + def actionBus: Observer[Action] + } + + def keyOfPage(page: ThisPage): PageKey = + (page.osobniCislo.value, page.parametr.value, page.kriterium.value) + + def onChangeAction(key: PageKey): Action = + FetchParameterCriteria( + key._1, + key._2, + key._3, + Page.UpravDukazKriteria(_, _, _) + ) + + class Connector(state: State)($page: Signal[ThisPage])(using Router[Page]): + val $paramChangeSignal = $page.splitOne(keyOfPage)((x, _, _) => x) + val $pageChangeSignal = $paramChangeSignal.map(onChangeAction) + + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + val $merged = + $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => + for { + da <- d + pa <- p + pb <- pa.find(_.id == pc._2) + ka <- pb.criteria.find(_.id == pc._3) + } yield (da, pb, ka) + ) + + def apply: HtmlElement = + AppPage(state.actionBus)( + $merged.split(_ => ())((_, s, $s) => + PageComponent( + $s.map(buildModel), + state.availableFiles, + state.actionBus.contramap { + case UpravDukazForm.Cancelled => + NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) + case UpravDukazForm.AvailableFilesRequested => + FetchAvailableFiles(s._1.personalNumber) + } + ) + ), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: Parameter, + k: ParameterCriteria + ): PageComponent.ViewModel = + import connectors.* + PageComponent.ViewModel( + o.toDetailOsoby, + p.toParametr(_ => a()), + k.toKriterium(_ => a()) + ) + + object PageComponent: + import components.* + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametr: DetailParametru.ViewModel, + kriterium: DetailKriteria.ViewModel + ) + + def apply( + $m: Signal[ViewModel], + availableFilesStream: EventStream[List[File]], + events: Observer[UpravDukazForm.Event] + ): HtmlElement = + div( + cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + div( + DetailOsoby.header($m.map(_.osoba)), + DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") + ), + div( + DetailKriteria($m.map(_.kriterium)), + UpravDukazForm(availableFilesStream)(events) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailKriteria.scala new file mode 100644 index 0000000..a3ab634 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailKriteria.scala @@ -0,0 +1,37 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.CustomAttrs +import fiftyforms.ui.components.tailwind.Icons +import java.time.LocalDate +import fiftyforms.ui.components.tailwind.Color + +object DetailKriteria: + case class ViewModel( + nazev: String, + kapitola: String, + bod: String, + status: String, + statusColor: Color, + splneno: Boolean, + dukazy: List[DukazKriteria.ViewModel], + container: HtmlElement = div() + ) { + val id = s"${kapitola}${bod}" + } + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "pb-5 border-b border-gray-200", + h3( + cls := "text-xl leading-6 font-bold text-gray-900", + child.text <-- $m.map(_.nazev) + ), + h3( + cls := "mt-2 max-w-4xl text-sm text-gray-500", + "Kapitola ", + child.text <-- $m.map(_.kapitola), + " bod ", + child <-- $m.map(_.bod) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailOsoby.scala new file mode 100644 index 0000000..0fe2ba5 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailOsoby.scala @@ -0,0 +1,100 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.OsobniCislo + +import java.time.LocalDate +import fiftyforms.ui.components.tailwind.Avatar + +object DetailOsoby: + + object Funkce: + case class ViewModel( + nazev: String, + stredisko: String, + voj: String + ) + + def render($m: Signal[ViewModel]): HtmlElement = + p( + cls := "text-sm font-medium text-gray-500", + child.text <-- $m.map(_.nazev), + span( + cls := "hidden md:inline", + " @ ", + child.text <-- $m.map(_.stredisko), + ", ", + child.text <-- $m.map(_.voj) + ) + ) + + object PracovniPomer: + case class ViewModel( + druh: String, + pocatek: LocalDate, + konec: Option[LocalDate] + ) + + def render($m: Signal[ViewModel]): HtmlElement = + import fiftyforms.ui.components.tailwind.CustomAttrs.datetime + p( + cls := "text-sm font-medium text-gray-500", + child.text <-- $m.map(_.druh), + " od ", + time( + datetime <-- $m.map(_.pocatek.toString), + child.text <-- $m.map(_.pocatek.toString) + ) + ) + + case class ViewModel( + osobniCislo: OsobniCislo, + jmeno: String, + email: Option[String], + telefon: Option[String], + img: Option[String], + hlavniFunkce: Option[Funkce.ViewModel], + pracovniPomer: Option[PracovniPomer.ViewModel] + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "md:flex md:items-center md:justify-between md:space-x-4", + div( + cls := "flex items-start space-x-4", + div( + cls := "flex-shrink-0", + Avatar($m.map(_.img)).avatar(16) + ), + div( + h1( + cls := "text-2xl font-bold text-gray-900", + child.text <-- $m.map(_.jmeno) + ), + child.maybe <-- $m.map(_.hlavniFunkce).split(_ => ())((_, _, d) => + Funkce.render(d) + ), + child.maybe <-- $m.map(_.pracovniPomer).split(_ => ())((_, _, d) => + PracovniPomer.render(d) + ) + ) + ) + ) + + def header($m: Signal[ViewModel]): HtmlElement = + div( + cls := "md:flex md:items-center md:justify-between md:space-x-5", + div( + cls := "flex items-start space-x-4", + div( + cls := "flex-shrink-0", + Avatar($m.map(_.img)).avatar(8) + ), + div( + h1( + cls := "text-lg font-medium text-gray-800", + child.text <-- $m.map(_.jmeno) + ) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailParametru.scala new file mode 100644 index 0000000..6310dde --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailParametru.scala @@ -0,0 +1,34 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.Color + +object DetailParametru: + case class ViewModel( + id: String, + nazev: String, + popis: String, + status: String, + statusColor: Color, + a: Anchor + ) + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "pb-5 border-b border-gray-200", + h2( + cls := "text-2xl leading-6 font-medium text-gray-900", + child.text <-- $m.map(_.nazev) + ), + p( + cls := "mt-2 max-w-4xl text-sm text-gray-500", + child.text <-- $m.map(_.popis) + ) + ) + + def header($m: Signal[ViewModel]): HtmlElement = + div( + h2( + cls := "text-lg leading-6 font-medium text-gray-600", + child.text <-- $m.map(_.nazev) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazKriteria.scala new file mode 100644 index 0000000..c292919 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazKriteria.scala @@ -0,0 +1,177 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import java.time.LocalDate +import fiftyforms.ui.components.tailwind.CustomAttrs + +object DukazKriteria: + case class Osoba(osobniCislo: String, jmeno: String) + case class Udalost(osoba: Osoba, datum: LocalDate) + case class Dokument( + url: String, + nazev: String, + pridal: Udalost, + odebral: Option[Udalost] + ) + case class ViewModel( + dokumenty: List[Dokument], + autorizoval: Option[Udalost], + platiDo: Option[LocalDate], + poznámka: Option[String] + ) + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "bg-white shadow overflow-hidden sm:rounded-lg", + div( + cls := "px-4 py-5 sm:px-6", + h3( + cls := "text-lg leading-6 font-medium text-gray-900", + """Applicant Information""" + ), + p( + cls := "mt-1 max-w-2xl text-sm text-gray-500", + """Personal details and application.""" + ) + ), + div( + cls := "border-t border-gray-200 px-4 py-5 sm:px-6", + dl( + cls := "grid grid-cols-1 gap-x-4 gap-y-8 sm:grid-cols-2", + div( + cls := "sm:col-span-1", + dt( + cls := "text-sm font-medium text-gray-500", + """Full name""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """Margot Foster""" + ) + ), + div( + cls := "sm:col-span-1", + dt( + cls := "text-sm font-medium text-gray-500", + """Application for""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """Backend Developer""" + ) + ), + div( + cls := "sm:col-span-1", + dt( + cls := "text-sm font-medium text-gray-500", + """Email address""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """margotfoster@example.com""" + ) + ), + div( + cls := "sm:col-span-1", + dt( + cls := "text-sm font-medium text-gray-500", + """Salary expectation""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """$120,000""" + ) + ), + div( + cls := "sm:col-span-2", + dt( + cls := "text-sm font-medium text-gray-500", + """About""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """Fugiat ipsum ipsum deserunt culpa aute sint do nostrud anim incididunt cillum culpa consequat. Excepteur qui ipsum aliquip consequat sint. Sit id mollit nulla mollit nostrud in ea officia proident. Irure nostrud pariatur mollit ad adipisicing reprehenderit deserunt qui eu.""" + ) + ), + div( + cls := "sm:col-span-2", + dt( + cls := "text-sm font-medium text-gray-500", + """Attachments""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + ul( + role := "list", + cls := "border border-gray-200 rounded-md divide-y divide-gray-200", + li( + cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", + div( + cls := "w-0 flex-1 flex items-center", { + import svg.* + import CustomAttrs.svg.ariaHidden + svg( + cls := "flex-shrink-0 h-5 w-5 text-gray-400", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + ariaHidden := true, + path( + fillRule := "evenodd", + d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", + clipRule := "evenodd" + ) + ) + }, + span( + cls := "ml-2 flex-1 w-0 truncate", + """resume_back_end_developer.pdf""" + ) + ), + div( + cls := "ml-4 flex-shrink-0", + a( + href := "#", + cls := "font-medium text-indigo-600 hover:text-indigo-500", + """Download""" + ) + ) + ), + li( + cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", + div( + cls := "w-0 flex-1 flex items-center", { + import svg.* + import CustomAttrs.svg.ariaHidden + svg( + cls := "flex-shrink-0 h-5 w-5 text-gray-400", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + ariaHidden := true, + path( + fillRule := "evenodd", + d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", + clipRule := "evenodd" + ) + ) + }, + span( + cls := "ml-2 flex-1 w-0 truncate", + """coverletter_back_end_developer.pdf""" + ) + ), + div( + cls := "ml-4 flex-shrink-0", + a( + href := "#", + cls := "font-medium text-indigo-600 hover:text-indigo-500", + """Download""" + ) + ) + ) + ) + ) + ) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala new file mode 100644 index 0000000..d604860 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala @@ -0,0 +1,35 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.Icons + +object DukazyKriteria: + sealed trait Action + case object Add extends Action + + type ViewModel = List[DukazKriteria.ViewModel] + + def apply($m: Signal[ViewModel])(actions: Observer[Action]): HtmlElement = + div( + child.maybe <-- $m.map(m => + if (m.isEmpty) then Some(prazdnyDukaz(actions)) + else None + ), + children <-- $m.map(_.zipWithIndex).split(_._2)((_, _, $s) => + DukazKriteria($s.map(_._1)) + ) + ) + + private def prazdnyDukaz(actions: Observer[Action]): HtmlElement = + button( + tpe := "button", + onClick.preventDefault.mapTo(Add) --> actions, + cls := "relative block w-full border-2 border-gray-300 border-dashed rounded-lg p-12 text-center hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + Icons.outline + .`document-add`(12) + .amend(svg.cls := "mx-auto text-gray-400"), + span( + cls := "mt-2 block text-sm font-medium text-gray-900", + "Přidat důkaz" + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala new file mode 100644 index 0000000..7e77a7b --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala @@ -0,0 +1,34 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.Color +import fiftyforms.ui.components.tailwind.list.StackedList +import fiftyforms.ui.components.tailwind.list.ListRow +import fiftyforms.ui.components.tailwind.list.RowTag +import fiftyforms.ui.components.tailwind.list.RowNext +import java.time.LocalDate + +object SeznamKriterii: + type ViewModel = List[DetailKriteria.ViewModel] + + private val kritList = new StackedList[DetailKriteria.ViewModel] + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "bg-white shadow overflow-hidden sm:rounded-md", + kritList($m, _.id) { $i => + $i.map { i => + ListRow.ViewModel( + title = i.nazev, + topRight = RowTag.render( + $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) + ), + bottomLeft = + p(cls := "text-sm text-gray-500", s"${i.kapitola}${i.bod}"), + bottomRight = emptyNode, + farRight = RowNext.render, + containerElement = i.container + ) + } + } + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamParametru.scala new file mode 100644 index 0000000..0760b32 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamParametru.scala @@ -0,0 +1,37 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.list.{ + StackedList, + ListRow, + RowTag, + PropList, + IconText, + RowNext +} +import fiftyforms.ui.components.tailwind.Color +import fiftyforms.ui.components.tailwind.LinkSupport.* + +object SeznamParametru: + type ViewModel = List[DetailParametru.ViewModel] + + private val parametrList = new StackedList[DetailParametru.ViewModel] + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "bg-white shadow overflow-hidden sm:rounded-md", + parametrList($m, _.id) { $i => + $i.map { i => + ListRow.ViewModel( + title = i.nazev, + topRight = RowTag.render( + $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) + ), + bottomLeft = emptyNode, + bottomRight = emptyNode, + farRight = RowNext.render, + containerElement = i.a + ) + } + } + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala new file mode 100644 index 0000000..28753c9 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala @@ -0,0 +1,101 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import io.laminext.syntax.core.* +import fiftyforms.ui.components.tailwind.CustomAttrs +import fiftyforms.ui.components.tailwind.form.* +import org.scalajs.dom +import com.raquo.laminar.nodes.ReactiveHtmlElement +import fiftyforms.services.files.components.tailwind.FilePicker +import mdr.pdb.frontend.AutorizujDukaz +import mdr.pdb.frontend.DocumentRef +import fiftyforms.services.files.File + +object UpravDukazForm: + sealed trait Event + case object Cancelled extends Event + case object AvailableFilesRequested extends Event + def apply(availableFilesStream: EventStream[List[File]])( + updates: Observer[Event] + ): HtmlElement = + val (filesStream, filesObserver) = + EventStream.withObserver[FilePicker.Event] + val files = Var[List[File]](Nil) + def submitButtons: HtmlElement = + div( + cls := "pt-5", + div( + cls := "flex justify-end", + button( + tpe := "button", + cls := "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + "Zrušit", + onClick.mapTo(Cancelled) --> updates + ), + button( + tpe := "submit", + cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + disabled <-- files.signal.map(_.isEmpty), + "Autorizovat" + ) + ) + ) + + div( + filesStream.collect { case FilePicker.SelectionUpdated(files) => + files.to(List) + } --> files.writer, + filesStream.collect { case FilePicker.AvailableFilesRequested => + AvailableFilesRequested + } --> updates, + cls := "bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6", + Form( + Form.Body( + Form.Section( + FormHeader( + FormHeader.ViewModel( + "Doložení kritéria", + "Sestavte doklady poskytující důkaz kritéria a potvrďte odesláním formuláře. Případné limitace či jiné relevantní údaje vepište do pole Komentář." + ) + ), + FormFields( + FormRow( + "dokumenty", + "Dokumenty", + FilePicker(files.signal, availableFilesStream)(filesObserver) + .amend(idAttr := "dokumenty", cls("max-w-lg")) + ).toHtml, + FormRow( + "platnost", + "Platnost", + input( + idAttr := "platnost", + name := "platnost", + tpe := "date", + autoComplete := "date", + cls := "block max-w-lg w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" + ) + ).toHtml, + FormRow( + "komentar", + "Komentář", + div( + cls := "mt-1 sm:mt-0 sm:col-span-2", + textArea( + idAttr := "komentar", + name := "about", + rows := 3, + cls := "max-w-lg shadow-sm block w-full focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border border-gray-300 rounded-md" + ), + p( + cls := "mt-2 text-sm text-gray-500", + "Doplňte prosím potřebné informace související s doložením kritéria, včetně případných limitací." + ) + ) + ).toHtml + ) + ) + ), + submitButtons + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/directory/DirectoryPage.scala b/app/src/main/scala/mdr/pdb/app/pages/directory/DirectoryPage.scala new file mode 100644 index 0000000..7a72ca3 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/directory/DirectoryPage.scala @@ -0,0 +1,40 @@ +package mdr.pdb.app.pages.directory + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.UserInfo + +import components._ + +object DirectoryPage: + + type ViewModel = List[UserRow.ViewModel] + + def apply($m: Signal[ViewModel]): HtmlElement = + val (actionsStream, actionObserver) = + EventStream.withObserver[SearchForm.Action] + val $filter = actionsStream + .collect { + case SearchForm.ClearFilter => None + case SearchForm.SetFilter(t) => Some(t) + } + .startWith(None) + val byLetter = for { + d <- $m + f <- $filter + } yield for { + (letter, users) <- d + .filter { user => + f match + case None => true + case Some(t) => user.search.contains(t) + } + .groupBy(_.prijmeni.head) + .to(List) + .sortBy(_._1) + } yield (letter.toString, users.sortBy(_.prijmeni)) + + div( + cls := "h-full max-w-7xl mx-auto order-first flex flex-col flex-shrink-0", + SearchForm(actionObserver), + Directory(byLetter) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/directory/components/Directory.scala b/app/src/main/scala/mdr/pdb/app/pages/directory/components/Directory.scala new file mode 100644 index 0000000..f7d6f99 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/directory/components/Directory.scala @@ -0,0 +1,37 @@ +package mdr.pdb.app.pages.directory.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.UserInfo + +object Directory: + + object Header: + type ViewModel = String + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500", + h3(child.text <-- $m) + ) + + object UserList: + type ViewModel = List[UserRow.ViewModel] + def apply($m: Signal[ViewModel]): HtmlElement = + ul( + role := "list", + cls := "relative z-0 divide-y divide-gray-200", + children <-- $m.split(_.osobniCislo)((_, _, s) => UserRow(s)) + ) + + type ViewModel = List[(String, List[UserRow.ViewModel])] + def apply($m: Signal[ViewModel]): HtmlElement = + nav( + cls := "flex-1 min-h-0 overflow-y-auto", + aria.label := "Directory", + children <-- $m.split(_._1)((_, _, s) => + div( + cls := "relative", + Header(s.map(_._1)), + UserList(s.map(_._2)) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/directory/components/SearchForm.scala b/app/src/main/scala/mdr/pdb/app/pages/directory/components/SearchForm.scala new file mode 100644 index 0000000..fb9ecc4 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/directory/components/SearchForm.scala @@ -0,0 +1,54 @@ +package mdr.pdb.app.pages.directory.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.Icons + +object SearchForm: + sealed trait Action + case object SubmitFilter extends Action + sealed trait FilterAction extends Action + case object ClearFilter extends FilterAction + case class SetFilter(value: String) extends FilterAction + + def apply(actions: Observer[Action]): HtmlElement = + div( + cls := "px-6 pt-4 pb-4", + form( + cls := "flex space-x-4", + onSubmit.mapTo(SubmitFilter) --> actions, + div( + cls := "flex-1 min-w-0", + label( + forId := "search", + cls := "sr-only", + "Hledat" + ), + div( + cls := "relative rounded-md shadow-sm", + div( + cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", + Icons.solid.search().amend(svg.cls := "text-gray-400") + ), + input( + tpe := "search", + name := "search", + idAttr := "search", + cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", + placeholder := "Hledat", + composeEvents(onInput.mapToValue.setAsValue.map(SetFilter(_)))( + _.throttle(500) + ) --> actions + ) + ) + ), + button( + tpe := "submit", + cls := "inline-flex justify-center px-3.5 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500", + Icons.solid.filter().amend(svg.cls := "text-gray-400"), + span( + cls := "sr-only", + "Hledat" + ) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/directory/components/UserRow.scala b/app/src/main/scala/mdr/pdb/app/pages/directory/components/UserRow.scala new file mode 100644 index 0000000..4aa1672 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/directory/components/UserRow.scala @@ -0,0 +1,52 @@ +package mdr.pdb.app.pages.directory.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.airstream.core.Signal +import fiftyforms.ui.components.tailwind.Avatar + +object UserRow: + case class ViewModel( + osobniCislo: String, + celeJmeno: String, + prijmeni: String, + hlavniFunkce: Option[String], + img: Option[String], + container: () => HtmlElement = () => div() + ) { + val search = prijmeni.toLowerCase + } + + def apply($m: Signal[ViewModel]): HtmlElement = + inline def avatarImage = + Avatar($m.map(_.img)).avatarImage(10) + + li( + div( + cls := "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500", + div( + cls := "flex-shrink-0", + child <-- avatarImage + ), + div( + cls := "flex-1 min-w-0", + child <-- $m.map { o => + o.container() + .amend( + cls := "focus:outline-none", + span( + cls := "absolute inset-0", + aria.hidden := true + ), + p( + cls := "text-sm font-medium text-gray-900", + o.celeJmeno + ), + p( + cls := "text-sm text-gray-500 truncate", + o.hlavniFunkce + ) + ) + } + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/errors/ErrorPage.scala b/app/src/main/scala/mdr/pdb/app/pages/errors/ErrorPage.scala new file mode 100644 index 0000000..6b195ef --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/errors/ErrorPage.scala @@ -0,0 +1,99 @@ +package mdr.pdb.app.pages.errors + +import com.raquo.domtypes.generic.codecs.StringAsIsCodec +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.Page +import mdr.pdb.app.Action +import com.raquo.waypoint.Router + +object ErrorPage: + case class ViewModel( + homePage: Page, + errorName: String, + title: String, + subTitle: String + ) + + def apply(m: ViewModel, actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + val ViewModel(homePage, errorName, title, subTitle) = m + div( + cls := "min-h-full pt-16 pb-12 flex flex-col bg-white", + main( + cls := "flex-grow flex flex-col justify-center max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", + div( + cls := "flex-shrink-0 flex justify-center", + a( + href := "/", + cls := "inline-flex", + span( + cls := "sr-only", + """Workflow""" + ), + img( + cls := "h-12 w-auto", + src := "https://tailwindui.com/img/logos/workflow-mark.svg?color=indigo&shade=600", + alt := "" + ) + ) + ), + div( + cls := "py-16", + div( + cls := "text-center", + p( + cls := "text-sm font-semibold text-indigo-600 uppercase tracking-wide", + errorName + ), + h1( + cls := "mt-2 text-4xl font-extrabold text-gray-900 tracking-tight sm:text-5xl", + title + ), + p( + cls := "mt-2 text-base text-gray-500", + subTitle + ), + div( + cls := "mt-6", + PageLink + .container(homePage, actionBus) + .amend( + cls := "text-base font-medium text-indigo-600 hover:text-indigo-500", + """Go back home""" + ) + ) + ) + ) + ), + footer( + cls := "flex-shrink-0 max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", + nav( + cls := "flex justify-center space-x-4", + a( + href := "#", + cls := "text-sm font-medium text-gray-500 hover:text-gray-600", + """Contact Support""" + ), + span( + cls := "inline-block border-l border-gray-300", + aria.hidden := true + ), + a( + href := "#", + cls := "text-sm font-medium text-gray-500 hover:text-gray-600", + """Status""" + ), + span( + cls := "inline-block border-l border-gray-300", + aria.hidden := true + ), + a( + href := "#", + cls := "text-sm font-medium text-gray-500 hover:text-gray-600", + """Twitter""" + ) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/errors/NotFoundPage.scala b/app/src/main/scala/mdr/pdb/app/pages/errors/NotFoundPage.scala new file mode 100644 index 0000000..97760e1 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/errors/NotFoundPage.scala @@ -0,0 +1,21 @@ +package mdr.pdb.app.pages.errors + +import com.raquo.domtypes.generic.codecs.StringAsIsCodec +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.Page +import mdr.pdb.app.Action + +object NotFoundPage: + def apply(homePage: Page, url: String, actionBus: Observer[Action])(using + router: Router[Page] + ): HtmlElement = + ErrorPage( + ErrorPage.ViewModel( + homePage, + "404 error", + "Page not found.", + s"Sorry, but page $url doesn't exist." + ), + actionBus + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala b/app/src/main/scala/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala new file mode 100644 index 0000000..77b0af8 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala @@ -0,0 +1,32 @@ +package mdr.pdb.app.pages.errors + +import com.raquo.domtypes.generic.codecs.StringAsIsCodec +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.Page +import mdr.pdb.app.Action + +object UnhandledErrorPage: + + case class ViewModel( + homePage: Page, + errorName: Option[String], + errorMessage: Option[String] + ) + + def apply(m: ViewModel, actionBus: Observer[Action])(using + router: Router[Page] + ): HtmlElement = + ErrorPage( + ErrorPage.ViewModel( + m.homePage, + "Unexpected error occurred", + m.errorName.getOrElse( + "Uh oh!" + ), // TODO: translations, better text than uh oh + m.errorMessage.getOrElse( + "This wasn't supposed to happen! Please try again." + ) + ), + actionBus + ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala deleted file mode 100644 index a3b00af..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala +++ /dev/null @@ -1,103 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import scala.scalajs.js.annotation.JSExportTopLevel -import scala.scalajs.js.annotation.JSExport -import scala.scalajs.js.annotation.JSImport -import scala.scalajs.js -import org.scalajs.dom -import com.raquo.laminar.api.L.{*, given} -import scala.scalajs.js.Date -import com.raquo.waypoint.Router -import com.raquo.waypoint.SplitRender -import cz.e_bs.cmi.mdr.pdb.app.services.DataFetcher -import scala.scalajs.js.JSON -import zio.json._ -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.state.AppState - -@js.native -@JSImport("stylesheets/main.css", JSImport.Namespace) -object Css extends js.Any - -@js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native -@JSImport("params/pdb-params.json", JSImport.Default) -object pdbParams extends js.Object - -@JSExportTopLevel("app") -object Main: - - @JSExport - def main(args: Array[String]): Unit = - import Routes.given - onLoad { - setupAirstream() - val appContainer = dom.document.querySelector("#app") - val _ = - render( - appContainer, - renderPage(state.MockAppState(using unsafeWindowOwner, router)) - ) - } - - private def onLoad(f: => Unit): Unit = - documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) - - private def setupAirstream()(using router: Router[Page]): Unit = - AirstreamError.registerUnhandledErrorCallback(err => - router.forcePage( - Page.UnhandledError( - Some(err.getClass.getName), // TODO: Fill only in dev mode - Some(err.getMessage) - ) - ) - ) - - def renderPage(state: AppState)(using - router: Router[Page] - ): HtmlElement = - val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) - .collectSignal[Page.Detail]( - connectors - .DetailPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailParametru]( - connectors - .DetailParametruPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailKriteria]( - connectors - .DetailKriteriaPageConnector(state)(_) - .apply - ) - .collectSignal[Page.UpravDukazKriteria]( - pages.detail.UpravDukaz.Connector(state)(_).apply - ) - .collectStatic(Page.Dashboard)( - connectors.DashboardPageConnector(state.actionBus).apply - ) - .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) - ) - .collect[Page.UnhandledError](pg => - pages.errors - .UnhandledErrorPage( - pages.errors.UnhandledErrorPage - .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), - state.actionBus - ) - ) - .collectStatic(Page.Directory)( - connectors - .DirectoryPageConnector(state.users, state.actionBus) - .apply - ) - div(cls := "h-full", child <-- pageSplitter.$view) - - // Pull in the stylesheet - val css: Css.type = Css diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala deleted file mode 100644 index 3bbc8c0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ /dev/null @@ -1,198 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.* -import org.scalajs.dom -import zio.json.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import scala.scalajs.js -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.Page.Titled - -// enum is not working with Waypoints' SplitRender collectStatic -sealed abstract class Page( - val id: String, - val title: String, - val parent: Option[Page] -) { - val path: Vector[Page] = - parent match - case None => Vector(this) - case Some(p) => p.path :+ this - - val isRoot: Boolean = parent.isEmpty -} - -object Page: - - case class Titled[V](value: V, title: Option[String] = None): - val show: String = title.getOrElse(value.toString) - - case object Directory extends Page("directory", "Adresář", None) - - case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) - - case class Detail(osobniCislo: Titled[OsobniCislo]) - extends Page("user", osobniCislo.show, Some(Directory)) - - object Detail { - def apply(o: UserInfo): Detail = Detail( - Titled(o.personalNumber, Some(o.name)) - ) - } - - case class DetailParametru( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String] - ) extends Page( - "parameter", - parametr.show, - Some(Detail(osobniCislo)) - ) - - object DetailParametru { - def apply(o: UserInfo, p: Parameter): DetailParametru = - DetailParametru( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)) - ) - } - - case class DetailKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "criteria", - kriterium.show, - Some(DetailParametru(osobniCislo, parametr)) - ) - - object DetailKriteria { - def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = - DetailKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class UpravDukazKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "addProof", - "Důkaz", - Some(DetailKriteria(osobniCislo, parametr, kriterium)) - ) - - object UpravDukazKriteria { - def apply( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): UpravDukazKriteria = - UpravDukazKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class NotFound(url: String) extends Page("404", "404", Some(Directory)) - - case class UnhandledError( - errorName: Option[String], - errorMessage: Option[String] - ) extends Page("500", "Unexpected error", Some(Directory)) - -object Routes: - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) - given [V: JsonEncoder]: JsonEncoder[Titled[V]] = - DeriveJsonEncoder.gen[Titled[V]] - given [V: JsonDecoder]: JsonDecoder[Titled[V]] = - DeriveJsonDecoder.gen[Titled[V]] - given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] - given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] - - val base = - js.`import`.meta.env.BASE_URL - .asInstanceOf[String] + "app" - - val homePage: Page = Page.Directory - - given router: Router[Page] = Router[Page]( - routes = List( - Route.static(homePage, root / endOfSegments, basePath = base), - Route.static( - Page.Dashboard, - root / "dashboard" / endOfSegments, - basePath = base - ), - Route[Page.Detail, String]( - encode = _.osobniCislo.value.toString, - decode = osc => Page.Detail(Titled(OsobniCislo(osc))), - root / "osoba" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.DetailParametru, (String, String)]( - encode = p => (p.osobniCislo.value.toString, p.parametr.value), - decode = - p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / endOfSegments, - basePath = base - ), - Route[Page.DetailKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.DetailKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.UpravDukazKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.UpravDukazKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / "edit" / endOfSegments, - basePath = base - ) - ), - serializePage = _.toJson, - deserializePage = _.fromJson[Page] - .fold(s => throw IllegalStateException(s), identity), - getPageTitle = _.title, - routeFallback = url => Page.NotFound(url), - deserializeFallback = _ => Page.Dashboard - )( - $popStateEvent = windowEvents.onPopState, - owner = unsafeWindowOwner - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala deleted file mode 100644 index b24b82b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -sealed trait Action - -case object FetchDirectory extends Action -case class FetchUserDetails(osc: OsobniCislo) extends Action -case class FetchParameters(osc: OsobniCislo) extends Action -case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action -case class FetchParameterCriteria( - osc: OsobniCislo, - paramId: String, - critId: String, - page: (UserInfo, Parameter, ParameterCriteria) => Page -) extends Action -case class FetchAvailableFiles(osc: OsobniCislo) extends Action -case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala deleted file mode 100644 index c637d35..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala +++ /dev/null @@ -1,82 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.{UserProfile, UserInfo, OsobniCislo} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo -import cz.e_bs.cmi.mdr.pdb.UserFunction - -object AppPage: - // TODO: pages by logged in user - val pages: List[Page] = List(Page.Directory, Page.Dashboard) - - import NavigationBar.{Logo, MenuItem} - - val logo = Logo( - "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", - "Workflow" - ) - - // TODO: menu items by user profile - val userMenu = - List( - MenuItem("Your Profile"), - MenuItem("Settings"), - MenuItem("Sign out") - ) - - // TODO: load user profile - val $userProfile = Var( - UserProfile( - "tom", - UserInfo( - OsobniCislo("1031"), - "tom", - "Tom", - "Cook", - None, - None, - Some("tom@example.com"), - Some("+420 222 866 180"), - Some( - UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") - ), - Nil, - None - ) - ) - ) - - val $userInfo = $userProfile.signal.map(_.userInfo) - - type ViewModel = Option[HtmlElement] - def apply( - actionBus: Observer[Action] - )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using - router: Router[Page] - ): HtmlElement = - PageLayout(actionBus)( - $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => - PageLayout.ViewModel( - NavigationBar.ViewModel( - u, - pages.map(p => - NavigationBar.Link( - () => - PageLink( - p, - actionBus - ), - p == cp - ) - ), - userMenu, - logo - ), - c - ) - ), - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala deleted file mode 100644 index bf473c6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ /dev/null @@ -1,143 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden -import fiftyforms.ui.components.tailwind.list.IconText.ViewModel -import fiftyforms.ui.components.tailwind.Icons -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Action - -object Breadcrumbs: - - private def slash = - import svg.{*, given} - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-300", - xmlns := "http://www.w3.org/2000/svg", - fill := "currentColor", - viewBox := "0 0 20 20", - ariaHidden := true, - path( - d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" - ) - ) - - def backIcon = - Icons.solid - .`arrow-narrow-left`() - .amend( - svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" - ) - - object Link: - case class ViewModel( - page: Page, - icon: Option[SvgElement], - extraClasses: String, - text: String, - textClasses: Option[String] = None - ) - - val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" - - def shortHome(p: Page) = ViewModel( - p, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - "Zpět na úvodní stránku", - None - ) - - def fullHome(p: Page) = ViewModel( - p, - Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), - "text-gray-400 hover:text-gray-500", - "Domů", - Some("sr-only") - ) - - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - PageLink - .container($m.map(_.page), actionBus) - .amend( - cls <-- $m.map(_.extraClasses), - child.maybe <-- $m.map(_.icon), - span( - cls <-- $m.map(_.textClasses.getOrElse("")), - child.text <-- $m.map(_.text) - ) - ) - - object FullBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - ol( - role := "list", - cls := "flex items-center space-x-4", - children <-- $m.map(_.path).split(_.id)((_, _, $p) => - li( - div( - cls := "flex items-center", - child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), - Link( - $p.map(p => - if (p.isRoot) then Link.fullHome(p) - else - Link.ViewModel( - p, - None, - s"ml-4 max-w-xs truncate ${Link.baseClasses}", - p.title - ) - ), - actionBus - ) - ) - ) - ) - ) - - object ShortBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - Link( - $m.map { p => - val target = p.parent.getOrElse(p) - if target.isRoot then Link.shortHome(target) - else - Link.ViewModel( - target, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - s"Zpět na ${target.title}" - ) - }, - actionBus - ) - - def apply(actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - nav( - cls := "flex", - aria.label := "Breadcrumb", - div( - cls := "flex sm:hidden", - ShortBreadcrumbs(router.$currentPage, actionBus) - ), - div( - cls := "hidden sm:block", - FullBreadcrumbs(router.$currentPage, actionBus) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala deleted file mode 100644 index 0db2047..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ /dev/null @@ -1,242 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent -import fiftyforms.ui.components.tailwind.Icons -import fiftyforms.ui.components.tailwind.Avatar -import cz.e_bs.cmi.mdr.pdb.UserInfo -import io.laminext.syntax.core.* - -object NavigationBar: - - case class Logo(img: String, name: String) - case class Link(a: () => Anchor, active: Boolean) - case class MenuItem(title: String) - - case class ViewModel( - userInfo: UserInfo, - pages: List[Link], - userMenu: List[MenuItem], - logo: Logo - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - val $userInfo = $m.map(_.userInfo) - val mobileMenuOpen = Var(false) - - val desktopOnly = cls("hidden md:block") - val mobileOnly = cls("md:hidden") - - inline def avatarImage(size: Int = 8) = - Avatar($userInfo.map(_.img)).avatarImage(size) - - def notificationButton = button( - tpe := "button", - cls := List( - "bg-indigo-600", - "focus:outline-none", - "focus:ring-2", - "focus:ring-offset-2", - "focus:ring-offset-indigo-600", - "focus:ring-white", - "hover:text-white", - "p-1", - "rounded-full", - "text-indigo-200" - ), - span(cls := "sr-only", "View notifications"), - Icons.outline.bell() - ) - - def userProfile: HtmlElement = - val menuOpen = Var(false) - - def menuItem(item: MenuItem, idx: Int): HtmlElement = - a( - href := "#", - cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", - role := "menuitem", - tabIndex := -1, - idAttr := s"user-menu-item-$idx", - item.title - ) - - div( - cls := "ml-3 relative", - div( - button( - tpe := "button", - cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - idAttr := "user-menu-button", - aria.expanded <-- menuOpen.signal, - aria.hasPopup := true, - span(cls := "sr-only", "Open user menu"), - child <-- avatarImage(), - onClick.preventDefault.mapTo( - !menuOpen.now() - ) --> menuOpen.writer - ) - ), - /* - * */ - div( - cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", - cls <-- menuOpen.signal.map { o => - if (o) "md:block" else "md:hidden" - }, - role := "menu", - aria.orientation := "vertical", - aria.labelledBy := "user-menu-button", - tabIndex := -1, - // : keyboard navigation - children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) - ) - ) - - def mobileProfile = - def menuItem(item: MenuItem): HtmlElement = - a( - href := "#", - cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", - item.title - ) - - div( - cls := "pt-4 pb-3 border-t border-indigo-700", - div( - cls := "flex items-center px-5", - div( - cls := "flex-shrink-0", - child <-- avatarImage(10) - ), - div( - cls := "ml-3", - div( - cls := "text-base font-medium text-white", - child.text <-- $userInfo.map(_.name) - ), - child.maybe <-- $userInfo.map( - _.email.map(e => - div( - cls := "text-sm font-medium text-indigo-300", - e - ) - ) - ) - ), - notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) - ), - div( - cls := "mt-3 px-2 space-y-1", - children <-- $m.map(_.userMenu.map(menuItem)) - ) - ) - - def pageLink(page: Link): Anchor = - page - .a() - .amend( - cls := "text-white px-3 py-2 rounded-md text-sm font-medium", - cls := Seq( - "bg-indigo-700" -> page.active, - "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active - ), - ariaCurrent := (if page.active then "page" else "false") - ) - - def logoImg: Image = - img( - cls := "h-8 w-8", - src <-- $m.map(_.logo.img), - alt <-- $m.map(_.logo.name) - ) - - def mobileMenuButton = button( - tpe := "button", - cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - aria.controls := "mobile-menu", - aria.expanded <-- mobileMenuOpen.signal, - span(cls := "sr-only", "Open main menu"), - Icons.outline - .menu() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") - ), - Icons.outline - .x() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") - ), - onClick.preventDefault.mapTo( - !mobileMenuOpen.now() - ) --> mobileMenuOpen.writer - ) - - def navBarLeft = - div( - cls := "flex items-center", - div(cls := "flex-shrink-0", logoImg), - div( - desktopOnly, - div( - cls := "ml-10 flex items-baseline space-x-4", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ) - ) - ) - - def navBarRight = - div( - desktopOnly, - div( - cls := "ml-4 flex items-center md:ml-6", - notificationButton, - userProfile - ) - ) - - def navBarMobile = - div( - cls := "-mr-2 flex", - mobileOnly, - mobileMenuButton - ) - - def navBar = - div( - cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex items-center justify-between h-16", - navBarLeft, - navBarRight, - navBarMobile - ) - ) - - def mobileMenu = - div( - mobileOnly, - cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }, - idAttr := "mobile-menu", - div( - cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ), - mobileProfile - ) - - nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala deleted file mode 100644 index 75339df..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object PageHeader: - def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = - header( - cls := "bg-white shadow-sm", - div( - cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", - h1( - cls := "text-lg leading-6 font-semibold text-gray-900", - Breadcrumbs(actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala deleted file mode 100644 index 5096463..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ /dev/null @@ -1,30 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import fiftyforms.ui.components.tailwind.Loading -import io.laminext.syntax.core.* - -object PageLayout: - case class ViewModel( - navigation: NavigationBar.ViewModel, - content: Option[HtmlElement] - ) - def apply(actionBus: Observer[Action])( - $m: Signal[ViewModel], - mods: Modifier[HtmlElement]* - )(using router: Router[Page]): HtmlElement = - val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) - div( - cls := "h-full flex flex-col", - NavigationBar($m.map(_.navigation)), - child.maybe <-- router.$currentPage.map(_.isRoot) - .switch(None, Some(PageHeader(actionBus))), - main( - cls := "flex-grow-1 overflow-y-auto", - mods, - child <-- $maybeContent.map(_.getOrElse(Loading)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala deleted file mode 100644 index 62d820f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.LinkSupport.* -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo - -object PageLink: - type ViewModel = Page - def apply($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = - a(mods($m, actions), child.text <-- $m.map(_.title)) - - def apply(m: ViewModel, actions: Observer[Action])(using - router: Router[Page] - ): Anchor = apply(Val(m), actions) - - def container($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = a(mods($m, actions)) - - def container(m: ViewModel, actions: Observer[Action])(using - Router[Page] - ): Anchor = container(Val(m), actions) - - private def mods($m: Signal[Page], actions: Observer[Action])(using - router: Router[Page] - ): Modifier[Anchor] = - Seq( - href <-- $m.map(router.absoluteUrlForPage).recover { case _ => - Some("invalid url") - }, - composeEvents(onClick.noKeyMod.preventDefault)( - _.sample($m) - .map(NavigateTo.apply) - ) --> actions - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala deleted file mode 100644 index 87f825a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.pages.dashboard.DashboardPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -class DashboardPageConnector(actionBus: Observer[Action])(using - router: Router[Page] -): - def apply: HtmlElement = - AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala deleted file mode 100644 index e9b0f71..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala +++ /dev/null @@ -1,76 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailKriteriaPage -import pages.detail.components.DukazyKriteria -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -object DetailKriteriaPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailKriteriaPageConnector( - state: DetailKriteriaPageConnector.AppState -)( - $page: Signal[Page.DetailKriteria] -)(using Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => - (p.osobniCislo.value, p.parametr.value, p.kriterium.value) - )((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map( - FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) - ) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, s, $s) => - DetailKriteriaPage($s)(state.actionBus.contramap { - case DukazyKriteria.Add => - NavigateTo( - Page.UpravDukazKriteria( - Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), - Page.Titled(s.parametr.id, Some(s.parametr.nazev)), - Page.Titled(s.kriterium.id, Some(s.kriterium.id)) - ) - ) - }) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): DetailKriteriaPage.ViewModel = - DetailKriteriaPage.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala deleted file mode 100644 index 4d8024f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ /dev/null @@ -1,57 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import pages.detail.DetailPage -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import fiftyforms.ui.components.tailwind.Color - -object DetailPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailPageConnector(state: DetailPageConnector.AppState)( - $page: Signal[Page.Detail] -)(using router: Router[Page]): - val $oscChangeSignal = $page.splitOne(_.osobniCislo.value)((osc, _, _) => osc) - val $pageChangeSignal = - $oscChangeSignal.flatMap(osc => - EventStream.fromSeq(Seq(FetchUserDetails(osc), FetchParameters(osc))) - ) - // TODO: filter the value based on the current osc - // OSC change will fetch new data, but still - // - we need to be sure that what we got is really what we ought to display - // - we want to display stale data accordingly (at least with loading indicator) - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - def apply: HtmlElement = - AppPage(state.actionBus)( - $data.combineWithFn($params)(_ zip _) - .map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: List[Parameter] - ): DetailPage.ViewModel = - DetailPage.ViewModel( - o.toDetailOsoby, - p.map( - _.toParametr(param => - PageLink.container(Page.DetailParametru(o, param), state.actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala deleted file mode 100644 index fdaa7f8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailParametruPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink - -object DetailParametruPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailParametruPageConnector( - state: DetailParametruPageConnector.AppState -)( - $page: Signal[Page.DetailParametru] -)(using router: Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => (p.osobniCislo.value, p.parametr.value))((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map(FetchParameter(_, _)) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - } yield (da, pb) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailParametruPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter - ): DetailParametruPage.ViewModel = - DetailParametruPage.ViewModel( - o.toDetailOsoby, - p.toParametr(p => - PageLink.container(Page.DetailParametru(o, p), state.actionBus) - ), - p.criteria.map( - _.toKriterium { c => - PageLink.container( - Page.DetailKriteria(o, p, c), - state.actionBus - ) - } - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala deleted file mode 100644 index 049aa22..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -case class DirectoryPageConnector( - $input: EventStream[List[UserInfo]], - actionBus: Observer[Action] -)(using router: Router[Page]): - val $data = $input.startWithNone - val $actionSignal = EventStream.fromValue(FetchDirectory) - - def apply: HtmlElement = - AppPage(actionBus)( - $data.split(_ => ())((_, _, s) => - pages.directory.DirectoryPage( - s.map( - _.map( - _.toUserRow(u => - PageLink.container( - Page.Detail(Page.Titled(u.personalNumber)), - actionBus - ) - ) - ) - ) - ) - ), - $actionSignal --> actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala deleted file mode 100644 index 88e7450..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ /dev/null @@ -1,68 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii -import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria -import fiftyforms.ui.components.tailwind.Color - -extension (o: UserInfo) - def toDetailOsoby: DetailOsoby.ViewModel = - DetailOsoby.ViewModel( - o.personalNumber, - o.name, - o.email, - o.phone, - o.img, - o.mainFunction.map(f => - DetailOsoby.Funkce.ViewModel(f.name, f.dept, f.ou) - ), - o.userContracts.headOption.map(c => - DetailOsoby.PracovniPomer.ViewModel(c.rel, c.startDate, c.endDate) - ) - ) - -extension (param: Parameter) - def toParametr(container: Parameter => Anchor): DetailParametru.ViewModel = - DetailParametru.ViewModel( - id = param.id, - nazev = param.name, - popis = param.description, - status = "Nesplněno", - statusColor = Color.red, - a = container(param) - ) - -extension (crit: ParameterCriteria) - def toKriterium( - container: ParameterCriteria => Anchor - ): DetailKriteria.ViewModel = - DetailKriteria.ViewModel( - nazev = crit.criteriumText, - kapitola = crit.chapterId, - bod = crit.itemId, - status = "Nesplněno", - statusColor = Color.red, - splneno = false, - dukazy = Nil, - container = container(crit) - ) - -extension (user: UserInfo) - def toUserRow( - container: UserInfo => HtmlElement = (_: UserInfo) => div() - ): UserRow.ViewModel = - UserRow.ViewModel( - osobniCislo = user.personalNumber.toString, - celeJmeno = user.name, - prijmeni = user.surname, - hlavniFunkce = user.mainFunction.map(_.name), - img = user.img, - container = () => container(user) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala deleted file mode 100644 index 7d5b2c1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.dashboard - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.Page - -object DashboardPage: - - def render: HtmlElement = - div("Dashboard page") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala deleted file mode 100644 index 9ef0698..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala +++ /dev/null @@ -1,31 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailKriteriaPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply($m: Signal[ViewModel])( - action: Observer[DukazyKriteria.Action] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - DukazyKriteria($m.map(_.kriterium.dukazy))(action) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala deleted file mode 100644 index 3b316e1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object DetailPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametry: SeznamParametru.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby($m.map(_.osoba)), - SeznamParametru($m.map(_.parametry)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala deleted file mode 100644 index 17224cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala +++ /dev/null @@ -1,24 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailParametruPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriteria: SeznamKriterii.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby.header($m.map(_.osoba)), - DetailParametru($m.map(_.parametr)), - SeznamKriterii($m.map(_.kriteria)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala deleted file mode 100644 index dc912fa..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ /dev/null @@ -1,107 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb -package app -package pages.detail - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.UpravDukazForm -import fiftyforms.services.files.File - -object UpravDukaz: - - type ThisPage = Page.UpravDukazKriteria - type PageKey = (OsobniCislo, String, String) - - trait State { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def availableFiles: EventStream[List[File]] - def actionBus: Observer[Action] - } - - def keyOfPage(page: ThisPage): PageKey = - (page.osobniCislo.value, page.parametr.value, page.kriterium.value) - - def onChangeAction(key: PageKey): Action = - FetchParameterCriteria( - key._1, - key._2, - key._3, - Page.UpravDukazKriteria(_, _, _) - ) - - class Connector(state: State)($page: Signal[ThisPage])(using Router[Page]): - val $paramChangeSignal = $page.splitOne(keyOfPage)((x, _, _) => x) - val $pageChangeSignal = $paramChangeSignal.map(onChangeAction) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.split(_ => ())((_, s, $s) => - PageComponent( - $s.map(buildModel), - state.availableFiles, - state.actionBus.contramap { - case UpravDukazForm.Cancelled => - NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) - case UpravDukazForm.AvailableFilesRequested => - FetchAvailableFiles(s._1.personalNumber) - } - ) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): PageComponent.ViewModel = - import connectors.* - PageComponent.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) - - object PageComponent: - import components.* - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply( - $m: Signal[ViewModel], - availableFilesStream: EventStream[List[File]], - events: Observer[UpravDukazForm.Event] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - UpravDukazForm(availableFilesStream)(events) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala deleted file mode 100644 index ca311b0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.Icons -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Color - -object DetailKriteria: - case class ViewModel( - nazev: String, - kapitola: String, - bod: String, - status: String, - statusColor: Color, - splneno: Boolean, - dukazy: List[DukazKriteria.ViewModel], - container: HtmlElement = div() - ) { - val id = s"${kapitola}${bod}" - } - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h3( - cls := "text-xl leading-6 font-bold text-gray-900", - child.text <-- $m.map(_.nazev) - ), - h3( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - "Kapitola ", - child.text <-- $m.map(_.kapitola), - " bod ", - child <-- $m.map(_.bod) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala deleted file mode 100644 index ae90290..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala +++ /dev/null @@ -1,100 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Avatar - -object DetailOsoby: - - object Funkce: - case class ViewModel( - nazev: String, - stredisko: String, - voj: String - ) - - def render($m: Signal[ViewModel]): HtmlElement = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.nazev), - span( - cls := "hidden md:inline", - " @ ", - child.text <-- $m.map(_.stredisko), - ", ", - child.text <-- $m.map(_.voj) - ) - ) - - object PracovniPomer: - case class ViewModel( - druh: String, - pocatek: LocalDate, - konec: Option[LocalDate] - ) - - def render($m: Signal[ViewModel]): HtmlElement = - import fiftyforms.ui.components.tailwind.CustomAttrs.datetime - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.druh), - " od ", - time( - datetime <-- $m.map(_.pocatek.toString), - child.text <-- $m.map(_.pocatek.toString) - ) - ) - - case class ViewModel( - osobniCislo: OsobniCislo, - jmeno: String, - email: Option[String], - telefon: Option[String], - img: Option[String], - hlavniFunkce: Option[Funkce.ViewModel], - pracovniPomer: Option[PracovniPomer.ViewModel] - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-4", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(16) - ), - div( - h1( - cls := "text-2xl font-bold text-gray-900", - child.text <-- $m.map(_.jmeno) - ), - child.maybe <-- $m.map(_.hlavniFunkce).split(_ => ())((_, _, d) => - Funkce.render(d) - ), - child.maybe <-- $m.map(_.pracovniPomer).split(_ => ())((_, _, d) => - PracovniPomer.render(d) - ) - ) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-5", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(8) - ), - div( - h1( - cls := "text-lg font-medium text-gray-800", - child.text <-- $m.map(_.jmeno) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala deleted file mode 100644 index e11cb62..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color - -object DetailParametru: - case class ViewModel( - id: String, - nazev: String, - popis: String, - status: String, - statusColor: Color, - a: Anchor - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h2( - cls := "text-2xl leading-6 font-medium text-gray-900", - child.text <-- $m.map(_.nazev) - ), - p( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - child.text <-- $m.map(_.popis) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - h2( - cls := "text-lg leading-6 font-medium text-gray-600", - child.text <-- $m.map(_.nazev) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala deleted file mode 100644 index a00ede8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala +++ /dev/null @@ -1,177 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.CustomAttrs - -object DukazKriteria: - case class Osoba(osobniCislo: String, jmeno: String) - case class Udalost(osoba: Osoba, datum: LocalDate) - case class Dokument( - url: String, - nazev: String, - pridal: Udalost, - odebral: Option[Udalost] - ) - case class ViewModel( - dokumenty: List[Dokument], - autorizoval: Option[Udalost], - platiDo: Option[LocalDate], - poznámka: Option[String] - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-lg", - div( - cls := "px-4 py-5 sm:px-6", - h3( - cls := "text-lg leading-6 font-medium text-gray-900", - """Applicant Information""" - ), - p( - cls := "mt-1 max-w-2xl text-sm text-gray-500", - """Personal details and application.""" - ) - ), - div( - cls := "border-t border-gray-200 px-4 py-5 sm:px-6", - dl( - cls := "grid grid-cols-1 gap-x-4 gap-y-8 sm:grid-cols-2", - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Full name""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Margot Foster""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Application for""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Backend Developer""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Email address""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """margotfoster@example.com""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Salary expectation""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """$120,000""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """About""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Fugiat ipsum ipsum deserunt culpa aute sint do nostrud anim incididunt cillum culpa consequat. Excepteur qui ipsum aliquip consequat sint. Sit id mollit nulla mollit nostrud in ea officia proident. Irure nostrud pariatur mollit ad adipisicing reprehenderit deserunt qui eu.""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """Attachments""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - ul( - role := "list", - cls := "border border-gray-200 rounded-md divide-y divide-gray-200", - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """resume_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ), - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """coverletter_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala deleted file mode 100644 index 22f134f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala +++ /dev/null @@ -1,35 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object DukazyKriteria: - sealed trait Action - case object Add extends Action - - type ViewModel = List[DukazKriteria.ViewModel] - - def apply($m: Signal[ViewModel])(actions: Observer[Action]): HtmlElement = - div( - child.maybe <-- $m.map(m => - if (m.isEmpty) then Some(prazdnyDukaz(actions)) - else None - ), - children <-- $m.map(_.zipWithIndex).split(_._2)((_, _, $s) => - DukazKriteria($s.map(_._1)) - ) - ) - - private def prazdnyDukaz(actions: Observer[Action]): HtmlElement = - button( - tpe := "button", - onClick.preventDefault.mapTo(Add) --> actions, - cls := "relative block w-full border-2 border-gray-300 border-dashed rounded-lg p-12 text-center hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - Icons.outline - .`document-add`(12) - .amend(svg.cls := "mx-auto text-gray-400"), - span( - cls := "mt-2 block text-sm font-medium text-gray-900", - "Přidat důkaz" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala deleted file mode 100644 index f628697..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.list.StackedList -import fiftyforms.ui.components.tailwind.list.ListRow -import fiftyforms.ui.components.tailwind.list.RowTag -import fiftyforms.ui.components.tailwind.list.RowNext -import java.time.LocalDate - -object SeznamKriterii: - type ViewModel = List[DetailKriteria.ViewModel] - - private val kritList = new StackedList[DetailKriteria.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - kritList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = - p(cls := "text-sm text-gray-500", s"${i.kapitola}${i.bod}"), - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.container - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala deleted file mode 100644 index 2bfe4bc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.list.{ - StackedList, - ListRow, - RowTag, - PropList, - IconText, - RowNext -} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.LinkSupport.* - -object SeznamParametru: - type ViewModel = List[DetailParametru.ViewModel] - - private val parametrList = new StackedList[DetailParametru.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - parametrList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = emptyNode, - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.a - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala deleted file mode 100644 index f048e9d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ /dev/null @@ -1,101 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.form.* -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement -import fiftyforms.services.files.components.tailwind.FilePicker -import cz.e_bs.cmi.mdr.pdb.frontend.AutorizujDukaz -import cz.e_bs.cmi.mdr.pdb.frontend.DocumentRef -import fiftyforms.services.files.File - -object UpravDukazForm: - sealed trait Event - case object Cancelled extends Event - case object AvailableFilesRequested extends Event - def apply(availableFilesStream: EventStream[List[File]])( - updates: Observer[Event] - ): HtmlElement = - val (filesStream, filesObserver) = - EventStream.withObserver[FilePicker.Event] - val files = Var[List[File]](Nil) - def submitButtons: HtmlElement = - div( - cls := "pt-5", - div( - cls := "flex justify-end", - button( - tpe := "button", - cls := "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zrušit", - onClick.mapTo(Cancelled) --> updates - ), - button( - tpe := "submit", - cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - disabled <-- files.signal.map(_.isEmpty), - "Autorizovat" - ) - ) - ) - - div( - filesStream.collect { case FilePicker.SelectionUpdated(files) => - files.to(List) - } --> files.writer, - filesStream.collect { case FilePicker.AvailableFilesRequested => - AvailableFilesRequested - } --> updates, - cls := "bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6", - Form( - Form.Body( - Form.Section( - FormHeader( - FormHeader.ViewModel( - "Doložení kritéria", - "Sestavte doklady poskytující důkaz kritéria a potvrďte odesláním formuláře. Případné limitace či jiné relevantní údaje vepište do pole Komentář." - ) - ), - FormFields( - FormRow( - "dokumenty", - "Dokumenty", - FilePicker(files.signal, availableFilesStream)(filesObserver) - .amend(idAttr := "dokumenty", cls("max-w-lg")) - ).toHtml, - FormRow( - "platnost", - "Platnost", - input( - idAttr := "platnost", - name := "platnost", - tpe := "date", - autoComplete := "date", - cls := "block max-w-lg w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" - ) - ).toHtml, - FormRow( - "komentar", - "Komentář", - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - textArea( - idAttr := "komentar", - name := "about", - rows := 3, - cls := "max-w-lg shadow-sm block w-full focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border border-gray-300 rounded-md" - ), - p( - cls := "mt-2 text-sm text-gray-500", - "Doplňte prosím potřebné informace související s doložením kritéria, včetně případných limitací." - ) - ) - ).toHtml - ) - ) - ), - submitButtons - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala deleted file mode 100644 index 41033f6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -import components._ - -object DirectoryPage: - - type ViewModel = List[UserRow.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - val (actionsStream, actionObserver) = - EventStream.withObserver[SearchForm.Action] - val $filter = actionsStream - .collect { - case SearchForm.ClearFilter => None - case SearchForm.SetFilter(t) => Some(t) - } - .startWith(None) - val byLetter = for { - d <- $m - f <- $filter - } yield for { - (letter, users) <- d - .filter { user => - f match - case None => true - case Some(t) => user.search.contains(t) - } - .groupBy(_.prijmeni.head) - .to(List) - .sortBy(_._1) - } yield (letter.toString, users.sortBy(_.prijmeni)) - - div( - cls := "h-full max-w-7xl mx-auto order-first flex flex-col flex-shrink-0", - SearchForm(actionObserver), - Directory(byLetter) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala deleted file mode 100644 index 73f915d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -object Directory: - - object Header: - type ViewModel = String - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500", - h3(child.text <-- $m) - ) - - object UserList: - type ViewModel = List[UserRow.ViewModel] - def apply($m: Signal[ViewModel]): HtmlElement = - ul( - role := "list", - cls := "relative z-0 divide-y divide-gray-200", - children <-- $m.split(_.osobniCislo)((_, _, s) => UserRow(s)) - ) - - type ViewModel = List[(String, List[UserRow.ViewModel])] - def apply($m: Signal[ViewModel]): HtmlElement = - nav( - cls := "flex-1 min-h-0 overflow-y-auto", - aria.label := "Directory", - children <-- $m.split(_._1)((_, _, s) => - div( - cls := "relative", - Header(s.map(_._1)), - UserList(s.map(_._2)) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala deleted file mode 100644 index e182910..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala +++ /dev/null @@ -1,54 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object SearchForm: - sealed trait Action - case object SubmitFilter extends Action - sealed trait FilterAction extends Action - case object ClearFilter extends FilterAction - case class SetFilter(value: String) extends FilterAction - - def apply(actions: Observer[Action]): HtmlElement = - div( - cls := "px-6 pt-4 pb-4", - form( - cls := "flex space-x-4", - onSubmit.mapTo(SubmitFilter) --> actions, - div( - cls := "flex-1 min-w-0", - label( - forId := "search", - cls := "sr-only", - "Hledat" - ), - div( - cls := "relative rounded-md shadow-sm", - div( - cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", - Icons.solid.search().amend(svg.cls := "text-gray-400") - ), - input( - tpe := "search", - name := "search", - idAttr := "search", - cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", - placeholder := "Hledat", - composeEvents(onInput.mapToValue.setAsValue.map(SetFilter(_)))( - _.throttle(500) - ) --> actions - ) - ) - ), - button( - tpe := "submit", - cls := "inline-flex justify-center px-3.5 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500", - Icons.solid.filter().amend(svg.cls := "text-gray-400"), - span( - cls := "sr-only", - "Hledat" - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala deleted file mode 100644 index 82f7723..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala +++ /dev/null @@ -1,52 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.airstream.core.Signal -import fiftyforms.ui.components.tailwind.Avatar - -object UserRow: - case class ViewModel( - osobniCislo: String, - celeJmeno: String, - prijmeni: String, - hlavniFunkce: Option[String], - img: Option[String], - container: () => HtmlElement = () => div() - ) { - val search = prijmeni.toLowerCase - } - - def apply($m: Signal[ViewModel]): HtmlElement = - inline def avatarImage = - Avatar($m.map(_.img)).avatarImage(10) - - li( - div( - cls := "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500", - div( - cls := "flex-shrink-0", - child <-- avatarImage - ), - div( - cls := "flex-1 min-w-0", - child <-- $m.map { o => - o.container() - .amend( - cls := "focus:outline-none", - span( - cls := "absolute inset-0", - aria.hidden := true - ), - p( - cls := "text-sm font-medium text-gray-900", - o.celeJmeno - ), - p( - cls := "text-sm text-gray-500 truncate", - o.hlavniFunkce - ) - ) - } - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala deleted file mode 100644 index 467c5f5..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala +++ /dev/null @@ -1,99 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router - -object ErrorPage: - case class ViewModel( - homePage: Page, - errorName: String, - title: String, - subTitle: String - ) - - def apply(m: ViewModel, actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - val ViewModel(homePage, errorName, title, subTitle) = m - div( - cls := "min-h-full pt-16 pb-12 flex flex-col bg-white", - main( - cls := "flex-grow flex flex-col justify-center max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex-shrink-0 flex justify-center", - a( - href := "/", - cls := "inline-flex", - span( - cls := "sr-only", - """Workflow""" - ), - img( - cls := "h-12 w-auto", - src := "https://tailwindui.com/img/logos/workflow-mark.svg?color=indigo&shade=600", - alt := "" - ) - ) - ), - div( - cls := "py-16", - div( - cls := "text-center", - p( - cls := "text-sm font-semibold text-indigo-600 uppercase tracking-wide", - errorName - ), - h1( - cls := "mt-2 text-4xl font-extrabold text-gray-900 tracking-tight sm:text-5xl", - title - ), - p( - cls := "mt-2 text-base text-gray-500", - subTitle - ), - div( - cls := "mt-6", - PageLink - .container(homePage, actionBus) - .amend( - cls := "text-base font-medium text-indigo-600 hover:text-indigo-500", - """Go back home""" - ) - ) - ) - ) - ), - footer( - cls := "flex-shrink-0 max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", - nav( - cls := "flex justify-center space-x-4", - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Contact Support""" - ), - span( - cls := "inline-block border-l border-gray-300", - aria.hidden := true - ), - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Status""" - ), - span( - cls := "inline-block border-l border-gray-300", - aria.hidden := true - ), - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Twitter""" - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala deleted file mode 100644 index bd9baec..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action - -object NotFoundPage: - def apply(homePage: Page, url: String, actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - ErrorPage( - ErrorPage.ViewModel( - homePage, - "404 error", - "Page not found.", - s"Sorry, but page $url doesn't exist." - ), - actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala deleted file mode 100644 index 476fe3c..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action - -object UnhandledErrorPage: - - case class ViewModel( - homePage: Page, - errorName: Option[String], - errorMessage: Option[String] - ) - - def apply(m: ViewModel, actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - ErrorPage( - ErrorPage.ViewModel( - m.homePage, - "Unexpected error occurred", - m.errorName.getOrElse( - "Uh oh!" - ), // TODO: translations, better text than uh oh - m.errorMessage.getOrElse( - "This wasn't supposed to happen! Please try again." - ) - ), - actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala deleted file mode 100644 index 25f059f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala +++ /dev/null @@ -1,6 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.services - -import com.raquo.airstream.core.Observer - -trait DataFetcher[K, A]: - def fetch(id: K, sink: Observer[A]): Unit diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala deleted file mode 100644 index a469830..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala +++ /dev/null @@ -1,120 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package state - -import com.raquo.airstream.core.EventStream -import cz.e_bs.cmi.mdr.pdb.{UserInfo, OsobniCislo} -import com.raquo.airstream.core.Observer -import scala.scalajs.js -import scala.scalajs.js.JSON -import zio.json._ -import com.raquo.airstream.eventbus.EventBus -import com.raquo.airstream.ownership.Owner -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.UserFunction -import cz.e_bs.cmi.mdr.pdb.UserContract -import fiftyforms.services.files.File - -trait AppState - extends connectors.DetailPageConnector.AppState - with connectors.DetailParametruPageConnector.AppState - with connectors.DetailKriteriaPageConnector.AppState - with pages.detail.UpravDukaz.State: - def users: EventStream[List[UserInfo]] - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - -class MockAppState(implicit owner: Owner, router: Router[Page]) - extends AppState: - - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonDecoder[UserFunction] = DeriveJsonDecoder.gen - given JsonDecoder[UserContract] = DeriveJsonDecoder.gen - given JsonDecoder[UserInfo] = DeriveJsonDecoder.gen - - given JsonDecoder[ParameterCriteria] = DeriveJsonDecoder.gen - given JsonDecoder[Parameter] = DeriveJsonDecoder.gen - - private val actions = EventBus[Action]() - private val (parametersStream, pushParameters) = - EventStream.withCallback[List[Parameter]] - private val (usersStream, pushUsers) = - EventStream.withCallback[List[UserInfo]] - private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] - private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] - - private val mockData: List[UserInfo] = - mockUsers - .asInstanceOf[js.Dictionary[js.Object]] - .values - // TODO: is there a more efficient way to parse from JS object directly? - .map { o => - val parsed = JSON.stringify(o).fromJson[UserInfo] - parsed.left.foreach(org.scalajs.dom.console.log(_)) - parsed - } - .collect { case Right(u) => - u - } - .toList - - private val mockParameters: List[Parameter] = - pdbParams - .asInstanceOf[js.Dictionary[js.Object]] - .values - .map(o => JSON.stringify(o).fromJson[Parameter]) - .collect { case Right(p) => p } - .toList - - // TODO: Extract to separate event handler - actions.events.foreach { - case FetchDirectory => pushUsers(mockData) - case FetchUserDetails(osc) => - mockData.find(_.personalNumber == osc).foreach { o => - pushDetails(o) - router.replaceState(Page.Detail(o)) - } - case FetchParameters(osc) => - pushParameters(mockParameters) - case FetchParameter(osc, paramId) => - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(Page.DetailParametru(o, p)) - case FetchParameterCriteria(osc, paramId, critId, page) => - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - c <- p.criteria.find(_.id == critId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(page(o, p, c)) - case NavigateTo(page) => router.pushState(page) - case FetchAvailableFiles(osc) => - pushFiles( - List( - File("https://tc163.cmi.cz/here", "Example file") - ) - ) - } - - override def users: EventStream[List[UserInfo]] = - usersStream.debugWithName("users") - - override def details: EventStream[UserInfo] = - detailsStream.debugWithName("details") - - override def parameters: EventStream[List[Parameter]] = - parametersStream.debugWithName("parameters") - - override def availableFiles: EventStream[List[File]] = - filesStream.debugWithName("available files") - - override def actionBus: Observer[Action] = - actions.writer.debugWithName("actions writer") diff --git a/app/src/main/scala/mdr/pdb/app/Main.scala b/app/src/main/scala/mdr/pdb/app/Main.scala new file mode 100644 index 0000000..081b4ee --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/Main.scala @@ -0,0 +1,103 @@ +package mdr.pdb.app + +import scala.scalajs.js.annotation.JSExportTopLevel +import scala.scalajs.js.annotation.JSExport +import scala.scalajs.js.annotation.JSImport +import scala.scalajs.js +import org.scalajs.dom +import com.raquo.laminar.api.L.{*, given} +import scala.scalajs.js.Date +import com.raquo.waypoint.Router +import com.raquo.waypoint.SplitRender +import mdr.pdb.app.services.DataFetcher +import scala.scalajs.js.JSON +import zio.json._ +import mdr.pdb.UserInfo +import mdr.pdb.app.state.AppState + +@js.native +@JSImport("stylesheets/main.css", JSImport.Namespace) +object Css extends js.Any + +@js.native +@JSImport("data/users.json", JSImport.Default) +object mockUsers extends js.Object + +@js.native +@JSImport("params/pdb-params.json", JSImport.Default) +object pdbParams extends js.Object + +@JSExportTopLevel("app") +object Main: + + @JSExport + def main(args: Array[String]): Unit = + import Routes.given + onLoad { + setupAirstream() + val appContainer = dom.document.querySelector("#app") + val _ = + render( + appContainer, + renderPage(state.MockAppState(using unsafeWindowOwner, router)) + ) + } + + private def onLoad(f: => Unit): Unit = + documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) + + private def setupAirstream()(using router: Router[Page]): Unit = + AirstreamError.registerUnhandledErrorCallback(err => + router.forcePage( + Page.UnhandledError( + Some(err.getClass.getName), // TODO: Fill only in dev mode + Some(err.getMessage) + ) + ) + ) + + def renderPage(state: AppState)(using + router: Router[Page] + ): HtmlElement = + val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) + .collectSignal[Page.Detail]( + connectors + .DetailPageConnector(state)(_) + .apply + ) + .collectSignal[Page.DetailParametru]( + connectors + .DetailParametruPageConnector(state)(_) + .apply + ) + .collectSignal[Page.DetailKriteria]( + connectors + .DetailKriteriaPageConnector(state)(_) + .apply + ) + .collectSignal[Page.UpravDukazKriteria]( + pages.detail.UpravDukaz.Connector(state)(_).apply + ) + .collectStatic(Page.Dashboard)( + connectors.DashboardPageConnector(state.actionBus).apply + ) + .collect[Page.NotFound](pg => + pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) + ) + .collect[Page.UnhandledError](pg => + pages.errors + .UnhandledErrorPage( + pages.errors.UnhandledErrorPage + .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), + state.actionBus + ) + ) + .collectStatic(Page.Directory)( + connectors + .DirectoryPageConnector(state.users, state.actionBus) + .apply + ) + div(cls := "h-full", child <-- pageSplitter.$view) + + // Pull in the stylesheet + val css: Css.type = Css diff --git a/app/src/main/scala/mdr/pdb/app/Routes.scala b/app/src/main/scala/mdr/pdb/app/Routes.scala new file mode 100644 index 0000000..2cc860c --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/Routes.scala @@ -0,0 +1,198 @@ +package mdr.pdb.app + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.* +import org.scalajs.dom +import zio.json.{*, given} +import mdr.pdb.OsobniCislo + +import scala.scalajs.js +import mdr.pdb.UserInfo +import mdr.pdb.Parameter +import mdr.pdb.ParameterCriteria +import mdr.pdb.app.Page.Titled + +// enum is not working with Waypoints' SplitRender collectStatic +sealed abstract class Page( + val id: String, + val title: String, + val parent: Option[Page] +) { + val path: Vector[Page] = + parent match + case None => Vector(this) + case Some(p) => p.path :+ this + + val isRoot: Boolean = parent.isEmpty +} + +object Page: + + case class Titled[V](value: V, title: Option[String] = None): + val show: String = title.getOrElse(value.toString) + + case object Directory extends Page("directory", "Adresář", None) + + case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) + + case class Detail(osobniCislo: Titled[OsobniCislo]) + extends Page("user", osobniCislo.show, Some(Directory)) + + object Detail { + def apply(o: UserInfo): Detail = Detail( + Titled(o.personalNumber, Some(o.name)) + ) + } + + case class DetailParametru( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String] + ) extends Page( + "parameter", + parametr.show, + Some(Detail(osobniCislo)) + ) + + object DetailParametru { + def apply(o: UserInfo, p: Parameter): DetailParametru = + DetailParametru( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)) + ) + } + + case class DetailKriteria( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String], + kriterium: Titled[String] + ) extends Page( + "criteria", + kriterium.show, + Some(DetailParametru(osobniCislo, parametr)) + ) + + object DetailKriteria { + def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = + DetailKriteria( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)), + Titled(k.id, Some(k.id)) + ) + } + + case class UpravDukazKriteria( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String], + kriterium: Titled[String] + ) extends Page( + "addProof", + "Důkaz", + Some(DetailKriteria(osobniCislo, parametr, kriterium)) + ) + + object UpravDukazKriteria { + def apply( + o: UserInfo, + p: Parameter, + k: ParameterCriteria + ): UpravDukazKriteria = + UpravDukazKriteria( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)), + Titled(k.id, Some(k.id)) + ) + } + + case class NotFound(url: String) extends Page("404", "404", Some(Directory)) + + case class UnhandledError( + errorName: Option[String], + errorMessage: Option[String] + ) extends Page("500", "Unexpected error", Some(Directory)) + +object Routes: + given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) + given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) + given [V: JsonEncoder]: JsonEncoder[Titled[V]] = + DeriveJsonEncoder.gen[Titled[V]] + given [V: JsonDecoder]: JsonDecoder[Titled[V]] = + DeriveJsonDecoder.gen[Titled[V]] + given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] + given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] + + val base = + js.`import`.meta.env.BASE_URL + .asInstanceOf[String] + "app" + + val homePage: Page = Page.Directory + + given router: Router[Page] = Router[Page]( + routes = List( + Route.static(homePage, root / endOfSegments, basePath = base), + Route.static( + Page.Dashboard, + root / "dashboard" / endOfSegments, + basePath = base + ), + Route[Page.Detail, String]( + encode = _.osobniCislo.value.toString, + decode = osc => Page.Detail(Titled(OsobniCislo(osc))), + root / "osoba" / segment[String] / endOfSegments, + basePath = base + ), + Route[Page.DetailParametru, (String, String)]( + encode = p => (p.osobniCislo.value.toString, p.parametr.value), + decode = + p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / endOfSegments, + basePath = base + ), + Route[Page.DetailKriteria, (String, String, String)]( + encode = p => + ( + p.osobniCislo.value.toString, + p.parametr.value, + p.kriterium.value.replaceAll("\\.", "--") + ), + decode = p => + Page.DetailKriteria( + Titled(OsobniCislo(p._1)), + Titled(p._2), + Titled(p._3.replaceAll("--", ".")) + ), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / "kriterium" / segment[String] / endOfSegments, + basePath = base + ), + Route[Page.UpravDukazKriteria, (String, String, String)]( + encode = p => + ( + p.osobniCislo.value.toString, + p.parametr.value, + p.kriterium.value.replaceAll("\\.", "--") + ), + decode = p => + Page.UpravDukazKriteria( + Titled(OsobniCislo(p._1)), + Titled(p._2), + Titled(p._3.replaceAll("--", ".")) + ), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / "kriterium" / segment[String] / "edit" / endOfSegments, + basePath = base + ) + ), + serializePage = _.toJson, + deserializePage = _.fromJson[Page] + .fold(s => throw IllegalStateException(s), identity), + getPageTitle = _.title, + routeFallback = url => Page.NotFound(url), + deserializeFallback = _ => Page.Dashboard + )( + $popStateEvent = windowEvents.onPopState, + owner = unsafeWindowOwner + ) diff --git a/app/src/main/scala/mdr/pdb/app/actions.scala b/app/src/main/scala/mdr/pdb/app/actions.scala new file mode 100644 index 0000000..d078b3a --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/actions.scala @@ -0,0 +1,21 @@ +package mdr.pdb.app + +import mdr.pdb.OsobniCislo +import mdr.pdb.UserInfo +import mdr.pdb.Parameter +import mdr.pdb.ParameterCriteria + +sealed trait Action + +case object FetchDirectory extends Action +case class FetchUserDetails(osc: OsobniCislo) extends Action +case class FetchParameters(osc: OsobniCislo) extends Action +case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action +case class FetchParameterCriteria( + osc: OsobniCislo, + paramId: String, + critId: String, + page: (UserInfo, Parameter, ParameterCriteria) => Page +) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action +case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/mdr/pdb/app/components/AppPage.scala new file mode 100644 index 0000000..130c817 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/AppPage.scala @@ -0,0 +1,82 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.Page +import mdr.pdb.{UserProfile, UserInfo, OsobniCislo} +import com.raquo.waypoint.Router +import mdr.pdb.app.Action +import mdr.pdb.app.NavigateTo +import mdr.pdb.UserFunction + +object AppPage: + // TODO: pages by logged in user + val pages: List[Page] = List(Page.Directory, Page.Dashboard) + + import NavigationBar.{Logo, MenuItem} + + val logo = Logo( + "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", + "Workflow" + ) + + // TODO: menu items by user profile + val userMenu = + List( + MenuItem("Your Profile"), + MenuItem("Settings"), + MenuItem("Sign out") + ) + + // TODO: load user profile + val $userProfile = Var( + UserProfile( + "tom", + UserInfo( + OsobniCislo("1031"), + "tom", + "Tom", + "Cook", + None, + None, + Some("tom@example.com"), + Some("+420 222 866 180"), + Some( + UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") + ), + Nil, + None + ) + ) + ) + + val $userInfo = $userProfile.signal.map(_.userInfo) + + type ViewModel = Option[HtmlElement] + def apply( + actionBus: Observer[Action] + )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using + router: Router[Page] + ): HtmlElement = + PageLayout(actionBus)( + $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => + PageLayout.ViewModel( + NavigationBar.ViewModel( + u, + pages.map(p => + NavigationBar.Link( + () => + PageLink( + p, + actionBus + ), + p == cp + ) + ), + userMenu, + logo + ), + c + ) + ), + mods + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala new file mode 100644 index 0000000..9375398 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala @@ -0,0 +1,143 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.tailwind.list.IconText.ViewModel +import fiftyforms.ui.components.tailwind.Icons +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.Page + +import com.raquo.laminar.api.L.{*, given} +import io.laminext.syntax.core.{*, given} +import mdr.pdb.app.Action + +object Breadcrumbs: + + private def slash = + import svg.{*, given} + svg( + cls := "flex-shrink-0 h-5 w-5 text-gray-300", + xmlns := "http://www.w3.org/2000/svg", + fill := "currentColor", + viewBox := "0 0 20 20", + ariaHidden := true, + path( + d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" + ) + ) + + def backIcon = + Icons.solid + .`arrow-narrow-left`() + .amend( + svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" + ) + + object Link: + case class ViewModel( + page: Page, + icon: Option[SvgElement], + extraClasses: String, + text: String, + textClasses: Option[String] = None + ) + + val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" + + def shortHome(p: Page) = ViewModel( + p, + Some(backIcon), + "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", + "Zpět na úvodní stránku", + None + ) + + def fullHome(p: Page) = ViewModel( + p, + Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), + "text-gray-400 hover:text-gray-500", + "Domů", + Some("sr-only") + ) + + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + PageLink + .container($m.map(_.page), actionBus) + .amend( + cls <-- $m.map(_.extraClasses), + child.maybe <-- $m.map(_.icon), + span( + cls <-- $m.map(_.textClasses.getOrElse("")), + child.text <-- $m.map(_.text) + ) + ) + + object FullBreadcrumbs: + type ViewModel = Page + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + ol( + role := "list", + cls := "flex items-center space-x-4", + children <-- $m.map(_.path).split(_.id)((_, _, $p) => + li( + div( + cls := "flex items-center", + child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), + Link( + $p.map(p => + if (p.isRoot) then Link.fullHome(p) + else + Link.ViewModel( + p, + None, + s"ml-4 max-w-xs truncate ${Link.baseClasses}", + p.title + ) + ), + actionBus + ) + ) + ) + ) + ) + + object ShortBreadcrumbs: + type ViewModel = Page + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + Link( + $m.map { p => + val target = p.parent.getOrElse(p) + if target.isRoot then Link.shortHome(target) + else + Link.ViewModel( + target, + Some(backIcon), + "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", + s"Zpět na ${target.title}" + ) + }, + actionBus + ) + + def apply(actionBus: Observer[Action])(using + router: Router[Page] + ): HtmlElement = + nav( + cls := "flex", + aria.label := "Breadcrumb", + div( + cls := "flex sm:hidden", + ShortBreadcrumbs(router.$currentPage, actionBus) + ), + div( + cls := "hidden sm:block", + FullBreadcrumbs(router.$currentPage, actionBus) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala new file mode 100644 index 0000000..4bb8b8d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala @@ -0,0 +1,242 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent +import fiftyforms.ui.components.tailwind.Icons +import fiftyforms.ui.components.tailwind.Avatar +import mdr.pdb.UserInfo +import io.laminext.syntax.core.* + +object NavigationBar: + + case class Logo(img: String, name: String) + case class Link(a: () => Anchor, active: Boolean) + case class MenuItem(title: String) + + case class ViewModel( + userInfo: UserInfo, + pages: List[Link], + userMenu: List[MenuItem], + logo: Logo + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + val $userInfo = $m.map(_.userInfo) + val mobileMenuOpen = Var(false) + + val desktopOnly = cls("hidden md:block") + val mobileOnly = cls("md:hidden") + + inline def avatarImage(size: Int = 8) = + Avatar($userInfo.map(_.img)).avatarImage(size) + + def notificationButton = button( + tpe := "button", + cls := List( + "bg-indigo-600", + "focus:outline-none", + "focus:ring-2", + "focus:ring-offset-2", + "focus:ring-offset-indigo-600", + "focus:ring-white", + "hover:text-white", + "p-1", + "rounded-full", + "text-indigo-200" + ), + span(cls := "sr-only", "View notifications"), + Icons.outline.bell() + ) + + def userProfile: HtmlElement = + val menuOpen = Var(false) + + def menuItem(item: MenuItem, idx: Int): HtmlElement = + a( + href := "#", + cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", + role := "menuitem", + tabIndex := -1, + idAttr := s"user-menu-item-$idx", + item.title + ) + + div( + cls := "ml-3 relative", + div( + button( + tpe := "button", + cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", + idAttr := "user-menu-button", + aria.expanded <-- menuOpen.signal, + aria.hasPopup := true, + span(cls := "sr-only", "Open user menu"), + child <-- avatarImage(), + onClick.preventDefault.mapTo( + !menuOpen.now() + ) --> menuOpen.writer + ) + ), + /* + * */ + div( + cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", + cls <-- menuOpen.signal.map { o => + if (o) "md:block" else "md:hidden" + }, + role := "menu", + aria.orientation := "vertical", + aria.labelledBy := "user-menu-button", + tabIndex := -1, + // : keyboard navigation + children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) + ) + ) + + def mobileProfile = + def menuItem(item: MenuItem): HtmlElement = + a( + href := "#", + cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", + item.title + ) + + div( + cls := "pt-4 pb-3 border-t border-indigo-700", + div( + cls := "flex items-center px-5", + div( + cls := "flex-shrink-0", + child <-- avatarImage(10) + ), + div( + cls := "ml-3", + div( + cls := "text-base font-medium text-white", + child.text <-- $userInfo.map(_.name) + ), + child.maybe <-- $userInfo.map( + _.email.map(e => + div( + cls := "text-sm font-medium text-indigo-300", + e + ) + ) + ) + ), + notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) + ), + div( + cls := "mt-3 px-2 space-y-1", + children <-- $m.map(_.userMenu.map(menuItem)) + ) + ) + + def pageLink(page: Link): Anchor = + page + .a() + .amend( + cls := "text-white px-3 py-2 rounded-md text-sm font-medium", + cls := Seq( + "bg-indigo-700" -> page.active, + "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active + ), + ariaCurrent := (if page.active then "page" else "false") + ) + + def logoImg: Image = + img( + cls := "h-8 w-8", + src <-- $m.map(_.logo.img), + alt <-- $m.map(_.logo.name) + ) + + def mobileMenuButton = button( + tpe := "button", + cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", + aria.controls := "mobile-menu", + aria.expanded <-- mobileMenuOpen.signal, + span(cls := "sr-only", "Open main menu"), + Icons.outline + .menu() + .amend( + svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") + ), + Icons.outline + .x() + .amend( + svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") + ), + onClick.preventDefault.mapTo( + !mobileMenuOpen.now() + ) --> mobileMenuOpen.writer + ) + + def navBarLeft = + div( + cls := "flex items-center", + div(cls := "flex-shrink-0", logoImg), + div( + desktopOnly, + div( + cls := "ml-10 flex items-baseline space-x-4", + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) + ) + ) + ) + + def navBarRight = + div( + desktopOnly, + div( + cls := "ml-4 flex items-center md:ml-6", + notificationButton, + userProfile + ) + ) + + def navBarMobile = + div( + cls := "-mr-2 flex", + mobileOnly, + mobileMenuButton + ) + + def navBar = + div( + cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", + div( + cls := "flex items-center justify-between h-16", + navBarLeft, + navBarRight, + navBarMobile + ) + ) + + def mobileMenu = + div( + mobileOnly, + cls <-- mobileMenuOpen.signal.map { o => + if (o) "block" else "hidden" + }, + idAttr := "mobile-menu", + div( + cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) + ), + mobileProfile + ) + + nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala new file mode 100644 index 0000000..c4cb10d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala @@ -0,0 +1,19 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.Page +import com.raquo.waypoint.Router +import mdr.pdb.app.Action + +object PageHeader: + def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = + header( + cls := "bg-white shadow-sm", + div( + cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", + h1( + cls := "text-lg leading-6 font-semibold text-gray-900", + Breadcrumbs(actionBus) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala new file mode 100644 index 0000000..aae4539 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala @@ -0,0 +1,30 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.Page +import mdr.pdb.app.Action +import fiftyforms.ui.components.tailwind.Loading +import io.laminext.syntax.core.* + +object PageLayout: + case class ViewModel( + navigation: NavigationBar.ViewModel, + content: Option[HtmlElement] + ) + def apply(actionBus: Observer[Action])( + $m: Signal[ViewModel], + mods: Modifier[HtmlElement]* + )(using router: Router[Page]): HtmlElement = + val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) + div( + cls := "h-full flex flex-col", + NavigationBar($m.map(_.navigation)), + child.maybe <-- router.$currentPage.map(_.isRoot) + .switch(None, Some(PageHeader(actionBus))), + main( + cls := "flex-grow-1 overflow-y-auto", + mods, + child <-- $maybeContent.map(_.getOrElse(Loading)) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/mdr/pdb/app/components/PageLink.scala new file mode 100644 index 0000000..e6e7052 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageLink.scala @@ -0,0 +1,40 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.LinkSupport.* +import mdr.pdb.app.Page +import mdr.pdb.app.Action +import com.raquo.waypoint.Router +import mdr.pdb.app.NavigateTo + +object PageLink: + type ViewModel = Page + def apply($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = + a(mods($m, actions), child.text <-- $m.map(_.title)) + + def apply(m: ViewModel, actions: Observer[Action])(using + router: Router[Page] + ): Anchor = apply(Val(m), actions) + + def container($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = a(mods($m, actions)) + + def container(m: ViewModel, actions: Observer[Action])(using + Router[Page] + ): Anchor = container(Val(m), actions) + + private def mods($m: Signal[Page], actions: Observer[Action])(using + router: Router[Page] + ): Modifier[Anchor] = + Seq( + href <-- $m.map(router.absoluteUrlForPage).recover { case _ => + Some("invalid url") + }, + composeEvents(onClick.noKeyMod.preventDefault)( + _.sample($m) + .map(NavigateTo.apply) + ) --> actions + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala new file mode 100644 index 0000000..7682772 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala @@ -0,0 +1,13 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.pages.dashboard.DashboardPage +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage + +class DashboardPageConnector(actionBus: Observer[Action])(using + router: Router[Page] +): + def apply: HtmlElement = + AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala new file mode 100644 index 0000000..76b9133 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala @@ -0,0 +1,76 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.Parameter +import mdr.pdb.UserInfo +import mdr.pdb.app.pages.detail.DetailParametruPage +import pages.detail.DetailKriteriaPage +import pages.detail.components.DukazyKriteria +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.ParameterCriteria + +object DetailKriteriaPageConnector { + trait AppState { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + } +} + +case class DetailKriteriaPageConnector( + state: DetailKriteriaPageConnector.AppState +)( + $page: Signal[Page.DetailKriteria] +)(using Router[Page]): + val $paramChangeSignal = + $page.splitOne(p => + (p.osobniCislo.value, p.parametr.value, p.kriterium.value) + )((x, _, _) => x) + val $pageChangeSignal = + $paramChangeSignal.map( + FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) + ) + + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + val $merged = + $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => + for { + da <- d + pa <- p + pb <- pa.find(_.id == pc._2) + ka <- pb.criteria.find(_.id == pc._3) + } yield (da, pb, ka) + ) + + def apply: HtmlElement = + AppPage(state.actionBus)( + $merged.map(_.map(buildModel)) + .split(_ => ())((_, s, $s) => + DetailKriteriaPage($s)(state.actionBus.contramap { + case DukazyKriteria.Add => + NavigateTo( + Page.UpravDukazKriteria( + Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), + Page.Titled(s.parametr.id, Some(s.parametr.nazev)), + Page.Titled(s.kriterium.id, Some(s.kriterium.id)) + ) + ) + }) + ), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: Parameter, + k: ParameterCriteria + ): DetailKriteriaPage.ViewModel = + DetailKriteriaPage.ViewModel( + o.toDetailOsoby, + p.toParametr(_ => a()), + k.toKriterium(_ => a()) + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DetailPageConnector.scala new file mode 100644 index 0000000..df3a6b7 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DetailPageConnector.scala @@ -0,0 +1,57 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.UserInfo +import pages.detail.DetailPage +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.pages.detail.components.DetailOsoby +import mdr.pdb.Parameter +import mdr.pdb.app.pages.detail.components.SeznamParametru +import fiftyforms.ui.components.tailwind.Color + +object DetailPageConnector { + trait AppState { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + } +} + +case class DetailPageConnector(state: DetailPageConnector.AppState)( + $page: Signal[Page.Detail] +)(using router: Router[Page]): + val $oscChangeSignal = $page.splitOne(_.osobniCislo.value)((osc, _, _) => osc) + val $pageChangeSignal = + $oscChangeSignal.flatMap(osc => + EventStream.fromSeq(Seq(FetchUserDetails(osc), FetchParameters(osc))) + ) + // TODO: filter the value based on the current osc + // OSC change will fetch new data, but still + // - we need to be sure that what we got is really what we ought to display + // - we want to display stale data accordingly (at least with loading indicator) + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + def apply: HtmlElement = + AppPage(state.actionBus)( + $data.combineWithFn($params)(_ zip _) + .map(_.map(buildModel)) + .split(_ => ())((_, _, s) => DetailPage(s)), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: List[Parameter] + ): DetailPage.ViewModel = + DetailPage.ViewModel( + o.toDetailOsoby, + p.map( + _.toParametr(param => + PageLink.container(Page.DetailParametru(o, param), state.actionBus) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DetailParametruPageConnector.scala new file mode 100644 index 0000000..fa7193d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DetailParametruPageConnector.scala @@ -0,0 +1,67 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.Parameter +import mdr.pdb.UserInfo +import mdr.pdb.app.pages.detail.DetailParametruPage +import pages.detail.DetailParametruPage +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.components.PageLink + +object DetailParametruPageConnector { + trait AppState { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + } +} + +case class DetailParametruPageConnector( + state: DetailParametruPageConnector.AppState +)( + $page: Signal[Page.DetailParametru] +)(using router: Router[Page]): + val $paramChangeSignal = + $page.splitOne(p => (p.osobniCislo.value, p.parametr.value))((x, _, _) => x) + val $pageChangeSignal = + $paramChangeSignal.map(FetchParameter(_, _)) + + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + val $merged = + $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => + for { + da <- d + pa <- p + pb <- pa.find(_.id == pc._2) + } yield (da, pb) + ) + + def apply: HtmlElement = + AppPage(state.actionBus)( + $merged.map(_.map(buildModel)) + .split(_ => ())((_, _, s) => DetailParametruPage(s)), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: Parameter + ): DetailParametruPage.ViewModel = + DetailParametruPage.ViewModel( + o.toDetailOsoby, + p.toParametr(p => + PageLink.container(Page.DetailParametru(o, p), state.actionBus) + ), + p.criteria.map( + _.toKriterium { c => + PageLink.container( + Page.DetailKriteria(o, p, c), + state.actionBus + ) + } + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DirectoryPageConnector.scala new file mode 100644 index 0000000..ab2c045 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DirectoryPageConnector.scala @@ -0,0 +1,34 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.UserInfo +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.components.AppPage + +case class DirectoryPageConnector( + $input: EventStream[List[UserInfo]], + actionBus: Observer[Action] +)(using router: Router[Page]): + val $data = $input.startWithNone + val $actionSignal = EventStream.fromValue(FetchDirectory) + + def apply: HtmlElement = + AppPage(actionBus)( + $data.split(_ => ())((_, _, s) => + pages.directory.DirectoryPage( + s.map( + _.map( + _.toUserRow(u => + PageLink.container( + Page.Detail(Page.Titled(u.personalNumber)), + actionBus + ) + ) + ) + ) + ) + ), + $actionSignal --> actionBus + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/mdr/pdb/app/connectors/conversions.scala new file mode 100644 index 0000000..f581796 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/conversions.scala @@ -0,0 +1,68 @@ +package mdr.pdb.app.connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.UserInfo +import mdr.pdb.app.pages.detail.components.DetailOsoby +import mdr.pdb.Parameter +import mdr.pdb.app.pages.detail.components.SeznamParametru +import mdr.pdb.ParameterCriteria +import mdr.pdb.app.pages.detail.components.SeznamKriterii +import mdr.pdb.app.pages.directory.components.UserRow +import mdr.pdb.app.pages.detail.components.DetailParametru +import mdr.pdb.app.pages.detail.components.DetailKriteria +import fiftyforms.ui.components.tailwind.Color + +extension (o: UserInfo) + def toDetailOsoby: DetailOsoby.ViewModel = + DetailOsoby.ViewModel( + o.personalNumber, + o.name, + o.email, + o.phone, + o.img, + o.mainFunction.map(f => + DetailOsoby.Funkce.ViewModel(f.name, f.dept, f.ou) + ), + o.userContracts.headOption.map(c => + DetailOsoby.PracovniPomer.ViewModel(c.rel, c.startDate, c.endDate) + ) + ) + +extension (param: Parameter) + def toParametr(container: Parameter => Anchor): DetailParametru.ViewModel = + DetailParametru.ViewModel( + id = param.id, + nazev = param.name, + popis = param.description, + status = "Nesplněno", + statusColor = Color.red, + a = container(param) + ) + +extension (crit: ParameterCriteria) + def toKriterium( + container: ParameterCriteria => Anchor + ): DetailKriteria.ViewModel = + DetailKriteria.ViewModel( + nazev = crit.criteriumText, + kapitola = crit.chapterId, + bod = crit.itemId, + status = "Nesplněno", + statusColor = Color.red, + splneno = false, + dukazy = Nil, + container = container(crit) + ) + +extension (user: UserInfo) + def toUserRow( + container: UserInfo => HtmlElement = (_: UserInfo) => div() + ): UserRow.ViewModel = + UserRow.ViewModel( + osobniCislo = user.personalNumber.toString, + celeJmeno = user.name, + prijmeni = user.surname, + hlavniFunkce = user.mainFunction.map(_.name), + img = user.img, + container = () => container(user) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/dashboard/DashboardPage.scala b/app/src/main/scala/mdr/pdb/app/pages/dashboard/DashboardPage.scala new file mode 100644 index 0000000..0eb8f7a --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/dashboard/DashboardPage.scala @@ -0,0 +1,11 @@ +package mdr.pdb.app.pages.dashboard + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.Page + +object DashboardPage: + + def render: HtmlElement = + div("Dashboard page") diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala new file mode 100644 index 0000000..ff3a25f --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala @@ -0,0 +1,31 @@ +package mdr.pdb.app.pages.detail + +import com.raquo.laminar.api.L.{*, given} + +import components._ + +object DetailKriteriaPage: + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametr: DetailParametru.ViewModel, + kriterium: DetailKriteria.ViewModel + ) + + def apply($m: Signal[ViewModel])( + action: Observer[DukazyKriteria.Action] + ): HtmlElement = + div( + cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + div( + DetailOsoby.header($m.map(_.osoba)), + DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") + ), + div( + DetailKriteria($m.map(_.kriterium)), + DukazyKriteria($m.map(_.kriterium.dukazy))(action) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/DetailPage.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailPage.scala new file mode 100644 index 0000000..b591cc9 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailPage.scala @@ -0,0 +1,25 @@ +package mdr.pdb.app.pages.detail + +import com.raquo.laminar.api.L.{*, given} + +import components._ +import mdr.pdb.app.Page +import com.raquo.waypoint.Router +import mdr.pdb.app.Action + +object DetailPage: + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametry: SeznamParametru.ViewModel + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + DetailOsoby($m.map(_.osoba)), + SeznamParametru($m.map(_.parametry)) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/DetailParametruPage.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailParametruPage.scala new file mode 100644 index 0000000..3322279 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailParametruPage.scala @@ -0,0 +1,24 @@ +package mdr.pdb.app.pages.detail + +import com.raquo.laminar.api.L.{*, given} + +import components._ + +object DetailParametruPage: + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametr: DetailParametru.ViewModel, + kriteria: SeznamKriterii.ViewModel + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + DetailOsoby.header($m.map(_.osoba)), + DetailParametru($m.map(_.parametr)), + SeznamKriterii($m.map(_.kriteria)) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala new file mode 100644 index 0000000..b58618a --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala @@ -0,0 +1,107 @@ +package mdr.pdb +package app +package pages.detail + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.pages.detail.components.UpravDukazForm +import fiftyforms.services.files.File + +object UpravDukaz: + + type ThisPage = Page.UpravDukazKriteria + type PageKey = (OsobniCislo, String, String) + + trait State { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def availableFiles: EventStream[List[File]] + def actionBus: Observer[Action] + } + + def keyOfPage(page: ThisPage): PageKey = + (page.osobniCislo.value, page.parametr.value, page.kriterium.value) + + def onChangeAction(key: PageKey): Action = + FetchParameterCriteria( + key._1, + key._2, + key._3, + Page.UpravDukazKriteria(_, _, _) + ) + + class Connector(state: State)($page: Signal[ThisPage])(using Router[Page]): + val $paramChangeSignal = $page.splitOne(keyOfPage)((x, _, _) => x) + val $pageChangeSignal = $paramChangeSignal.map(onChangeAction) + + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + val $merged = + $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => + for { + da <- d + pa <- p + pb <- pa.find(_.id == pc._2) + ka <- pb.criteria.find(_.id == pc._3) + } yield (da, pb, ka) + ) + + def apply: HtmlElement = + AppPage(state.actionBus)( + $merged.split(_ => ())((_, s, $s) => + PageComponent( + $s.map(buildModel), + state.availableFiles, + state.actionBus.contramap { + case UpravDukazForm.Cancelled => + NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) + case UpravDukazForm.AvailableFilesRequested => + FetchAvailableFiles(s._1.personalNumber) + } + ) + ), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: Parameter, + k: ParameterCriteria + ): PageComponent.ViewModel = + import connectors.* + PageComponent.ViewModel( + o.toDetailOsoby, + p.toParametr(_ => a()), + k.toKriterium(_ => a()) + ) + + object PageComponent: + import components.* + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametr: DetailParametru.ViewModel, + kriterium: DetailKriteria.ViewModel + ) + + def apply( + $m: Signal[ViewModel], + availableFilesStream: EventStream[List[File]], + events: Observer[UpravDukazForm.Event] + ): HtmlElement = + div( + cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + div( + DetailOsoby.header($m.map(_.osoba)), + DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") + ), + div( + DetailKriteria($m.map(_.kriterium)), + UpravDukazForm(availableFilesStream)(events) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailKriteria.scala new file mode 100644 index 0000000..a3ab634 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailKriteria.scala @@ -0,0 +1,37 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.CustomAttrs +import fiftyforms.ui.components.tailwind.Icons +import java.time.LocalDate +import fiftyforms.ui.components.tailwind.Color + +object DetailKriteria: + case class ViewModel( + nazev: String, + kapitola: String, + bod: String, + status: String, + statusColor: Color, + splneno: Boolean, + dukazy: List[DukazKriteria.ViewModel], + container: HtmlElement = div() + ) { + val id = s"${kapitola}${bod}" + } + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "pb-5 border-b border-gray-200", + h3( + cls := "text-xl leading-6 font-bold text-gray-900", + child.text <-- $m.map(_.nazev) + ), + h3( + cls := "mt-2 max-w-4xl text-sm text-gray-500", + "Kapitola ", + child.text <-- $m.map(_.kapitola), + " bod ", + child <-- $m.map(_.bod) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailOsoby.scala new file mode 100644 index 0000000..0fe2ba5 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailOsoby.scala @@ -0,0 +1,100 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.OsobniCislo + +import java.time.LocalDate +import fiftyforms.ui.components.tailwind.Avatar + +object DetailOsoby: + + object Funkce: + case class ViewModel( + nazev: String, + stredisko: String, + voj: String + ) + + def render($m: Signal[ViewModel]): HtmlElement = + p( + cls := "text-sm font-medium text-gray-500", + child.text <-- $m.map(_.nazev), + span( + cls := "hidden md:inline", + " @ ", + child.text <-- $m.map(_.stredisko), + ", ", + child.text <-- $m.map(_.voj) + ) + ) + + object PracovniPomer: + case class ViewModel( + druh: String, + pocatek: LocalDate, + konec: Option[LocalDate] + ) + + def render($m: Signal[ViewModel]): HtmlElement = + import fiftyforms.ui.components.tailwind.CustomAttrs.datetime + p( + cls := "text-sm font-medium text-gray-500", + child.text <-- $m.map(_.druh), + " od ", + time( + datetime <-- $m.map(_.pocatek.toString), + child.text <-- $m.map(_.pocatek.toString) + ) + ) + + case class ViewModel( + osobniCislo: OsobniCislo, + jmeno: String, + email: Option[String], + telefon: Option[String], + img: Option[String], + hlavniFunkce: Option[Funkce.ViewModel], + pracovniPomer: Option[PracovniPomer.ViewModel] + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "md:flex md:items-center md:justify-between md:space-x-4", + div( + cls := "flex items-start space-x-4", + div( + cls := "flex-shrink-0", + Avatar($m.map(_.img)).avatar(16) + ), + div( + h1( + cls := "text-2xl font-bold text-gray-900", + child.text <-- $m.map(_.jmeno) + ), + child.maybe <-- $m.map(_.hlavniFunkce).split(_ => ())((_, _, d) => + Funkce.render(d) + ), + child.maybe <-- $m.map(_.pracovniPomer).split(_ => ())((_, _, d) => + PracovniPomer.render(d) + ) + ) + ) + ) + + def header($m: Signal[ViewModel]): HtmlElement = + div( + cls := "md:flex md:items-center md:justify-between md:space-x-5", + div( + cls := "flex items-start space-x-4", + div( + cls := "flex-shrink-0", + Avatar($m.map(_.img)).avatar(8) + ), + div( + h1( + cls := "text-lg font-medium text-gray-800", + child.text <-- $m.map(_.jmeno) + ) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailParametru.scala new file mode 100644 index 0000000..6310dde --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailParametru.scala @@ -0,0 +1,34 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.Color + +object DetailParametru: + case class ViewModel( + id: String, + nazev: String, + popis: String, + status: String, + statusColor: Color, + a: Anchor + ) + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "pb-5 border-b border-gray-200", + h2( + cls := "text-2xl leading-6 font-medium text-gray-900", + child.text <-- $m.map(_.nazev) + ), + p( + cls := "mt-2 max-w-4xl text-sm text-gray-500", + child.text <-- $m.map(_.popis) + ) + ) + + def header($m: Signal[ViewModel]): HtmlElement = + div( + h2( + cls := "text-lg leading-6 font-medium text-gray-600", + child.text <-- $m.map(_.nazev) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazKriteria.scala new file mode 100644 index 0000000..c292919 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazKriteria.scala @@ -0,0 +1,177 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import java.time.LocalDate +import fiftyforms.ui.components.tailwind.CustomAttrs + +object DukazKriteria: + case class Osoba(osobniCislo: String, jmeno: String) + case class Udalost(osoba: Osoba, datum: LocalDate) + case class Dokument( + url: String, + nazev: String, + pridal: Udalost, + odebral: Option[Udalost] + ) + case class ViewModel( + dokumenty: List[Dokument], + autorizoval: Option[Udalost], + platiDo: Option[LocalDate], + poznámka: Option[String] + ) + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "bg-white shadow overflow-hidden sm:rounded-lg", + div( + cls := "px-4 py-5 sm:px-6", + h3( + cls := "text-lg leading-6 font-medium text-gray-900", + """Applicant Information""" + ), + p( + cls := "mt-1 max-w-2xl text-sm text-gray-500", + """Personal details and application.""" + ) + ), + div( + cls := "border-t border-gray-200 px-4 py-5 sm:px-6", + dl( + cls := "grid grid-cols-1 gap-x-4 gap-y-8 sm:grid-cols-2", + div( + cls := "sm:col-span-1", + dt( + cls := "text-sm font-medium text-gray-500", + """Full name""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """Margot Foster""" + ) + ), + div( + cls := "sm:col-span-1", + dt( + cls := "text-sm font-medium text-gray-500", + """Application for""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """Backend Developer""" + ) + ), + div( + cls := "sm:col-span-1", + dt( + cls := "text-sm font-medium text-gray-500", + """Email address""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """margotfoster@example.com""" + ) + ), + div( + cls := "sm:col-span-1", + dt( + cls := "text-sm font-medium text-gray-500", + """Salary expectation""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """$120,000""" + ) + ), + div( + cls := "sm:col-span-2", + dt( + cls := "text-sm font-medium text-gray-500", + """About""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """Fugiat ipsum ipsum deserunt culpa aute sint do nostrud anim incididunt cillum culpa consequat. Excepteur qui ipsum aliquip consequat sint. Sit id mollit nulla mollit nostrud in ea officia proident. Irure nostrud pariatur mollit ad adipisicing reprehenderit deserunt qui eu.""" + ) + ), + div( + cls := "sm:col-span-2", + dt( + cls := "text-sm font-medium text-gray-500", + """Attachments""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + ul( + role := "list", + cls := "border border-gray-200 rounded-md divide-y divide-gray-200", + li( + cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", + div( + cls := "w-0 flex-1 flex items-center", { + import svg.* + import CustomAttrs.svg.ariaHidden + svg( + cls := "flex-shrink-0 h-5 w-5 text-gray-400", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + ariaHidden := true, + path( + fillRule := "evenodd", + d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", + clipRule := "evenodd" + ) + ) + }, + span( + cls := "ml-2 flex-1 w-0 truncate", + """resume_back_end_developer.pdf""" + ) + ), + div( + cls := "ml-4 flex-shrink-0", + a( + href := "#", + cls := "font-medium text-indigo-600 hover:text-indigo-500", + """Download""" + ) + ) + ), + li( + cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", + div( + cls := "w-0 flex-1 flex items-center", { + import svg.* + import CustomAttrs.svg.ariaHidden + svg( + cls := "flex-shrink-0 h-5 w-5 text-gray-400", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + ariaHidden := true, + path( + fillRule := "evenodd", + d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", + clipRule := "evenodd" + ) + ) + }, + span( + cls := "ml-2 flex-1 w-0 truncate", + """coverletter_back_end_developer.pdf""" + ) + ), + div( + cls := "ml-4 flex-shrink-0", + a( + href := "#", + cls := "font-medium text-indigo-600 hover:text-indigo-500", + """Download""" + ) + ) + ) + ) + ) + ) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala new file mode 100644 index 0000000..d604860 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala @@ -0,0 +1,35 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.Icons + +object DukazyKriteria: + sealed trait Action + case object Add extends Action + + type ViewModel = List[DukazKriteria.ViewModel] + + def apply($m: Signal[ViewModel])(actions: Observer[Action]): HtmlElement = + div( + child.maybe <-- $m.map(m => + if (m.isEmpty) then Some(prazdnyDukaz(actions)) + else None + ), + children <-- $m.map(_.zipWithIndex).split(_._2)((_, _, $s) => + DukazKriteria($s.map(_._1)) + ) + ) + + private def prazdnyDukaz(actions: Observer[Action]): HtmlElement = + button( + tpe := "button", + onClick.preventDefault.mapTo(Add) --> actions, + cls := "relative block w-full border-2 border-gray-300 border-dashed rounded-lg p-12 text-center hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + Icons.outline + .`document-add`(12) + .amend(svg.cls := "mx-auto text-gray-400"), + span( + cls := "mt-2 block text-sm font-medium text-gray-900", + "Přidat důkaz" + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala new file mode 100644 index 0000000..7e77a7b --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala @@ -0,0 +1,34 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.Color +import fiftyforms.ui.components.tailwind.list.StackedList +import fiftyforms.ui.components.tailwind.list.ListRow +import fiftyforms.ui.components.tailwind.list.RowTag +import fiftyforms.ui.components.tailwind.list.RowNext +import java.time.LocalDate + +object SeznamKriterii: + type ViewModel = List[DetailKriteria.ViewModel] + + private val kritList = new StackedList[DetailKriteria.ViewModel] + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "bg-white shadow overflow-hidden sm:rounded-md", + kritList($m, _.id) { $i => + $i.map { i => + ListRow.ViewModel( + title = i.nazev, + topRight = RowTag.render( + $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) + ), + bottomLeft = + p(cls := "text-sm text-gray-500", s"${i.kapitola}${i.bod}"), + bottomRight = emptyNode, + farRight = RowNext.render, + containerElement = i.container + ) + } + } + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamParametru.scala new file mode 100644 index 0000000..0760b32 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamParametru.scala @@ -0,0 +1,37 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.list.{ + StackedList, + ListRow, + RowTag, + PropList, + IconText, + RowNext +} +import fiftyforms.ui.components.tailwind.Color +import fiftyforms.ui.components.tailwind.LinkSupport.* + +object SeznamParametru: + type ViewModel = List[DetailParametru.ViewModel] + + private val parametrList = new StackedList[DetailParametru.ViewModel] + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "bg-white shadow overflow-hidden sm:rounded-md", + parametrList($m, _.id) { $i => + $i.map { i => + ListRow.ViewModel( + title = i.nazev, + topRight = RowTag.render( + $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) + ), + bottomLeft = emptyNode, + bottomRight = emptyNode, + farRight = RowNext.render, + containerElement = i.a + ) + } + } + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala new file mode 100644 index 0000000..28753c9 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala @@ -0,0 +1,101 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import io.laminext.syntax.core.* +import fiftyforms.ui.components.tailwind.CustomAttrs +import fiftyforms.ui.components.tailwind.form.* +import org.scalajs.dom +import com.raquo.laminar.nodes.ReactiveHtmlElement +import fiftyforms.services.files.components.tailwind.FilePicker +import mdr.pdb.frontend.AutorizujDukaz +import mdr.pdb.frontend.DocumentRef +import fiftyforms.services.files.File + +object UpravDukazForm: + sealed trait Event + case object Cancelled extends Event + case object AvailableFilesRequested extends Event + def apply(availableFilesStream: EventStream[List[File]])( + updates: Observer[Event] + ): HtmlElement = + val (filesStream, filesObserver) = + EventStream.withObserver[FilePicker.Event] + val files = Var[List[File]](Nil) + def submitButtons: HtmlElement = + div( + cls := "pt-5", + div( + cls := "flex justify-end", + button( + tpe := "button", + cls := "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + "Zrušit", + onClick.mapTo(Cancelled) --> updates + ), + button( + tpe := "submit", + cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + disabled <-- files.signal.map(_.isEmpty), + "Autorizovat" + ) + ) + ) + + div( + filesStream.collect { case FilePicker.SelectionUpdated(files) => + files.to(List) + } --> files.writer, + filesStream.collect { case FilePicker.AvailableFilesRequested => + AvailableFilesRequested + } --> updates, + cls := "bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6", + Form( + Form.Body( + Form.Section( + FormHeader( + FormHeader.ViewModel( + "Doložení kritéria", + "Sestavte doklady poskytující důkaz kritéria a potvrďte odesláním formuláře. Případné limitace či jiné relevantní údaje vepište do pole Komentář." + ) + ), + FormFields( + FormRow( + "dokumenty", + "Dokumenty", + FilePicker(files.signal, availableFilesStream)(filesObserver) + .amend(idAttr := "dokumenty", cls("max-w-lg")) + ).toHtml, + FormRow( + "platnost", + "Platnost", + input( + idAttr := "platnost", + name := "platnost", + tpe := "date", + autoComplete := "date", + cls := "block max-w-lg w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" + ) + ).toHtml, + FormRow( + "komentar", + "Komentář", + div( + cls := "mt-1 sm:mt-0 sm:col-span-2", + textArea( + idAttr := "komentar", + name := "about", + rows := 3, + cls := "max-w-lg shadow-sm block w-full focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border border-gray-300 rounded-md" + ), + p( + cls := "mt-2 text-sm text-gray-500", + "Doplňte prosím potřebné informace související s doložením kritéria, včetně případných limitací." + ) + ) + ).toHtml + ) + ) + ), + submitButtons + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/directory/DirectoryPage.scala b/app/src/main/scala/mdr/pdb/app/pages/directory/DirectoryPage.scala new file mode 100644 index 0000000..7a72ca3 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/directory/DirectoryPage.scala @@ -0,0 +1,40 @@ +package mdr.pdb.app.pages.directory + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.UserInfo + +import components._ + +object DirectoryPage: + + type ViewModel = List[UserRow.ViewModel] + + def apply($m: Signal[ViewModel]): HtmlElement = + val (actionsStream, actionObserver) = + EventStream.withObserver[SearchForm.Action] + val $filter = actionsStream + .collect { + case SearchForm.ClearFilter => None + case SearchForm.SetFilter(t) => Some(t) + } + .startWith(None) + val byLetter = for { + d <- $m + f <- $filter + } yield for { + (letter, users) <- d + .filter { user => + f match + case None => true + case Some(t) => user.search.contains(t) + } + .groupBy(_.prijmeni.head) + .to(List) + .sortBy(_._1) + } yield (letter.toString, users.sortBy(_.prijmeni)) + + div( + cls := "h-full max-w-7xl mx-auto order-first flex flex-col flex-shrink-0", + SearchForm(actionObserver), + Directory(byLetter) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/directory/components/Directory.scala b/app/src/main/scala/mdr/pdb/app/pages/directory/components/Directory.scala new file mode 100644 index 0000000..f7d6f99 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/directory/components/Directory.scala @@ -0,0 +1,37 @@ +package mdr.pdb.app.pages.directory.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.UserInfo + +object Directory: + + object Header: + type ViewModel = String + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500", + h3(child.text <-- $m) + ) + + object UserList: + type ViewModel = List[UserRow.ViewModel] + def apply($m: Signal[ViewModel]): HtmlElement = + ul( + role := "list", + cls := "relative z-0 divide-y divide-gray-200", + children <-- $m.split(_.osobniCislo)((_, _, s) => UserRow(s)) + ) + + type ViewModel = List[(String, List[UserRow.ViewModel])] + def apply($m: Signal[ViewModel]): HtmlElement = + nav( + cls := "flex-1 min-h-0 overflow-y-auto", + aria.label := "Directory", + children <-- $m.split(_._1)((_, _, s) => + div( + cls := "relative", + Header(s.map(_._1)), + UserList(s.map(_._2)) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/directory/components/SearchForm.scala b/app/src/main/scala/mdr/pdb/app/pages/directory/components/SearchForm.scala new file mode 100644 index 0000000..fb9ecc4 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/directory/components/SearchForm.scala @@ -0,0 +1,54 @@ +package mdr.pdb.app.pages.directory.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.Icons + +object SearchForm: + sealed trait Action + case object SubmitFilter extends Action + sealed trait FilterAction extends Action + case object ClearFilter extends FilterAction + case class SetFilter(value: String) extends FilterAction + + def apply(actions: Observer[Action]): HtmlElement = + div( + cls := "px-6 pt-4 pb-4", + form( + cls := "flex space-x-4", + onSubmit.mapTo(SubmitFilter) --> actions, + div( + cls := "flex-1 min-w-0", + label( + forId := "search", + cls := "sr-only", + "Hledat" + ), + div( + cls := "relative rounded-md shadow-sm", + div( + cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", + Icons.solid.search().amend(svg.cls := "text-gray-400") + ), + input( + tpe := "search", + name := "search", + idAttr := "search", + cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", + placeholder := "Hledat", + composeEvents(onInput.mapToValue.setAsValue.map(SetFilter(_)))( + _.throttle(500) + ) --> actions + ) + ) + ), + button( + tpe := "submit", + cls := "inline-flex justify-center px-3.5 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500", + Icons.solid.filter().amend(svg.cls := "text-gray-400"), + span( + cls := "sr-only", + "Hledat" + ) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/directory/components/UserRow.scala b/app/src/main/scala/mdr/pdb/app/pages/directory/components/UserRow.scala new file mode 100644 index 0000000..4aa1672 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/directory/components/UserRow.scala @@ -0,0 +1,52 @@ +package mdr.pdb.app.pages.directory.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.airstream.core.Signal +import fiftyforms.ui.components.tailwind.Avatar + +object UserRow: + case class ViewModel( + osobniCislo: String, + celeJmeno: String, + prijmeni: String, + hlavniFunkce: Option[String], + img: Option[String], + container: () => HtmlElement = () => div() + ) { + val search = prijmeni.toLowerCase + } + + def apply($m: Signal[ViewModel]): HtmlElement = + inline def avatarImage = + Avatar($m.map(_.img)).avatarImage(10) + + li( + div( + cls := "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500", + div( + cls := "flex-shrink-0", + child <-- avatarImage + ), + div( + cls := "flex-1 min-w-0", + child <-- $m.map { o => + o.container() + .amend( + cls := "focus:outline-none", + span( + cls := "absolute inset-0", + aria.hidden := true + ), + p( + cls := "text-sm font-medium text-gray-900", + o.celeJmeno + ), + p( + cls := "text-sm text-gray-500 truncate", + o.hlavniFunkce + ) + ) + } + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/errors/ErrorPage.scala b/app/src/main/scala/mdr/pdb/app/pages/errors/ErrorPage.scala new file mode 100644 index 0000000..6b195ef --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/errors/ErrorPage.scala @@ -0,0 +1,99 @@ +package mdr.pdb.app.pages.errors + +import com.raquo.domtypes.generic.codecs.StringAsIsCodec +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.Page +import mdr.pdb.app.Action +import com.raquo.waypoint.Router + +object ErrorPage: + case class ViewModel( + homePage: Page, + errorName: String, + title: String, + subTitle: String + ) + + def apply(m: ViewModel, actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + val ViewModel(homePage, errorName, title, subTitle) = m + div( + cls := "min-h-full pt-16 pb-12 flex flex-col bg-white", + main( + cls := "flex-grow flex flex-col justify-center max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", + div( + cls := "flex-shrink-0 flex justify-center", + a( + href := "/", + cls := "inline-flex", + span( + cls := "sr-only", + """Workflow""" + ), + img( + cls := "h-12 w-auto", + src := "https://tailwindui.com/img/logos/workflow-mark.svg?color=indigo&shade=600", + alt := "" + ) + ) + ), + div( + cls := "py-16", + div( + cls := "text-center", + p( + cls := "text-sm font-semibold text-indigo-600 uppercase tracking-wide", + errorName + ), + h1( + cls := "mt-2 text-4xl font-extrabold text-gray-900 tracking-tight sm:text-5xl", + title + ), + p( + cls := "mt-2 text-base text-gray-500", + subTitle + ), + div( + cls := "mt-6", + PageLink + .container(homePage, actionBus) + .amend( + cls := "text-base font-medium text-indigo-600 hover:text-indigo-500", + """Go back home""" + ) + ) + ) + ) + ), + footer( + cls := "flex-shrink-0 max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", + nav( + cls := "flex justify-center space-x-4", + a( + href := "#", + cls := "text-sm font-medium text-gray-500 hover:text-gray-600", + """Contact Support""" + ), + span( + cls := "inline-block border-l border-gray-300", + aria.hidden := true + ), + a( + href := "#", + cls := "text-sm font-medium text-gray-500 hover:text-gray-600", + """Status""" + ), + span( + cls := "inline-block border-l border-gray-300", + aria.hidden := true + ), + a( + href := "#", + cls := "text-sm font-medium text-gray-500 hover:text-gray-600", + """Twitter""" + ) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/errors/NotFoundPage.scala b/app/src/main/scala/mdr/pdb/app/pages/errors/NotFoundPage.scala new file mode 100644 index 0000000..97760e1 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/errors/NotFoundPage.scala @@ -0,0 +1,21 @@ +package mdr.pdb.app.pages.errors + +import com.raquo.domtypes.generic.codecs.StringAsIsCodec +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.Page +import mdr.pdb.app.Action + +object NotFoundPage: + def apply(homePage: Page, url: String, actionBus: Observer[Action])(using + router: Router[Page] + ): HtmlElement = + ErrorPage( + ErrorPage.ViewModel( + homePage, + "404 error", + "Page not found.", + s"Sorry, but page $url doesn't exist." + ), + actionBus + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala b/app/src/main/scala/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala new file mode 100644 index 0000000..77b0af8 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala @@ -0,0 +1,32 @@ +package mdr.pdb.app.pages.errors + +import com.raquo.domtypes.generic.codecs.StringAsIsCodec +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.Page +import mdr.pdb.app.Action + +object UnhandledErrorPage: + + case class ViewModel( + homePage: Page, + errorName: Option[String], + errorMessage: Option[String] + ) + + def apply(m: ViewModel, actionBus: Observer[Action])(using + router: Router[Page] + ): HtmlElement = + ErrorPage( + ErrorPage.ViewModel( + m.homePage, + "Unexpected error occurred", + m.errorName.getOrElse( + "Uh oh!" + ), // TODO: translations, better text than uh oh + m.errorMessage.getOrElse( + "This wasn't supposed to happen! Please try again." + ) + ), + actionBus + ) diff --git a/app/src/main/scala/mdr/pdb/app/services/DataFetcher.scala b/app/src/main/scala/mdr/pdb/app/services/DataFetcher.scala new file mode 100644 index 0000000..134c221 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/services/DataFetcher.scala @@ -0,0 +1,6 @@ +package mdr.pdb.app.services + +import com.raquo.airstream.core.Observer + +trait DataFetcher[K, A]: + def fetch(id: K, sink: Observer[A]): Unit diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala deleted file mode 100644 index a3b00af..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala +++ /dev/null @@ -1,103 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import scala.scalajs.js.annotation.JSExportTopLevel -import scala.scalajs.js.annotation.JSExport -import scala.scalajs.js.annotation.JSImport -import scala.scalajs.js -import org.scalajs.dom -import com.raquo.laminar.api.L.{*, given} -import scala.scalajs.js.Date -import com.raquo.waypoint.Router -import com.raquo.waypoint.SplitRender -import cz.e_bs.cmi.mdr.pdb.app.services.DataFetcher -import scala.scalajs.js.JSON -import zio.json._ -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.state.AppState - -@js.native -@JSImport("stylesheets/main.css", JSImport.Namespace) -object Css extends js.Any - -@js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native -@JSImport("params/pdb-params.json", JSImport.Default) -object pdbParams extends js.Object - -@JSExportTopLevel("app") -object Main: - - @JSExport - def main(args: Array[String]): Unit = - import Routes.given - onLoad { - setupAirstream() - val appContainer = dom.document.querySelector("#app") - val _ = - render( - appContainer, - renderPage(state.MockAppState(using unsafeWindowOwner, router)) - ) - } - - private def onLoad(f: => Unit): Unit = - documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) - - private def setupAirstream()(using router: Router[Page]): Unit = - AirstreamError.registerUnhandledErrorCallback(err => - router.forcePage( - Page.UnhandledError( - Some(err.getClass.getName), // TODO: Fill only in dev mode - Some(err.getMessage) - ) - ) - ) - - def renderPage(state: AppState)(using - router: Router[Page] - ): HtmlElement = - val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) - .collectSignal[Page.Detail]( - connectors - .DetailPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailParametru]( - connectors - .DetailParametruPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailKriteria]( - connectors - .DetailKriteriaPageConnector(state)(_) - .apply - ) - .collectSignal[Page.UpravDukazKriteria]( - pages.detail.UpravDukaz.Connector(state)(_).apply - ) - .collectStatic(Page.Dashboard)( - connectors.DashboardPageConnector(state.actionBus).apply - ) - .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) - ) - .collect[Page.UnhandledError](pg => - pages.errors - .UnhandledErrorPage( - pages.errors.UnhandledErrorPage - .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), - state.actionBus - ) - ) - .collectStatic(Page.Directory)( - connectors - .DirectoryPageConnector(state.users, state.actionBus) - .apply - ) - div(cls := "h-full", child <-- pageSplitter.$view) - - // Pull in the stylesheet - val css: Css.type = Css diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala deleted file mode 100644 index 3bbc8c0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ /dev/null @@ -1,198 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.* -import org.scalajs.dom -import zio.json.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import scala.scalajs.js -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.Page.Titled - -// enum is not working with Waypoints' SplitRender collectStatic -sealed abstract class Page( - val id: String, - val title: String, - val parent: Option[Page] -) { - val path: Vector[Page] = - parent match - case None => Vector(this) - case Some(p) => p.path :+ this - - val isRoot: Boolean = parent.isEmpty -} - -object Page: - - case class Titled[V](value: V, title: Option[String] = None): - val show: String = title.getOrElse(value.toString) - - case object Directory extends Page("directory", "Adresář", None) - - case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) - - case class Detail(osobniCislo: Titled[OsobniCislo]) - extends Page("user", osobniCislo.show, Some(Directory)) - - object Detail { - def apply(o: UserInfo): Detail = Detail( - Titled(o.personalNumber, Some(o.name)) - ) - } - - case class DetailParametru( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String] - ) extends Page( - "parameter", - parametr.show, - Some(Detail(osobniCislo)) - ) - - object DetailParametru { - def apply(o: UserInfo, p: Parameter): DetailParametru = - DetailParametru( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)) - ) - } - - case class DetailKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "criteria", - kriterium.show, - Some(DetailParametru(osobniCislo, parametr)) - ) - - object DetailKriteria { - def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = - DetailKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class UpravDukazKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "addProof", - "Důkaz", - Some(DetailKriteria(osobniCislo, parametr, kriterium)) - ) - - object UpravDukazKriteria { - def apply( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): UpravDukazKriteria = - UpravDukazKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class NotFound(url: String) extends Page("404", "404", Some(Directory)) - - case class UnhandledError( - errorName: Option[String], - errorMessage: Option[String] - ) extends Page("500", "Unexpected error", Some(Directory)) - -object Routes: - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) - given [V: JsonEncoder]: JsonEncoder[Titled[V]] = - DeriveJsonEncoder.gen[Titled[V]] - given [V: JsonDecoder]: JsonDecoder[Titled[V]] = - DeriveJsonDecoder.gen[Titled[V]] - given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] - given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] - - val base = - js.`import`.meta.env.BASE_URL - .asInstanceOf[String] + "app" - - val homePage: Page = Page.Directory - - given router: Router[Page] = Router[Page]( - routes = List( - Route.static(homePage, root / endOfSegments, basePath = base), - Route.static( - Page.Dashboard, - root / "dashboard" / endOfSegments, - basePath = base - ), - Route[Page.Detail, String]( - encode = _.osobniCislo.value.toString, - decode = osc => Page.Detail(Titled(OsobniCislo(osc))), - root / "osoba" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.DetailParametru, (String, String)]( - encode = p => (p.osobniCislo.value.toString, p.parametr.value), - decode = - p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / endOfSegments, - basePath = base - ), - Route[Page.DetailKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.DetailKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.UpravDukazKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.UpravDukazKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / "edit" / endOfSegments, - basePath = base - ) - ), - serializePage = _.toJson, - deserializePage = _.fromJson[Page] - .fold(s => throw IllegalStateException(s), identity), - getPageTitle = _.title, - routeFallback = url => Page.NotFound(url), - deserializeFallback = _ => Page.Dashboard - )( - $popStateEvent = windowEvents.onPopState, - owner = unsafeWindowOwner - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala deleted file mode 100644 index b24b82b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -sealed trait Action - -case object FetchDirectory extends Action -case class FetchUserDetails(osc: OsobniCislo) extends Action -case class FetchParameters(osc: OsobniCislo) extends Action -case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action -case class FetchParameterCriteria( - osc: OsobniCislo, - paramId: String, - critId: String, - page: (UserInfo, Parameter, ParameterCriteria) => Page -) extends Action -case class FetchAvailableFiles(osc: OsobniCislo) extends Action -case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala deleted file mode 100644 index c637d35..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala +++ /dev/null @@ -1,82 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.{UserProfile, UserInfo, OsobniCislo} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo -import cz.e_bs.cmi.mdr.pdb.UserFunction - -object AppPage: - // TODO: pages by logged in user - val pages: List[Page] = List(Page.Directory, Page.Dashboard) - - import NavigationBar.{Logo, MenuItem} - - val logo = Logo( - "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", - "Workflow" - ) - - // TODO: menu items by user profile - val userMenu = - List( - MenuItem("Your Profile"), - MenuItem("Settings"), - MenuItem("Sign out") - ) - - // TODO: load user profile - val $userProfile = Var( - UserProfile( - "tom", - UserInfo( - OsobniCislo("1031"), - "tom", - "Tom", - "Cook", - None, - None, - Some("tom@example.com"), - Some("+420 222 866 180"), - Some( - UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") - ), - Nil, - None - ) - ) - ) - - val $userInfo = $userProfile.signal.map(_.userInfo) - - type ViewModel = Option[HtmlElement] - def apply( - actionBus: Observer[Action] - )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using - router: Router[Page] - ): HtmlElement = - PageLayout(actionBus)( - $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => - PageLayout.ViewModel( - NavigationBar.ViewModel( - u, - pages.map(p => - NavigationBar.Link( - () => - PageLink( - p, - actionBus - ), - p == cp - ) - ), - userMenu, - logo - ), - c - ) - ), - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala deleted file mode 100644 index bf473c6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ /dev/null @@ -1,143 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden -import fiftyforms.ui.components.tailwind.list.IconText.ViewModel -import fiftyforms.ui.components.tailwind.Icons -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Action - -object Breadcrumbs: - - private def slash = - import svg.{*, given} - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-300", - xmlns := "http://www.w3.org/2000/svg", - fill := "currentColor", - viewBox := "0 0 20 20", - ariaHidden := true, - path( - d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" - ) - ) - - def backIcon = - Icons.solid - .`arrow-narrow-left`() - .amend( - svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" - ) - - object Link: - case class ViewModel( - page: Page, - icon: Option[SvgElement], - extraClasses: String, - text: String, - textClasses: Option[String] = None - ) - - val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" - - def shortHome(p: Page) = ViewModel( - p, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - "Zpět na úvodní stránku", - None - ) - - def fullHome(p: Page) = ViewModel( - p, - Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), - "text-gray-400 hover:text-gray-500", - "Domů", - Some("sr-only") - ) - - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - PageLink - .container($m.map(_.page), actionBus) - .amend( - cls <-- $m.map(_.extraClasses), - child.maybe <-- $m.map(_.icon), - span( - cls <-- $m.map(_.textClasses.getOrElse("")), - child.text <-- $m.map(_.text) - ) - ) - - object FullBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - ol( - role := "list", - cls := "flex items-center space-x-4", - children <-- $m.map(_.path).split(_.id)((_, _, $p) => - li( - div( - cls := "flex items-center", - child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), - Link( - $p.map(p => - if (p.isRoot) then Link.fullHome(p) - else - Link.ViewModel( - p, - None, - s"ml-4 max-w-xs truncate ${Link.baseClasses}", - p.title - ) - ), - actionBus - ) - ) - ) - ) - ) - - object ShortBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - Link( - $m.map { p => - val target = p.parent.getOrElse(p) - if target.isRoot then Link.shortHome(target) - else - Link.ViewModel( - target, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - s"Zpět na ${target.title}" - ) - }, - actionBus - ) - - def apply(actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - nav( - cls := "flex", - aria.label := "Breadcrumb", - div( - cls := "flex sm:hidden", - ShortBreadcrumbs(router.$currentPage, actionBus) - ), - div( - cls := "hidden sm:block", - FullBreadcrumbs(router.$currentPage, actionBus) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala deleted file mode 100644 index 0db2047..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ /dev/null @@ -1,242 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent -import fiftyforms.ui.components.tailwind.Icons -import fiftyforms.ui.components.tailwind.Avatar -import cz.e_bs.cmi.mdr.pdb.UserInfo -import io.laminext.syntax.core.* - -object NavigationBar: - - case class Logo(img: String, name: String) - case class Link(a: () => Anchor, active: Boolean) - case class MenuItem(title: String) - - case class ViewModel( - userInfo: UserInfo, - pages: List[Link], - userMenu: List[MenuItem], - logo: Logo - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - val $userInfo = $m.map(_.userInfo) - val mobileMenuOpen = Var(false) - - val desktopOnly = cls("hidden md:block") - val mobileOnly = cls("md:hidden") - - inline def avatarImage(size: Int = 8) = - Avatar($userInfo.map(_.img)).avatarImage(size) - - def notificationButton = button( - tpe := "button", - cls := List( - "bg-indigo-600", - "focus:outline-none", - "focus:ring-2", - "focus:ring-offset-2", - "focus:ring-offset-indigo-600", - "focus:ring-white", - "hover:text-white", - "p-1", - "rounded-full", - "text-indigo-200" - ), - span(cls := "sr-only", "View notifications"), - Icons.outline.bell() - ) - - def userProfile: HtmlElement = - val menuOpen = Var(false) - - def menuItem(item: MenuItem, idx: Int): HtmlElement = - a( - href := "#", - cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", - role := "menuitem", - tabIndex := -1, - idAttr := s"user-menu-item-$idx", - item.title - ) - - div( - cls := "ml-3 relative", - div( - button( - tpe := "button", - cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - idAttr := "user-menu-button", - aria.expanded <-- menuOpen.signal, - aria.hasPopup := true, - span(cls := "sr-only", "Open user menu"), - child <-- avatarImage(), - onClick.preventDefault.mapTo( - !menuOpen.now() - ) --> menuOpen.writer - ) - ), - /* - * */ - div( - cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", - cls <-- menuOpen.signal.map { o => - if (o) "md:block" else "md:hidden" - }, - role := "menu", - aria.orientation := "vertical", - aria.labelledBy := "user-menu-button", - tabIndex := -1, - // : keyboard navigation - children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) - ) - ) - - def mobileProfile = - def menuItem(item: MenuItem): HtmlElement = - a( - href := "#", - cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", - item.title - ) - - div( - cls := "pt-4 pb-3 border-t border-indigo-700", - div( - cls := "flex items-center px-5", - div( - cls := "flex-shrink-0", - child <-- avatarImage(10) - ), - div( - cls := "ml-3", - div( - cls := "text-base font-medium text-white", - child.text <-- $userInfo.map(_.name) - ), - child.maybe <-- $userInfo.map( - _.email.map(e => - div( - cls := "text-sm font-medium text-indigo-300", - e - ) - ) - ) - ), - notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) - ), - div( - cls := "mt-3 px-2 space-y-1", - children <-- $m.map(_.userMenu.map(menuItem)) - ) - ) - - def pageLink(page: Link): Anchor = - page - .a() - .amend( - cls := "text-white px-3 py-2 rounded-md text-sm font-medium", - cls := Seq( - "bg-indigo-700" -> page.active, - "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active - ), - ariaCurrent := (if page.active then "page" else "false") - ) - - def logoImg: Image = - img( - cls := "h-8 w-8", - src <-- $m.map(_.logo.img), - alt <-- $m.map(_.logo.name) - ) - - def mobileMenuButton = button( - tpe := "button", - cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - aria.controls := "mobile-menu", - aria.expanded <-- mobileMenuOpen.signal, - span(cls := "sr-only", "Open main menu"), - Icons.outline - .menu() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") - ), - Icons.outline - .x() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") - ), - onClick.preventDefault.mapTo( - !mobileMenuOpen.now() - ) --> mobileMenuOpen.writer - ) - - def navBarLeft = - div( - cls := "flex items-center", - div(cls := "flex-shrink-0", logoImg), - div( - desktopOnly, - div( - cls := "ml-10 flex items-baseline space-x-4", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ) - ) - ) - - def navBarRight = - div( - desktopOnly, - div( - cls := "ml-4 flex items-center md:ml-6", - notificationButton, - userProfile - ) - ) - - def navBarMobile = - div( - cls := "-mr-2 flex", - mobileOnly, - mobileMenuButton - ) - - def navBar = - div( - cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex items-center justify-between h-16", - navBarLeft, - navBarRight, - navBarMobile - ) - ) - - def mobileMenu = - div( - mobileOnly, - cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }, - idAttr := "mobile-menu", - div( - cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ), - mobileProfile - ) - - nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala deleted file mode 100644 index 75339df..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object PageHeader: - def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = - header( - cls := "bg-white shadow-sm", - div( - cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", - h1( - cls := "text-lg leading-6 font-semibold text-gray-900", - Breadcrumbs(actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala deleted file mode 100644 index 5096463..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ /dev/null @@ -1,30 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import fiftyforms.ui.components.tailwind.Loading -import io.laminext.syntax.core.* - -object PageLayout: - case class ViewModel( - navigation: NavigationBar.ViewModel, - content: Option[HtmlElement] - ) - def apply(actionBus: Observer[Action])( - $m: Signal[ViewModel], - mods: Modifier[HtmlElement]* - )(using router: Router[Page]): HtmlElement = - val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) - div( - cls := "h-full flex flex-col", - NavigationBar($m.map(_.navigation)), - child.maybe <-- router.$currentPage.map(_.isRoot) - .switch(None, Some(PageHeader(actionBus))), - main( - cls := "flex-grow-1 overflow-y-auto", - mods, - child <-- $maybeContent.map(_.getOrElse(Loading)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala deleted file mode 100644 index 62d820f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.LinkSupport.* -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo - -object PageLink: - type ViewModel = Page - def apply($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = - a(mods($m, actions), child.text <-- $m.map(_.title)) - - def apply(m: ViewModel, actions: Observer[Action])(using - router: Router[Page] - ): Anchor = apply(Val(m), actions) - - def container($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = a(mods($m, actions)) - - def container(m: ViewModel, actions: Observer[Action])(using - Router[Page] - ): Anchor = container(Val(m), actions) - - private def mods($m: Signal[Page], actions: Observer[Action])(using - router: Router[Page] - ): Modifier[Anchor] = - Seq( - href <-- $m.map(router.absoluteUrlForPage).recover { case _ => - Some("invalid url") - }, - composeEvents(onClick.noKeyMod.preventDefault)( - _.sample($m) - .map(NavigateTo.apply) - ) --> actions - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala deleted file mode 100644 index 87f825a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.pages.dashboard.DashboardPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -class DashboardPageConnector(actionBus: Observer[Action])(using - router: Router[Page] -): - def apply: HtmlElement = - AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala deleted file mode 100644 index e9b0f71..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala +++ /dev/null @@ -1,76 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailKriteriaPage -import pages.detail.components.DukazyKriteria -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -object DetailKriteriaPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailKriteriaPageConnector( - state: DetailKriteriaPageConnector.AppState -)( - $page: Signal[Page.DetailKriteria] -)(using Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => - (p.osobniCislo.value, p.parametr.value, p.kriterium.value) - )((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map( - FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) - ) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, s, $s) => - DetailKriteriaPage($s)(state.actionBus.contramap { - case DukazyKriteria.Add => - NavigateTo( - Page.UpravDukazKriteria( - Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), - Page.Titled(s.parametr.id, Some(s.parametr.nazev)), - Page.Titled(s.kriterium.id, Some(s.kriterium.id)) - ) - ) - }) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): DetailKriteriaPage.ViewModel = - DetailKriteriaPage.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala deleted file mode 100644 index 4d8024f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ /dev/null @@ -1,57 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import pages.detail.DetailPage -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import fiftyforms.ui.components.tailwind.Color - -object DetailPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailPageConnector(state: DetailPageConnector.AppState)( - $page: Signal[Page.Detail] -)(using router: Router[Page]): - val $oscChangeSignal = $page.splitOne(_.osobniCislo.value)((osc, _, _) => osc) - val $pageChangeSignal = - $oscChangeSignal.flatMap(osc => - EventStream.fromSeq(Seq(FetchUserDetails(osc), FetchParameters(osc))) - ) - // TODO: filter the value based on the current osc - // OSC change will fetch new data, but still - // - we need to be sure that what we got is really what we ought to display - // - we want to display stale data accordingly (at least with loading indicator) - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - def apply: HtmlElement = - AppPage(state.actionBus)( - $data.combineWithFn($params)(_ zip _) - .map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: List[Parameter] - ): DetailPage.ViewModel = - DetailPage.ViewModel( - o.toDetailOsoby, - p.map( - _.toParametr(param => - PageLink.container(Page.DetailParametru(o, param), state.actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala deleted file mode 100644 index fdaa7f8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailParametruPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink - -object DetailParametruPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailParametruPageConnector( - state: DetailParametruPageConnector.AppState -)( - $page: Signal[Page.DetailParametru] -)(using router: Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => (p.osobniCislo.value, p.parametr.value))((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map(FetchParameter(_, _)) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - } yield (da, pb) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailParametruPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter - ): DetailParametruPage.ViewModel = - DetailParametruPage.ViewModel( - o.toDetailOsoby, - p.toParametr(p => - PageLink.container(Page.DetailParametru(o, p), state.actionBus) - ), - p.criteria.map( - _.toKriterium { c => - PageLink.container( - Page.DetailKriteria(o, p, c), - state.actionBus - ) - } - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala deleted file mode 100644 index 049aa22..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -case class DirectoryPageConnector( - $input: EventStream[List[UserInfo]], - actionBus: Observer[Action] -)(using router: Router[Page]): - val $data = $input.startWithNone - val $actionSignal = EventStream.fromValue(FetchDirectory) - - def apply: HtmlElement = - AppPage(actionBus)( - $data.split(_ => ())((_, _, s) => - pages.directory.DirectoryPage( - s.map( - _.map( - _.toUserRow(u => - PageLink.container( - Page.Detail(Page.Titled(u.personalNumber)), - actionBus - ) - ) - ) - ) - ) - ), - $actionSignal --> actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala deleted file mode 100644 index 88e7450..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ /dev/null @@ -1,68 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii -import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria -import fiftyforms.ui.components.tailwind.Color - -extension (o: UserInfo) - def toDetailOsoby: DetailOsoby.ViewModel = - DetailOsoby.ViewModel( - o.personalNumber, - o.name, - o.email, - o.phone, - o.img, - o.mainFunction.map(f => - DetailOsoby.Funkce.ViewModel(f.name, f.dept, f.ou) - ), - o.userContracts.headOption.map(c => - DetailOsoby.PracovniPomer.ViewModel(c.rel, c.startDate, c.endDate) - ) - ) - -extension (param: Parameter) - def toParametr(container: Parameter => Anchor): DetailParametru.ViewModel = - DetailParametru.ViewModel( - id = param.id, - nazev = param.name, - popis = param.description, - status = "Nesplněno", - statusColor = Color.red, - a = container(param) - ) - -extension (crit: ParameterCriteria) - def toKriterium( - container: ParameterCriteria => Anchor - ): DetailKriteria.ViewModel = - DetailKriteria.ViewModel( - nazev = crit.criteriumText, - kapitola = crit.chapterId, - bod = crit.itemId, - status = "Nesplněno", - statusColor = Color.red, - splneno = false, - dukazy = Nil, - container = container(crit) - ) - -extension (user: UserInfo) - def toUserRow( - container: UserInfo => HtmlElement = (_: UserInfo) => div() - ): UserRow.ViewModel = - UserRow.ViewModel( - osobniCislo = user.personalNumber.toString, - celeJmeno = user.name, - prijmeni = user.surname, - hlavniFunkce = user.mainFunction.map(_.name), - img = user.img, - container = () => container(user) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala deleted file mode 100644 index 7d5b2c1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.dashboard - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.Page - -object DashboardPage: - - def render: HtmlElement = - div("Dashboard page") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala deleted file mode 100644 index 9ef0698..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala +++ /dev/null @@ -1,31 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailKriteriaPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply($m: Signal[ViewModel])( - action: Observer[DukazyKriteria.Action] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - DukazyKriteria($m.map(_.kriterium.dukazy))(action) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala deleted file mode 100644 index 3b316e1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object DetailPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametry: SeznamParametru.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby($m.map(_.osoba)), - SeznamParametru($m.map(_.parametry)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala deleted file mode 100644 index 17224cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala +++ /dev/null @@ -1,24 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailParametruPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriteria: SeznamKriterii.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby.header($m.map(_.osoba)), - DetailParametru($m.map(_.parametr)), - SeznamKriterii($m.map(_.kriteria)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala deleted file mode 100644 index dc912fa..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ /dev/null @@ -1,107 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb -package app -package pages.detail - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.UpravDukazForm -import fiftyforms.services.files.File - -object UpravDukaz: - - type ThisPage = Page.UpravDukazKriteria - type PageKey = (OsobniCislo, String, String) - - trait State { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def availableFiles: EventStream[List[File]] - def actionBus: Observer[Action] - } - - def keyOfPage(page: ThisPage): PageKey = - (page.osobniCislo.value, page.parametr.value, page.kriterium.value) - - def onChangeAction(key: PageKey): Action = - FetchParameterCriteria( - key._1, - key._2, - key._3, - Page.UpravDukazKriteria(_, _, _) - ) - - class Connector(state: State)($page: Signal[ThisPage])(using Router[Page]): - val $paramChangeSignal = $page.splitOne(keyOfPage)((x, _, _) => x) - val $pageChangeSignal = $paramChangeSignal.map(onChangeAction) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.split(_ => ())((_, s, $s) => - PageComponent( - $s.map(buildModel), - state.availableFiles, - state.actionBus.contramap { - case UpravDukazForm.Cancelled => - NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) - case UpravDukazForm.AvailableFilesRequested => - FetchAvailableFiles(s._1.personalNumber) - } - ) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): PageComponent.ViewModel = - import connectors.* - PageComponent.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) - - object PageComponent: - import components.* - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply( - $m: Signal[ViewModel], - availableFilesStream: EventStream[List[File]], - events: Observer[UpravDukazForm.Event] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - UpravDukazForm(availableFilesStream)(events) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala deleted file mode 100644 index ca311b0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.Icons -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Color - -object DetailKriteria: - case class ViewModel( - nazev: String, - kapitola: String, - bod: String, - status: String, - statusColor: Color, - splneno: Boolean, - dukazy: List[DukazKriteria.ViewModel], - container: HtmlElement = div() - ) { - val id = s"${kapitola}${bod}" - } - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h3( - cls := "text-xl leading-6 font-bold text-gray-900", - child.text <-- $m.map(_.nazev) - ), - h3( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - "Kapitola ", - child.text <-- $m.map(_.kapitola), - " bod ", - child <-- $m.map(_.bod) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala deleted file mode 100644 index ae90290..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala +++ /dev/null @@ -1,100 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Avatar - -object DetailOsoby: - - object Funkce: - case class ViewModel( - nazev: String, - stredisko: String, - voj: String - ) - - def render($m: Signal[ViewModel]): HtmlElement = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.nazev), - span( - cls := "hidden md:inline", - " @ ", - child.text <-- $m.map(_.stredisko), - ", ", - child.text <-- $m.map(_.voj) - ) - ) - - object PracovniPomer: - case class ViewModel( - druh: String, - pocatek: LocalDate, - konec: Option[LocalDate] - ) - - def render($m: Signal[ViewModel]): HtmlElement = - import fiftyforms.ui.components.tailwind.CustomAttrs.datetime - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.druh), - " od ", - time( - datetime <-- $m.map(_.pocatek.toString), - child.text <-- $m.map(_.pocatek.toString) - ) - ) - - case class ViewModel( - osobniCislo: OsobniCislo, - jmeno: String, - email: Option[String], - telefon: Option[String], - img: Option[String], - hlavniFunkce: Option[Funkce.ViewModel], - pracovniPomer: Option[PracovniPomer.ViewModel] - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-4", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(16) - ), - div( - h1( - cls := "text-2xl font-bold text-gray-900", - child.text <-- $m.map(_.jmeno) - ), - child.maybe <-- $m.map(_.hlavniFunkce).split(_ => ())((_, _, d) => - Funkce.render(d) - ), - child.maybe <-- $m.map(_.pracovniPomer).split(_ => ())((_, _, d) => - PracovniPomer.render(d) - ) - ) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-5", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(8) - ), - div( - h1( - cls := "text-lg font-medium text-gray-800", - child.text <-- $m.map(_.jmeno) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala deleted file mode 100644 index e11cb62..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color - -object DetailParametru: - case class ViewModel( - id: String, - nazev: String, - popis: String, - status: String, - statusColor: Color, - a: Anchor - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h2( - cls := "text-2xl leading-6 font-medium text-gray-900", - child.text <-- $m.map(_.nazev) - ), - p( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - child.text <-- $m.map(_.popis) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - h2( - cls := "text-lg leading-6 font-medium text-gray-600", - child.text <-- $m.map(_.nazev) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala deleted file mode 100644 index a00ede8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala +++ /dev/null @@ -1,177 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.CustomAttrs - -object DukazKriteria: - case class Osoba(osobniCislo: String, jmeno: String) - case class Udalost(osoba: Osoba, datum: LocalDate) - case class Dokument( - url: String, - nazev: String, - pridal: Udalost, - odebral: Option[Udalost] - ) - case class ViewModel( - dokumenty: List[Dokument], - autorizoval: Option[Udalost], - platiDo: Option[LocalDate], - poznámka: Option[String] - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-lg", - div( - cls := "px-4 py-5 sm:px-6", - h3( - cls := "text-lg leading-6 font-medium text-gray-900", - """Applicant Information""" - ), - p( - cls := "mt-1 max-w-2xl text-sm text-gray-500", - """Personal details and application.""" - ) - ), - div( - cls := "border-t border-gray-200 px-4 py-5 sm:px-6", - dl( - cls := "grid grid-cols-1 gap-x-4 gap-y-8 sm:grid-cols-2", - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Full name""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Margot Foster""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Application for""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Backend Developer""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Email address""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """margotfoster@example.com""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Salary expectation""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """$120,000""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """About""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Fugiat ipsum ipsum deserunt culpa aute sint do nostrud anim incididunt cillum culpa consequat. Excepteur qui ipsum aliquip consequat sint. Sit id mollit nulla mollit nostrud in ea officia proident. Irure nostrud pariatur mollit ad adipisicing reprehenderit deserunt qui eu.""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """Attachments""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - ul( - role := "list", - cls := "border border-gray-200 rounded-md divide-y divide-gray-200", - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """resume_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ), - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """coverletter_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala deleted file mode 100644 index 22f134f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala +++ /dev/null @@ -1,35 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object DukazyKriteria: - sealed trait Action - case object Add extends Action - - type ViewModel = List[DukazKriteria.ViewModel] - - def apply($m: Signal[ViewModel])(actions: Observer[Action]): HtmlElement = - div( - child.maybe <-- $m.map(m => - if (m.isEmpty) then Some(prazdnyDukaz(actions)) - else None - ), - children <-- $m.map(_.zipWithIndex).split(_._2)((_, _, $s) => - DukazKriteria($s.map(_._1)) - ) - ) - - private def prazdnyDukaz(actions: Observer[Action]): HtmlElement = - button( - tpe := "button", - onClick.preventDefault.mapTo(Add) --> actions, - cls := "relative block w-full border-2 border-gray-300 border-dashed rounded-lg p-12 text-center hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - Icons.outline - .`document-add`(12) - .amend(svg.cls := "mx-auto text-gray-400"), - span( - cls := "mt-2 block text-sm font-medium text-gray-900", - "Přidat důkaz" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala deleted file mode 100644 index f628697..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.list.StackedList -import fiftyforms.ui.components.tailwind.list.ListRow -import fiftyforms.ui.components.tailwind.list.RowTag -import fiftyforms.ui.components.tailwind.list.RowNext -import java.time.LocalDate - -object SeznamKriterii: - type ViewModel = List[DetailKriteria.ViewModel] - - private val kritList = new StackedList[DetailKriteria.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - kritList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = - p(cls := "text-sm text-gray-500", s"${i.kapitola}${i.bod}"), - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.container - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala deleted file mode 100644 index 2bfe4bc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.list.{ - StackedList, - ListRow, - RowTag, - PropList, - IconText, - RowNext -} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.LinkSupport.* - -object SeznamParametru: - type ViewModel = List[DetailParametru.ViewModel] - - private val parametrList = new StackedList[DetailParametru.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - parametrList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = emptyNode, - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.a - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala deleted file mode 100644 index f048e9d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ /dev/null @@ -1,101 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.form.* -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement -import fiftyforms.services.files.components.tailwind.FilePicker -import cz.e_bs.cmi.mdr.pdb.frontend.AutorizujDukaz -import cz.e_bs.cmi.mdr.pdb.frontend.DocumentRef -import fiftyforms.services.files.File - -object UpravDukazForm: - sealed trait Event - case object Cancelled extends Event - case object AvailableFilesRequested extends Event - def apply(availableFilesStream: EventStream[List[File]])( - updates: Observer[Event] - ): HtmlElement = - val (filesStream, filesObserver) = - EventStream.withObserver[FilePicker.Event] - val files = Var[List[File]](Nil) - def submitButtons: HtmlElement = - div( - cls := "pt-5", - div( - cls := "flex justify-end", - button( - tpe := "button", - cls := "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zrušit", - onClick.mapTo(Cancelled) --> updates - ), - button( - tpe := "submit", - cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - disabled <-- files.signal.map(_.isEmpty), - "Autorizovat" - ) - ) - ) - - div( - filesStream.collect { case FilePicker.SelectionUpdated(files) => - files.to(List) - } --> files.writer, - filesStream.collect { case FilePicker.AvailableFilesRequested => - AvailableFilesRequested - } --> updates, - cls := "bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6", - Form( - Form.Body( - Form.Section( - FormHeader( - FormHeader.ViewModel( - "Doložení kritéria", - "Sestavte doklady poskytující důkaz kritéria a potvrďte odesláním formuláře. Případné limitace či jiné relevantní údaje vepište do pole Komentář." - ) - ), - FormFields( - FormRow( - "dokumenty", - "Dokumenty", - FilePicker(files.signal, availableFilesStream)(filesObserver) - .amend(idAttr := "dokumenty", cls("max-w-lg")) - ).toHtml, - FormRow( - "platnost", - "Platnost", - input( - idAttr := "platnost", - name := "platnost", - tpe := "date", - autoComplete := "date", - cls := "block max-w-lg w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" - ) - ).toHtml, - FormRow( - "komentar", - "Komentář", - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - textArea( - idAttr := "komentar", - name := "about", - rows := 3, - cls := "max-w-lg shadow-sm block w-full focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border border-gray-300 rounded-md" - ), - p( - cls := "mt-2 text-sm text-gray-500", - "Doplňte prosím potřebné informace související s doložením kritéria, včetně případných limitací." - ) - ) - ).toHtml - ) - ) - ), - submitButtons - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala deleted file mode 100644 index 41033f6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -import components._ - -object DirectoryPage: - - type ViewModel = List[UserRow.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - val (actionsStream, actionObserver) = - EventStream.withObserver[SearchForm.Action] - val $filter = actionsStream - .collect { - case SearchForm.ClearFilter => None - case SearchForm.SetFilter(t) => Some(t) - } - .startWith(None) - val byLetter = for { - d <- $m - f <- $filter - } yield for { - (letter, users) <- d - .filter { user => - f match - case None => true - case Some(t) => user.search.contains(t) - } - .groupBy(_.prijmeni.head) - .to(List) - .sortBy(_._1) - } yield (letter.toString, users.sortBy(_.prijmeni)) - - div( - cls := "h-full max-w-7xl mx-auto order-first flex flex-col flex-shrink-0", - SearchForm(actionObserver), - Directory(byLetter) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala deleted file mode 100644 index 73f915d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -object Directory: - - object Header: - type ViewModel = String - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500", - h3(child.text <-- $m) - ) - - object UserList: - type ViewModel = List[UserRow.ViewModel] - def apply($m: Signal[ViewModel]): HtmlElement = - ul( - role := "list", - cls := "relative z-0 divide-y divide-gray-200", - children <-- $m.split(_.osobniCislo)((_, _, s) => UserRow(s)) - ) - - type ViewModel = List[(String, List[UserRow.ViewModel])] - def apply($m: Signal[ViewModel]): HtmlElement = - nav( - cls := "flex-1 min-h-0 overflow-y-auto", - aria.label := "Directory", - children <-- $m.split(_._1)((_, _, s) => - div( - cls := "relative", - Header(s.map(_._1)), - UserList(s.map(_._2)) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala deleted file mode 100644 index e182910..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala +++ /dev/null @@ -1,54 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object SearchForm: - sealed trait Action - case object SubmitFilter extends Action - sealed trait FilterAction extends Action - case object ClearFilter extends FilterAction - case class SetFilter(value: String) extends FilterAction - - def apply(actions: Observer[Action]): HtmlElement = - div( - cls := "px-6 pt-4 pb-4", - form( - cls := "flex space-x-4", - onSubmit.mapTo(SubmitFilter) --> actions, - div( - cls := "flex-1 min-w-0", - label( - forId := "search", - cls := "sr-only", - "Hledat" - ), - div( - cls := "relative rounded-md shadow-sm", - div( - cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", - Icons.solid.search().amend(svg.cls := "text-gray-400") - ), - input( - tpe := "search", - name := "search", - idAttr := "search", - cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", - placeholder := "Hledat", - composeEvents(onInput.mapToValue.setAsValue.map(SetFilter(_)))( - _.throttle(500) - ) --> actions - ) - ) - ), - button( - tpe := "submit", - cls := "inline-flex justify-center px-3.5 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500", - Icons.solid.filter().amend(svg.cls := "text-gray-400"), - span( - cls := "sr-only", - "Hledat" - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala deleted file mode 100644 index 82f7723..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala +++ /dev/null @@ -1,52 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.airstream.core.Signal -import fiftyforms.ui.components.tailwind.Avatar - -object UserRow: - case class ViewModel( - osobniCislo: String, - celeJmeno: String, - prijmeni: String, - hlavniFunkce: Option[String], - img: Option[String], - container: () => HtmlElement = () => div() - ) { - val search = prijmeni.toLowerCase - } - - def apply($m: Signal[ViewModel]): HtmlElement = - inline def avatarImage = - Avatar($m.map(_.img)).avatarImage(10) - - li( - div( - cls := "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500", - div( - cls := "flex-shrink-0", - child <-- avatarImage - ), - div( - cls := "flex-1 min-w-0", - child <-- $m.map { o => - o.container() - .amend( - cls := "focus:outline-none", - span( - cls := "absolute inset-0", - aria.hidden := true - ), - p( - cls := "text-sm font-medium text-gray-900", - o.celeJmeno - ), - p( - cls := "text-sm text-gray-500 truncate", - o.hlavniFunkce - ) - ) - } - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala deleted file mode 100644 index 467c5f5..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala +++ /dev/null @@ -1,99 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router - -object ErrorPage: - case class ViewModel( - homePage: Page, - errorName: String, - title: String, - subTitle: String - ) - - def apply(m: ViewModel, actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - val ViewModel(homePage, errorName, title, subTitle) = m - div( - cls := "min-h-full pt-16 pb-12 flex flex-col bg-white", - main( - cls := "flex-grow flex flex-col justify-center max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex-shrink-0 flex justify-center", - a( - href := "/", - cls := "inline-flex", - span( - cls := "sr-only", - """Workflow""" - ), - img( - cls := "h-12 w-auto", - src := "https://tailwindui.com/img/logos/workflow-mark.svg?color=indigo&shade=600", - alt := "" - ) - ) - ), - div( - cls := "py-16", - div( - cls := "text-center", - p( - cls := "text-sm font-semibold text-indigo-600 uppercase tracking-wide", - errorName - ), - h1( - cls := "mt-2 text-4xl font-extrabold text-gray-900 tracking-tight sm:text-5xl", - title - ), - p( - cls := "mt-2 text-base text-gray-500", - subTitle - ), - div( - cls := "mt-6", - PageLink - .container(homePage, actionBus) - .amend( - cls := "text-base font-medium text-indigo-600 hover:text-indigo-500", - """Go back home""" - ) - ) - ) - ) - ), - footer( - cls := "flex-shrink-0 max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", - nav( - cls := "flex justify-center space-x-4", - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Contact Support""" - ), - span( - cls := "inline-block border-l border-gray-300", - aria.hidden := true - ), - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Status""" - ), - span( - cls := "inline-block border-l border-gray-300", - aria.hidden := true - ), - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Twitter""" - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala deleted file mode 100644 index bd9baec..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action - -object NotFoundPage: - def apply(homePage: Page, url: String, actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - ErrorPage( - ErrorPage.ViewModel( - homePage, - "404 error", - "Page not found.", - s"Sorry, but page $url doesn't exist." - ), - actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala deleted file mode 100644 index 476fe3c..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action - -object UnhandledErrorPage: - - case class ViewModel( - homePage: Page, - errorName: Option[String], - errorMessage: Option[String] - ) - - def apply(m: ViewModel, actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - ErrorPage( - ErrorPage.ViewModel( - m.homePage, - "Unexpected error occurred", - m.errorName.getOrElse( - "Uh oh!" - ), // TODO: translations, better text than uh oh - m.errorMessage.getOrElse( - "This wasn't supposed to happen! Please try again." - ) - ), - actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala deleted file mode 100644 index 25f059f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala +++ /dev/null @@ -1,6 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.services - -import com.raquo.airstream.core.Observer - -trait DataFetcher[K, A]: - def fetch(id: K, sink: Observer[A]): Unit diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala deleted file mode 100644 index a469830..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala +++ /dev/null @@ -1,120 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package state - -import com.raquo.airstream.core.EventStream -import cz.e_bs.cmi.mdr.pdb.{UserInfo, OsobniCislo} -import com.raquo.airstream.core.Observer -import scala.scalajs.js -import scala.scalajs.js.JSON -import zio.json._ -import com.raquo.airstream.eventbus.EventBus -import com.raquo.airstream.ownership.Owner -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.UserFunction -import cz.e_bs.cmi.mdr.pdb.UserContract -import fiftyforms.services.files.File - -trait AppState - extends connectors.DetailPageConnector.AppState - with connectors.DetailParametruPageConnector.AppState - with connectors.DetailKriteriaPageConnector.AppState - with pages.detail.UpravDukaz.State: - def users: EventStream[List[UserInfo]] - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - -class MockAppState(implicit owner: Owner, router: Router[Page]) - extends AppState: - - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonDecoder[UserFunction] = DeriveJsonDecoder.gen - given JsonDecoder[UserContract] = DeriveJsonDecoder.gen - given JsonDecoder[UserInfo] = DeriveJsonDecoder.gen - - given JsonDecoder[ParameterCriteria] = DeriveJsonDecoder.gen - given JsonDecoder[Parameter] = DeriveJsonDecoder.gen - - private val actions = EventBus[Action]() - private val (parametersStream, pushParameters) = - EventStream.withCallback[List[Parameter]] - private val (usersStream, pushUsers) = - EventStream.withCallback[List[UserInfo]] - private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] - private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] - - private val mockData: List[UserInfo] = - mockUsers - .asInstanceOf[js.Dictionary[js.Object]] - .values - // TODO: is there a more efficient way to parse from JS object directly? - .map { o => - val parsed = JSON.stringify(o).fromJson[UserInfo] - parsed.left.foreach(org.scalajs.dom.console.log(_)) - parsed - } - .collect { case Right(u) => - u - } - .toList - - private val mockParameters: List[Parameter] = - pdbParams - .asInstanceOf[js.Dictionary[js.Object]] - .values - .map(o => JSON.stringify(o).fromJson[Parameter]) - .collect { case Right(p) => p } - .toList - - // TODO: Extract to separate event handler - actions.events.foreach { - case FetchDirectory => pushUsers(mockData) - case FetchUserDetails(osc) => - mockData.find(_.personalNumber == osc).foreach { o => - pushDetails(o) - router.replaceState(Page.Detail(o)) - } - case FetchParameters(osc) => - pushParameters(mockParameters) - case FetchParameter(osc, paramId) => - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(Page.DetailParametru(o, p)) - case FetchParameterCriteria(osc, paramId, critId, page) => - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - c <- p.criteria.find(_.id == critId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(page(o, p, c)) - case NavigateTo(page) => router.pushState(page) - case FetchAvailableFiles(osc) => - pushFiles( - List( - File("https://tc163.cmi.cz/here", "Example file") - ) - ) - } - - override def users: EventStream[List[UserInfo]] = - usersStream.debugWithName("users") - - override def details: EventStream[UserInfo] = - detailsStream.debugWithName("details") - - override def parameters: EventStream[List[Parameter]] = - parametersStream.debugWithName("parameters") - - override def availableFiles: EventStream[List[File]] = - filesStream.debugWithName("available files") - - override def actionBus: Observer[Action] = - actions.writer.debugWithName("actions writer") diff --git a/app/src/main/scala/mdr/pdb/app/Main.scala b/app/src/main/scala/mdr/pdb/app/Main.scala new file mode 100644 index 0000000..081b4ee --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/Main.scala @@ -0,0 +1,103 @@ +package mdr.pdb.app + +import scala.scalajs.js.annotation.JSExportTopLevel +import scala.scalajs.js.annotation.JSExport +import scala.scalajs.js.annotation.JSImport +import scala.scalajs.js +import org.scalajs.dom +import com.raquo.laminar.api.L.{*, given} +import scala.scalajs.js.Date +import com.raquo.waypoint.Router +import com.raquo.waypoint.SplitRender +import mdr.pdb.app.services.DataFetcher +import scala.scalajs.js.JSON +import zio.json._ +import mdr.pdb.UserInfo +import mdr.pdb.app.state.AppState + +@js.native +@JSImport("stylesheets/main.css", JSImport.Namespace) +object Css extends js.Any + +@js.native +@JSImport("data/users.json", JSImport.Default) +object mockUsers extends js.Object + +@js.native +@JSImport("params/pdb-params.json", JSImport.Default) +object pdbParams extends js.Object + +@JSExportTopLevel("app") +object Main: + + @JSExport + def main(args: Array[String]): Unit = + import Routes.given + onLoad { + setupAirstream() + val appContainer = dom.document.querySelector("#app") + val _ = + render( + appContainer, + renderPage(state.MockAppState(using unsafeWindowOwner, router)) + ) + } + + private def onLoad(f: => Unit): Unit = + documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) + + private def setupAirstream()(using router: Router[Page]): Unit = + AirstreamError.registerUnhandledErrorCallback(err => + router.forcePage( + Page.UnhandledError( + Some(err.getClass.getName), // TODO: Fill only in dev mode + Some(err.getMessage) + ) + ) + ) + + def renderPage(state: AppState)(using + router: Router[Page] + ): HtmlElement = + val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) + .collectSignal[Page.Detail]( + connectors + .DetailPageConnector(state)(_) + .apply + ) + .collectSignal[Page.DetailParametru]( + connectors + .DetailParametruPageConnector(state)(_) + .apply + ) + .collectSignal[Page.DetailKriteria]( + connectors + .DetailKriteriaPageConnector(state)(_) + .apply + ) + .collectSignal[Page.UpravDukazKriteria]( + pages.detail.UpravDukaz.Connector(state)(_).apply + ) + .collectStatic(Page.Dashboard)( + connectors.DashboardPageConnector(state.actionBus).apply + ) + .collect[Page.NotFound](pg => + pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) + ) + .collect[Page.UnhandledError](pg => + pages.errors + .UnhandledErrorPage( + pages.errors.UnhandledErrorPage + .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), + state.actionBus + ) + ) + .collectStatic(Page.Directory)( + connectors + .DirectoryPageConnector(state.users, state.actionBus) + .apply + ) + div(cls := "h-full", child <-- pageSplitter.$view) + + // Pull in the stylesheet + val css: Css.type = Css diff --git a/app/src/main/scala/mdr/pdb/app/Routes.scala b/app/src/main/scala/mdr/pdb/app/Routes.scala new file mode 100644 index 0000000..2cc860c --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/Routes.scala @@ -0,0 +1,198 @@ +package mdr.pdb.app + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.* +import org.scalajs.dom +import zio.json.{*, given} +import mdr.pdb.OsobniCislo + +import scala.scalajs.js +import mdr.pdb.UserInfo +import mdr.pdb.Parameter +import mdr.pdb.ParameterCriteria +import mdr.pdb.app.Page.Titled + +// enum is not working with Waypoints' SplitRender collectStatic +sealed abstract class Page( + val id: String, + val title: String, + val parent: Option[Page] +) { + val path: Vector[Page] = + parent match + case None => Vector(this) + case Some(p) => p.path :+ this + + val isRoot: Boolean = parent.isEmpty +} + +object Page: + + case class Titled[V](value: V, title: Option[String] = None): + val show: String = title.getOrElse(value.toString) + + case object Directory extends Page("directory", "Adresář", None) + + case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) + + case class Detail(osobniCislo: Titled[OsobniCislo]) + extends Page("user", osobniCislo.show, Some(Directory)) + + object Detail { + def apply(o: UserInfo): Detail = Detail( + Titled(o.personalNumber, Some(o.name)) + ) + } + + case class DetailParametru( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String] + ) extends Page( + "parameter", + parametr.show, + Some(Detail(osobniCislo)) + ) + + object DetailParametru { + def apply(o: UserInfo, p: Parameter): DetailParametru = + DetailParametru( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)) + ) + } + + case class DetailKriteria( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String], + kriterium: Titled[String] + ) extends Page( + "criteria", + kriterium.show, + Some(DetailParametru(osobniCislo, parametr)) + ) + + object DetailKriteria { + def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = + DetailKriteria( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)), + Titled(k.id, Some(k.id)) + ) + } + + case class UpravDukazKriteria( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String], + kriterium: Titled[String] + ) extends Page( + "addProof", + "Důkaz", + Some(DetailKriteria(osobniCislo, parametr, kriterium)) + ) + + object UpravDukazKriteria { + def apply( + o: UserInfo, + p: Parameter, + k: ParameterCriteria + ): UpravDukazKriteria = + UpravDukazKriteria( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)), + Titled(k.id, Some(k.id)) + ) + } + + case class NotFound(url: String) extends Page("404", "404", Some(Directory)) + + case class UnhandledError( + errorName: Option[String], + errorMessage: Option[String] + ) extends Page("500", "Unexpected error", Some(Directory)) + +object Routes: + given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) + given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) + given [V: JsonEncoder]: JsonEncoder[Titled[V]] = + DeriveJsonEncoder.gen[Titled[V]] + given [V: JsonDecoder]: JsonDecoder[Titled[V]] = + DeriveJsonDecoder.gen[Titled[V]] + given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] + given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] + + val base = + js.`import`.meta.env.BASE_URL + .asInstanceOf[String] + "app" + + val homePage: Page = Page.Directory + + given router: Router[Page] = Router[Page]( + routes = List( + Route.static(homePage, root / endOfSegments, basePath = base), + Route.static( + Page.Dashboard, + root / "dashboard" / endOfSegments, + basePath = base + ), + Route[Page.Detail, String]( + encode = _.osobniCislo.value.toString, + decode = osc => Page.Detail(Titled(OsobniCislo(osc))), + root / "osoba" / segment[String] / endOfSegments, + basePath = base + ), + Route[Page.DetailParametru, (String, String)]( + encode = p => (p.osobniCislo.value.toString, p.parametr.value), + decode = + p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / endOfSegments, + basePath = base + ), + Route[Page.DetailKriteria, (String, String, String)]( + encode = p => + ( + p.osobniCislo.value.toString, + p.parametr.value, + p.kriterium.value.replaceAll("\\.", "--") + ), + decode = p => + Page.DetailKriteria( + Titled(OsobniCislo(p._1)), + Titled(p._2), + Titled(p._3.replaceAll("--", ".")) + ), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / "kriterium" / segment[String] / endOfSegments, + basePath = base + ), + Route[Page.UpravDukazKriteria, (String, String, String)]( + encode = p => + ( + p.osobniCislo.value.toString, + p.parametr.value, + p.kriterium.value.replaceAll("\\.", "--") + ), + decode = p => + Page.UpravDukazKriteria( + Titled(OsobniCislo(p._1)), + Titled(p._2), + Titled(p._3.replaceAll("--", ".")) + ), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / "kriterium" / segment[String] / "edit" / endOfSegments, + basePath = base + ) + ), + serializePage = _.toJson, + deserializePage = _.fromJson[Page] + .fold(s => throw IllegalStateException(s), identity), + getPageTitle = _.title, + routeFallback = url => Page.NotFound(url), + deserializeFallback = _ => Page.Dashboard + )( + $popStateEvent = windowEvents.onPopState, + owner = unsafeWindowOwner + ) diff --git a/app/src/main/scala/mdr/pdb/app/actions.scala b/app/src/main/scala/mdr/pdb/app/actions.scala new file mode 100644 index 0000000..d078b3a --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/actions.scala @@ -0,0 +1,21 @@ +package mdr.pdb.app + +import mdr.pdb.OsobniCislo +import mdr.pdb.UserInfo +import mdr.pdb.Parameter +import mdr.pdb.ParameterCriteria + +sealed trait Action + +case object FetchDirectory extends Action +case class FetchUserDetails(osc: OsobniCislo) extends Action +case class FetchParameters(osc: OsobniCislo) extends Action +case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action +case class FetchParameterCriteria( + osc: OsobniCislo, + paramId: String, + critId: String, + page: (UserInfo, Parameter, ParameterCriteria) => Page +) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action +case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/mdr/pdb/app/components/AppPage.scala new file mode 100644 index 0000000..130c817 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/AppPage.scala @@ -0,0 +1,82 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.Page +import mdr.pdb.{UserProfile, UserInfo, OsobniCislo} +import com.raquo.waypoint.Router +import mdr.pdb.app.Action +import mdr.pdb.app.NavigateTo +import mdr.pdb.UserFunction + +object AppPage: + // TODO: pages by logged in user + val pages: List[Page] = List(Page.Directory, Page.Dashboard) + + import NavigationBar.{Logo, MenuItem} + + val logo = Logo( + "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", + "Workflow" + ) + + // TODO: menu items by user profile + val userMenu = + List( + MenuItem("Your Profile"), + MenuItem("Settings"), + MenuItem("Sign out") + ) + + // TODO: load user profile + val $userProfile = Var( + UserProfile( + "tom", + UserInfo( + OsobniCislo("1031"), + "tom", + "Tom", + "Cook", + None, + None, + Some("tom@example.com"), + Some("+420 222 866 180"), + Some( + UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") + ), + Nil, + None + ) + ) + ) + + val $userInfo = $userProfile.signal.map(_.userInfo) + + type ViewModel = Option[HtmlElement] + def apply( + actionBus: Observer[Action] + )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using + router: Router[Page] + ): HtmlElement = + PageLayout(actionBus)( + $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => + PageLayout.ViewModel( + NavigationBar.ViewModel( + u, + pages.map(p => + NavigationBar.Link( + () => + PageLink( + p, + actionBus + ), + p == cp + ) + ), + userMenu, + logo + ), + c + ) + ), + mods + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala new file mode 100644 index 0000000..9375398 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala @@ -0,0 +1,143 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.tailwind.list.IconText.ViewModel +import fiftyforms.ui.components.tailwind.Icons +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.Page + +import com.raquo.laminar.api.L.{*, given} +import io.laminext.syntax.core.{*, given} +import mdr.pdb.app.Action + +object Breadcrumbs: + + private def slash = + import svg.{*, given} + svg( + cls := "flex-shrink-0 h-5 w-5 text-gray-300", + xmlns := "http://www.w3.org/2000/svg", + fill := "currentColor", + viewBox := "0 0 20 20", + ariaHidden := true, + path( + d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" + ) + ) + + def backIcon = + Icons.solid + .`arrow-narrow-left`() + .amend( + svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" + ) + + object Link: + case class ViewModel( + page: Page, + icon: Option[SvgElement], + extraClasses: String, + text: String, + textClasses: Option[String] = None + ) + + val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" + + def shortHome(p: Page) = ViewModel( + p, + Some(backIcon), + "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", + "Zpět na úvodní stránku", + None + ) + + def fullHome(p: Page) = ViewModel( + p, + Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), + "text-gray-400 hover:text-gray-500", + "Domů", + Some("sr-only") + ) + + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + PageLink + .container($m.map(_.page), actionBus) + .amend( + cls <-- $m.map(_.extraClasses), + child.maybe <-- $m.map(_.icon), + span( + cls <-- $m.map(_.textClasses.getOrElse("")), + child.text <-- $m.map(_.text) + ) + ) + + object FullBreadcrumbs: + type ViewModel = Page + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + ol( + role := "list", + cls := "flex items-center space-x-4", + children <-- $m.map(_.path).split(_.id)((_, _, $p) => + li( + div( + cls := "flex items-center", + child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), + Link( + $p.map(p => + if (p.isRoot) then Link.fullHome(p) + else + Link.ViewModel( + p, + None, + s"ml-4 max-w-xs truncate ${Link.baseClasses}", + p.title + ) + ), + actionBus + ) + ) + ) + ) + ) + + object ShortBreadcrumbs: + type ViewModel = Page + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + Link( + $m.map { p => + val target = p.parent.getOrElse(p) + if target.isRoot then Link.shortHome(target) + else + Link.ViewModel( + target, + Some(backIcon), + "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", + s"Zpět na ${target.title}" + ) + }, + actionBus + ) + + def apply(actionBus: Observer[Action])(using + router: Router[Page] + ): HtmlElement = + nav( + cls := "flex", + aria.label := "Breadcrumb", + div( + cls := "flex sm:hidden", + ShortBreadcrumbs(router.$currentPage, actionBus) + ), + div( + cls := "hidden sm:block", + FullBreadcrumbs(router.$currentPage, actionBus) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala new file mode 100644 index 0000000..4bb8b8d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala @@ -0,0 +1,242 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent +import fiftyforms.ui.components.tailwind.Icons +import fiftyforms.ui.components.tailwind.Avatar +import mdr.pdb.UserInfo +import io.laminext.syntax.core.* + +object NavigationBar: + + case class Logo(img: String, name: String) + case class Link(a: () => Anchor, active: Boolean) + case class MenuItem(title: String) + + case class ViewModel( + userInfo: UserInfo, + pages: List[Link], + userMenu: List[MenuItem], + logo: Logo + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + val $userInfo = $m.map(_.userInfo) + val mobileMenuOpen = Var(false) + + val desktopOnly = cls("hidden md:block") + val mobileOnly = cls("md:hidden") + + inline def avatarImage(size: Int = 8) = + Avatar($userInfo.map(_.img)).avatarImage(size) + + def notificationButton = button( + tpe := "button", + cls := List( + "bg-indigo-600", + "focus:outline-none", + "focus:ring-2", + "focus:ring-offset-2", + "focus:ring-offset-indigo-600", + "focus:ring-white", + "hover:text-white", + "p-1", + "rounded-full", + "text-indigo-200" + ), + span(cls := "sr-only", "View notifications"), + Icons.outline.bell() + ) + + def userProfile: HtmlElement = + val menuOpen = Var(false) + + def menuItem(item: MenuItem, idx: Int): HtmlElement = + a( + href := "#", + cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", + role := "menuitem", + tabIndex := -1, + idAttr := s"user-menu-item-$idx", + item.title + ) + + div( + cls := "ml-3 relative", + div( + button( + tpe := "button", + cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", + idAttr := "user-menu-button", + aria.expanded <-- menuOpen.signal, + aria.hasPopup := true, + span(cls := "sr-only", "Open user menu"), + child <-- avatarImage(), + onClick.preventDefault.mapTo( + !menuOpen.now() + ) --> menuOpen.writer + ) + ), + /* + * */ + div( + cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", + cls <-- menuOpen.signal.map { o => + if (o) "md:block" else "md:hidden" + }, + role := "menu", + aria.orientation := "vertical", + aria.labelledBy := "user-menu-button", + tabIndex := -1, + // : keyboard navigation + children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) + ) + ) + + def mobileProfile = + def menuItem(item: MenuItem): HtmlElement = + a( + href := "#", + cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", + item.title + ) + + div( + cls := "pt-4 pb-3 border-t border-indigo-700", + div( + cls := "flex items-center px-5", + div( + cls := "flex-shrink-0", + child <-- avatarImage(10) + ), + div( + cls := "ml-3", + div( + cls := "text-base font-medium text-white", + child.text <-- $userInfo.map(_.name) + ), + child.maybe <-- $userInfo.map( + _.email.map(e => + div( + cls := "text-sm font-medium text-indigo-300", + e + ) + ) + ) + ), + notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) + ), + div( + cls := "mt-3 px-2 space-y-1", + children <-- $m.map(_.userMenu.map(menuItem)) + ) + ) + + def pageLink(page: Link): Anchor = + page + .a() + .amend( + cls := "text-white px-3 py-2 rounded-md text-sm font-medium", + cls := Seq( + "bg-indigo-700" -> page.active, + "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active + ), + ariaCurrent := (if page.active then "page" else "false") + ) + + def logoImg: Image = + img( + cls := "h-8 w-8", + src <-- $m.map(_.logo.img), + alt <-- $m.map(_.logo.name) + ) + + def mobileMenuButton = button( + tpe := "button", + cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", + aria.controls := "mobile-menu", + aria.expanded <-- mobileMenuOpen.signal, + span(cls := "sr-only", "Open main menu"), + Icons.outline + .menu() + .amend( + svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") + ), + Icons.outline + .x() + .amend( + svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") + ), + onClick.preventDefault.mapTo( + !mobileMenuOpen.now() + ) --> mobileMenuOpen.writer + ) + + def navBarLeft = + div( + cls := "flex items-center", + div(cls := "flex-shrink-0", logoImg), + div( + desktopOnly, + div( + cls := "ml-10 flex items-baseline space-x-4", + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) + ) + ) + ) + + def navBarRight = + div( + desktopOnly, + div( + cls := "ml-4 flex items-center md:ml-6", + notificationButton, + userProfile + ) + ) + + def navBarMobile = + div( + cls := "-mr-2 flex", + mobileOnly, + mobileMenuButton + ) + + def navBar = + div( + cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", + div( + cls := "flex items-center justify-between h-16", + navBarLeft, + navBarRight, + navBarMobile + ) + ) + + def mobileMenu = + div( + mobileOnly, + cls <-- mobileMenuOpen.signal.map { o => + if (o) "block" else "hidden" + }, + idAttr := "mobile-menu", + div( + cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) + ), + mobileProfile + ) + + nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala new file mode 100644 index 0000000..c4cb10d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala @@ -0,0 +1,19 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.Page +import com.raquo.waypoint.Router +import mdr.pdb.app.Action + +object PageHeader: + def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = + header( + cls := "bg-white shadow-sm", + div( + cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", + h1( + cls := "text-lg leading-6 font-semibold text-gray-900", + Breadcrumbs(actionBus) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala new file mode 100644 index 0000000..aae4539 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala @@ -0,0 +1,30 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.Page +import mdr.pdb.app.Action +import fiftyforms.ui.components.tailwind.Loading +import io.laminext.syntax.core.* + +object PageLayout: + case class ViewModel( + navigation: NavigationBar.ViewModel, + content: Option[HtmlElement] + ) + def apply(actionBus: Observer[Action])( + $m: Signal[ViewModel], + mods: Modifier[HtmlElement]* + )(using router: Router[Page]): HtmlElement = + val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) + div( + cls := "h-full flex flex-col", + NavigationBar($m.map(_.navigation)), + child.maybe <-- router.$currentPage.map(_.isRoot) + .switch(None, Some(PageHeader(actionBus))), + main( + cls := "flex-grow-1 overflow-y-auto", + mods, + child <-- $maybeContent.map(_.getOrElse(Loading)) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/mdr/pdb/app/components/PageLink.scala new file mode 100644 index 0000000..e6e7052 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageLink.scala @@ -0,0 +1,40 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.LinkSupport.* +import mdr.pdb.app.Page +import mdr.pdb.app.Action +import com.raquo.waypoint.Router +import mdr.pdb.app.NavigateTo + +object PageLink: + type ViewModel = Page + def apply($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = + a(mods($m, actions), child.text <-- $m.map(_.title)) + + def apply(m: ViewModel, actions: Observer[Action])(using + router: Router[Page] + ): Anchor = apply(Val(m), actions) + + def container($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = a(mods($m, actions)) + + def container(m: ViewModel, actions: Observer[Action])(using + Router[Page] + ): Anchor = container(Val(m), actions) + + private def mods($m: Signal[Page], actions: Observer[Action])(using + router: Router[Page] + ): Modifier[Anchor] = + Seq( + href <-- $m.map(router.absoluteUrlForPage).recover { case _ => + Some("invalid url") + }, + composeEvents(onClick.noKeyMod.preventDefault)( + _.sample($m) + .map(NavigateTo.apply) + ) --> actions + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala new file mode 100644 index 0000000..7682772 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala @@ -0,0 +1,13 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.pages.dashboard.DashboardPage +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage + +class DashboardPageConnector(actionBus: Observer[Action])(using + router: Router[Page] +): + def apply: HtmlElement = + AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala new file mode 100644 index 0000000..76b9133 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala @@ -0,0 +1,76 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.Parameter +import mdr.pdb.UserInfo +import mdr.pdb.app.pages.detail.DetailParametruPage +import pages.detail.DetailKriteriaPage +import pages.detail.components.DukazyKriteria +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.ParameterCriteria + +object DetailKriteriaPageConnector { + trait AppState { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + } +} + +case class DetailKriteriaPageConnector( + state: DetailKriteriaPageConnector.AppState +)( + $page: Signal[Page.DetailKriteria] +)(using Router[Page]): + val $paramChangeSignal = + $page.splitOne(p => + (p.osobniCislo.value, p.parametr.value, p.kriterium.value) + )((x, _, _) => x) + val $pageChangeSignal = + $paramChangeSignal.map( + FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) + ) + + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + val $merged = + $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => + for { + da <- d + pa <- p + pb <- pa.find(_.id == pc._2) + ka <- pb.criteria.find(_.id == pc._3) + } yield (da, pb, ka) + ) + + def apply: HtmlElement = + AppPage(state.actionBus)( + $merged.map(_.map(buildModel)) + .split(_ => ())((_, s, $s) => + DetailKriteriaPage($s)(state.actionBus.contramap { + case DukazyKriteria.Add => + NavigateTo( + Page.UpravDukazKriteria( + Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), + Page.Titled(s.parametr.id, Some(s.parametr.nazev)), + Page.Titled(s.kriterium.id, Some(s.kriterium.id)) + ) + ) + }) + ), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: Parameter, + k: ParameterCriteria + ): DetailKriteriaPage.ViewModel = + DetailKriteriaPage.ViewModel( + o.toDetailOsoby, + p.toParametr(_ => a()), + k.toKriterium(_ => a()) + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DetailPageConnector.scala new file mode 100644 index 0000000..df3a6b7 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DetailPageConnector.scala @@ -0,0 +1,57 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.UserInfo +import pages.detail.DetailPage +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.pages.detail.components.DetailOsoby +import mdr.pdb.Parameter +import mdr.pdb.app.pages.detail.components.SeznamParametru +import fiftyforms.ui.components.tailwind.Color + +object DetailPageConnector { + trait AppState { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + } +} + +case class DetailPageConnector(state: DetailPageConnector.AppState)( + $page: Signal[Page.Detail] +)(using router: Router[Page]): + val $oscChangeSignal = $page.splitOne(_.osobniCislo.value)((osc, _, _) => osc) + val $pageChangeSignal = + $oscChangeSignal.flatMap(osc => + EventStream.fromSeq(Seq(FetchUserDetails(osc), FetchParameters(osc))) + ) + // TODO: filter the value based on the current osc + // OSC change will fetch new data, but still + // - we need to be sure that what we got is really what we ought to display + // - we want to display stale data accordingly (at least with loading indicator) + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + def apply: HtmlElement = + AppPage(state.actionBus)( + $data.combineWithFn($params)(_ zip _) + .map(_.map(buildModel)) + .split(_ => ())((_, _, s) => DetailPage(s)), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: List[Parameter] + ): DetailPage.ViewModel = + DetailPage.ViewModel( + o.toDetailOsoby, + p.map( + _.toParametr(param => + PageLink.container(Page.DetailParametru(o, param), state.actionBus) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DetailParametruPageConnector.scala new file mode 100644 index 0000000..fa7193d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DetailParametruPageConnector.scala @@ -0,0 +1,67 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.Parameter +import mdr.pdb.UserInfo +import mdr.pdb.app.pages.detail.DetailParametruPage +import pages.detail.DetailParametruPage +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.components.PageLink + +object DetailParametruPageConnector { + trait AppState { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + } +} + +case class DetailParametruPageConnector( + state: DetailParametruPageConnector.AppState +)( + $page: Signal[Page.DetailParametru] +)(using router: Router[Page]): + val $paramChangeSignal = + $page.splitOne(p => (p.osobniCislo.value, p.parametr.value))((x, _, _) => x) + val $pageChangeSignal = + $paramChangeSignal.map(FetchParameter(_, _)) + + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + val $merged = + $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => + for { + da <- d + pa <- p + pb <- pa.find(_.id == pc._2) + } yield (da, pb) + ) + + def apply: HtmlElement = + AppPage(state.actionBus)( + $merged.map(_.map(buildModel)) + .split(_ => ())((_, _, s) => DetailParametruPage(s)), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: Parameter + ): DetailParametruPage.ViewModel = + DetailParametruPage.ViewModel( + o.toDetailOsoby, + p.toParametr(p => + PageLink.container(Page.DetailParametru(o, p), state.actionBus) + ), + p.criteria.map( + _.toKriterium { c => + PageLink.container( + Page.DetailKriteria(o, p, c), + state.actionBus + ) + } + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DirectoryPageConnector.scala new file mode 100644 index 0000000..ab2c045 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DirectoryPageConnector.scala @@ -0,0 +1,34 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.UserInfo +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.components.AppPage + +case class DirectoryPageConnector( + $input: EventStream[List[UserInfo]], + actionBus: Observer[Action] +)(using router: Router[Page]): + val $data = $input.startWithNone + val $actionSignal = EventStream.fromValue(FetchDirectory) + + def apply: HtmlElement = + AppPage(actionBus)( + $data.split(_ => ())((_, _, s) => + pages.directory.DirectoryPage( + s.map( + _.map( + _.toUserRow(u => + PageLink.container( + Page.Detail(Page.Titled(u.personalNumber)), + actionBus + ) + ) + ) + ) + ) + ), + $actionSignal --> actionBus + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/mdr/pdb/app/connectors/conversions.scala new file mode 100644 index 0000000..f581796 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/conversions.scala @@ -0,0 +1,68 @@ +package mdr.pdb.app.connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.UserInfo +import mdr.pdb.app.pages.detail.components.DetailOsoby +import mdr.pdb.Parameter +import mdr.pdb.app.pages.detail.components.SeznamParametru +import mdr.pdb.ParameterCriteria +import mdr.pdb.app.pages.detail.components.SeznamKriterii +import mdr.pdb.app.pages.directory.components.UserRow +import mdr.pdb.app.pages.detail.components.DetailParametru +import mdr.pdb.app.pages.detail.components.DetailKriteria +import fiftyforms.ui.components.tailwind.Color + +extension (o: UserInfo) + def toDetailOsoby: DetailOsoby.ViewModel = + DetailOsoby.ViewModel( + o.personalNumber, + o.name, + o.email, + o.phone, + o.img, + o.mainFunction.map(f => + DetailOsoby.Funkce.ViewModel(f.name, f.dept, f.ou) + ), + o.userContracts.headOption.map(c => + DetailOsoby.PracovniPomer.ViewModel(c.rel, c.startDate, c.endDate) + ) + ) + +extension (param: Parameter) + def toParametr(container: Parameter => Anchor): DetailParametru.ViewModel = + DetailParametru.ViewModel( + id = param.id, + nazev = param.name, + popis = param.description, + status = "Nesplněno", + statusColor = Color.red, + a = container(param) + ) + +extension (crit: ParameterCriteria) + def toKriterium( + container: ParameterCriteria => Anchor + ): DetailKriteria.ViewModel = + DetailKriteria.ViewModel( + nazev = crit.criteriumText, + kapitola = crit.chapterId, + bod = crit.itemId, + status = "Nesplněno", + statusColor = Color.red, + splneno = false, + dukazy = Nil, + container = container(crit) + ) + +extension (user: UserInfo) + def toUserRow( + container: UserInfo => HtmlElement = (_: UserInfo) => div() + ): UserRow.ViewModel = + UserRow.ViewModel( + osobniCislo = user.personalNumber.toString, + celeJmeno = user.name, + prijmeni = user.surname, + hlavniFunkce = user.mainFunction.map(_.name), + img = user.img, + container = () => container(user) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/dashboard/DashboardPage.scala b/app/src/main/scala/mdr/pdb/app/pages/dashboard/DashboardPage.scala new file mode 100644 index 0000000..0eb8f7a --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/dashboard/DashboardPage.scala @@ -0,0 +1,11 @@ +package mdr.pdb.app.pages.dashboard + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.Page + +object DashboardPage: + + def render: HtmlElement = + div("Dashboard page") diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala new file mode 100644 index 0000000..ff3a25f --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala @@ -0,0 +1,31 @@ +package mdr.pdb.app.pages.detail + +import com.raquo.laminar.api.L.{*, given} + +import components._ + +object DetailKriteriaPage: + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametr: DetailParametru.ViewModel, + kriterium: DetailKriteria.ViewModel + ) + + def apply($m: Signal[ViewModel])( + action: Observer[DukazyKriteria.Action] + ): HtmlElement = + div( + cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + div( + DetailOsoby.header($m.map(_.osoba)), + DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") + ), + div( + DetailKriteria($m.map(_.kriterium)), + DukazyKriteria($m.map(_.kriterium.dukazy))(action) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/DetailPage.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailPage.scala new file mode 100644 index 0000000..b591cc9 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailPage.scala @@ -0,0 +1,25 @@ +package mdr.pdb.app.pages.detail + +import com.raquo.laminar.api.L.{*, given} + +import components._ +import mdr.pdb.app.Page +import com.raquo.waypoint.Router +import mdr.pdb.app.Action + +object DetailPage: + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametry: SeznamParametru.ViewModel + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + DetailOsoby($m.map(_.osoba)), + SeznamParametru($m.map(_.parametry)) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/DetailParametruPage.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailParametruPage.scala new file mode 100644 index 0000000..3322279 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailParametruPage.scala @@ -0,0 +1,24 @@ +package mdr.pdb.app.pages.detail + +import com.raquo.laminar.api.L.{*, given} + +import components._ + +object DetailParametruPage: + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametr: DetailParametru.ViewModel, + kriteria: SeznamKriterii.ViewModel + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + DetailOsoby.header($m.map(_.osoba)), + DetailParametru($m.map(_.parametr)), + SeznamKriterii($m.map(_.kriteria)) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala new file mode 100644 index 0000000..b58618a --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala @@ -0,0 +1,107 @@ +package mdr.pdb +package app +package pages.detail + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.pages.detail.components.UpravDukazForm +import fiftyforms.services.files.File + +object UpravDukaz: + + type ThisPage = Page.UpravDukazKriteria + type PageKey = (OsobniCislo, String, String) + + trait State { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def availableFiles: EventStream[List[File]] + def actionBus: Observer[Action] + } + + def keyOfPage(page: ThisPage): PageKey = + (page.osobniCislo.value, page.parametr.value, page.kriterium.value) + + def onChangeAction(key: PageKey): Action = + FetchParameterCriteria( + key._1, + key._2, + key._3, + Page.UpravDukazKriteria(_, _, _) + ) + + class Connector(state: State)($page: Signal[ThisPage])(using Router[Page]): + val $paramChangeSignal = $page.splitOne(keyOfPage)((x, _, _) => x) + val $pageChangeSignal = $paramChangeSignal.map(onChangeAction) + + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + val $merged = + $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => + for { + da <- d + pa <- p + pb <- pa.find(_.id == pc._2) + ka <- pb.criteria.find(_.id == pc._3) + } yield (da, pb, ka) + ) + + def apply: HtmlElement = + AppPage(state.actionBus)( + $merged.split(_ => ())((_, s, $s) => + PageComponent( + $s.map(buildModel), + state.availableFiles, + state.actionBus.contramap { + case UpravDukazForm.Cancelled => + NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) + case UpravDukazForm.AvailableFilesRequested => + FetchAvailableFiles(s._1.personalNumber) + } + ) + ), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: Parameter, + k: ParameterCriteria + ): PageComponent.ViewModel = + import connectors.* + PageComponent.ViewModel( + o.toDetailOsoby, + p.toParametr(_ => a()), + k.toKriterium(_ => a()) + ) + + object PageComponent: + import components.* + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametr: DetailParametru.ViewModel, + kriterium: DetailKriteria.ViewModel + ) + + def apply( + $m: Signal[ViewModel], + availableFilesStream: EventStream[List[File]], + events: Observer[UpravDukazForm.Event] + ): HtmlElement = + div( + cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + div( + DetailOsoby.header($m.map(_.osoba)), + DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") + ), + div( + DetailKriteria($m.map(_.kriterium)), + UpravDukazForm(availableFilesStream)(events) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailKriteria.scala new file mode 100644 index 0000000..a3ab634 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailKriteria.scala @@ -0,0 +1,37 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.CustomAttrs +import fiftyforms.ui.components.tailwind.Icons +import java.time.LocalDate +import fiftyforms.ui.components.tailwind.Color + +object DetailKriteria: + case class ViewModel( + nazev: String, + kapitola: String, + bod: String, + status: String, + statusColor: Color, + splneno: Boolean, + dukazy: List[DukazKriteria.ViewModel], + container: HtmlElement = div() + ) { + val id = s"${kapitola}${bod}" + } + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "pb-5 border-b border-gray-200", + h3( + cls := "text-xl leading-6 font-bold text-gray-900", + child.text <-- $m.map(_.nazev) + ), + h3( + cls := "mt-2 max-w-4xl text-sm text-gray-500", + "Kapitola ", + child.text <-- $m.map(_.kapitola), + " bod ", + child <-- $m.map(_.bod) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailOsoby.scala new file mode 100644 index 0000000..0fe2ba5 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailOsoby.scala @@ -0,0 +1,100 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.OsobniCislo + +import java.time.LocalDate +import fiftyforms.ui.components.tailwind.Avatar + +object DetailOsoby: + + object Funkce: + case class ViewModel( + nazev: String, + stredisko: String, + voj: String + ) + + def render($m: Signal[ViewModel]): HtmlElement = + p( + cls := "text-sm font-medium text-gray-500", + child.text <-- $m.map(_.nazev), + span( + cls := "hidden md:inline", + " @ ", + child.text <-- $m.map(_.stredisko), + ", ", + child.text <-- $m.map(_.voj) + ) + ) + + object PracovniPomer: + case class ViewModel( + druh: String, + pocatek: LocalDate, + konec: Option[LocalDate] + ) + + def render($m: Signal[ViewModel]): HtmlElement = + import fiftyforms.ui.components.tailwind.CustomAttrs.datetime + p( + cls := "text-sm font-medium text-gray-500", + child.text <-- $m.map(_.druh), + " od ", + time( + datetime <-- $m.map(_.pocatek.toString), + child.text <-- $m.map(_.pocatek.toString) + ) + ) + + case class ViewModel( + osobniCislo: OsobniCislo, + jmeno: String, + email: Option[String], + telefon: Option[String], + img: Option[String], + hlavniFunkce: Option[Funkce.ViewModel], + pracovniPomer: Option[PracovniPomer.ViewModel] + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "md:flex md:items-center md:justify-between md:space-x-4", + div( + cls := "flex items-start space-x-4", + div( + cls := "flex-shrink-0", + Avatar($m.map(_.img)).avatar(16) + ), + div( + h1( + cls := "text-2xl font-bold text-gray-900", + child.text <-- $m.map(_.jmeno) + ), + child.maybe <-- $m.map(_.hlavniFunkce).split(_ => ())((_, _, d) => + Funkce.render(d) + ), + child.maybe <-- $m.map(_.pracovniPomer).split(_ => ())((_, _, d) => + PracovniPomer.render(d) + ) + ) + ) + ) + + def header($m: Signal[ViewModel]): HtmlElement = + div( + cls := "md:flex md:items-center md:justify-between md:space-x-5", + div( + cls := "flex items-start space-x-4", + div( + cls := "flex-shrink-0", + Avatar($m.map(_.img)).avatar(8) + ), + div( + h1( + cls := "text-lg font-medium text-gray-800", + child.text <-- $m.map(_.jmeno) + ) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailParametru.scala new file mode 100644 index 0000000..6310dde --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailParametru.scala @@ -0,0 +1,34 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.Color + +object DetailParametru: + case class ViewModel( + id: String, + nazev: String, + popis: String, + status: String, + statusColor: Color, + a: Anchor + ) + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "pb-5 border-b border-gray-200", + h2( + cls := "text-2xl leading-6 font-medium text-gray-900", + child.text <-- $m.map(_.nazev) + ), + p( + cls := "mt-2 max-w-4xl text-sm text-gray-500", + child.text <-- $m.map(_.popis) + ) + ) + + def header($m: Signal[ViewModel]): HtmlElement = + div( + h2( + cls := "text-lg leading-6 font-medium text-gray-600", + child.text <-- $m.map(_.nazev) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazKriteria.scala new file mode 100644 index 0000000..c292919 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazKriteria.scala @@ -0,0 +1,177 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import java.time.LocalDate +import fiftyforms.ui.components.tailwind.CustomAttrs + +object DukazKriteria: + case class Osoba(osobniCislo: String, jmeno: String) + case class Udalost(osoba: Osoba, datum: LocalDate) + case class Dokument( + url: String, + nazev: String, + pridal: Udalost, + odebral: Option[Udalost] + ) + case class ViewModel( + dokumenty: List[Dokument], + autorizoval: Option[Udalost], + platiDo: Option[LocalDate], + poznámka: Option[String] + ) + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "bg-white shadow overflow-hidden sm:rounded-lg", + div( + cls := "px-4 py-5 sm:px-6", + h3( + cls := "text-lg leading-6 font-medium text-gray-900", + """Applicant Information""" + ), + p( + cls := "mt-1 max-w-2xl text-sm text-gray-500", + """Personal details and application.""" + ) + ), + div( + cls := "border-t border-gray-200 px-4 py-5 sm:px-6", + dl( + cls := "grid grid-cols-1 gap-x-4 gap-y-8 sm:grid-cols-2", + div( + cls := "sm:col-span-1", + dt( + cls := "text-sm font-medium text-gray-500", + """Full name""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """Margot Foster""" + ) + ), + div( + cls := "sm:col-span-1", + dt( + cls := "text-sm font-medium text-gray-500", + """Application for""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """Backend Developer""" + ) + ), + div( + cls := "sm:col-span-1", + dt( + cls := "text-sm font-medium text-gray-500", + """Email address""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """margotfoster@example.com""" + ) + ), + div( + cls := "sm:col-span-1", + dt( + cls := "text-sm font-medium text-gray-500", + """Salary expectation""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """$120,000""" + ) + ), + div( + cls := "sm:col-span-2", + dt( + cls := "text-sm font-medium text-gray-500", + """About""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """Fugiat ipsum ipsum deserunt culpa aute sint do nostrud anim incididunt cillum culpa consequat. Excepteur qui ipsum aliquip consequat sint. Sit id mollit nulla mollit nostrud in ea officia proident. Irure nostrud pariatur mollit ad adipisicing reprehenderit deserunt qui eu.""" + ) + ), + div( + cls := "sm:col-span-2", + dt( + cls := "text-sm font-medium text-gray-500", + """Attachments""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + ul( + role := "list", + cls := "border border-gray-200 rounded-md divide-y divide-gray-200", + li( + cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", + div( + cls := "w-0 flex-1 flex items-center", { + import svg.* + import CustomAttrs.svg.ariaHidden + svg( + cls := "flex-shrink-0 h-5 w-5 text-gray-400", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + ariaHidden := true, + path( + fillRule := "evenodd", + d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", + clipRule := "evenodd" + ) + ) + }, + span( + cls := "ml-2 flex-1 w-0 truncate", + """resume_back_end_developer.pdf""" + ) + ), + div( + cls := "ml-4 flex-shrink-0", + a( + href := "#", + cls := "font-medium text-indigo-600 hover:text-indigo-500", + """Download""" + ) + ) + ), + li( + cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", + div( + cls := "w-0 flex-1 flex items-center", { + import svg.* + import CustomAttrs.svg.ariaHidden + svg( + cls := "flex-shrink-0 h-5 w-5 text-gray-400", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + ariaHidden := true, + path( + fillRule := "evenodd", + d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", + clipRule := "evenodd" + ) + ) + }, + span( + cls := "ml-2 flex-1 w-0 truncate", + """coverletter_back_end_developer.pdf""" + ) + ), + div( + cls := "ml-4 flex-shrink-0", + a( + href := "#", + cls := "font-medium text-indigo-600 hover:text-indigo-500", + """Download""" + ) + ) + ) + ) + ) + ) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala new file mode 100644 index 0000000..d604860 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala @@ -0,0 +1,35 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.Icons + +object DukazyKriteria: + sealed trait Action + case object Add extends Action + + type ViewModel = List[DukazKriteria.ViewModel] + + def apply($m: Signal[ViewModel])(actions: Observer[Action]): HtmlElement = + div( + child.maybe <-- $m.map(m => + if (m.isEmpty) then Some(prazdnyDukaz(actions)) + else None + ), + children <-- $m.map(_.zipWithIndex).split(_._2)((_, _, $s) => + DukazKriteria($s.map(_._1)) + ) + ) + + private def prazdnyDukaz(actions: Observer[Action]): HtmlElement = + button( + tpe := "button", + onClick.preventDefault.mapTo(Add) --> actions, + cls := "relative block w-full border-2 border-gray-300 border-dashed rounded-lg p-12 text-center hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + Icons.outline + .`document-add`(12) + .amend(svg.cls := "mx-auto text-gray-400"), + span( + cls := "mt-2 block text-sm font-medium text-gray-900", + "Přidat důkaz" + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala new file mode 100644 index 0000000..7e77a7b --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala @@ -0,0 +1,34 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.Color +import fiftyforms.ui.components.tailwind.list.StackedList +import fiftyforms.ui.components.tailwind.list.ListRow +import fiftyforms.ui.components.tailwind.list.RowTag +import fiftyforms.ui.components.tailwind.list.RowNext +import java.time.LocalDate + +object SeznamKriterii: + type ViewModel = List[DetailKriteria.ViewModel] + + private val kritList = new StackedList[DetailKriteria.ViewModel] + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "bg-white shadow overflow-hidden sm:rounded-md", + kritList($m, _.id) { $i => + $i.map { i => + ListRow.ViewModel( + title = i.nazev, + topRight = RowTag.render( + $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) + ), + bottomLeft = + p(cls := "text-sm text-gray-500", s"${i.kapitola}${i.bod}"), + bottomRight = emptyNode, + farRight = RowNext.render, + containerElement = i.container + ) + } + } + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamParametru.scala new file mode 100644 index 0000000..0760b32 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamParametru.scala @@ -0,0 +1,37 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.list.{ + StackedList, + ListRow, + RowTag, + PropList, + IconText, + RowNext +} +import fiftyforms.ui.components.tailwind.Color +import fiftyforms.ui.components.tailwind.LinkSupport.* + +object SeznamParametru: + type ViewModel = List[DetailParametru.ViewModel] + + private val parametrList = new StackedList[DetailParametru.ViewModel] + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "bg-white shadow overflow-hidden sm:rounded-md", + parametrList($m, _.id) { $i => + $i.map { i => + ListRow.ViewModel( + title = i.nazev, + topRight = RowTag.render( + $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) + ), + bottomLeft = emptyNode, + bottomRight = emptyNode, + farRight = RowNext.render, + containerElement = i.a + ) + } + } + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala new file mode 100644 index 0000000..28753c9 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala @@ -0,0 +1,101 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import io.laminext.syntax.core.* +import fiftyforms.ui.components.tailwind.CustomAttrs +import fiftyforms.ui.components.tailwind.form.* +import org.scalajs.dom +import com.raquo.laminar.nodes.ReactiveHtmlElement +import fiftyforms.services.files.components.tailwind.FilePicker +import mdr.pdb.frontend.AutorizujDukaz +import mdr.pdb.frontend.DocumentRef +import fiftyforms.services.files.File + +object UpravDukazForm: + sealed trait Event + case object Cancelled extends Event + case object AvailableFilesRequested extends Event + def apply(availableFilesStream: EventStream[List[File]])( + updates: Observer[Event] + ): HtmlElement = + val (filesStream, filesObserver) = + EventStream.withObserver[FilePicker.Event] + val files = Var[List[File]](Nil) + def submitButtons: HtmlElement = + div( + cls := "pt-5", + div( + cls := "flex justify-end", + button( + tpe := "button", + cls := "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + "Zrušit", + onClick.mapTo(Cancelled) --> updates + ), + button( + tpe := "submit", + cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + disabled <-- files.signal.map(_.isEmpty), + "Autorizovat" + ) + ) + ) + + div( + filesStream.collect { case FilePicker.SelectionUpdated(files) => + files.to(List) + } --> files.writer, + filesStream.collect { case FilePicker.AvailableFilesRequested => + AvailableFilesRequested + } --> updates, + cls := "bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6", + Form( + Form.Body( + Form.Section( + FormHeader( + FormHeader.ViewModel( + "Doložení kritéria", + "Sestavte doklady poskytující důkaz kritéria a potvrďte odesláním formuláře. Případné limitace či jiné relevantní údaje vepište do pole Komentář." + ) + ), + FormFields( + FormRow( + "dokumenty", + "Dokumenty", + FilePicker(files.signal, availableFilesStream)(filesObserver) + .amend(idAttr := "dokumenty", cls("max-w-lg")) + ).toHtml, + FormRow( + "platnost", + "Platnost", + input( + idAttr := "platnost", + name := "platnost", + tpe := "date", + autoComplete := "date", + cls := "block max-w-lg w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" + ) + ).toHtml, + FormRow( + "komentar", + "Komentář", + div( + cls := "mt-1 sm:mt-0 sm:col-span-2", + textArea( + idAttr := "komentar", + name := "about", + rows := 3, + cls := "max-w-lg shadow-sm block w-full focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border border-gray-300 rounded-md" + ), + p( + cls := "mt-2 text-sm text-gray-500", + "Doplňte prosím potřebné informace související s doložením kritéria, včetně případných limitací." + ) + ) + ).toHtml + ) + ) + ), + submitButtons + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/directory/DirectoryPage.scala b/app/src/main/scala/mdr/pdb/app/pages/directory/DirectoryPage.scala new file mode 100644 index 0000000..7a72ca3 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/directory/DirectoryPage.scala @@ -0,0 +1,40 @@ +package mdr.pdb.app.pages.directory + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.UserInfo + +import components._ + +object DirectoryPage: + + type ViewModel = List[UserRow.ViewModel] + + def apply($m: Signal[ViewModel]): HtmlElement = + val (actionsStream, actionObserver) = + EventStream.withObserver[SearchForm.Action] + val $filter = actionsStream + .collect { + case SearchForm.ClearFilter => None + case SearchForm.SetFilter(t) => Some(t) + } + .startWith(None) + val byLetter = for { + d <- $m + f <- $filter + } yield for { + (letter, users) <- d + .filter { user => + f match + case None => true + case Some(t) => user.search.contains(t) + } + .groupBy(_.prijmeni.head) + .to(List) + .sortBy(_._1) + } yield (letter.toString, users.sortBy(_.prijmeni)) + + div( + cls := "h-full max-w-7xl mx-auto order-first flex flex-col flex-shrink-0", + SearchForm(actionObserver), + Directory(byLetter) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/directory/components/Directory.scala b/app/src/main/scala/mdr/pdb/app/pages/directory/components/Directory.scala new file mode 100644 index 0000000..f7d6f99 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/directory/components/Directory.scala @@ -0,0 +1,37 @@ +package mdr.pdb.app.pages.directory.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.UserInfo + +object Directory: + + object Header: + type ViewModel = String + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500", + h3(child.text <-- $m) + ) + + object UserList: + type ViewModel = List[UserRow.ViewModel] + def apply($m: Signal[ViewModel]): HtmlElement = + ul( + role := "list", + cls := "relative z-0 divide-y divide-gray-200", + children <-- $m.split(_.osobniCislo)((_, _, s) => UserRow(s)) + ) + + type ViewModel = List[(String, List[UserRow.ViewModel])] + def apply($m: Signal[ViewModel]): HtmlElement = + nav( + cls := "flex-1 min-h-0 overflow-y-auto", + aria.label := "Directory", + children <-- $m.split(_._1)((_, _, s) => + div( + cls := "relative", + Header(s.map(_._1)), + UserList(s.map(_._2)) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/directory/components/SearchForm.scala b/app/src/main/scala/mdr/pdb/app/pages/directory/components/SearchForm.scala new file mode 100644 index 0000000..fb9ecc4 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/directory/components/SearchForm.scala @@ -0,0 +1,54 @@ +package mdr.pdb.app.pages.directory.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.Icons + +object SearchForm: + sealed trait Action + case object SubmitFilter extends Action + sealed trait FilterAction extends Action + case object ClearFilter extends FilterAction + case class SetFilter(value: String) extends FilterAction + + def apply(actions: Observer[Action]): HtmlElement = + div( + cls := "px-6 pt-4 pb-4", + form( + cls := "flex space-x-4", + onSubmit.mapTo(SubmitFilter) --> actions, + div( + cls := "flex-1 min-w-0", + label( + forId := "search", + cls := "sr-only", + "Hledat" + ), + div( + cls := "relative rounded-md shadow-sm", + div( + cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", + Icons.solid.search().amend(svg.cls := "text-gray-400") + ), + input( + tpe := "search", + name := "search", + idAttr := "search", + cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", + placeholder := "Hledat", + composeEvents(onInput.mapToValue.setAsValue.map(SetFilter(_)))( + _.throttle(500) + ) --> actions + ) + ) + ), + button( + tpe := "submit", + cls := "inline-flex justify-center px-3.5 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500", + Icons.solid.filter().amend(svg.cls := "text-gray-400"), + span( + cls := "sr-only", + "Hledat" + ) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/directory/components/UserRow.scala b/app/src/main/scala/mdr/pdb/app/pages/directory/components/UserRow.scala new file mode 100644 index 0000000..4aa1672 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/directory/components/UserRow.scala @@ -0,0 +1,52 @@ +package mdr.pdb.app.pages.directory.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.airstream.core.Signal +import fiftyforms.ui.components.tailwind.Avatar + +object UserRow: + case class ViewModel( + osobniCislo: String, + celeJmeno: String, + prijmeni: String, + hlavniFunkce: Option[String], + img: Option[String], + container: () => HtmlElement = () => div() + ) { + val search = prijmeni.toLowerCase + } + + def apply($m: Signal[ViewModel]): HtmlElement = + inline def avatarImage = + Avatar($m.map(_.img)).avatarImage(10) + + li( + div( + cls := "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500", + div( + cls := "flex-shrink-0", + child <-- avatarImage + ), + div( + cls := "flex-1 min-w-0", + child <-- $m.map { o => + o.container() + .amend( + cls := "focus:outline-none", + span( + cls := "absolute inset-0", + aria.hidden := true + ), + p( + cls := "text-sm font-medium text-gray-900", + o.celeJmeno + ), + p( + cls := "text-sm text-gray-500 truncate", + o.hlavniFunkce + ) + ) + } + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/errors/ErrorPage.scala b/app/src/main/scala/mdr/pdb/app/pages/errors/ErrorPage.scala new file mode 100644 index 0000000..6b195ef --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/errors/ErrorPage.scala @@ -0,0 +1,99 @@ +package mdr.pdb.app.pages.errors + +import com.raquo.domtypes.generic.codecs.StringAsIsCodec +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.Page +import mdr.pdb.app.Action +import com.raquo.waypoint.Router + +object ErrorPage: + case class ViewModel( + homePage: Page, + errorName: String, + title: String, + subTitle: String + ) + + def apply(m: ViewModel, actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + val ViewModel(homePage, errorName, title, subTitle) = m + div( + cls := "min-h-full pt-16 pb-12 flex flex-col bg-white", + main( + cls := "flex-grow flex flex-col justify-center max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", + div( + cls := "flex-shrink-0 flex justify-center", + a( + href := "/", + cls := "inline-flex", + span( + cls := "sr-only", + """Workflow""" + ), + img( + cls := "h-12 w-auto", + src := "https://tailwindui.com/img/logos/workflow-mark.svg?color=indigo&shade=600", + alt := "" + ) + ) + ), + div( + cls := "py-16", + div( + cls := "text-center", + p( + cls := "text-sm font-semibold text-indigo-600 uppercase tracking-wide", + errorName + ), + h1( + cls := "mt-2 text-4xl font-extrabold text-gray-900 tracking-tight sm:text-5xl", + title + ), + p( + cls := "mt-2 text-base text-gray-500", + subTitle + ), + div( + cls := "mt-6", + PageLink + .container(homePage, actionBus) + .amend( + cls := "text-base font-medium text-indigo-600 hover:text-indigo-500", + """Go back home""" + ) + ) + ) + ) + ), + footer( + cls := "flex-shrink-0 max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", + nav( + cls := "flex justify-center space-x-4", + a( + href := "#", + cls := "text-sm font-medium text-gray-500 hover:text-gray-600", + """Contact Support""" + ), + span( + cls := "inline-block border-l border-gray-300", + aria.hidden := true + ), + a( + href := "#", + cls := "text-sm font-medium text-gray-500 hover:text-gray-600", + """Status""" + ), + span( + cls := "inline-block border-l border-gray-300", + aria.hidden := true + ), + a( + href := "#", + cls := "text-sm font-medium text-gray-500 hover:text-gray-600", + """Twitter""" + ) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/errors/NotFoundPage.scala b/app/src/main/scala/mdr/pdb/app/pages/errors/NotFoundPage.scala new file mode 100644 index 0000000..97760e1 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/errors/NotFoundPage.scala @@ -0,0 +1,21 @@ +package mdr.pdb.app.pages.errors + +import com.raquo.domtypes.generic.codecs.StringAsIsCodec +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.Page +import mdr.pdb.app.Action + +object NotFoundPage: + def apply(homePage: Page, url: String, actionBus: Observer[Action])(using + router: Router[Page] + ): HtmlElement = + ErrorPage( + ErrorPage.ViewModel( + homePage, + "404 error", + "Page not found.", + s"Sorry, but page $url doesn't exist." + ), + actionBus + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala b/app/src/main/scala/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala new file mode 100644 index 0000000..77b0af8 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala @@ -0,0 +1,32 @@ +package mdr.pdb.app.pages.errors + +import com.raquo.domtypes.generic.codecs.StringAsIsCodec +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.Page +import mdr.pdb.app.Action + +object UnhandledErrorPage: + + case class ViewModel( + homePage: Page, + errorName: Option[String], + errorMessage: Option[String] + ) + + def apply(m: ViewModel, actionBus: Observer[Action])(using + router: Router[Page] + ): HtmlElement = + ErrorPage( + ErrorPage.ViewModel( + m.homePage, + "Unexpected error occurred", + m.errorName.getOrElse( + "Uh oh!" + ), // TODO: translations, better text than uh oh + m.errorMessage.getOrElse( + "This wasn't supposed to happen! Please try again." + ) + ), + actionBus + ) diff --git a/app/src/main/scala/mdr/pdb/app/services/DataFetcher.scala b/app/src/main/scala/mdr/pdb/app/services/DataFetcher.scala new file mode 100644 index 0000000..134c221 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/services/DataFetcher.scala @@ -0,0 +1,6 @@ +package mdr.pdb.app.services + +import com.raquo.airstream.core.Observer + +trait DataFetcher[K, A]: + def fetch(id: K, sink: Observer[A]): Unit diff --git a/app/src/main/scala/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/mdr/pdb/app/state/AppState.scala new file mode 100644 index 0000000..e44b2df --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/state/AppState.scala @@ -0,0 +1,120 @@ +package mdr.pdb.app +package state + +import com.raquo.airstream.core.EventStream +import mdr.pdb.{UserInfo, OsobniCislo} +import com.raquo.airstream.core.Observer +import scala.scalajs.js +import scala.scalajs.js.JSON +import zio.json._ +import com.raquo.airstream.eventbus.EventBus +import com.raquo.airstream.ownership.Owner +import com.raquo.waypoint.Router +import mdr.pdb.Parameter +import mdr.pdb.ParameterCriteria +import mdr.pdb.UserFunction +import mdr.pdb.UserContract +import fiftyforms.services.files.File + +trait AppState + extends connectors.DetailPageConnector.AppState + with connectors.DetailParametruPageConnector.AppState + with connectors.DetailKriteriaPageConnector.AppState + with pages.detail.UpravDukaz.State: + def users: EventStream[List[UserInfo]] + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + +class MockAppState(implicit owner: Owner, router: Router[Page]) + extends AppState: + + given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) + given JsonDecoder[UserFunction] = DeriveJsonDecoder.gen + given JsonDecoder[UserContract] = DeriveJsonDecoder.gen + given JsonDecoder[UserInfo] = DeriveJsonDecoder.gen + + given JsonDecoder[ParameterCriteria] = DeriveJsonDecoder.gen + given JsonDecoder[Parameter] = DeriveJsonDecoder.gen + + private val actions = EventBus[Action]() + private val (parametersStream, pushParameters) = + EventStream.withCallback[List[Parameter]] + private val (usersStream, pushUsers) = + EventStream.withCallback[List[UserInfo]] + private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] + private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] + + private val mockData: List[UserInfo] = + mockUsers + .asInstanceOf[js.Dictionary[js.Object]] + .values + // TODO: is there a more efficient way to parse from JS object directly? + .map { o => + val parsed = JSON.stringify(o).fromJson[UserInfo] + parsed.left.foreach(org.scalajs.dom.console.log(_)) + parsed + } + .collect { case Right(u) => + u + } + .toList + + private val mockParameters: List[Parameter] = + pdbParams + .asInstanceOf[js.Dictionary[js.Object]] + .values + .map(o => JSON.stringify(o).fromJson[Parameter]) + .collect { case Right(p) => p } + .toList + + // TODO: Extract to separate event handler + actions.events.foreach { + case FetchDirectory => pushUsers(mockData) + case FetchUserDetails(osc) => + mockData.find(_.personalNumber == osc).foreach { o => + pushDetails(o) + router.replaceState(Page.Detail(o)) + } + case FetchParameters(osc) => + pushParameters(mockParameters) + case FetchParameter(osc, paramId) => + for + o <- mockData.find(_.personalNumber == osc) + p <- mockParameters.find(_.id == paramId) + do + pushDetails(o) + pushParameters(mockParameters) + router.replaceState(Page.DetailParametru(o, p)) + case FetchParameterCriteria(osc, paramId, critId, page) => + for + o <- mockData.find(_.personalNumber == osc) + p <- mockParameters.find(_.id == paramId) + c <- p.criteria.find(_.id == critId) + do + pushDetails(o) + pushParameters(mockParameters) + router.replaceState(page(o, p, c)) + case NavigateTo(page) => router.pushState(page) + case FetchAvailableFiles(osc) => + pushFiles( + List( + File("https://tc163.cmi.cz/here", "Example file") + ) + ) + } + + override def users: EventStream[List[UserInfo]] = + usersStream.debugWithName("users") + + override def details: EventStream[UserInfo] = + detailsStream.debugWithName("details") + + override def parameters: EventStream[List[Parameter]] = + parametersStream.debugWithName("parameters") + + override def availableFiles: EventStream[List[File]] = + filesStream.debugWithName("available files") + + override def actionBus: Observer[Action] = + actions.writer.debugWithName("actions writer") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala deleted file mode 100644 index a3b00af..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala +++ /dev/null @@ -1,103 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import scala.scalajs.js.annotation.JSExportTopLevel -import scala.scalajs.js.annotation.JSExport -import scala.scalajs.js.annotation.JSImport -import scala.scalajs.js -import org.scalajs.dom -import com.raquo.laminar.api.L.{*, given} -import scala.scalajs.js.Date -import com.raquo.waypoint.Router -import com.raquo.waypoint.SplitRender -import cz.e_bs.cmi.mdr.pdb.app.services.DataFetcher -import scala.scalajs.js.JSON -import zio.json._ -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.state.AppState - -@js.native -@JSImport("stylesheets/main.css", JSImport.Namespace) -object Css extends js.Any - -@js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native -@JSImport("params/pdb-params.json", JSImport.Default) -object pdbParams extends js.Object - -@JSExportTopLevel("app") -object Main: - - @JSExport - def main(args: Array[String]): Unit = - import Routes.given - onLoad { - setupAirstream() - val appContainer = dom.document.querySelector("#app") - val _ = - render( - appContainer, - renderPage(state.MockAppState(using unsafeWindowOwner, router)) - ) - } - - private def onLoad(f: => Unit): Unit = - documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) - - private def setupAirstream()(using router: Router[Page]): Unit = - AirstreamError.registerUnhandledErrorCallback(err => - router.forcePage( - Page.UnhandledError( - Some(err.getClass.getName), // TODO: Fill only in dev mode - Some(err.getMessage) - ) - ) - ) - - def renderPage(state: AppState)(using - router: Router[Page] - ): HtmlElement = - val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) - .collectSignal[Page.Detail]( - connectors - .DetailPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailParametru]( - connectors - .DetailParametruPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailKriteria]( - connectors - .DetailKriteriaPageConnector(state)(_) - .apply - ) - .collectSignal[Page.UpravDukazKriteria]( - pages.detail.UpravDukaz.Connector(state)(_).apply - ) - .collectStatic(Page.Dashboard)( - connectors.DashboardPageConnector(state.actionBus).apply - ) - .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) - ) - .collect[Page.UnhandledError](pg => - pages.errors - .UnhandledErrorPage( - pages.errors.UnhandledErrorPage - .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), - state.actionBus - ) - ) - .collectStatic(Page.Directory)( - connectors - .DirectoryPageConnector(state.users, state.actionBus) - .apply - ) - div(cls := "h-full", child <-- pageSplitter.$view) - - // Pull in the stylesheet - val css: Css.type = Css diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala deleted file mode 100644 index 3bbc8c0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ /dev/null @@ -1,198 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.* -import org.scalajs.dom -import zio.json.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import scala.scalajs.js -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.Page.Titled - -// enum is not working with Waypoints' SplitRender collectStatic -sealed abstract class Page( - val id: String, - val title: String, - val parent: Option[Page] -) { - val path: Vector[Page] = - parent match - case None => Vector(this) - case Some(p) => p.path :+ this - - val isRoot: Boolean = parent.isEmpty -} - -object Page: - - case class Titled[V](value: V, title: Option[String] = None): - val show: String = title.getOrElse(value.toString) - - case object Directory extends Page("directory", "Adresář", None) - - case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) - - case class Detail(osobniCislo: Titled[OsobniCislo]) - extends Page("user", osobniCislo.show, Some(Directory)) - - object Detail { - def apply(o: UserInfo): Detail = Detail( - Titled(o.personalNumber, Some(o.name)) - ) - } - - case class DetailParametru( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String] - ) extends Page( - "parameter", - parametr.show, - Some(Detail(osobniCislo)) - ) - - object DetailParametru { - def apply(o: UserInfo, p: Parameter): DetailParametru = - DetailParametru( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)) - ) - } - - case class DetailKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "criteria", - kriterium.show, - Some(DetailParametru(osobniCislo, parametr)) - ) - - object DetailKriteria { - def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = - DetailKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class UpravDukazKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "addProof", - "Důkaz", - Some(DetailKriteria(osobniCislo, parametr, kriterium)) - ) - - object UpravDukazKriteria { - def apply( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): UpravDukazKriteria = - UpravDukazKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class NotFound(url: String) extends Page("404", "404", Some(Directory)) - - case class UnhandledError( - errorName: Option[String], - errorMessage: Option[String] - ) extends Page("500", "Unexpected error", Some(Directory)) - -object Routes: - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) - given [V: JsonEncoder]: JsonEncoder[Titled[V]] = - DeriveJsonEncoder.gen[Titled[V]] - given [V: JsonDecoder]: JsonDecoder[Titled[V]] = - DeriveJsonDecoder.gen[Titled[V]] - given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] - given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] - - val base = - js.`import`.meta.env.BASE_URL - .asInstanceOf[String] + "app" - - val homePage: Page = Page.Directory - - given router: Router[Page] = Router[Page]( - routes = List( - Route.static(homePage, root / endOfSegments, basePath = base), - Route.static( - Page.Dashboard, - root / "dashboard" / endOfSegments, - basePath = base - ), - Route[Page.Detail, String]( - encode = _.osobniCislo.value.toString, - decode = osc => Page.Detail(Titled(OsobniCislo(osc))), - root / "osoba" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.DetailParametru, (String, String)]( - encode = p => (p.osobniCislo.value.toString, p.parametr.value), - decode = - p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / endOfSegments, - basePath = base - ), - Route[Page.DetailKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.DetailKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.UpravDukazKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.UpravDukazKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / "edit" / endOfSegments, - basePath = base - ) - ), - serializePage = _.toJson, - deserializePage = _.fromJson[Page] - .fold(s => throw IllegalStateException(s), identity), - getPageTitle = _.title, - routeFallback = url => Page.NotFound(url), - deserializeFallback = _ => Page.Dashboard - )( - $popStateEvent = windowEvents.onPopState, - owner = unsafeWindowOwner - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala deleted file mode 100644 index b24b82b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -sealed trait Action - -case object FetchDirectory extends Action -case class FetchUserDetails(osc: OsobniCislo) extends Action -case class FetchParameters(osc: OsobniCislo) extends Action -case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action -case class FetchParameterCriteria( - osc: OsobniCislo, - paramId: String, - critId: String, - page: (UserInfo, Parameter, ParameterCriteria) => Page -) extends Action -case class FetchAvailableFiles(osc: OsobniCislo) extends Action -case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala deleted file mode 100644 index c637d35..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala +++ /dev/null @@ -1,82 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.{UserProfile, UserInfo, OsobniCislo} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo -import cz.e_bs.cmi.mdr.pdb.UserFunction - -object AppPage: - // TODO: pages by logged in user - val pages: List[Page] = List(Page.Directory, Page.Dashboard) - - import NavigationBar.{Logo, MenuItem} - - val logo = Logo( - "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", - "Workflow" - ) - - // TODO: menu items by user profile - val userMenu = - List( - MenuItem("Your Profile"), - MenuItem("Settings"), - MenuItem("Sign out") - ) - - // TODO: load user profile - val $userProfile = Var( - UserProfile( - "tom", - UserInfo( - OsobniCislo("1031"), - "tom", - "Tom", - "Cook", - None, - None, - Some("tom@example.com"), - Some("+420 222 866 180"), - Some( - UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") - ), - Nil, - None - ) - ) - ) - - val $userInfo = $userProfile.signal.map(_.userInfo) - - type ViewModel = Option[HtmlElement] - def apply( - actionBus: Observer[Action] - )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using - router: Router[Page] - ): HtmlElement = - PageLayout(actionBus)( - $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => - PageLayout.ViewModel( - NavigationBar.ViewModel( - u, - pages.map(p => - NavigationBar.Link( - () => - PageLink( - p, - actionBus - ), - p == cp - ) - ), - userMenu, - logo - ), - c - ) - ), - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala deleted file mode 100644 index bf473c6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ /dev/null @@ -1,143 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden -import fiftyforms.ui.components.tailwind.list.IconText.ViewModel -import fiftyforms.ui.components.tailwind.Icons -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Action - -object Breadcrumbs: - - private def slash = - import svg.{*, given} - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-300", - xmlns := "http://www.w3.org/2000/svg", - fill := "currentColor", - viewBox := "0 0 20 20", - ariaHidden := true, - path( - d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" - ) - ) - - def backIcon = - Icons.solid - .`arrow-narrow-left`() - .amend( - svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" - ) - - object Link: - case class ViewModel( - page: Page, - icon: Option[SvgElement], - extraClasses: String, - text: String, - textClasses: Option[String] = None - ) - - val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" - - def shortHome(p: Page) = ViewModel( - p, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - "Zpět na úvodní stránku", - None - ) - - def fullHome(p: Page) = ViewModel( - p, - Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), - "text-gray-400 hover:text-gray-500", - "Domů", - Some("sr-only") - ) - - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - PageLink - .container($m.map(_.page), actionBus) - .amend( - cls <-- $m.map(_.extraClasses), - child.maybe <-- $m.map(_.icon), - span( - cls <-- $m.map(_.textClasses.getOrElse("")), - child.text <-- $m.map(_.text) - ) - ) - - object FullBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - ol( - role := "list", - cls := "flex items-center space-x-4", - children <-- $m.map(_.path).split(_.id)((_, _, $p) => - li( - div( - cls := "flex items-center", - child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), - Link( - $p.map(p => - if (p.isRoot) then Link.fullHome(p) - else - Link.ViewModel( - p, - None, - s"ml-4 max-w-xs truncate ${Link.baseClasses}", - p.title - ) - ), - actionBus - ) - ) - ) - ) - ) - - object ShortBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - Link( - $m.map { p => - val target = p.parent.getOrElse(p) - if target.isRoot then Link.shortHome(target) - else - Link.ViewModel( - target, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - s"Zpět na ${target.title}" - ) - }, - actionBus - ) - - def apply(actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - nav( - cls := "flex", - aria.label := "Breadcrumb", - div( - cls := "flex sm:hidden", - ShortBreadcrumbs(router.$currentPage, actionBus) - ), - div( - cls := "hidden sm:block", - FullBreadcrumbs(router.$currentPage, actionBus) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala deleted file mode 100644 index 0db2047..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ /dev/null @@ -1,242 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent -import fiftyforms.ui.components.tailwind.Icons -import fiftyforms.ui.components.tailwind.Avatar -import cz.e_bs.cmi.mdr.pdb.UserInfo -import io.laminext.syntax.core.* - -object NavigationBar: - - case class Logo(img: String, name: String) - case class Link(a: () => Anchor, active: Boolean) - case class MenuItem(title: String) - - case class ViewModel( - userInfo: UserInfo, - pages: List[Link], - userMenu: List[MenuItem], - logo: Logo - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - val $userInfo = $m.map(_.userInfo) - val mobileMenuOpen = Var(false) - - val desktopOnly = cls("hidden md:block") - val mobileOnly = cls("md:hidden") - - inline def avatarImage(size: Int = 8) = - Avatar($userInfo.map(_.img)).avatarImage(size) - - def notificationButton = button( - tpe := "button", - cls := List( - "bg-indigo-600", - "focus:outline-none", - "focus:ring-2", - "focus:ring-offset-2", - "focus:ring-offset-indigo-600", - "focus:ring-white", - "hover:text-white", - "p-1", - "rounded-full", - "text-indigo-200" - ), - span(cls := "sr-only", "View notifications"), - Icons.outline.bell() - ) - - def userProfile: HtmlElement = - val menuOpen = Var(false) - - def menuItem(item: MenuItem, idx: Int): HtmlElement = - a( - href := "#", - cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", - role := "menuitem", - tabIndex := -1, - idAttr := s"user-menu-item-$idx", - item.title - ) - - div( - cls := "ml-3 relative", - div( - button( - tpe := "button", - cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - idAttr := "user-menu-button", - aria.expanded <-- menuOpen.signal, - aria.hasPopup := true, - span(cls := "sr-only", "Open user menu"), - child <-- avatarImage(), - onClick.preventDefault.mapTo( - !menuOpen.now() - ) --> menuOpen.writer - ) - ), - /* - * */ - div( - cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", - cls <-- menuOpen.signal.map { o => - if (o) "md:block" else "md:hidden" - }, - role := "menu", - aria.orientation := "vertical", - aria.labelledBy := "user-menu-button", - tabIndex := -1, - // : keyboard navigation - children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) - ) - ) - - def mobileProfile = - def menuItem(item: MenuItem): HtmlElement = - a( - href := "#", - cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", - item.title - ) - - div( - cls := "pt-4 pb-3 border-t border-indigo-700", - div( - cls := "flex items-center px-5", - div( - cls := "flex-shrink-0", - child <-- avatarImage(10) - ), - div( - cls := "ml-3", - div( - cls := "text-base font-medium text-white", - child.text <-- $userInfo.map(_.name) - ), - child.maybe <-- $userInfo.map( - _.email.map(e => - div( - cls := "text-sm font-medium text-indigo-300", - e - ) - ) - ) - ), - notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) - ), - div( - cls := "mt-3 px-2 space-y-1", - children <-- $m.map(_.userMenu.map(menuItem)) - ) - ) - - def pageLink(page: Link): Anchor = - page - .a() - .amend( - cls := "text-white px-3 py-2 rounded-md text-sm font-medium", - cls := Seq( - "bg-indigo-700" -> page.active, - "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active - ), - ariaCurrent := (if page.active then "page" else "false") - ) - - def logoImg: Image = - img( - cls := "h-8 w-8", - src <-- $m.map(_.logo.img), - alt <-- $m.map(_.logo.name) - ) - - def mobileMenuButton = button( - tpe := "button", - cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - aria.controls := "mobile-menu", - aria.expanded <-- mobileMenuOpen.signal, - span(cls := "sr-only", "Open main menu"), - Icons.outline - .menu() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") - ), - Icons.outline - .x() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") - ), - onClick.preventDefault.mapTo( - !mobileMenuOpen.now() - ) --> mobileMenuOpen.writer - ) - - def navBarLeft = - div( - cls := "flex items-center", - div(cls := "flex-shrink-0", logoImg), - div( - desktopOnly, - div( - cls := "ml-10 flex items-baseline space-x-4", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ) - ) - ) - - def navBarRight = - div( - desktopOnly, - div( - cls := "ml-4 flex items-center md:ml-6", - notificationButton, - userProfile - ) - ) - - def navBarMobile = - div( - cls := "-mr-2 flex", - mobileOnly, - mobileMenuButton - ) - - def navBar = - div( - cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex items-center justify-between h-16", - navBarLeft, - navBarRight, - navBarMobile - ) - ) - - def mobileMenu = - div( - mobileOnly, - cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }, - idAttr := "mobile-menu", - div( - cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ), - mobileProfile - ) - - nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala deleted file mode 100644 index 75339df..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object PageHeader: - def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = - header( - cls := "bg-white shadow-sm", - div( - cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", - h1( - cls := "text-lg leading-6 font-semibold text-gray-900", - Breadcrumbs(actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala deleted file mode 100644 index 5096463..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ /dev/null @@ -1,30 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import fiftyforms.ui.components.tailwind.Loading -import io.laminext.syntax.core.* - -object PageLayout: - case class ViewModel( - navigation: NavigationBar.ViewModel, - content: Option[HtmlElement] - ) - def apply(actionBus: Observer[Action])( - $m: Signal[ViewModel], - mods: Modifier[HtmlElement]* - )(using router: Router[Page]): HtmlElement = - val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) - div( - cls := "h-full flex flex-col", - NavigationBar($m.map(_.navigation)), - child.maybe <-- router.$currentPage.map(_.isRoot) - .switch(None, Some(PageHeader(actionBus))), - main( - cls := "flex-grow-1 overflow-y-auto", - mods, - child <-- $maybeContent.map(_.getOrElse(Loading)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala deleted file mode 100644 index 62d820f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.LinkSupport.* -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo - -object PageLink: - type ViewModel = Page - def apply($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = - a(mods($m, actions), child.text <-- $m.map(_.title)) - - def apply(m: ViewModel, actions: Observer[Action])(using - router: Router[Page] - ): Anchor = apply(Val(m), actions) - - def container($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = a(mods($m, actions)) - - def container(m: ViewModel, actions: Observer[Action])(using - Router[Page] - ): Anchor = container(Val(m), actions) - - private def mods($m: Signal[Page], actions: Observer[Action])(using - router: Router[Page] - ): Modifier[Anchor] = - Seq( - href <-- $m.map(router.absoluteUrlForPage).recover { case _ => - Some("invalid url") - }, - composeEvents(onClick.noKeyMod.preventDefault)( - _.sample($m) - .map(NavigateTo.apply) - ) --> actions - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala deleted file mode 100644 index 87f825a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.pages.dashboard.DashboardPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -class DashboardPageConnector(actionBus: Observer[Action])(using - router: Router[Page] -): - def apply: HtmlElement = - AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala deleted file mode 100644 index e9b0f71..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala +++ /dev/null @@ -1,76 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailKriteriaPage -import pages.detail.components.DukazyKriteria -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -object DetailKriteriaPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailKriteriaPageConnector( - state: DetailKriteriaPageConnector.AppState -)( - $page: Signal[Page.DetailKriteria] -)(using Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => - (p.osobniCislo.value, p.parametr.value, p.kriterium.value) - )((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map( - FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) - ) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, s, $s) => - DetailKriteriaPage($s)(state.actionBus.contramap { - case DukazyKriteria.Add => - NavigateTo( - Page.UpravDukazKriteria( - Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), - Page.Titled(s.parametr.id, Some(s.parametr.nazev)), - Page.Titled(s.kriterium.id, Some(s.kriterium.id)) - ) - ) - }) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): DetailKriteriaPage.ViewModel = - DetailKriteriaPage.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala deleted file mode 100644 index 4d8024f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ /dev/null @@ -1,57 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import pages.detail.DetailPage -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import fiftyforms.ui.components.tailwind.Color - -object DetailPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailPageConnector(state: DetailPageConnector.AppState)( - $page: Signal[Page.Detail] -)(using router: Router[Page]): - val $oscChangeSignal = $page.splitOne(_.osobniCislo.value)((osc, _, _) => osc) - val $pageChangeSignal = - $oscChangeSignal.flatMap(osc => - EventStream.fromSeq(Seq(FetchUserDetails(osc), FetchParameters(osc))) - ) - // TODO: filter the value based on the current osc - // OSC change will fetch new data, but still - // - we need to be sure that what we got is really what we ought to display - // - we want to display stale data accordingly (at least with loading indicator) - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - def apply: HtmlElement = - AppPage(state.actionBus)( - $data.combineWithFn($params)(_ zip _) - .map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: List[Parameter] - ): DetailPage.ViewModel = - DetailPage.ViewModel( - o.toDetailOsoby, - p.map( - _.toParametr(param => - PageLink.container(Page.DetailParametru(o, param), state.actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala deleted file mode 100644 index fdaa7f8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailParametruPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink - -object DetailParametruPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailParametruPageConnector( - state: DetailParametruPageConnector.AppState -)( - $page: Signal[Page.DetailParametru] -)(using router: Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => (p.osobniCislo.value, p.parametr.value))((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map(FetchParameter(_, _)) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - } yield (da, pb) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailParametruPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter - ): DetailParametruPage.ViewModel = - DetailParametruPage.ViewModel( - o.toDetailOsoby, - p.toParametr(p => - PageLink.container(Page.DetailParametru(o, p), state.actionBus) - ), - p.criteria.map( - _.toKriterium { c => - PageLink.container( - Page.DetailKriteria(o, p, c), - state.actionBus - ) - } - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala deleted file mode 100644 index 049aa22..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -case class DirectoryPageConnector( - $input: EventStream[List[UserInfo]], - actionBus: Observer[Action] -)(using router: Router[Page]): - val $data = $input.startWithNone - val $actionSignal = EventStream.fromValue(FetchDirectory) - - def apply: HtmlElement = - AppPage(actionBus)( - $data.split(_ => ())((_, _, s) => - pages.directory.DirectoryPage( - s.map( - _.map( - _.toUserRow(u => - PageLink.container( - Page.Detail(Page.Titled(u.personalNumber)), - actionBus - ) - ) - ) - ) - ) - ), - $actionSignal --> actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala deleted file mode 100644 index 88e7450..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ /dev/null @@ -1,68 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii -import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria -import fiftyforms.ui.components.tailwind.Color - -extension (o: UserInfo) - def toDetailOsoby: DetailOsoby.ViewModel = - DetailOsoby.ViewModel( - o.personalNumber, - o.name, - o.email, - o.phone, - o.img, - o.mainFunction.map(f => - DetailOsoby.Funkce.ViewModel(f.name, f.dept, f.ou) - ), - o.userContracts.headOption.map(c => - DetailOsoby.PracovniPomer.ViewModel(c.rel, c.startDate, c.endDate) - ) - ) - -extension (param: Parameter) - def toParametr(container: Parameter => Anchor): DetailParametru.ViewModel = - DetailParametru.ViewModel( - id = param.id, - nazev = param.name, - popis = param.description, - status = "Nesplněno", - statusColor = Color.red, - a = container(param) - ) - -extension (crit: ParameterCriteria) - def toKriterium( - container: ParameterCriteria => Anchor - ): DetailKriteria.ViewModel = - DetailKriteria.ViewModel( - nazev = crit.criteriumText, - kapitola = crit.chapterId, - bod = crit.itemId, - status = "Nesplněno", - statusColor = Color.red, - splneno = false, - dukazy = Nil, - container = container(crit) - ) - -extension (user: UserInfo) - def toUserRow( - container: UserInfo => HtmlElement = (_: UserInfo) => div() - ): UserRow.ViewModel = - UserRow.ViewModel( - osobniCislo = user.personalNumber.toString, - celeJmeno = user.name, - prijmeni = user.surname, - hlavniFunkce = user.mainFunction.map(_.name), - img = user.img, - container = () => container(user) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala deleted file mode 100644 index 7d5b2c1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.dashboard - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.Page - -object DashboardPage: - - def render: HtmlElement = - div("Dashboard page") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala deleted file mode 100644 index 9ef0698..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala +++ /dev/null @@ -1,31 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailKriteriaPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply($m: Signal[ViewModel])( - action: Observer[DukazyKriteria.Action] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - DukazyKriteria($m.map(_.kriterium.dukazy))(action) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala deleted file mode 100644 index 3b316e1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object DetailPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametry: SeznamParametru.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby($m.map(_.osoba)), - SeznamParametru($m.map(_.parametry)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala deleted file mode 100644 index 17224cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala +++ /dev/null @@ -1,24 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailParametruPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriteria: SeznamKriterii.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby.header($m.map(_.osoba)), - DetailParametru($m.map(_.parametr)), - SeznamKriterii($m.map(_.kriteria)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala deleted file mode 100644 index dc912fa..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ /dev/null @@ -1,107 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb -package app -package pages.detail - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.UpravDukazForm -import fiftyforms.services.files.File - -object UpravDukaz: - - type ThisPage = Page.UpravDukazKriteria - type PageKey = (OsobniCislo, String, String) - - trait State { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def availableFiles: EventStream[List[File]] - def actionBus: Observer[Action] - } - - def keyOfPage(page: ThisPage): PageKey = - (page.osobniCislo.value, page.parametr.value, page.kriterium.value) - - def onChangeAction(key: PageKey): Action = - FetchParameterCriteria( - key._1, - key._2, - key._3, - Page.UpravDukazKriteria(_, _, _) - ) - - class Connector(state: State)($page: Signal[ThisPage])(using Router[Page]): - val $paramChangeSignal = $page.splitOne(keyOfPage)((x, _, _) => x) - val $pageChangeSignal = $paramChangeSignal.map(onChangeAction) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.split(_ => ())((_, s, $s) => - PageComponent( - $s.map(buildModel), - state.availableFiles, - state.actionBus.contramap { - case UpravDukazForm.Cancelled => - NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) - case UpravDukazForm.AvailableFilesRequested => - FetchAvailableFiles(s._1.personalNumber) - } - ) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): PageComponent.ViewModel = - import connectors.* - PageComponent.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) - - object PageComponent: - import components.* - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply( - $m: Signal[ViewModel], - availableFilesStream: EventStream[List[File]], - events: Observer[UpravDukazForm.Event] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - UpravDukazForm(availableFilesStream)(events) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala deleted file mode 100644 index ca311b0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.Icons -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Color - -object DetailKriteria: - case class ViewModel( - nazev: String, - kapitola: String, - bod: String, - status: String, - statusColor: Color, - splneno: Boolean, - dukazy: List[DukazKriteria.ViewModel], - container: HtmlElement = div() - ) { - val id = s"${kapitola}${bod}" - } - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h3( - cls := "text-xl leading-6 font-bold text-gray-900", - child.text <-- $m.map(_.nazev) - ), - h3( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - "Kapitola ", - child.text <-- $m.map(_.kapitola), - " bod ", - child <-- $m.map(_.bod) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala deleted file mode 100644 index ae90290..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala +++ /dev/null @@ -1,100 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Avatar - -object DetailOsoby: - - object Funkce: - case class ViewModel( - nazev: String, - stredisko: String, - voj: String - ) - - def render($m: Signal[ViewModel]): HtmlElement = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.nazev), - span( - cls := "hidden md:inline", - " @ ", - child.text <-- $m.map(_.stredisko), - ", ", - child.text <-- $m.map(_.voj) - ) - ) - - object PracovniPomer: - case class ViewModel( - druh: String, - pocatek: LocalDate, - konec: Option[LocalDate] - ) - - def render($m: Signal[ViewModel]): HtmlElement = - import fiftyforms.ui.components.tailwind.CustomAttrs.datetime - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.druh), - " od ", - time( - datetime <-- $m.map(_.pocatek.toString), - child.text <-- $m.map(_.pocatek.toString) - ) - ) - - case class ViewModel( - osobniCislo: OsobniCislo, - jmeno: String, - email: Option[String], - telefon: Option[String], - img: Option[String], - hlavniFunkce: Option[Funkce.ViewModel], - pracovniPomer: Option[PracovniPomer.ViewModel] - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-4", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(16) - ), - div( - h1( - cls := "text-2xl font-bold text-gray-900", - child.text <-- $m.map(_.jmeno) - ), - child.maybe <-- $m.map(_.hlavniFunkce).split(_ => ())((_, _, d) => - Funkce.render(d) - ), - child.maybe <-- $m.map(_.pracovniPomer).split(_ => ())((_, _, d) => - PracovniPomer.render(d) - ) - ) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-5", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(8) - ), - div( - h1( - cls := "text-lg font-medium text-gray-800", - child.text <-- $m.map(_.jmeno) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala deleted file mode 100644 index e11cb62..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color - -object DetailParametru: - case class ViewModel( - id: String, - nazev: String, - popis: String, - status: String, - statusColor: Color, - a: Anchor - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h2( - cls := "text-2xl leading-6 font-medium text-gray-900", - child.text <-- $m.map(_.nazev) - ), - p( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - child.text <-- $m.map(_.popis) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - h2( - cls := "text-lg leading-6 font-medium text-gray-600", - child.text <-- $m.map(_.nazev) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala deleted file mode 100644 index a00ede8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala +++ /dev/null @@ -1,177 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.CustomAttrs - -object DukazKriteria: - case class Osoba(osobniCislo: String, jmeno: String) - case class Udalost(osoba: Osoba, datum: LocalDate) - case class Dokument( - url: String, - nazev: String, - pridal: Udalost, - odebral: Option[Udalost] - ) - case class ViewModel( - dokumenty: List[Dokument], - autorizoval: Option[Udalost], - platiDo: Option[LocalDate], - poznámka: Option[String] - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-lg", - div( - cls := "px-4 py-5 sm:px-6", - h3( - cls := "text-lg leading-6 font-medium text-gray-900", - """Applicant Information""" - ), - p( - cls := "mt-1 max-w-2xl text-sm text-gray-500", - """Personal details and application.""" - ) - ), - div( - cls := "border-t border-gray-200 px-4 py-5 sm:px-6", - dl( - cls := "grid grid-cols-1 gap-x-4 gap-y-8 sm:grid-cols-2", - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Full name""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Margot Foster""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Application for""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Backend Developer""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Email address""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """margotfoster@example.com""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Salary expectation""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """$120,000""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """About""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Fugiat ipsum ipsum deserunt culpa aute sint do nostrud anim incididunt cillum culpa consequat. Excepteur qui ipsum aliquip consequat sint. Sit id mollit nulla mollit nostrud in ea officia proident. Irure nostrud pariatur mollit ad adipisicing reprehenderit deserunt qui eu.""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """Attachments""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - ul( - role := "list", - cls := "border border-gray-200 rounded-md divide-y divide-gray-200", - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """resume_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ), - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """coverletter_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala deleted file mode 100644 index 22f134f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala +++ /dev/null @@ -1,35 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object DukazyKriteria: - sealed trait Action - case object Add extends Action - - type ViewModel = List[DukazKriteria.ViewModel] - - def apply($m: Signal[ViewModel])(actions: Observer[Action]): HtmlElement = - div( - child.maybe <-- $m.map(m => - if (m.isEmpty) then Some(prazdnyDukaz(actions)) - else None - ), - children <-- $m.map(_.zipWithIndex).split(_._2)((_, _, $s) => - DukazKriteria($s.map(_._1)) - ) - ) - - private def prazdnyDukaz(actions: Observer[Action]): HtmlElement = - button( - tpe := "button", - onClick.preventDefault.mapTo(Add) --> actions, - cls := "relative block w-full border-2 border-gray-300 border-dashed rounded-lg p-12 text-center hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - Icons.outline - .`document-add`(12) - .amend(svg.cls := "mx-auto text-gray-400"), - span( - cls := "mt-2 block text-sm font-medium text-gray-900", - "Přidat důkaz" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala deleted file mode 100644 index f628697..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.list.StackedList -import fiftyforms.ui.components.tailwind.list.ListRow -import fiftyforms.ui.components.tailwind.list.RowTag -import fiftyforms.ui.components.tailwind.list.RowNext -import java.time.LocalDate - -object SeznamKriterii: - type ViewModel = List[DetailKriteria.ViewModel] - - private val kritList = new StackedList[DetailKriteria.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - kritList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = - p(cls := "text-sm text-gray-500", s"${i.kapitola}${i.bod}"), - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.container - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala deleted file mode 100644 index 2bfe4bc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.list.{ - StackedList, - ListRow, - RowTag, - PropList, - IconText, - RowNext -} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.LinkSupport.* - -object SeznamParametru: - type ViewModel = List[DetailParametru.ViewModel] - - private val parametrList = new StackedList[DetailParametru.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - parametrList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = emptyNode, - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.a - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala deleted file mode 100644 index f048e9d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ /dev/null @@ -1,101 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.form.* -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement -import fiftyforms.services.files.components.tailwind.FilePicker -import cz.e_bs.cmi.mdr.pdb.frontend.AutorizujDukaz -import cz.e_bs.cmi.mdr.pdb.frontend.DocumentRef -import fiftyforms.services.files.File - -object UpravDukazForm: - sealed trait Event - case object Cancelled extends Event - case object AvailableFilesRequested extends Event - def apply(availableFilesStream: EventStream[List[File]])( - updates: Observer[Event] - ): HtmlElement = - val (filesStream, filesObserver) = - EventStream.withObserver[FilePicker.Event] - val files = Var[List[File]](Nil) - def submitButtons: HtmlElement = - div( - cls := "pt-5", - div( - cls := "flex justify-end", - button( - tpe := "button", - cls := "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zrušit", - onClick.mapTo(Cancelled) --> updates - ), - button( - tpe := "submit", - cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - disabled <-- files.signal.map(_.isEmpty), - "Autorizovat" - ) - ) - ) - - div( - filesStream.collect { case FilePicker.SelectionUpdated(files) => - files.to(List) - } --> files.writer, - filesStream.collect { case FilePicker.AvailableFilesRequested => - AvailableFilesRequested - } --> updates, - cls := "bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6", - Form( - Form.Body( - Form.Section( - FormHeader( - FormHeader.ViewModel( - "Doložení kritéria", - "Sestavte doklady poskytující důkaz kritéria a potvrďte odesláním formuláře. Případné limitace či jiné relevantní údaje vepište do pole Komentář." - ) - ), - FormFields( - FormRow( - "dokumenty", - "Dokumenty", - FilePicker(files.signal, availableFilesStream)(filesObserver) - .amend(idAttr := "dokumenty", cls("max-w-lg")) - ).toHtml, - FormRow( - "platnost", - "Platnost", - input( - idAttr := "platnost", - name := "platnost", - tpe := "date", - autoComplete := "date", - cls := "block max-w-lg w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" - ) - ).toHtml, - FormRow( - "komentar", - "Komentář", - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - textArea( - idAttr := "komentar", - name := "about", - rows := 3, - cls := "max-w-lg shadow-sm block w-full focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border border-gray-300 rounded-md" - ), - p( - cls := "mt-2 text-sm text-gray-500", - "Doplňte prosím potřebné informace související s doložením kritéria, včetně případných limitací." - ) - ) - ).toHtml - ) - ) - ), - submitButtons - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala deleted file mode 100644 index 41033f6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -import components._ - -object DirectoryPage: - - type ViewModel = List[UserRow.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - val (actionsStream, actionObserver) = - EventStream.withObserver[SearchForm.Action] - val $filter = actionsStream - .collect { - case SearchForm.ClearFilter => None - case SearchForm.SetFilter(t) => Some(t) - } - .startWith(None) - val byLetter = for { - d <- $m - f <- $filter - } yield for { - (letter, users) <- d - .filter { user => - f match - case None => true - case Some(t) => user.search.contains(t) - } - .groupBy(_.prijmeni.head) - .to(List) - .sortBy(_._1) - } yield (letter.toString, users.sortBy(_.prijmeni)) - - div( - cls := "h-full max-w-7xl mx-auto order-first flex flex-col flex-shrink-0", - SearchForm(actionObserver), - Directory(byLetter) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala deleted file mode 100644 index 73f915d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -object Directory: - - object Header: - type ViewModel = String - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500", - h3(child.text <-- $m) - ) - - object UserList: - type ViewModel = List[UserRow.ViewModel] - def apply($m: Signal[ViewModel]): HtmlElement = - ul( - role := "list", - cls := "relative z-0 divide-y divide-gray-200", - children <-- $m.split(_.osobniCislo)((_, _, s) => UserRow(s)) - ) - - type ViewModel = List[(String, List[UserRow.ViewModel])] - def apply($m: Signal[ViewModel]): HtmlElement = - nav( - cls := "flex-1 min-h-0 overflow-y-auto", - aria.label := "Directory", - children <-- $m.split(_._1)((_, _, s) => - div( - cls := "relative", - Header(s.map(_._1)), - UserList(s.map(_._2)) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala deleted file mode 100644 index e182910..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala +++ /dev/null @@ -1,54 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object SearchForm: - sealed trait Action - case object SubmitFilter extends Action - sealed trait FilterAction extends Action - case object ClearFilter extends FilterAction - case class SetFilter(value: String) extends FilterAction - - def apply(actions: Observer[Action]): HtmlElement = - div( - cls := "px-6 pt-4 pb-4", - form( - cls := "flex space-x-4", - onSubmit.mapTo(SubmitFilter) --> actions, - div( - cls := "flex-1 min-w-0", - label( - forId := "search", - cls := "sr-only", - "Hledat" - ), - div( - cls := "relative rounded-md shadow-sm", - div( - cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", - Icons.solid.search().amend(svg.cls := "text-gray-400") - ), - input( - tpe := "search", - name := "search", - idAttr := "search", - cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", - placeholder := "Hledat", - composeEvents(onInput.mapToValue.setAsValue.map(SetFilter(_)))( - _.throttle(500) - ) --> actions - ) - ) - ), - button( - tpe := "submit", - cls := "inline-flex justify-center px-3.5 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500", - Icons.solid.filter().amend(svg.cls := "text-gray-400"), - span( - cls := "sr-only", - "Hledat" - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala deleted file mode 100644 index 82f7723..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala +++ /dev/null @@ -1,52 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.airstream.core.Signal -import fiftyforms.ui.components.tailwind.Avatar - -object UserRow: - case class ViewModel( - osobniCislo: String, - celeJmeno: String, - prijmeni: String, - hlavniFunkce: Option[String], - img: Option[String], - container: () => HtmlElement = () => div() - ) { - val search = prijmeni.toLowerCase - } - - def apply($m: Signal[ViewModel]): HtmlElement = - inline def avatarImage = - Avatar($m.map(_.img)).avatarImage(10) - - li( - div( - cls := "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500", - div( - cls := "flex-shrink-0", - child <-- avatarImage - ), - div( - cls := "flex-1 min-w-0", - child <-- $m.map { o => - o.container() - .amend( - cls := "focus:outline-none", - span( - cls := "absolute inset-0", - aria.hidden := true - ), - p( - cls := "text-sm font-medium text-gray-900", - o.celeJmeno - ), - p( - cls := "text-sm text-gray-500 truncate", - o.hlavniFunkce - ) - ) - } - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala deleted file mode 100644 index 467c5f5..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala +++ /dev/null @@ -1,99 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router - -object ErrorPage: - case class ViewModel( - homePage: Page, - errorName: String, - title: String, - subTitle: String - ) - - def apply(m: ViewModel, actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - val ViewModel(homePage, errorName, title, subTitle) = m - div( - cls := "min-h-full pt-16 pb-12 flex flex-col bg-white", - main( - cls := "flex-grow flex flex-col justify-center max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex-shrink-0 flex justify-center", - a( - href := "/", - cls := "inline-flex", - span( - cls := "sr-only", - """Workflow""" - ), - img( - cls := "h-12 w-auto", - src := "https://tailwindui.com/img/logos/workflow-mark.svg?color=indigo&shade=600", - alt := "" - ) - ) - ), - div( - cls := "py-16", - div( - cls := "text-center", - p( - cls := "text-sm font-semibold text-indigo-600 uppercase tracking-wide", - errorName - ), - h1( - cls := "mt-2 text-4xl font-extrabold text-gray-900 tracking-tight sm:text-5xl", - title - ), - p( - cls := "mt-2 text-base text-gray-500", - subTitle - ), - div( - cls := "mt-6", - PageLink - .container(homePage, actionBus) - .amend( - cls := "text-base font-medium text-indigo-600 hover:text-indigo-500", - """Go back home""" - ) - ) - ) - ) - ), - footer( - cls := "flex-shrink-0 max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", - nav( - cls := "flex justify-center space-x-4", - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Contact Support""" - ), - span( - cls := "inline-block border-l border-gray-300", - aria.hidden := true - ), - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Status""" - ), - span( - cls := "inline-block border-l border-gray-300", - aria.hidden := true - ), - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Twitter""" - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala deleted file mode 100644 index bd9baec..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action - -object NotFoundPage: - def apply(homePage: Page, url: String, actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - ErrorPage( - ErrorPage.ViewModel( - homePage, - "404 error", - "Page not found.", - s"Sorry, but page $url doesn't exist." - ), - actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala deleted file mode 100644 index 476fe3c..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action - -object UnhandledErrorPage: - - case class ViewModel( - homePage: Page, - errorName: Option[String], - errorMessage: Option[String] - ) - - def apply(m: ViewModel, actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - ErrorPage( - ErrorPage.ViewModel( - m.homePage, - "Unexpected error occurred", - m.errorName.getOrElse( - "Uh oh!" - ), // TODO: translations, better text than uh oh - m.errorMessage.getOrElse( - "This wasn't supposed to happen! Please try again." - ) - ), - actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala deleted file mode 100644 index 25f059f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala +++ /dev/null @@ -1,6 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.services - -import com.raquo.airstream.core.Observer - -trait DataFetcher[K, A]: - def fetch(id: K, sink: Observer[A]): Unit diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala deleted file mode 100644 index a469830..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala +++ /dev/null @@ -1,120 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package state - -import com.raquo.airstream.core.EventStream -import cz.e_bs.cmi.mdr.pdb.{UserInfo, OsobniCislo} -import com.raquo.airstream.core.Observer -import scala.scalajs.js -import scala.scalajs.js.JSON -import zio.json._ -import com.raquo.airstream.eventbus.EventBus -import com.raquo.airstream.ownership.Owner -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.UserFunction -import cz.e_bs.cmi.mdr.pdb.UserContract -import fiftyforms.services.files.File - -trait AppState - extends connectors.DetailPageConnector.AppState - with connectors.DetailParametruPageConnector.AppState - with connectors.DetailKriteriaPageConnector.AppState - with pages.detail.UpravDukaz.State: - def users: EventStream[List[UserInfo]] - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - -class MockAppState(implicit owner: Owner, router: Router[Page]) - extends AppState: - - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonDecoder[UserFunction] = DeriveJsonDecoder.gen - given JsonDecoder[UserContract] = DeriveJsonDecoder.gen - given JsonDecoder[UserInfo] = DeriveJsonDecoder.gen - - given JsonDecoder[ParameterCriteria] = DeriveJsonDecoder.gen - given JsonDecoder[Parameter] = DeriveJsonDecoder.gen - - private val actions = EventBus[Action]() - private val (parametersStream, pushParameters) = - EventStream.withCallback[List[Parameter]] - private val (usersStream, pushUsers) = - EventStream.withCallback[List[UserInfo]] - private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] - private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] - - private val mockData: List[UserInfo] = - mockUsers - .asInstanceOf[js.Dictionary[js.Object]] - .values - // TODO: is there a more efficient way to parse from JS object directly? - .map { o => - val parsed = JSON.stringify(o).fromJson[UserInfo] - parsed.left.foreach(org.scalajs.dom.console.log(_)) - parsed - } - .collect { case Right(u) => - u - } - .toList - - private val mockParameters: List[Parameter] = - pdbParams - .asInstanceOf[js.Dictionary[js.Object]] - .values - .map(o => JSON.stringify(o).fromJson[Parameter]) - .collect { case Right(p) => p } - .toList - - // TODO: Extract to separate event handler - actions.events.foreach { - case FetchDirectory => pushUsers(mockData) - case FetchUserDetails(osc) => - mockData.find(_.personalNumber == osc).foreach { o => - pushDetails(o) - router.replaceState(Page.Detail(o)) - } - case FetchParameters(osc) => - pushParameters(mockParameters) - case FetchParameter(osc, paramId) => - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(Page.DetailParametru(o, p)) - case FetchParameterCriteria(osc, paramId, critId, page) => - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - c <- p.criteria.find(_.id == critId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(page(o, p, c)) - case NavigateTo(page) => router.pushState(page) - case FetchAvailableFiles(osc) => - pushFiles( - List( - File("https://tc163.cmi.cz/here", "Example file") - ) - ) - } - - override def users: EventStream[List[UserInfo]] = - usersStream.debugWithName("users") - - override def details: EventStream[UserInfo] = - detailsStream.debugWithName("details") - - override def parameters: EventStream[List[Parameter]] = - parametersStream.debugWithName("parameters") - - override def availableFiles: EventStream[List[File]] = - filesStream.debugWithName("available files") - - override def actionBus: Observer[Action] = - actions.writer.debugWithName("actions writer") diff --git a/app/src/main/scala/mdr/pdb/app/Main.scala b/app/src/main/scala/mdr/pdb/app/Main.scala new file mode 100644 index 0000000..081b4ee --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/Main.scala @@ -0,0 +1,103 @@ +package mdr.pdb.app + +import scala.scalajs.js.annotation.JSExportTopLevel +import scala.scalajs.js.annotation.JSExport +import scala.scalajs.js.annotation.JSImport +import scala.scalajs.js +import org.scalajs.dom +import com.raquo.laminar.api.L.{*, given} +import scala.scalajs.js.Date +import com.raquo.waypoint.Router +import com.raquo.waypoint.SplitRender +import mdr.pdb.app.services.DataFetcher +import scala.scalajs.js.JSON +import zio.json._ +import mdr.pdb.UserInfo +import mdr.pdb.app.state.AppState + +@js.native +@JSImport("stylesheets/main.css", JSImport.Namespace) +object Css extends js.Any + +@js.native +@JSImport("data/users.json", JSImport.Default) +object mockUsers extends js.Object + +@js.native +@JSImport("params/pdb-params.json", JSImport.Default) +object pdbParams extends js.Object + +@JSExportTopLevel("app") +object Main: + + @JSExport + def main(args: Array[String]): Unit = + import Routes.given + onLoad { + setupAirstream() + val appContainer = dom.document.querySelector("#app") + val _ = + render( + appContainer, + renderPage(state.MockAppState(using unsafeWindowOwner, router)) + ) + } + + private def onLoad(f: => Unit): Unit = + documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) + + private def setupAirstream()(using router: Router[Page]): Unit = + AirstreamError.registerUnhandledErrorCallback(err => + router.forcePage( + Page.UnhandledError( + Some(err.getClass.getName), // TODO: Fill only in dev mode + Some(err.getMessage) + ) + ) + ) + + def renderPage(state: AppState)(using + router: Router[Page] + ): HtmlElement = + val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) + .collectSignal[Page.Detail]( + connectors + .DetailPageConnector(state)(_) + .apply + ) + .collectSignal[Page.DetailParametru]( + connectors + .DetailParametruPageConnector(state)(_) + .apply + ) + .collectSignal[Page.DetailKriteria]( + connectors + .DetailKriteriaPageConnector(state)(_) + .apply + ) + .collectSignal[Page.UpravDukazKriteria]( + pages.detail.UpravDukaz.Connector(state)(_).apply + ) + .collectStatic(Page.Dashboard)( + connectors.DashboardPageConnector(state.actionBus).apply + ) + .collect[Page.NotFound](pg => + pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) + ) + .collect[Page.UnhandledError](pg => + pages.errors + .UnhandledErrorPage( + pages.errors.UnhandledErrorPage + .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), + state.actionBus + ) + ) + .collectStatic(Page.Directory)( + connectors + .DirectoryPageConnector(state.users, state.actionBus) + .apply + ) + div(cls := "h-full", child <-- pageSplitter.$view) + + // Pull in the stylesheet + val css: Css.type = Css diff --git a/app/src/main/scala/mdr/pdb/app/Routes.scala b/app/src/main/scala/mdr/pdb/app/Routes.scala new file mode 100644 index 0000000..2cc860c --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/Routes.scala @@ -0,0 +1,198 @@ +package mdr.pdb.app + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.* +import org.scalajs.dom +import zio.json.{*, given} +import mdr.pdb.OsobniCislo + +import scala.scalajs.js +import mdr.pdb.UserInfo +import mdr.pdb.Parameter +import mdr.pdb.ParameterCriteria +import mdr.pdb.app.Page.Titled + +// enum is not working with Waypoints' SplitRender collectStatic +sealed abstract class Page( + val id: String, + val title: String, + val parent: Option[Page] +) { + val path: Vector[Page] = + parent match + case None => Vector(this) + case Some(p) => p.path :+ this + + val isRoot: Boolean = parent.isEmpty +} + +object Page: + + case class Titled[V](value: V, title: Option[String] = None): + val show: String = title.getOrElse(value.toString) + + case object Directory extends Page("directory", "Adresář", None) + + case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) + + case class Detail(osobniCislo: Titled[OsobniCislo]) + extends Page("user", osobniCislo.show, Some(Directory)) + + object Detail { + def apply(o: UserInfo): Detail = Detail( + Titled(o.personalNumber, Some(o.name)) + ) + } + + case class DetailParametru( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String] + ) extends Page( + "parameter", + parametr.show, + Some(Detail(osobniCislo)) + ) + + object DetailParametru { + def apply(o: UserInfo, p: Parameter): DetailParametru = + DetailParametru( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)) + ) + } + + case class DetailKriteria( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String], + kriterium: Titled[String] + ) extends Page( + "criteria", + kriterium.show, + Some(DetailParametru(osobniCislo, parametr)) + ) + + object DetailKriteria { + def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = + DetailKriteria( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)), + Titled(k.id, Some(k.id)) + ) + } + + case class UpravDukazKriteria( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String], + kriterium: Titled[String] + ) extends Page( + "addProof", + "Důkaz", + Some(DetailKriteria(osobniCislo, parametr, kriterium)) + ) + + object UpravDukazKriteria { + def apply( + o: UserInfo, + p: Parameter, + k: ParameterCriteria + ): UpravDukazKriteria = + UpravDukazKriteria( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)), + Titled(k.id, Some(k.id)) + ) + } + + case class NotFound(url: String) extends Page("404", "404", Some(Directory)) + + case class UnhandledError( + errorName: Option[String], + errorMessage: Option[String] + ) extends Page("500", "Unexpected error", Some(Directory)) + +object Routes: + given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) + given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) + given [V: JsonEncoder]: JsonEncoder[Titled[V]] = + DeriveJsonEncoder.gen[Titled[V]] + given [V: JsonDecoder]: JsonDecoder[Titled[V]] = + DeriveJsonDecoder.gen[Titled[V]] + given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] + given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] + + val base = + js.`import`.meta.env.BASE_URL + .asInstanceOf[String] + "app" + + val homePage: Page = Page.Directory + + given router: Router[Page] = Router[Page]( + routes = List( + Route.static(homePage, root / endOfSegments, basePath = base), + Route.static( + Page.Dashboard, + root / "dashboard" / endOfSegments, + basePath = base + ), + Route[Page.Detail, String]( + encode = _.osobniCislo.value.toString, + decode = osc => Page.Detail(Titled(OsobniCislo(osc))), + root / "osoba" / segment[String] / endOfSegments, + basePath = base + ), + Route[Page.DetailParametru, (String, String)]( + encode = p => (p.osobniCislo.value.toString, p.parametr.value), + decode = + p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / endOfSegments, + basePath = base + ), + Route[Page.DetailKriteria, (String, String, String)]( + encode = p => + ( + p.osobniCislo.value.toString, + p.parametr.value, + p.kriterium.value.replaceAll("\\.", "--") + ), + decode = p => + Page.DetailKriteria( + Titled(OsobniCislo(p._1)), + Titled(p._2), + Titled(p._3.replaceAll("--", ".")) + ), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / "kriterium" / segment[String] / endOfSegments, + basePath = base + ), + Route[Page.UpravDukazKriteria, (String, String, String)]( + encode = p => + ( + p.osobniCislo.value.toString, + p.parametr.value, + p.kriterium.value.replaceAll("\\.", "--") + ), + decode = p => + Page.UpravDukazKriteria( + Titled(OsobniCislo(p._1)), + Titled(p._2), + Titled(p._3.replaceAll("--", ".")) + ), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / "kriterium" / segment[String] / "edit" / endOfSegments, + basePath = base + ) + ), + serializePage = _.toJson, + deserializePage = _.fromJson[Page] + .fold(s => throw IllegalStateException(s), identity), + getPageTitle = _.title, + routeFallback = url => Page.NotFound(url), + deserializeFallback = _ => Page.Dashboard + )( + $popStateEvent = windowEvents.onPopState, + owner = unsafeWindowOwner + ) diff --git a/app/src/main/scala/mdr/pdb/app/actions.scala b/app/src/main/scala/mdr/pdb/app/actions.scala new file mode 100644 index 0000000..d078b3a --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/actions.scala @@ -0,0 +1,21 @@ +package mdr.pdb.app + +import mdr.pdb.OsobniCislo +import mdr.pdb.UserInfo +import mdr.pdb.Parameter +import mdr.pdb.ParameterCriteria + +sealed trait Action + +case object FetchDirectory extends Action +case class FetchUserDetails(osc: OsobniCislo) extends Action +case class FetchParameters(osc: OsobniCislo) extends Action +case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action +case class FetchParameterCriteria( + osc: OsobniCislo, + paramId: String, + critId: String, + page: (UserInfo, Parameter, ParameterCriteria) => Page +) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action +case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/mdr/pdb/app/components/AppPage.scala new file mode 100644 index 0000000..130c817 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/AppPage.scala @@ -0,0 +1,82 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.Page +import mdr.pdb.{UserProfile, UserInfo, OsobniCislo} +import com.raquo.waypoint.Router +import mdr.pdb.app.Action +import mdr.pdb.app.NavigateTo +import mdr.pdb.UserFunction + +object AppPage: + // TODO: pages by logged in user + val pages: List[Page] = List(Page.Directory, Page.Dashboard) + + import NavigationBar.{Logo, MenuItem} + + val logo = Logo( + "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", + "Workflow" + ) + + // TODO: menu items by user profile + val userMenu = + List( + MenuItem("Your Profile"), + MenuItem("Settings"), + MenuItem("Sign out") + ) + + // TODO: load user profile + val $userProfile = Var( + UserProfile( + "tom", + UserInfo( + OsobniCislo("1031"), + "tom", + "Tom", + "Cook", + None, + None, + Some("tom@example.com"), + Some("+420 222 866 180"), + Some( + UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") + ), + Nil, + None + ) + ) + ) + + val $userInfo = $userProfile.signal.map(_.userInfo) + + type ViewModel = Option[HtmlElement] + def apply( + actionBus: Observer[Action] + )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using + router: Router[Page] + ): HtmlElement = + PageLayout(actionBus)( + $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => + PageLayout.ViewModel( + NavigationBar.ViewModel( + u, + pages.map(p => + NavigationBar.Link( + () => + PageLink( + p, + actionBus + ), + p == cp + ) + ), + userMenu, + logo + ), + c + ) + ), + mods + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala new file mode 100644 index 0000000..9375398 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala @@ -0,0 +1,143 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.tailwind.list.IconText.ViewModel +import fiftyforms.ui.components.tailwind.Icons +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.Page + +import com.raquo.laminar.api.L.{*, given} +import io.laminext.syntax.core.{*, given} +import mdr.pdb.app.Action + +object Breadcrumbs: + + private def slash = + import svg.{*, given} + svg( + cls := "flex-shrink-0 h-5 w-5 text-gray-300", + xmlns := "http://www.w3.org/2000/svg", + fill := "currentColor", + viewBox := "0 0 20 20", + ariaHidden := true, + path( + d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" + ) + ) + + def backIcon = + Icons.solid + .`arrow-narrow-left`() + .amend( + svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" + ) + + object Link: + case class ViewModel( + page: Page, + icon: Option[SvgElement], + extraClasses: String, + text: String, + textClasses: Option[String] = None + ) + + val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" + + def shortHome(p: Page) = ViewModel( + p, + Some(backIcon), + "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", + "Zpět na úvodní stránku", + None + ) + + def fullHome(p: Page) = ViewModel( + p, + Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), + "text-gray-400 hover:text-gray-500", + "Domů", + Some("sr-only") + ) + + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + PageLink + .container($m.map(_.page), actionBus) + .amend( + cls <-- $m.map(_.extraClasses), + child.maybe <-- $m.map(_.icon), + span( + cls <-- $m.map(_.textClasses.getOrElse("")), + child.text <-- $m.map(_.text) + ) + ) + + object FullBreadcrumbs: + type ViewModel = Page + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + ol( + role := "list", + cls := "flex items-center space-x-4", + children <-- $m.map(_.path).split(_.id)((_, _, $p) => + li( + div( + cls := "flex items-center", + child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), + Link( + $p.map(p => + if (p.isRoot) then Link.fullHome(p) + else + Link.ViewModel( + p, + None, + s"ml-4 max-w-xs truncate ${Link.baseClasses}", + p.title + ) + ), + actionBus + ) + ) + ) + ) + ) + + object ShortBreadcrumbs: + type ViewModel = Page + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + Link( + $m.map { p => + val target = p.parent.getOrElse(p) + if target.isRoot then Link.shortHome(target) + else + Link.ViewModel( + target, + Some(backIcon), + "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", + s"Zpět na ${target.title}" + ) + }, + actionBus + ) + + def apply(actionBus: Observer[Action])(using + router: Router[Page] + ): HtmlElement = + nav( + cls := "flex", + aria.label := "Breadcrumb", + div( + cls := "flex sm:hidden", + ShortBreadcrumbs(router.$currentPage, actionBus) + ), + div( + cls := "hidden sm:block", + FullBreadcrumbs(router.$currentPage, actionBus) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala new file mode 100644 index 0000000..4bb8b8d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala @@ -0,0 +1,242 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent +import fiftyforms.ui.components.tailwind.Icons +import fiftyforms.ui.components.tailwind.Avatar +import mdr.pdb.UserInfo +import io.laminext.syntax.core.* + +object NavigationBar: + + case class Logo(img: String, name: String) + case class Link(a: () => Anchor, active: Boolean) + case class MenuItem(title: String) + + case class ViewModel( + userInfo: UserInfo, + pages: List[Link], + userMenu: List[MenuItem], + logo: Logo + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + val $userInfo = $m.map(_.userInfo) + val mobileMenuOpen = Var(false) + + val desktopOnly = cls("hidden md:block") + val mobileOnly = cls("md:hidden") + + inline def avatarImage(size: Int = 8) = + Avatar($userInfo.map(_.img)).avatarImage(size) + + def notificationButton = button( + tpe := "button", + cls := List( + "bg-indigo-600", + "focus:outline-none", + "focus:ring-2", + "focus:ring-offset-2", + "focus:ring-offset-indigo-600", + "focus:ring-white", + "hover:text-white", + "p-1", + "rounded-full", + "text-indigo-200" + ), + span(cls := "sr-only", "View notifications"), + Icons.outline.bell() + ) + + def userProfile: HtmlElement = + val menuOpen = Var(false) + + def menuItem(item: MenuItem, idx: Int): HtmlElement = + a( + href := "#", + cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", + role := "menuitem", + tabIndex := -1, + idAttr := s"user-menu-item-$idx", + item.title + ) + + div( + cls := "ml-3 relative", + div( + button( + tpe := "button", + cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", + idAttr := "user-menu-button", + aria.expanded <-- menuOpen.signal, + aria.hasPopup := true, + span(cls := "sr-only", "Open user menu"), + child <-- avatarImage(), + onClick.preventDefault.mapTo( + !menuOpen.now() + ) --> menuOpen.writer + ) + ), + /* + * */ + div( + cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", + cls <-- menuOpen.signal.map { o => + if (o) "md:block" else "md:hidden" + }, + role := "menu", + aria.orientation := "vertical", + aria.labelledBy := "user-menu-button", + tabIndex := -1, + // : keyboard navigation + children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) + ) + ) + + def mobileProfile = + def menuItem(item: MenuItem): HtmlElement = + a( + href := "#", + cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", + item.title + ) + + div( + cls := "pt-4 pb-3 border-t border-indigo-700", + div( + cls := "flex items-center px-5", + div( + cls := "flex-shrink-0", + child <-- avatarImage(10) + ), + div( + cls := "ml-3", + div( + cls := "text-base font-medium text-white", + child.text <-- $userInfo.map(_.name) + ), + child.maybe <-- $userInfo.map( + _.email.map(e => + div( + cls := "text-sm font-medium text-indigo-300", + e + ) + ) + ) + ), + notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) + ), + div( + cls := "mt-3 px-2 space-y-1", + children <-- $m.map(_.userMenu.map(menuItem)) + ) + ) + + def pageLink(page: Link): Anchor = + page + .a() + .amend( + cls := "text-white px-3 py-2 rounded-md text-sm font-medium", + cls := Seq( + "bg-indigo-700" -> page.active, + "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active + ), + ariaCurrent := (if page.active then "page" else "false") + ) + + def logoImg: Image = + img( + cls := "h-8 w-8", + src <-- $m.map(_.logo.img), + alt <-- $m.map(_.logo.name) + ) + + def mobileMenuButton = button( + tpe := "button", + cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", + aria.controls := "mobile-menu", + aria.expanded <-- mobileMenuOpen.signal, + span(cls := "sr-only", "Open main menu"), + Icons.outline + .menu() + .amend( + svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") + ), + Icons.outline + .x() + .amend( + svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") + ), + onClick.preventDefault.mapTo( + !mobileMenuOpen.now() + ) --> mobileMenuOpen.writer + ) + + def navBarLeft = + div( + cls := "flex items-center", + div(cls := "flex-shrink-0", logoImg), + div( + desktopOnly, + div( + cls := "ml-10 flex items-baseline space-x-4", + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) + ) + ) + ) + + def navBarRight = + div( + desktopOnly, + div( + cls := "ml-4 flex items-center md:ml-6", + notificationButton, + userProfile + ) + ) + + def navBarMobile = + div( + cls := "-mr-2 flex", + mobileOnly, + mobileMenuButton + ) + + def navBar = + div( + cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", + div( + cls := "flex items-center justify-between h-16", + navBarLeft, + navBarRight, + navBarMobile + ) + ) + + def mobileMenu = + div( + mobileOnly, + cls <-- mobileMenuOpen.signal.map { o => + if (o) "block" else "hidden" + }, + idAttr := "mobile-menu", + div( + cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) + ), + mobileProfile + ) + + nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala new file mode 100644 index 0000000..c4cb10d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala @@ -0,0 +1,19 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.Page +import com.raquo.waypoint.Router +import mdr.pdb.app.Action + +object PageHeader: + def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = + header( + cls := "bg-white shadow-sm", + div( + cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", + h1( + cls := "text-lg leading-6 font-semibold text-gray-900", + Breadcrumbs(actionBus) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala new file mode 100644 index 0000000..aae4539 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala @@ -0,0 +1,30 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.Page +import mdr.pdb.app.Action +import fiftyforms.ui.components.tailwind.Loading +import io.laminext.syntax.core.* + +object PageLayout: + case class ViewModel( + navigation: NavigationBar.ViewModel, + content: Option[HtmlElement] + ) + def apply(actionBus: Observer[Action])( + $m: Signal[ViewModel], + mods: Modifier[HtmlElement]* + )(using router: Router[Page]): HtmlElement = + val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) + div( + cls := "h-full flex flex-col", + NavigationBar($m.map(_.navigation)), + child.maybe <-- router.$currentPage.map(_.isRoot) + .switch(None, Some(PageHeader(actionBus))), + main( + cls := "flex-grow-1 overflow-y-auto", + mods, + child <-- $maybeContent.map(_.getOrElse(Loading)) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/mdr/pdb/app/components/PageLink.scala new file mode 100644 index 0000000..e6e7052 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageLink.scala @@ -0,0 +1,40 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.LinkSupport.* +import mdr.pdb.app.Page +import mdr.pdb.app.Action +import com.raquo.waypoint.Router +import mdr.pdb.app.NavigateTo + +object PageLink: + type ViewModel = Page + def apply($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = + a(mods($m, actions), child.text <-- $m.map(_.title)) + + def apply(m: ViewModel, actions: Observer[Action])(using + router: Router[Page] + ): Anchor = apply(Val(m), actions) + + def container($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = a(mods($m, actions)) + + def container(m: ViewModel, actions: Observer[Action])(using + Router[Page] + ): Anchor = container(Val(m), actions) + + private def mods($m: Signal[Page], actions: Observer[Action])(using + router: Router[Page] + ): Modifier[Anchor] = + Seq( + href <-- $m.map(router.absoluteUrlForPage).recover { case _ => + Some("invalid url") + }, + composeEvents(onClick.noKeyMod.preventDefault)( + _.sample($m) + .map(NavigateTo.apply) + ) --> actions + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala new file mode 100644 index 0000000..7682772 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala @@ -0,0 +1,13 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.pages.dashboard.DashboardPage +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage + +class DashboardPageConnector(actionBus: Observer[Action])(using + router: Router[Page] +): + def apply: HtmlElement = + AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala new file mode 100644 index 0000000..76b9133 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala @@ -0,0 +1,76 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.Parameter +import mdr.pdb.UserInfo +import mdr.pdb.app.pages.detail.DetailParametruPage +import pages.detail.DetailKriteriaPage +import pages.detail.components.DukazyKriteria +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.ParameterCriteria + +object DetailKriteriaPageConnector { + trait AppState { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + } +} + +case class DetailKriteriaPageConnector( + state: DetailKriteriaPageConnector.AppState +)( + $page: Signal[Page.DetailKriteria] +)(using Router[Page]): + val $paramChangeSignal = + $page.splitOne(p => + (p.osobniCislo.value, p.parametr.value, p.kriterium.value) + )((x, _, _) => x) + val $pageChangeSignal = + $paramChangeSignal.map( + FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) + ) + + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + val $merged = + $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => + for { + da <- d + pa <- p + pb <- pa.find(_.id == pc._2) + ka <- pb.criteria.find(_.id == pc._3) + } yield (da, pb, ka) + ) + + def apply: HtmlElement = + AppPage(state.actionBus)( + $merged.map(_.map(buildModel)) + .split(_ => ())((_, s, $s) => + DetailKriteriaPage($s)(state.actionBus.contramap { + case DukazyKriteria.Add => + NavigateTo( + Page.UpravDukazKriteria( + Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), + Page.Titled(s.parametr.id, Some(s.parametr.nazev)), + Page.Titled(s.kriterium.id, Some(s.kriterium.id)) + ) + ) + }) + ), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: Parameter, + k: ParameterCriteria + ): DetailKriteriaPage.ViewModel = + DetailKriteriaPage.ViewModel( + o.toDetailOsoby, + p.toParametr(_ => a()), + k.toKriterium(_ => a()) + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DetailPageConnector.scala new file mode 100644 index 0000000..df3a6b7 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DetailPageConnector.scala @@ -0,0 +1,57 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.UserInfo +import pages.detail.DetailPage +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.pages.detail.components.DetailOsoby +import mdr.pdb.Parameter +import mdr.pdb.app.pages.detail.components.SeznamParametru +import fiftyforms.ui.components.tailwind.Color + +object DetailPageConnector { + trait AppState { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + } +} + +case class DetailPageConnector(state: DetailPageConnector.AppState)( + $page: Signal[Page.Detail] +)(using router: Router[Page]): + val $oscChangeSignal = $page.splitOne(_.osobniCislo.value)((osc, _, _) => osc) + val $pageChangeSignal = + $oscChangeSignal.flatMap(osc => + EventStream.fromSeq(Seq(FetchUserDetails(osc), FetchParameters(osc))) + ) + // TODO: filter the value based on the current osc + // OSC change will fetch new data, but still + // - we need to be sure that what we got is really what we ought to display + // - we want to display stale data accordingly (at least with loading indicator) + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + def apply: HtmlElement = + AppPage(state.actionBus)( + $data.combineWithFn($params)(_ zip _) + .map(_.map(buildModel)) + .split(_ => ())((_, _, s) => DetailPage(s)), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: List[Parameter] + ): DetailPage.ViewModel = + DetailPage.ViewModel( + o.toDetailOsoby, + p.map( + _.toParametr(param => + PageLink.container(Page.DetailParametru(o, param), state.actionBus) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DetailParametruPageConnector.scala new file mode 100644 index 0000000..fa7193d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DetailParametruPageConnector.scala @@ -0,0 +1,67 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.Parameter +import mdr.pdb.UserInfo +import mdr.pdb.app.pages.detail.DetailParametruPage +import pages.detail.DetailParametruPage +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.components.PageLink + +object DetailParametruPageConnector { + trait AppState { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + } +} + +case class DetailParametruPageConnector( + state: DetailParametruPageConnector.AppState +)( + $page: Signal[Page.DetailParametru] +)(using router: Router[Page]): + val $paramChangeSignal = + $page.splitOne(p => (p.osobniCislo.value, p.parametr.value))((x, _, _) => x) + val $pageChangeSignal = + $paramChangeSignal.map(FetchParameter(_, _)) + + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + val $merged = + $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => + for { + da <- d + pa <- p + pb <- pa.find(_.id == pc._2) + } yield (da, pb) + ) + + def apply: HtmlElement = + AppPage(state.actionBus)( + $merged.map(_.map(buildModel)) + .split(_ => ())((_, _, s) => DetailParametruPage(s)), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: Parameter + ): DetailParametruPage.ViewModel = + DetailParametruPage.ViewModel( + o.toDetailOsoby, + p.toParametr(p => + PageLink.container(Page.DetailParametru(o, p), state.actionBus) + ), + p.criteria.map( + _.toKriterium { c => + PageLink.container( + Page.DetailKriteria(o, p, c), + state.actionBus + ) + } + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DirectoryPageConnector.scala new file mode 100644 index 0000000..ab2c045 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DirectoryPageConnector.scala @@ -0,0 +1,34 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.UserInfo +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.components.AppPage + +case class DirectoryPageConnector( + $input: EventStream[List[UserInfo]], + actionBus: Observer[Action] +)(using router: Router[Page]): + val $data = $input.startWithNone + val $actionSignal = EventStream.fromValue(FetchDirectory) + + def apply: HtmlElement = + AppPage(actionBus)( + $data.split(_ => ())((_, _, s) => + pages.directory.DirectoryPage( + s.map( + _.map( + _.toUserRow(u => + PageLink.container( + Page.Detail(Page.Titled(u.personalNumber)), + actionBus + ) + ) + ) + ) + ) + ), + $actionSignal --> actionBus + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/mdr/pdb/app/connectors/conversions.scala new file mode 100644 index 0000000..f581796 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/conversions.scala @@ -0,0 +1,68 @@ +package mdr.pdb.app.connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.UserInfo +import mdr.pdb.app.pages.detail.components.DetailOsoby +import mdr.pdb.Parameter +import mdr.pdb.app.pages.detail.components.SeznamParametru +import mdr.pdb.ParameterCriteria +import mdr.pdb.app.pages.detail.components.SeznamKriterii +import mdr.pdb.app.pages.directory.components.UserRow +import mdr.pdb.app.pages.detail.components.DetailParametru +import mdr.pdb.app.pages.detail.components.DetailKriteria +import fiftyforms.ui.components.tailwind.Color + +extension (o: UserInfo) + def toDetailOsoby: DetailOsoby.ViewModel = + DetailOsoby.ViewModel( + o.personalNumber, + o.name, + o.email, + o.phone, + o.img, + o.mainFunction.map(f => + DetailOsoby.Funkce.ViewModel(f.name, f.dept, f.ou) + ), + o.userContracts.headOption.map(c => + DetailOsoby.PracovniPomer.ViewModel(c.rel, c.startDate, c.endDate) + ) + ) + +extension (param: Parameter) + def toParametr(container: Parameter => Anchor): DetailParametru.ViewModel = + DetailParametru.ViewModel( + id = param.id, + nazev = param.name, + popis = param.description, + status = "Nesplněno", + statusColor = Color.red, + a = container(param) + ) + +extension (crit: ParameterCriteria) + def toKriterium( + container: ParameterCriteria => Anchor + ): DetailKriteria.ViewModel = + DetailKriteria.ViewModel( + nazev = crit.criteriumText, + kapitola = crit.chapterId, + bod = crit.itemId, + status = "Nesplněno", + statusColor = Color.red, + splneno = false, + dukazy = Nil, + container = container(crit) + ) + +extension (user: UserInfo) + def toUserRow( + container: UserInfo => HtmlElement = (_: UserInfo) => div() + ): UserRow.ViewModel = + UserRow.ViewModel( + osobniCislo = user.personalNumber.toString, + celeJmeno = user.name, + prijmeni = user.surname, + hlavniFunkce = user.mainFunction.map(_.name), + img = user.img, + container = () => container(user) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/dashboard/DashboardPage.scala b/app/src/main/scala/mdr/pdb/app/pages/dashboard/DashboardPage.scala new file mode 100644 index 0000000..0eb8f7a --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/dashboard/DashboardPage.scala @@ -0,0 +1,11 @@ +package mdr.pdb.app.pages.dashboard + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.Page + +object DashboardPage: + + def render: HtmlElement = + div("Dashboard page") diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala new file mode 100644 index 0000000..ff3a25f --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala @@ -0,0 +1,31 @@ +package mdr.pdb.app.pages.detail + +import com.raquo.laminar.api.L.{*, given} + +import components._ + +object DetailKriteriaPage: + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametr: DetailParametru.ViewModel, + kriterium: DetailKriteria.ViewModel + ) + + def apply($m: Signal[ViewModel])( + action: Observer[DukazyKriteria.Action] + ): HtmlElement = + div( + cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + div( + DetailOsoby.header($m.map(_.osoba)), + DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") + ), + div( + DetailKriteria($m.map(_.kriterium)), + DukazyKriteria($m.map(_.kriterium.dukazy))(action) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/DetailPage.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailPage.scala new file mode 100644 index 0000000..b591cc9 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailPage.scala @@ -0,0 +1,25 @@ +package mdr.pdb.app.pages.detail + +import com.raquo.laminar.api.L.{*, given} + +import components._ +import mdr.pdb.app.Page +import com.raquo.waypoint.Router +import mdr.pdb.app.Action + +object DetailPage: + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametry: SeznamParametru.ViewModel + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + DetailOsoby($m.map(_.osoba)), + SeznamParametru($m.map(_.parametry)) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/DetailParametruPage.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailParametruPage.scala new file mode 100644 index 0000000..3322279 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailParametruPage.scala @@ -0,0 +1,24 @@ +package mdr.pdb.app.pages.detail + +import com.raquo.laminar.api.L.{*, given} + +import components._ + +object DetailParametruPage: + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametr: DetailParametru.ViewModel, + kriteria: SeznamKriterii.ViewModel + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + DetailOsoby.header($m.map(_.osoba)), + DetailParametru($m.map(_.parametr)), + SeznamKriterii($m.map(_.kriteria)) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala new file mode 100644 index 0000000..b58618a --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala @@ -0,0 +1,107 @@ +package mdr.pdb +package app +package pages.detail + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.pages.detail.components.UpravDukazForm +import fiftyforms.services.files.File + +object UpravDukaz: + + type ThisPage = Page.UpravDukazKriteria + type PageKey = (OsobniCislo, String, String) + + trait State { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def availableFiles: EventStream[List[File]] + def actionBus: Observer[Action] + } + + def keyOfPage(page: ThisPage): PageKey = + (page.osobniCislo.value, page.parametr.value, page.kriterium.value) + + def onChangeAction(key: PageKey): Action = + FetchParameterCriteria( + key._1, + key._2, + key._3, + Page.UpravDukazKriteria(_, _, _) + ) + + class Connector(state: State)($page: Signal[ThisPage])(using Router[Page]): + val $paramChangeSignal = $page.splitOne(keyOfPage)((x, _, _) => x) + val $pageChangeSignal = $paramChangeSignal.map(onChangeAction) + + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + val $merged = + $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => + for { + da <- d + pa <- p + pb <- pa.find(_.id == pc._2) + ka <- pb.criteria.find(_.id == pc._3) + } yield (da, pb, ka) + ) + + def apply: HtmlElement = + AppPage(state.actionBus)( + $merged.split(_ => ())((_, s, $s) => + PageComponent( + $s.map(buildModel), + state.availableFiles, + state.actionBus.contramap { + case UpravDukazForm.Cancelled => + NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) + case UpravDukazForm.AvailableFilesRequested => + FetchAvailableFiles(s._1.personalNumber) + } + ) + ), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: Parameter, + k: ParameterCriteria + ): PageComponent.ViewModel = + import connectors.* + PageComponent.ViewModel( + o.toDetailOsoby, + p.toParametr(_ => a()), + k.toKriterium(_ => a()) + ) + + object PageComponent: + import components.* + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametr: DetailParametru.ViewModel, + kriterium: DetailKriteria.ViewModel + ) + + def apply( + $m: Signal[ViewModel], + availableFilesStream: EventStream[List[File]], + events: Observer[UpravDukazForm.Event] + ): HtmlElement = + div( + cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + div( + DetailOsoby.header($m.map(_.osoba)), + DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") + ), + div( + DetailKriteria($m.map(_.kriterium)), + UpravDukazForm(availableFilesStream)(events) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailKriteria.scala new file mode 100644 index 0000000..a3ab634 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailKriteria.scala @@ -0,0 +1,37 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.CustomAttrs +import fiftyforms.ui.components.tailwind.Icons +import java.time.LocalDate +import fiftyforms.ui.components.tailwind.Color + +object DetailKriteria: + case class ViewModel( + nazev: String, + kapitola: String, + bod: String, + status: String, + statusColor: Color, + splneno: Boolean, + dukazy: List[DukazKriteria.ViewModel], + container: HtmlElement = div() + ) { + val id = s"${kapitola}${bod}" + } + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "pb-5 border-b border-gray-200", + h3( + cls := "text-xl leading-6 font-bold text-gray-900", + child.text <-- $m.map(_.nazev) + ), + h3( + cls := "mt-2 max-w-4xl text-sm text-gray-500", + "Kapitola ", + child.text <-- $m.map(_.kapitola), + " bod ", + child <-- $m.map(_.bod) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailOsoby.scala new file mode 100644 index 0000000..0fe2ba5 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailOsoby.scala @@ -0,0 +1,100 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.OsobniCislo + +import java.time.LocalDate +import fiftyforms.ui.components.tailwind.Avatar + +object DetailOsoby: + + object Funkce: + case class ViewModel( + nazev: String, + stredisko: String, + voj: String + ) + + def render($m: Signal[ViewModel]): HtmlElement = + p( + cls := "text-sm font-medium text-gray-500", + child.text <-- $m.map(_.nazev), + span( + cls := "hidden md:inline", + " @ ", + child.text <-- $m.map(_.stredisko), + ", ", + child.text <-- $m.map(_.voj) + ) + ) + + object PracovniPomer: + case class ViewModel( + druh: String, + pocatek: LocalDate, + konec: Option[LocalDate] + ) + + def render($m: Signal[ViewModel]): HtmlElement = + import fiftyforms.ui.components.tailwind.CustomAttrs.datetime + p( + cls := "text-sm font-medium text-gray-500", + child.text <-- $m.map(_.druh), + " od ", + time( + datetime <-- $m.map(_.pocatek.toString), + child.text <-- $m.map(_.pocatek.toString) + ) + ) + + case class ViewModel( + osobniCislo: OsobniCislo, + jmeno: String, + email: Option[String], + telefon: Option[String], + img: Option[String], + hlavniFunkce: Option[Funkce.ViewModel], + pracovniPomer: Option[PracovniPomer.ViewModel] + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "md:flex md:items-center md:justify-between md:space-x-4", + div( + cls := "flex items-start space-x-4", + div( + cls := "flex-shrink-0", + Avatar($m.map(_.img)).avatar(16) + ), + div( + h1( + cls := "text-2xl font-bold text-gray-900", + child.text <-- $m.map(_.jmeno) + ), + child.maybe <-- $m.map(_.hlavniFunkce).split(_ => ())((_, _, d) => + Funkce.render(d) + ), + child.maybe <-- $m.map(_.pracovniPomer).split(_ => ())((_, _, d) => + PracovniPomer.render(d) + ) + ) + ) + ) + + def header($m: Signal[ViewModel]): HtmlElement = + div( + cls := "md:flex md:items-center md:justify-between md:space-x-5", + div( + cls := "flex items-start space-x-4", + div( + cls := "flex-shrink-0", + Avatar($m.map(_.img)).avatar(8) + ), + div( + h1( + cls := "text-lg font-medium text-gray-800", + child.text <-- $m.map(_.jmeno) + ) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailParametru.scala new file mode 100644 index 0000000..6310dde --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailParametru.scala @@ -0,0 +1,34 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.Color + +object DetailParametru: + case class ViewModel( + id: String, + nazev: String, + popis: String, + status: String, + statusColor: Color, + a: Anchor + ) + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "pb-5 border-b border-gray-200", + h2( + cls := "text-2xl leading-6 font-medium text-gray-900", + child.text <-- $m.map(_.nazev) + ), + p( + cls := "mt-2 max-w-4xl text-sm text-gray-500", + child.text <-- $m.map(_.popis) + ) + ) + + def header($m: Signal[ViewModel]): HtmlElement = + div( + h2( + cls := "text-lg leading-6 font-medium text-gray-600", + child.text <-- $m.map(_.nazev) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazKriteria.scala new file mode 100644 index 0000000..c292919 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazKriteria.scala @@ -0,0 +1,177 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import java.time.LocalDate +import fiftyforms.ui.components.tailwind.CustomAttrs + +object DukazKriteria: + case class Osoba(osobniCislo: String, jmeno: String) + case class Udalost(osoba: Osoba, datum: LocalDate) + case class Dokument( + url: String, + nazev: String, + pridal: Udalost, + odebral: Option[Udalost] + ) + case class ViewModel( + dokumenty: List[Dokument], + autorizoval: Option[Udalost], + platiDo: Option[LocalDate], + poznámka: Option[String] + ) + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "bg-white shadow overflow-hidden sm:rounded-lg", + div( + cls := "px-4 py-5 sm:px-6", + h3( + cls := "text-lg leading-6 font-medium text-gray-900", + """Applicant Information""" + ), + p( + cls := "mt-1 max-w-2xl text-sm text-gray-500", + """Personal details and application.""" + ) + ), + div( + cls := "border-t border-gray-200 px-4 py-5 sm:px-6", + dl( + cls := "grid grid-cols-1 gap-x-4 gap-y-8 sm:grid-cols-2", + div( + cls := "sm:col-span-1", + dt( + cls := "text-sm font-medium text-gray-500", + """Full name""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """Margot Foster""" + ) + ), + div( + cls := "sm:col-span-1", + dt( + cls := "text-sm font-medium text-gray-500", + """Application for""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """Backend Developer""" + ) + ), + div( + cls := "sm:col-span-1", + dt( + cls := "text-sm font-medium text-gray-500", + """Email address""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """margotfoster@example.com""" + ) + ), + div( + cls := "sm:col-span-1", + dt( + cls := "text-sm font-medium text-gray-500", + """Salary expectation""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """$120,000""" + ) + ), + div( + cls := "sm:col-span-2", + dt( + cls := "text-sm font-medium text-gray-500", + """About""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """Fugiat ipsum ipsum deserunt culpa aute sint do nostrud anim incididunt cillum culpa consequat. Excepteur qui ipsum aliquip consequat sint. Sit id mollit nulla mollit nostrud in ea officia proident. Irure nostrud pariatur mollit ad adipisicing reprehenderit deserunt qui eu.""" + ) + ), + div( + cls := "sm:col-span-2", + dt( + cls := "text-sm font-medium text-gray-500", + """Attachments""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + ul( + role := "list", + cls := "border border-gray-200 rounded-md divide-y divide-gray-200", + li( + cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", + div( + cls := "w-0 flex-1 flex items-center", { + import svg.* + import CustomAttrs.svg.ariaHidden + svg( + cls := "flex-shrink-0 h-5 w-5 text-gray-400", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + ariaHidden := true, + path( + fillRule := "evenodd", + d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", + clipRule := "evenodd" + ) + ) + }, + span( + cls := "ml-2 flex-1 w-0 truncate", + """resume_back_end_developer.pdf""" + ) + ), + div( + cls := "ml-4 flex-shrink-0", + a( + href := "#", + cls := "font-medium text-indigo-600 hover:text-indigo-500", + """Download""" + ) + ) + ), + li( + cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", + div( + cls := "w-0 flex-1 flex items-center", { + import svg.* + import CustomAttrs.svg.ariaHidden + svg( + cls := "flex-shrink-0 h-5 w-5 text-gray-400", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + ariaHidden := true, + path( + fillRule := "evenodd", + d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", + clipRule := "evenodd" + ) + ) + }, + span( + cls := "ml-2 flex-1 w-0 truncate", + """coverletter_back_end_developer.pdf""" + ) + ), + div( + cls := "ml-4 flex-shrink-0", + a( + href := "#", + cls := "font-medium text-indigo-600 hover:text-indigo-500", + """Download""" + ) + ) + ) + ) + ) + ) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala new file mode 100644 index 0000000..d604860 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala @@ -0,0 +1,35 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.Icons + +object DukazyKriteria: + sealed trait Action + case object Add extends Action + + type ViewModel = List[DukazKriteria.ViewModel] + + def apply($m: Signal[ViewModel])(actions: Observer[Action]): HtmlElement = + div( + child.maybe <-- $m.map(m => + if (m.isEmpty) then Some(prazdnyDukaz(actions)) + else None + ), + children <-- $m.map(_.zipWithIndex).split(_._2)((_, _, $s) => + DukazKriteria($s.map(_._1)) + ) + ) + + private def prazdnyDukaz(actions: Observer[Action]): HtmlElement = + button( + tpe := "button", + onClick.preventDefault.mapTo(Add) --> actions, + cls := "relative block w-full border-2 border-gray-300 border-dashed rounded-lg p-12 text-center hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + Icons.outline + .`document-add`(12) + .amend(svg.cls := "mx-auto text-gray-400"), + span( + cls := "mt-2 block text-sm font-medium text-gray-900", + "Přidat důkaz" + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala new file mode 100644 index 0000000..7e77a7b --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala @@ -0,0 +1,34 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.Color +import fiftyforms.ui.components.tailwind.list.StackedList +import fiftyforms.ui.components.tailwind.list.ListRow +import fiftyforms.ui.components.tailwind.list.RowTag +import fiftyforms.ui.components.tailwind.list.RowNext +import java.time.LocalDate + +object SeznamKriterii: + type ViewModel = List[DetailKriteria.ViewModel] + + private val kritList = new StackedList[DetailKriteria.ViewModel] + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "bg-white shadow overflow-hidden sm:rounded-md", + kritList($m, _.id) { $i => + $i.map { i => + ListRow.ViewModel( + title = i.nazev, + topRight = RowTag.render( + $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) + ), + bottomLeft = + p(cls := "text-sm text-gray-500", s"${i.kapitola}${i.bod}"), + bottomRight = emptyNode, + farRight = RowNext.render, + containerElement = i.container + ) + } + } + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamParametru.scala new file mode 100644 index 0000000..0760b32 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamParametru.scala @@ -0,0 +1,37 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.list.{ + StackedList, + ListRow, + RowTag, + PropList, + IconText, + RowNext +} +import fiftyforms.ui.components.tailwind.Color +import fiftyforms.ui.components.tailwind.LinkSupport.* + +object SeznamParametru: + type ViewModel = List[DetailParametru.ViewModel] + + private val parametrList = new StackedList[DetailParametru.ViewModel] + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "bg-white shadow overflow-hidden sm:rounded-md", + parametrList($m, _.id) { $i => + $i.map { i => + ListRow.ViewModel( + title = i.nazev, + topRight = RowTag.render( + $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) + ), + bottomLeft = emptyNode, + bottomRight = emptyNode, + farRight = RowNext.render, + containerElement = i.a + ) + } + } + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala new file mode 100644 index 0000000..28753c9 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala @@ -0,0 +1,101 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import io.laminext.syntax.core.* +import fiftyforms.ui.components.tailwind.CustomAttrs +import fiftyforms.ui.components.tailwind.form.* +import org.scalajs.dom +import com.raquo.laminar.nodes.ReactiveHtmlElement +import fiftyforms.services.files.components.tailwind.FilePicker +import mdr.pdb.frontend.AutorizujDukaz +import mdr.pdb.frontend.DocumentRef +import fiftyforms.services.files.File + +object UpravDukazForm: + sealed trait Event + case object Cancelled extends Event + case object AvailableFilesRequested extends Event + def apply(availableFilesStream: EventStream[List[File]])( + updates: Observer[Event] + ): HtmlElement = + val (filesStream, filesObserver) = + EventStream.withObserver[FilePicker.Event] + val files = Var[List[File]](Nil) + def submitButtons: HtmlElement = + div( + cls := "pt-5", + div( + cls := "flex justify-end", + button( + tpe := "button", + cls := "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + "Zrušit", + onClick.mapTo(Cancelled) --> updates + ), + button( + tpe := "submit", + cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + disabled <-- files.signal.map(_.isEmpty), + "Autorizovat" + ) + ) + ) + + div( + filesStream.collect { case FilePicker.SelectionUpdated(files) => + files.to(List) + } --> files.writer, + filesStream.collect { case FilePicker.AvailableFilesRequested => + AvailableFilesRequested + } --> updates, + cls := "bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6", + Form( + Form.Body( + Form.Section( + FormHeader( + FormHeader.ViewModel( + "Doložení kritéria", + "Sestavte doklady poskytující důkaz kritéria a potvrďte odesláním formuláře. Případné limitace či jiné relevantní údaje vepište do pole Komentář." + ) + ), + FormFields( + FormRow( + "dokumenty", + "Dokumenty", + FilePicker(files.signal, availableFilesStream)(filesObserver) + .amend(idAttr := "dokumenty", cls("max-w-lg")) + ).toHtml, + FormRow( + "platnost", + "Platnost", + input( + idAttr := "platnost", + name := "platnost", + tpe := "date", + autoComplete := "date", + cls := "block max-w-lg w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" + ) + ).toHtml, + FormRow( + "komentar", + "Komentář", + div( + cls := "mt-1 sm:mt-0 sm:col-span-2", + textArea( + idAttr := "komentar", + name := "about", + rows := 3, + cls := "max-w-lg shadow-sm block w-full focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border border-gray-300 rounded-md" + ), + p( + cls := "mt-2 text-sm text-gray-500", + "Doplňte prosím potřebné informace související s doložením kritéria, včetně případných limitací." + ) + ) + ).toHtml + ) + ) + ), + submitButtons + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/directory/DirectoryPage.scala b/app/src/main/scala/mdr/pdb/app/pages/directory/DirectoryPage.scala new file mode 100644 index 0000000..7a72ca3 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/directory/DirectoryPage.scala @@ -0,0 +1,40 @@ +package mdr.pdb.app.pages.directory + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.UserInfo + +import components._ + +object DirectoryPage: + + type ViewModel = List[UserRow.ViewModel] + + def apply($m: Signal[ViewModel]): HtmlElement = + val (actionsStream, actionObserver) = + EventStream.withObserver[SearchForm.Action] + val $filter = actionsStream + .collect { + case SearchForm.ClearFilter => None + case SearchForm.SetFilter(t) => Some(t) + } + .startWith(None) + val byLetter = for { + d <- $m + f <- $filter + } yield for { + (letter, users) <- d + .filter { user => + f match + case None => true + case Some(t) => user.search.contains(t) + } + .groupBy(_.prijmeni.head) + .to(List) + .sortBy(_._1) + } yield (letter.toString, users.sortBy(_.prijmeni)) + + div( + cls := "h-full max-w-7xl mx-auto order-first flex flex-col flex-shrink-0", + SearchForm(actionObserver), + Directory(byLetter) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/directory/components/Directory.scala b/app/src/main/scala/mdr/pdb/app/pages/directory/components/Directory.scala new file mode 100644 index 0000000..f7d6f99 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/directory/components/Directory.scala @@ -0,0 +1,37 @@ +package mdr.pdb.app.pages.directory.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.UserInfo + +object Directory: + + object Header: + type ViewModel = String + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500", + h3(child.text <-- $m) + ) + + object UserList: + type ViewModel = List[UserRow.ViewModel] + def apply($m: Signal[ViewModel]): HtmlElement = + ul( + role := "list", + cls := "relative z-0 divide-y divide-gray-200", + children <-- $m.split(_.osobniCislo)((_, _, s) => UserRow(s)) + ) + + type ViewModel = List[(String, List[UserRow.ViewModel])] + def apply($m: Signal[ViewModel]): HtmlElement = + nav( + cls := "flex-1 min-h-0 overflow-y-auto", + aria.label := "Directory", + children <-- $m.split(_._1)((_, _, s) => + div( + cls := "relative", + Header(s.map(_._1)), + UserList(s.map(_._2)) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/directory/components/SearchForm.scala b/app/src/main/scala/mdr/pdb/app/pages/directory/components/SearchForm.scala new file mode 100644 index 0000000..fb9ecc4 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/directory/components/SearchForm.scala @@ -0,0 +1,54 @@ +package mdr.pdb.app.pages.directory.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.Icons + +object SearchForm: + sealed trait Action + case object SubmitFilter extends Action + sealed trait FilterAction extends Action + case object ClearFilter extends FilterAction + case class SetFilter(value: String) extends FilterAction + + def apply(actions: Observer[Action]): HtmlElement = + div( + cls := "px-6 pt-4 pb-4", + form( + cls := "flex space-x-4", + onSubmit.mapTo(SubmitFilter) --> actions, + div( + cls := "flex-1 min-w-0", + label( + forId := "search", + cls := "sr-only", + "Hledat" + ), + div( + cls := "relative rounded-md shadow-sm", + div( + cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", + Icons.solid.search().amend(svg.cls := "text-gray-400") + ), + input( + tpe := "search", + name := "search", + idAttr := "search", + cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", + placeholder := "Hledat", + composeEvents(onInput.mapToValue.setAsValue.map(SetFilter(_)))( + _.throttle(500) + ) --> actions + ) + ) + ), + button( + tpe := "submit", + cls := "inline-flex justify-center px-3.5 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500", + Icons.solid.filter().amend(svg.cls := "text-gray-400"), + span( + cls := "sr-only", + "Hledat" + ) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/directory/components/UserRow.scala b/app/src/main/scala/mdr/pdb/app/pages/directory/components/UserRow.scala new file mode 100644 index 0000000..4aa1672 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/directory/components/UserRow.scala @@ -0,0 +1,52 @@ +package mdr.pdb.app.pages.directory.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.airstream.core.Signal +import fiftyforms.ui.components.tailwind.Avatar + +object UserRow: + case class ViewModel( + osobniCislo: String, + celeJmeno: String, + prijmeni: String, + hlavniFunkce: Option[String], + img: Option[String], + container: () => HtmlElement = () => div() + ) { + val search = prijmeni.toLowerCase + } + + def apply($m: Signal[ViewModel]): HtmlElement = + inline def avatarImage = + Avatar($m.map(_.img)).avatarImage(10) + + li( + div( + cls := "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500", + div( + cls := "flex-shrink-0", + child <-- avatarImage + ), + div( + cls := "flex-1 min-w-0", + child <-- $m.map { o => + o.container() + .amend( + cls := "focus:outline-none", + span( + cls := "absolute inset-0", + aria.hidden := true + ), + p( + cls := "text-sm font-medium text-gray-900", + o.celeJmeno + ), + p( + cls := "text-sm text-gray-500 truncate", + o.hlavniFunkce + ) + ) + } + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/errors/ErrorPage.scala b/app/src/main/scala/mdr/pdb/app/pages/errors/ErrorPage.scala new file mode 100644 index 0000000..6b195ef --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/errors/ErrorPage.scala @@ -0,0 +1,99 @@ +package mdr.pdb.app.pages.errors + +import com.raquo.domtypes.generic.codecs.StringAsIsCodec +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.Page +import mdr.pdb.app.Action +import com.raquo.waypoint.Router + +object ErrorPage: + case class ViewModel( + homePage: Page, + errorName: String, + title: String, + subTitle: String + ) + + def apply(m: ViewModel, actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + val ViewModel(homePage, errorName, title, subTitle) = m + div( + cls := "min-h-full pt-16 pb-12 flex flex-col bg-white", + main( + cls := "flex-grow flex flex-col justify-center max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", + div( + cls := "flex-shrink-0 flex justify-center", + a( + href := "/", + cls := "inline-flex", + span( + cls := "sr-only", + """Workflow""" + ), + img( + cls := "h-12 w-auto", + src := "https://tailwindui.com/img/logos/workflow-mark.svg?color=indigo&shade=600", + alt := "" + ) + ) + ), + div( + cls := "py-16", + div( + cls := "text-center", + p( + cls := "text-sm font-semibold text-indigo-600 uppercase tracking-wide", + errorName + ), + h1( + cls := "mt-2 text-4xl font-extrabold text-gray-900 tracking-tight sm:text-5xl", + title + ), + p( + cls := "mt-2 text-base text-gray-500", + subTitle + ), + div( + cls := "mt-6", + PageLink + .container(homePage, actionBus) + .amend( + cls := "text-base font-medium text-indigo-600 hover:text-indigo-500", + """Go back home""" + ) + ) + ) + ) + ), + footer( + cls := "flex-shrink-0 max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", + nav( + cls := "flex justify-center space-x-4", + a( + href := "#", + cls := "text-sm font-medium text-gray-500 hover:text-gray-600", + """Contact Support""" + ), + span( + cls := "inline-block border-l border-gray-300", + aria.hidden := true + ), + a( + href := "#", + cls := "text-sm font-medium text-gray-500 hover:text-gray-600", + """Status""" + ), + span( + cls := "inline-block border-l border-gray-300", + aria.hidden := true + ), + a( + href := "#", + cls := "text-sm font-medium text-gray-500 hover:text-gray-600", + """Twitter""" + ) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/errors/NotFoundPage.scala b/app/src/main/scala/mdr/pdb/app/pages/errors/NotFoundPage.scala new file mode 100644 index 0000000..97760e1 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/errors/NotFoundPage.scala @@ -0,0 +1,21 @@ +package mdr.pdb.app.pages.errors + +import com.raquo.domtypes.generic.codecs.StringAsIsCodec +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.Page +import mdr.pdb.app.Action + +object NotFoundPage: + def apply(homePage: Page, url: String, actionBus: Observer[Action])(using + router: Router[Page] + ): HtmlElement = + ErrorPage( + ErrorPage.ViewModel( + homePage, + "404 error", + "Page not found.", + s"Sorry, but page $url doesn't exist." + ), + actionBus + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala b/app/src/main/scala/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala new file mode 100644 index 0000000..77b0af8 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala @@ -0,0 +1,32 @@ +package mdr.pdb.app.pages.errors + +import com.raquo.domtypes.generic.codecs.StringAsIsCodec +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.Page +import mdr.pdb.app.Action + +object UnhandledErrorPage: + + case class ViewModel( + homePage: Page, + errorName: Option[String], + errorMessage: Option[String] + ) + + def apply(m: ViewModel, actionBus: Observer[Action])(using + router: Router[Page] + ): HtmlElement = + ErrorPage( + ErrorPage.ViewModel( + m.homePage, + "Unexpected error occurred", + m.errorName.getOrElse( + "Uh oh!" + ), // TODO: translations, better text than uh oh + m.errorMessage.getOrElse( + "This wasn't supposed to happen! Please try again." + ) + ), + actionBus + ) diff --git a/app/src/main/scala/mdr/pdb/app/services/DataFetcher.scala b/app/src/main/scala/mdr/pdb/app/services/DataFetcher.scala new file mode 100644 index 0000000..134c221 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/services/DataFetcher.scala @@ -0,0 +1,6 @@ +package mdr.pdb.app.services + +import com.raquo.airstream.core.Observer + +trait DataFetcher[K, A]: + def fetch(id: K, sink: Observer[A]): Unit diff --git a/app/src/main/scala/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/mdr/pdb/app/state/AppState.scala new file mode 100644 index 0000000..e44b2df --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/state/AppState.scala @@ -0,0 +1,120 @@ +package mdr.pdb.app +package state + +import com.raquo.airstream.core.EventStream +import mdr.pdb.{UserInfo, OsobniCislo} +import com.raquo.airstream.core.Observer +import scala.scalajs.js +import scala.scalajs.js.JSON +import zio.json._ +import com.raquo.airstream.eventbus.EventBus +import com.raquo.airstream.ownership.Owner +import com.raquo.waypoint.Router +import mdr.pdb.Parameter +import mdr.pdb.ParameterCriteria +import mdr.pdb.UserFunction +import mdr.pdb.UserContract +import fiftyforms.services.files.File + +trait AppState + extends connectors.DetailPageConnector.AppState + with connectors.DetailParametruPageConnector.AppState + with connectors.DetailKriteriaPageConnector.AppState + with pages.detail.UpravDukaz.State: + def users: EventStream[List[UserInfo]] + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + +class MockAppState(implicit owner: Owner, router: Router[Page]) + extends AppState: + + given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) + given JsonDecoder[UserFunction] = DeriveJsonDecoder.gen + given JsonDecoder[UserContract] = DeriveJsonDecoder.gen + given JsonDecoder[UserInfo] = DeriveJsonDecoder.gen + + given JsonDecoder[ParameterCriteria] = DeriveJsonDecoder.gen + given JsonDecoder[Parameter] = DeriveJsonDecoder.gen + + private val actions = EventBus[Action]() + private val (parametersStream, pushParameters) = + EventStream.withCallback[List[Parameter]] + private val (usersStream, pushUsers) = + EventStream.withCallback[List[UserInfo]] + private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] + private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] + + private val mockData: List[UserInfo] = + mockUsers + .asInstanceOf[js.Dictionary[js.Object]] + .values + // TODO: is there a more efficient way to parse from JS object directly? + .map { o => + val parsed = JSON.stringify(o).fromJson[UserInfo] + parsed.left.foreach(org.scalajs.dom.console.log(_)) + parsed + } + .collect { case Right(u) => + u + } + .toList + + private val mockParameters: List[Parameter] = + pdbParams + .asInstanceOf[js.Dictionary[js.Object]] + .values + .map(o => JSON.stringify(o).fromJson[Parameter]) + .collect { case Right(p) => p } + .toList + + // TODO: Extract to separate event handler + actions.events.foreach { + case FetchDirectory => pushUsers(mockData) + case FetchUserDetails(osc) => + mockData.find(_.personalNumber == osc).foreach { o => + pushDetails(o) + router.replaceState(Page.Detail(o)) + } + case FetchParameters(osc) => + pushParameters(mockParameters) + case FetchParameter(osc, paramId) => + for + o <- mockData.find(_.personalNumber == osc) + p <- mockParameters.find(_.id == paramId) + do + pushDetails(o) + pushParameters(mockParameters) + router.replaceState(Page.DetailParametru(o, p)) + case FetchParameterCriteria(osc, paramId, critId, page) => + for + o <- mockData.find(_.personalNumber == osc) + p <- mockParameters.find(_.id == paramId) + c <- p.criteria.find(_.id == critId) + do + pushDetails(o) + pushParameters(mockParameters) + router.replaceState(page(o, p, c)) + case NavigateTo(page) => router.pushState(page) + case FetchAvailableFiles(osc) => + pushFiles( + List( + File("https://tc163.cmi.cz/here", "Example file") + ) + ) + } + + override def users: EventStream[List[UserInfo]] = + usersStream.debugWithName("users") + + override def details: EventStream[UserInfo] = + detailsStream.debugWithName("details") + + override def parameters: EventStream[List[Parameter]] = + parametersStream.debugWithName("parameters") + + override def availableFiles: EventStream[List[File]] = + filesStream.debugWithName("available files") + + override def actionBus: Observer[Action] = + actions.writer.debugWithName("actions writer") diff --git a/build.sbt b/build.sbt index 2afc879..0daf796 100644 --- a/build.sbt +++ b/build.sbt @@ -11,6 +11,12 @@ lazy val core = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Pure) .in(file("core")) + .settings( + IWDeps.useZIO(Test), + IWDeps.tapirCore, + IWDeps.tapirZIO, + IWDeps.tapirZIOJson + ) lazy val ui = (project in file("ui")) .enablePlugins(ScalaJSPlugin) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala deleted file mode 100644 index a3b00af..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala +++ /dev/null @@ -1,103 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import scala.scalajs.js.annotation.JSExportTopLevel -import scala.scalajs.js.annotation.JSExport -import scala.scalajs.js.annotation.JSImport -import scala.scalajs.js -import org.scalajs.dom -import com.raquo.laminar.api.L.{*, given} -import scala.scalajs.js.Date -import com.raquo.waypoint.Router -import com.raquo.waypoint.SplitRender -import cz.e_bs.cmi.mdr.pdb.app.services.DataFetcher -import scala.scalajs.js.JSON -import zio.json._ -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.state.AppState - -@js.native -@JSImport("stylesheets/main.css", JSImport.Namespace) -object Css extends js.Any - -@js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native -@JSImport("params/pdb-params.json", JSImport.Default) -object pdbParams extends js.Object - -@JSExportTopLevel("app") -object Main: - - @JSExport - def main(args: Array[String]): Unit = - import Routes.given - onLoad { - setupAirstream() - val appContainer = dom.document.querySelector("#app") - val _ = - render( - appContainer, - renderPage(state.MockAppState(using unsafeWindowOwner, router)) - ) - } - - private def onLoad(f: => Unit): Unit = - documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) - - private def setupAirstream()(using router: Router[Page]): Unit = - AirstreamError.registerUnhandledErrorCallback(err => - router.forcePage( - Page.UnhandledError( - Some(err.getClass.getName), // TODO: Fill only in dev mode - Some(err.getMessage) - ) - ) - ) - - def renderPage(state: AppState)(using - router: Router[Page] - ): HtmlElement = - val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) - .collectSignal[Page.Detail]( - connectors - .DetailPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailParametru]( - connectors - .DetailParametruPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailKriteria]( - connectors - .DetailKriteriaPageConnector(state)(_) - .apply - ) - .collectSignal[Page.UpravDukazKriteria]( - pages.detail.UpravDukaz.Connector(state)(_).apply - ) - .collectStatic(Page.Dashboard)( - connectors.DashboardPageConnector(state.actionBus).apply - ) - .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) - ) - .collect[Page.UnhandledError](pg => - pages.errors - .UnhandledErrorPage( - pages.errors.UnhandledErrorPage - .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), - state.actionBus - ) - ) - .collectStatic(Page.Directory)( - connectors - .DirectoryPageConnector(state.users, state.actionBus) - .apply - ) - div(cls := "h-full", child <-- pageSplitter.$view) - - // Pull in the stylesheet - val css: Css.type = Css diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala deleted file mode 100644 index 3bbc8c0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ /dev/null @@ -1,198 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.* -import org.scalajs.dom -import zio.json.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import scala.scalajs.js -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.Page.Titled - -// enum is not working with Waypoints' SplitRender collectStatic -sealed abstract class Page( - val id: String, - val title: String, - val parent: Option[Page] -) { - val path: Vector[Page] = - parent match - case None => Vector(this) - case Some(p) => p.path :+ this - - val isRoot: Boolean = parent.isEmpty -} - -object Page: - - case class Titled[V](value: V, title: Option[String] = None): - val show: String = title.getOrElse(value.toString) - - case object Directory extends Page("directory", "Adresář", None) - - case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) - - case class Detail(osobniCislo: Titled[OsobniCislo]) - extends Page("user", osobniCislo.show, Some(Directory)) - - object Detail { - def apply(o: UserInfo): Detail = Detail( - Titled(o.personalNumber, Some(o.name)) - ) - } - - case class DetailParametru( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String] - ) extends Page( - "parameter", - parametr.show, - Some(Detail(osobniCislo)) - ) - - object DetailParametru { - def apply(o: UserInfo, p: Parameter): DetailParametru = - DetailParametru( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)) - ) - } - - case class DetailKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "criteria", - kriterium.show, - Some(DetailParametru(osobniCislo, parametr)) - ) - - object DetailKriteria { - def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = - DetailKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class UpravDukazKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "addProof", - "Důkaz", - Some(DetailKriteria(osobniCislo, parametr, kriterium)) - ) - - object UpravDukazKriteria { - def apply( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): UpravDukazKriteria = - UpravDukazKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class NotFound(url: String) extends Page("404", "404", Some(Directory)) - - case class UnhandledError( - errorName: Option[String], - errorMessage: Option[String] - ) extends Page("500", "Unexpected error", Some(Directory)) - -object Routes: - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) - given [V: JsonEncoder]: JsonEncoder[Titled[V]] = - DeriveJsonEncoder.gen[Titled[V]] - given [V: JsonDecoder]: JsonDecoder[Titled[V]] = - DeriveJsonDecoder.gen[Titled[V]] - given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] - given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] - - val base = - js.`import`.meta.env.BASE_URL - .asInstanceOf[String] + "app" - - val homePage: Page = Page.Directory - - given router: Router[Page] = Router[Page]( - routes = List( - Route.static(homePage, root / endOfSegments, basePath = base), - Route.static( - Page.Dashboard, - root / "dashboard" / endOfSegments, - basePath = base - ), - Route[Page.Detail, String]( - encode = _.osobniCislo.value.toString, - decode = osc => Page.Detail(Titled(OsobniCislo(osc))), - root / "osoba" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.DetailParametru, (String, String)]( - encode = p => (p.osobniCislo.value.toString, p.parametr.value), - decode = - p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / endOfSegments, - basePath = base - ), - Route[Page.DetailKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.DetailKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.UpravDukazKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.UpravDukazKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / "edit" / endOfSegments, - basePath = base - ) - ), - serializePage = _.toJson, - deserializePage = _.fromJson[Page] - .fold(s => throw IllegalStateException(s), identity), - getPageTitle = _.title, - routeFallback = url => Page.NotFound(url), - deserializeFallback = _ => Page.Dashboard - )( - $popStateEvent = windowEvents.onPopState, - owner = unsafeWindowOwner - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala deleted file mode 100644 index b24b82b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -sealed trait Action - -case object FetchDirectory extends Action -case class FetchUserDetails(osc: OsobniCislo) extends Action -case class FetchParameters(osc: OsobniCislo) extends Action -case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action -case class FetchParameterCriteria( - osc: OsobniCislo, - paramId: String, - critId: String, - page: (UserInfo, Parameter, ParameterCriteria) => Page -) extends Action -case class FetchAvailableFiles(osc: OsobniCislo) extends Action -case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala deleted file mode 100644 index c637d35..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala +++ /dev/null @@ -1,82 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.{UserProfile, UserInfo, OsobniCislo} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo -import cz.e_bs.cmi.mdr.pdb.UserFunction - -object AppPage: - // TODO: pages by logged in user - val pages: List[Page] = List(Page.Directory, Page.Dashboard) - - import NavigationBar.{Logo, MenuItem} - - val logo = Logo( - "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", - "Workflow" - ) - - // TODO: menu items by user profile - val userMenu = - List( - MenuItem("Your Profile"), - MenuItem("Settings"), - MenuItem("Sign out") - ) - - // TODO: load user profile - val $userProfile = Var( - UserProfile( - "tom", - UserInfo( - OsobniCislo("1031"), - "tom", - "Tom", - "Cook", - None, - None, - Some("tom@example.com"), - Some("+420 222 866 180"), - Some( - UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") - ), - Nil, - None - ) - ) - ) - - val $userInfo = $userProfile.signal.map(_.userInfo) - - type ViewModel = Option[HtmlElement] - def apply( - actionBus: Observer[Action] - )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using - router: Router[Page] - ): HtmlElement = - PageLayout(actionBus)( - $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => - PageLayout.ViewModel( - NavigationBar.ViewModel( - u, - pages.map(p => - NavigationBar.Link( - () => - PageLink( - p, - actionBus - ), - p == cp - ) - ), - userMenu, - logo - ), - c - ) - ), - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala deleted file mode 100644 index bf473c6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ /dev/null @@ -1,143 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden -import fiftyforms.ui.components.tailwind.list.IconText.ViewModel -import fiftyforms.ui.components.tailwind.Icons -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Action - -object Breadcrumbs: - - private def slash = - import svg.{*, given} - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-300", - xmlns := "http://www.w3.org/2000/svg", - fill := "currentColor", - viewBox := "0 0 20 20", - ariaHidden := true, - path( - d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" - ) - ) - - def backIcon = - Icons.solid - .`arrow-narrow-left`() - .amend( - svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" - ) - - object Link: - case class ViewModel( - page: Page, - icon: Option[SvgElement], - extraClasses: String, - text: String, - textClasses: Option[String] = None - ) - - val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" - - def shortHome(p: Page) = ViewModel( - p, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - "Zpět na úvodní stránku", - None - ) - - def fullHome(p: Page) = ViewModel( - p, - Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), - "text-gray-400 hover:text-gray-500", - "Domů", - Some("sr-only") - ) - - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - PageLink - .container($m.map(_.page), actionBus) - .amend( - cls <-- $m.map(_.extraClasses), - child.maybe <-- $m.map(_.icon), - span( - cls <-- $m.map(_.textClasses.getOrElse("")), - child.text <-- $m.map(_.text) - ) - ) - - object FullBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - ol( - role := "list", - cls := "flex items-center space-x-4", - children <-- $m.map(_.path).split(_.id)((_, _, $p) => - li( - div( - cls := "flex items-center", - child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), - Link( - $p.map(p => - if (p.isRoot) then Link.fullHome(p) - else - Link.ViewModel( - p, - None, - s"ml-4 max-w-xs truncate ${Link.baseClasses}", - p.title - ) - ), - actionBus - ) - ) - ) - ) - ) - - object ShortBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - Link( - $m.map { p => - val target = p.parent.getOrElse(p) - if target.isRoot then Link.shortHome(target) - else - Link.ViewModel( - target, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - s"Zpět na ${target.title}" - ) - }, - actionBus - ) - - def apply(actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - nav( - cls := "flex", - aria.label := "Breadcrumb", - div( - cls := "flex sm:hidden", - ShortBreadcrumbs(router.$currentPage, actionBus) - ), - div( - cls := "hidden sm:block", - FullBreadcrumbs(router.$currentPage, actionBus) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala deleted file mode 100644 index 0db2047..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ /dev/null @@ -1,242 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent -import fiftyforms.ui.components.tailwind.Icons -import fiftyforms.ui.components.tailwind.Avatar -import cz.e_bs.cmi.mdr.pdb.UserInfo -import io.laminext.syntax.core.* - -object NavigationBar: - - case class Logo(img: String, name: String) - case class Link(a: () => Anchor, active: Boolean) - case class MenuItem(title: String) - - case class ViewModel( - userInfo: UserInfo, - pages: List[Link], - userMenu: List[MenuItem], - logo: Logo - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - val $userInfo = $m.map(_.userInfo) - val mobileMenuOpen = Var(false) - - val desktopOnly = cls("hidden md:block") - val mobileOnly = cls("md:hidden") - - inline def avatarImage(size: Int = 8) = - Avatar($userInfo.map(_.img)).avatarImage(size) - - def notificationButton = button( - tpe := "button", - cls := List( - "bg-indigo-600", - "focus:outline-none", - "focus:ring-2", - "focus:ring-offset-2", - "focus:ring-offset-indigo-600", - "focus:ring-white", - "hover:text-white", - "p-1", - "rounded-full", - "text-indigo-200" - ), - span(cls := "sr-only", "View notifications"), - Icons.outline.bell() - ) - - def userProfile: HtmlElement = - val menuOpen = Var(false) - - def menuItem(item: MenuItem, idx: Int): HtmlElement = - a( - href := "#", - cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", - role := "menuitem", - tabIndex := -1, - idAttr := s"user-menu-item-$idx", - item.title - ) - - div( - cls := "ml-3 relative", - div( - button( - tpe := "button", - cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - idAttr := "user-menu-button", - aria.expanded <-- menuOpen.signal, - aria.hasPopup := true, - span(cls := "sr-only", "Open user menu"), - child <-- avatarImage(), - onClick.preventDefault.mapTo( - !menuOpen.now() - ) --> menuOpen.writer - ) - ), - /* - * */ - div( - cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", - cls <-- menuOpen.signal.map { o => - if (o) "md:block" else "md:hidden" - }, - role := "menu", - aria.orientation := "vertical", - aria.labelledBy := "user-menu-button", - tabIndex := -1, - // : keyboard navigation - children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) - ) - ) - - def mobileProfile = - def menuItem(item: MenuItem): HtmlElement = - a( - href := "#", - cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", - item.title - ) - - div( - cls := "pt-4 pb-3 border-t border-indigo-700", - div( - cls := "flex items-center px-5", - div( - cls := "flex-shrink-0", - child <-- avatarImage(10) - ), - div( - cls := "ml-3", - div( - cls := "text-base font-medium text-white", - child.text <-- $userInfo.map(_.name) - ), - child.maybe <-- $userInfo.map( - _.email.map(e => - div( - cls := "text-sm font-medium text-indigo-300", - e - ) - ) - ) - ), - notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) - ), - div( - cls := "mt-3 px-2 space-y-1", - children <-- $m.map(_.userMenu.map(menuItem)) - ) - ) - - def pageLink(page: Link): Anchor = - page - .a() - .amend( - cls := "text-white px-3 py-2 rounded-md text-sm font-medium", - cls := Seq( - "bg-indigo-700" -> page.active, - "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active - ), - ariaCurrent := (if page.active then "page" else "false") - ) - - def logoImg: Image = - img( - cls := "h-8 w-8", - src <-- $m.map(_.logo.img), - alt <-- $m.map(_.logo.name) - ) - - def mobileMenuButton = button( - tpe := "button", - cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - aria.controls := "mobile-menu", - aria.expanded <-- mobileMenuOpen.signal, - span(cls := "sr-only", "Open main menu"), - Icons.outline - .menu() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") - ), - Icons.outline - .x() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") - ), - onClick.preventDefault.mapTo( - !mobileMenuOpen.now() - ) --> mobileMenuOpen.writer - ) - - def navBarLeft = - div( - cls := "flex items-center", - div(cls := "flex-shrink-0", logoImg), - div( - desktopOnly, - div( - cls := "ml-10 flex items-baseline space-x-4", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ) - ) - ) - - def navBarRight = - div( - desktopOnly, - div( - cls := "ml-4 flex items-center md:ml-6", - notificationButton, - userProfile - ) - ) - - def navBarMobile = - div( - cls := "-mr-2 flex", - mobileOnly, - mobileMenuButton - ) - - def navBar = - div( - cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex items-center justify-between h-16", - navBarLeft, - navBarRight, - navBarMobile - ) - ) - - def mobileMenu = - div( - mobileOnly, - cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }, - idAttr := "mobile-menu", - div( - cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ), - mobileProfile - ) - - nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala deleted file mode 100644 index 75339df..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object PageHeader: - def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = - header( - cls := "bg-white shadow-sm", - div( - cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", - h1( - cls := "text-lg leading-6 font-semibold text-gray-900", - Breadcrumbs(actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala deleted file mode 100644 index 5096463..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ /dev/null @@ -1,30 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import fiftyforms.ui.components.tailwind.Loading -import io.laminext.syntax.core.* - -object PageLayout: - case class ViewModel( - navigation: NavigationBar.ViewModel, - content: Option[HtmlElement] - ) - def apply(actionBus: Observer[Action])( - $m: Signal[ViewModel], - mods: Modifier[HtmlElement]* - )(using router: Router[Page]): HtmlElement = - val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) - div( - cls := "h-full flex flex-col", - NavigationBar($m.map(_.navigation)), - child.maybe <-- router.$currentPage.map(_.isRoot) - .switch(None, Some(PageHeader(actionBus))), - main( - cls := "flex-grow-1 overflow-y-auto", - mods, - child <-- $maybeContent.map(_.getOrElse(Loading)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala deleted file mode 100644 index 62d820f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.LinkSupport.* -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo - -object PageLink: - type ViewModel = Page - def apply($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = - a(mods($m, actions), child.text <-- $m.map(_.title)) - - def apply(m: ViewModel, actions: Observer[Action])(using - router: Router[Page] - ): Anchor = apply(Val(m), actions) - - def container($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = a(mods($m, actions)) - - def container(m: ViewModel, actions: Observer[Action])(using - Router[Page] - ): Anchor = container(Val(m), actions) - - private def mods($m: Signal[Page], actions: Observer[Action])(using - router: Router[Page] - ): Modifier[Anchor] = - Seq( - href <-- $m.map(router.absoluteUrlForPage).recover { case _ => - Some("invalid url") - }, - composeEvents(onClick.noKeyMod.preventDefault)( - _.sample($m) - .map(NavigateTo.apply) - ) --> actions - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala deleted file mode 100644 index 87f825a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.pages.dashboard.DashboardPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -class DashboardPageConnector(actionBus: Observer[Action])(using - router: Router[Page] -): - def apply: HtmlElement = - AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala deleted file mode 100644 index e9b0f71..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala +++ /dev/null @@ -1,76 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailKriteriaPage -import pages.detail.components.DukazyKriteria -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -object DetailKriteriaPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailKriteriaPageConnector( - state: DetailKriteriaPageConnector.AppState -)( - $page: Signal[Page.DetailKriteria] -)(using Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => - (p.osobniCislo.value, p.parametr.value, p.kriterium.value) - )((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map( - FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) - ) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, s, $s) => - DetailKriteriaPage($s)(state.actionBus.contramap { - case DukazyKriteria.Add => - NavigateTo( - Page.UpravDukazKriteria( - Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), - Page.Titled(s.parametr.id, Some(s.parametr.nazev)), - Page.Titled(s.kriterium.id, Some(s.kriterium.id)) - ) - ) - }) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): DetailKriteriaPage.ViewModel = - DetailKriteriaPage.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala deleted file mode 100644 index 4d8024f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ /dev/null @@ -1,57 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import pages.detail.DetailPage -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import fiftyforms.ui.components.tailwind.Color - -object DetailPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailPageConnector(state: DetailPageConnector.AppState)( - $page: Signal[Page.Detail] -)(using router: Router[Page]): - val $oscChangeSignal = $page.splitOne(_.osobniCislo.value)((osc, _, _) => osc) - val $pageChangeSignal = - $oscChangeSignal.flatMap(osc => - EventStream.fromSeq(Seq(FetchUserDetails(osc), FetchParameters(osc))) - ) - // TODO: filter the value based on the current osc - // OSC change will fetch new data, but still - // - we need to be sure that what we got is really what we ought to display - // - we want to display stale data accordingly (at least with loading indicator) - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - def apply: HtmlElement = - AppPage(state.actionBus)( - $data.combineWithFn($params)(_ zip _) - .map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: List[Parameter] - ): DetailPage.ViewModel = - DetailPage.ViewModel( - o.toDetailOsoby, - p.map( - _.toParametr(param => - PageLink.container(Page.DetailParametru(o, param), state.actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala deleted file mode 100644 index fdaa7f8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailParametruPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink - -object DetailParametruPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailParametruPageConnector( - state: DetailParametruPageConnector.AppState -)( - $page: Signal[Page.DetailParametru] -)(using router: Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => (p.osobniCislo.value, p.parametr.value))((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map(FetchParameter(_, _)) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - } yield (da, pb) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailParametruPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter - ): DetailParametruPage.ViewModel = - DetailParametruPage.ViewModel( - o.toDetailOsoby, - p.toParametr(p => - PageLink.container(Page.DetailParametru(o, p), state.actionBus) - ), - p.criteria.map( - _.toKriterium { c => - PageLink.container( - Page.DetailKriteria(o, p, c), - state.actionBus - ) - } - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala deleted file mode 100644 index 049aa22..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -case class DirectoryPageConnector( - $input: EventStream[List[UserInfo]], - actionBus: Observer[Action] -)(using router: Router[Page]): - val $data = $input.startWithNone - val $actionSignal = EventStream.fromValue(FetchDirectory) - - def apply: HtmlElement = - AppPage(actionBus)( - $data.split(_ => ())((_, _, s) => - pages.directory.DirectoryPage( - s.map( - _.map( - _.toUserRow(u => - PageLink.container( - Page.Detail(Page.Titled(u.personalNumber)), - actionBus - ) - ) - ) - ) - ) - ), - $actionSignal --> actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala deleted file mode 100644 index 88e7450..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ /dev/null @@ -1,68 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii -import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria -import fiftyforms.ui.components.tailwind.Color - -extension (o: UserInfo) - def toDetailOsoby: DetailOsoby.ViewModel = - DetailOsoby.ViewModel( - o.personalNumber, - o.name, - o.email, - o.phone, - o.img, - o.mainFunction.map(f => - DetailOsoby.Funkce.ViewModel(f.name, f.dept, f.ou) - ), - o.userContracts.headOption.map(c => - DetailOsoby.PracovniPomer.ViewModel(c.rel, c.startDate, c.endDate) - ) - ) - -extension (param: Parameter) - def toParametr(container: Parameter => Anchor): DetailParametru.ViewModel = - DetailParametru.ViewModel( - id = param.id, - nazev = param.name, - popis = param.description, - status = "Nesplněno", - statusColor = Color.red, - a = container(param) - ) - -extension (crit: ParameterCriteria) - def toKriterium( - container: ParameterCriteria => Anchor - ): DetailKriteria.ViewModel = - DetailKriteria.ViewModel( - nazev = crit.criteriumText, - kapitola = crit.chapterId, - bod = crit.itemId, - status = "Nesplněno", - statusColor = Color.red, - splneno = false, - dukazy = Nil, - container = container(crit) - ) - -extension (user: UserInfo) - def toUserRow( - container: UserInfo => HtmlElement = (_: UserInfo) => div() - ): UserRow.ViewModel = - UserRow.ViewModel( - osobniCislo = user.personalNumber.toString, - celeJmeno = user.name, - prijmeni = user.surname, - hlavniFunkce = user.mainFunction.map(_.name), - img = user.img, - container = () => container(user) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala deleted file mode 100644 index 7d5b2c1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.dashboard - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.Page - -object DashboardPage: - - def render: HtmlElement = - div("Dashboard page") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala deleted file mode 100644 index 9ef0698..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala +++ /dev/null @@ -1,31 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailKriteriaPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply($m: Signal[ViewModel])( - action: Observer[DukazyKriteria.Action] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - DukazyKriteria($m.map(_.kriterium.dukazy))(action) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala deleted file mode 100644 index 3b316e1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object DetailPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametry: SeznamParametru.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby($m.map(_.osoba)), - SeznamParametru($m.map(_.parametry)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala deleted file mode 100644 index 17224cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala +++ /dev/null @@ -1,24 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailParametruPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriteria: SeznamKriterii.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby.header($m.map(_.osoba)), - DetailParametru($m.map(_.parametr)), - SeznamKriterii($m.map(_.kriteria)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala deleted file mode 100644 index dc912fa..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ /dev/null @@ -1,107 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb -package app -package pages.detail - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.UpravDukazForm -import fiftyforms.services.files.File - -object UpravDukaz: - - type ThisPage = Page.UpravDukazKriteria - type PageKey = (OsobniCislo, String, String) - - trait State { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def availableFiles: EventStream[List[File]] - def actionBus: Observer[Action] - } - - def keyOfPage(page: ThisPage): PageKey = - (page.osobniCislo.value, page.parametr.value, page.kriterium.value) - - def onChangeAction(key: PageKey): Action = - FetchParameterCriteria( - key._1, - key._2, - key._3, - Page.UpravDukazKriteria(_, _, _) - ) - - class Connector(state: State)($page: Signal[ThisPage])(using Router[Page]): - val $paramChangeSignal = $page.splitOne(keyOfPage)((x, _, _) => x) - val $pageChangeSignal = $paramChangeSignal.map(onChangeAction) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.split(_ => ())((_, s, $s) => - PageComponent( - $s.map(buildModel), - state.availableFiles, - state.actionBus.contramap { - case UpravDukazForm.Cancelled => - NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) - case UpravDukazForm.AvailableFilesRequested => - FetchAvailableFiles(s._1.personalNumber) - } - ) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): PageComponent.ViewModel = - import connectors.* - PageComponent.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) - - object PageComponent: - import components.* - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply( - $m: Signal[ViewModel], - availableFilesStream: EventStream[List[File]], - events: Observer[UpravDukazForm.Event] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - UpravDukazForm(availableFilesStream)(events) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala deleted file mode 100644 index ca311b0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.Icons -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Color - -object DetailKriteria: - case class ViewModel( - nazev: String, - kapitola: String, - bod: String, - status: String, - statusColor: Color, - splneno: Boolean, - dukazy: List[DukazKriteria.ViewModel], - container: HtmlElement = div() - ) { - val id = s"${kapitola}${bod}" - } - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h3( - cls := "text-xl leading-6 font-bold text-gray-900", - child.text <-- $m.map(_.nazev) - ), - h3( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - "Kapitola ", - child.text <-- $m.map(_.kapitola), - " bod ", - child <-- $m.map(_.bod) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala deleted file mode 100644 index ae90290..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala +++ /dev/null @@ -1,100 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Avatar - -object DetailOsoby: - - object Funkce: - case class ViewModel( - nazev: String, - stredisko: String, - voj: String - ) - - def render($m: Signal[ViewModel]): HtmlElement = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.nazev), - span( - cls := "hidden md:inline", - " @ ", - child.text <-- $m.map(_.stredisko), - ", ", - child.text <-- $m.map(_.voj) - ) - ) - - object PracovniPomer: - case class ViewModel( - druh: String, - pocatek: LocalDate, - konec: Option[LocalDate] - ) - - def render($m: Signal[ViewModel]): HtmlElement = - import fiftyforms.ui.components.tailwind.CustomAttrs.datetime - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.druh), - " od ", - time( - datetime <-- $m.map(_.pocatek.toString), - child.text <-- $m.map(_.pocatek.toString) - ) - ) - - case class ViewModel( - osobniCislo: OsobniCislo, - jmeno: String, - email: Option[String], - telefon: Option[String], - img: Option[String], - hlavniFunkce: Option[Funkce.ViewModel], - pracovniPomer: Option[PracovniPomer.ViewModel] - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-4", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(16) - ), - div( - h1( - cls := "text-2xl font-bold text-gray-900", - child.text <-- $m.map(_.jmeno) - ), - child.maybe <-- $m.map(_.hlavniFunkce).split(_ => ())((_, _, d) => - Funkce.render(d) - ), - child.maybe <-- $m.map(_.pracovniPomer).split(_ => ())((_, _, d) => - PracovniPomer.render(d) - ) - ) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-5", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(8) - ), - div( - h1( - cls := "text-lg font-medium text-gray-800", - child.text <-- $m.map(_.jmeno) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala deleted file mode 100644 index e11cb62..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color - -object DetailParametru: - case class ViewModel( - id: String, - nazev: String, - popis: String, - status: String, - statusColor: Color, - a: Anchor - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h2( - cls := "text-2xl leading-6 font-medium text-gray-900", - child.text <-- $m.map(_.nazev) - ), - p( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - child.text <-- $m.map(_.popis) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - h2( - cls := "text-lg leading-6 font-medium text-gray-600", - child.text <-- $m.map(_.nazev) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala deleted file mode 100644 index a00ede8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala +++ /dev/null @@ -1,177 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.CustomAttrs - -object DukazKriteria: - case class Osoba(osobniCislo: String, jmeno: String) - case class Udalost(osoba: Osoba, datum: LocalDate) - case class Dokument( - url: String, - nazev: String, - pridal: Udalost, - odebral: Option[Udalost] - ) - case class ViewModel( - dokumenty: List[Dokument], - autorizoval: Option[Udalost], - platiDo: Option[LocalDate], - poznámka: Option[String] - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-lg", - div( - cls := "px-4 py-5 sm:px-6", - h3( - cls := "text-lg leading-6 font-medium text-gray-900", - """Applicant Information""" - ), - p( - cls := "mt-1 max-w-2xl text-sm text-gray-500", - """Personal details and application.""" - ) - ), - div( - cls := "border-t border-gray-200 px-4 py-5 sm:px-6", - dl( - cls := "grid grid-cols-1 gap-x-4 gap-y-8 sm:grid-cols-2", - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Full name""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Margot Foster""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Application for""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Backend Developer""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Email address""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """margotfoster@example.com""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Salary expectation""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """$120,000""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """About""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Fugiat ipsum ipsum deserunt culpa aute sint do nostrud anim incididunt cillum culpa consequat. Excepteur qui ipsum aliquip consequat sint. Sit id mollit nulla mollit nostrud in ea officia proident. Irure nostrud pariatur mollit ad adipisicing reprehenderit deserunt qui eu.""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """Attachments""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - ul( - role := "list", - cls := "border border-gray-200 rounded-md divide-y divide-gray-200", - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """resume_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ), - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """coverletter_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala deleted file mode 100644 index 22f134f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala +++ /dev/null @@ -1,35 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object DukazyKriteria: - sealed trait Action - case object Add extends Action - - type ViewModel = List[DukazKriteria.ViewModel] - - def apply($m: Signal[ViewModel])(actions: Observer[Action]): HtmlElement = - div( - child.maybe <-- $m.map(m => - if (m.isEmpty) then Some(prazdnyDukaz(actions)) - else None - ), - children <-- $m.map(_.zipWithIndex).split(_._2)((_, _, $s) => - DukazKriteria($s.map(_._1)) - ) - ) - - private def prazdnyDukaz(actions: Observer[Action]): HtmlElement = - button( - tpe := "button", - onClick.preventDefault.mapTo(Add) --> actions, - cls := "relative block w-full border-2 border-gray-300 border-dashed rounded-lg p-12 text-center hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - Icons.outline - .`document-add`(12) - .amend(svg.cls := "mx-auto text-gray-400"), - span( - cls := "mt-2 block text-sm font-medium text-gray-900", - "Přidat důkaz" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala deleted file mode 100644 index f628697..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.list.StackedList -import fiftyforms.ui.components.tailwind.list.ListRow -import fiftyforms.ui.components.tailwind.list.RowTag -import fiftyforms.ui.components.tailwind.list.RowNext -import java.time.LocalDate - -object SeznamKriterii: - type ViewModel = List[DetailKriteria.ViewModel] - - private val kritList = new StackedList[DetailKriteria.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - kritList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = - p(cls := "text-sm text-gray-500", s"${i.kapitola}${i.bod}"), - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.container - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala deleted file mode 100644 index 2bfe4bc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.list.{ - StackedList, - ListRow, - RowTag, - PropList, - IconText, - RowNext -} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.LinkSupport.* - -object SeznamParametru: - type ViewModel = List[DetailParametru.ViewModel] - - private val parametrList = new StackedList[DetailParametru.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - parametrList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = emptyNode, - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.a - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala deleted file mode 100644 index f048e9d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ /dev/null @@ -1,101 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.form.* -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement -import fiftyforms.services.files.components.tailwind.FilePicker -import cz.e_bs.cmi.mdr.pdb.frontend.AutorizujDukaz -import cz.e_bs.cmi.mdr.pdb.frontend.DocumentRef -import fiftyforms.services.files.File - -object UpravDukazForm: - sealed trait Event - case object Cancelled extends Event - case object AvailableFilesRequested extends Event - def apply(availableFilesStream: EventStream[List[File]])( - updates: Observer[Event] - ): HtmlElement = - val (filesStream, filesObserver) = - EventStream.withObserver[FilePicker.Event] - val files = Var[List[File]](Nil) - def submitButtons: HtmlElement = - div( - cls := "pt-5", - div( - cls := "flex justify-end", - button( - tpe := "button", - cls := "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zrušit", - onClick.mapTo(Cancelled) --> updates - ), - button( - tpe := "submit", - cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - disabled <-- files.signal.map(_.isEmpty), - "Autorizovat" - ) - ) - ) - - div( - filesStream.collect { case FilePicker.SelectionUpdated(files) => - files.to(List) - } --> files.writer, - filesStream.collect { case FilePicker.AvailableFilesRequested => - AvailableFilesRequested - } --> updates, - cls := "bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6", - Form( - Form.Body( - Form.Section( - FormHeader( - FormHeader.ViewModel( - "Doložení kritéria", - "Sestavte doklady poskytující důkaz kritéria a potvrďte odesláním formuláře. Případné limitace či jiné relevantní údaje vepište do pole Komentář." - ) - ), - FormFields( - FormRow( - "dokumenty", - "Dokumenty", - FilePicker(files.signal, availableFilesStream)(filesObserver) - .amend(idAttr := "dokumenty", cls("max-w-lg")) - ).toHtml, - FormRow( - "platnost", - "Platnost", - input( - idAttr := "platnost", - name := "platnost", - tpe := "date", - autoComplete := "date", - cls := "block max-w-lg w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" - ) - ).toHtml, - FormRow( - "komentar", - "Komentář", - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - textArea( - idAttr := "komentar", - name := "about", - rows := 3, - cls := "max-w-lg shadow-sm block w-full focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border border-gray-300 rounded-md" - ), - p( - cls := "mt-2 text-sm text-gray-500", - "Doplňte prosím potřebné informace související s doložením kritéria, včetně případných limitací." - ) - ) - ).toHtml - ) - ) - ), - submitButtons - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala deleted file mode 100644 index 41033f6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -import components._ - -object DirectoryPage: - - type ViewModel = List[UserRow.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - val (actionsStream, actionObserver) = - EventStream.withObserver[SearchForm.Action] - val $filter = actionsStream - .collect { - case SearchForm.ClearFilter => None - case SearchForm.SetFilter(t) => Some(t) - } - .startWith(None) - val byLetter = for { - d <- $m - f <- $filter - } yield for { - (letter, users) <- d - .filter { user => - f match - case None => true - case Some(t) => user.search.contains(t) - } - .groupBy(_.prijmeni.head) - .to(List) - .sortBy(_._1) - } yield (letter.toString, users.sortBy(_.prijmeni)) - - div( - cls := "h-full max-w-7xl mx-auto order-first flex flex-col flex-shrink-0", - SearchForm(actionObserver), - Directory(byLetter) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala deleted file mode 100644 index 73f915d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -object Directory: - - object Header: - type ViewModel = String - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500", - h3(child.text <-- $m) - ) - - object UserList: - type ViewModel = List[UserRow.ViewModel] - def apply($m: Signal[ViewModel]): HtmlElement = - ul( - role := "list", - cls := "relative z-0 divide-y divide-gray-200", - children <-- $m.split(_.osobniCislo)((_, _, s) => UserRow(s)) - ) - - type ViewModel = List[(String, List[UserRow.ViewModel])] - def apply($m: Signal[ViewModel]): HtmlElement = - nav( - cls := "flex-1 min-h-0 overflow-y-auto", - aria.label := "Directory", - children <-- $m.split(_._1)((_, _, s) => - div( - cls := "relative", - Header(s.map(_._1)), - UserList(s.map(_._2)) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala deleted file mode 100644 index e182910..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala +++ /dev/null @@ -1,54 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object SearchForm: - sealed trait Action - case object SubmitFilter extends Action - sealed trait FilterAction extends Action - case object ClearFilter extends FilterAction - case class SetFilter(value: String) extends FilterAction - - def apply(actions: Observer[Action]): HtmlElement = - div( - cls := "px-6 pt-4 pb-4", - form( - cls := "flex space-x-4", - onSubmit.mapTo(SubmitFilter) --> actions, - div( - cls := "flex-1 min-w-0", - label( - forId := "search", - cls := "sr-only", - "Hledat" - ), - div( - cls := "relative rounded-md shadow-sm", - div( - cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", - Icons.solid.search().amend(svg.cls := "text-gray-400") - ), - input( - tpe := "search", - name := "search", - idAttr := "search", - cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", - placeholder := "Hledat", - composeEvents(onInput.mapToValue.setAsValue.map(SetFilter(_)))( - _.throttle(500) - ) --> actions - ) - ) - ), - button( - tpe := "submit", - cls := "inline-flex justify-center px-3.5 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500", - Icons.solid.filter().amend(svg.cls := "text-gray-400"), - span( - cls := "sr-only", - "Hledat" - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala deleted file mode 100644 index 82f7723..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala +++ /dev/null @@ -1,52 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.airstream.core.Signal -import fiftyforms.ui.components.tailwind.Avatar - -object UserRow: - case class ViewModel( - osobniCislo: String, - celeJmeno: String, - prijmeni: String, - hlavniFunkce: Option[String], - img: Option[String], - container: () => HtmlElement = () => div() - ) { - val search = prijmeni.toLowerCase - } - - def apply($m: Signal[ViewModel]): HtmlElement = - inline def avatarImage = - Avatar($m.map(_.img)).avatarImage(10) - - li( - div( - cls := "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500", - div( - cls := "flex-shrink-0", - child <-- avatarImage - ), - div( - cls := "flex-1 min-w-0", - child <-- $m.map { o => - o.container() - .amend( - cls := "focus:outline-none", - span( - cls := "absolute inset-0", - aria.hidden := true - ), - p( - cls := "text-sm font-medium text-gray-900", - o.celeJmeno - ), - p( - cls := "text-sm text-gray-500 truncate", - o.hlavniFunkce - ) - ) - } - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala deleted file mode 100644 index 467c5f5..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala +++ /dev/null @@ -1,99 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router - -object ErrorPage: - case class ViewModel( - homePage: Page, - errorName: String, - title: String, - subTitle: String - ) - - def apply(m: ViewModel, actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - val ViewModel(homePage, errorName, title, subTitle) = m - div( - cls := "min-h-full pt-16 pb-12 flex flex-col bg-white", - main( - cls := "flex-grow flex flex-col justify-center max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex-shrink-0 flex justify-center", - a( - href := "/", - cls := "inline-flex", - span( - cls := "sr-only", - """Workflow""" - ), - img( - cls := "h-12 w-auto", - src := "https://tailwindui.com/img/logos/workflow-mark.svg?color=indigo&shade=600", - alt := "" - ) - ) - ), - div( - cls := "py-16", - div( - cls := "text-center", - p( - cls := "text-sm font-semibold text-indigo-600 uppercase tracking-wide", - errorName - ), - h1( - cls := "mt-2 text-4xl font-extrabold text-gray-900 tracking-tight sm:text-5xl", - title - ), - p( - cls := "mt-2 text-base text-gray-500", - subTitle - ), - div( - cls := "mt-6", - PageLink - .container(homePage, actionBus) - .amend( - cls := "text-base font-medium text-indigo-600 hover:text-indigo-500", - """Go back home""" - ) - ) - ) - ) - ), - footer( - cls := "flex-shrink-0 max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", - nav( - cls := "flex justify-center space-x-4", - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Contact Support""" - ), - span( - cls := "inline-block border-l border-gray-300", - aria.hidden := true - ), - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Status""" - ), - span( - cls := "inline-block border-l border-gray-300", - aria.hidden := true - ), - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Twitter""" - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala deleted file mode 100644 index bd9baec..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action - -object NotFoundPage: - def apply(homePage: Page, url: String, actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - ErrorPage( - ErrorPage.ViewModel( - homePage, - "404 error", - "Page not found.", - s"Sorry, but page $url doesn't exist." - ), - actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala deleted file mode 100644 index 476fe3c..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action - -object UnhandledErrorPage: - - case class ViewModel( - homePage: Page, - errorName: Option[String], - errorMessage: Option[String] - ) - - def apply(m: ViewModel, actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - ErrorPage( - ErrorPage.ViewModel( - m.homePage, - "Unexpected error occurred", - m.errorName.getOrElse( - "Uh oh!" - ), // TODO: translations, better text than uh oh - m.errorMessage.getOrElse( - "This wasn't supposed to happen! Please try again." - ) - ), - actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala deleted file mode 100644 index 25f059f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala +++ /dev/null @@ -1,6 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.services - -import com.raquo.airstream.core.Observer - -trait DataFetcher[K, A]: - def fetch(id: K, sink: Observer[A]): Unit diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala deleted file mode 100644 index a469830..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala +++ /dev/null @@ -1,120 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package state - -import com.raquo.airstream.core.EventStream -import cz.e_bs.cmi.mdr.pdb.{UserInfo, OsobniCislo} -import com.raquo.airstream.core.Observer -import scala.scalajs.js -import scala.scalajs.js.JSON -import zio.json._ -import com.raquo.airstream.eventbus.EventBus -import com.raquo.airstream.ownership.Owner -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.UserFunction -import cz.e_bs.cmi.mdr.pdb.UserContract -import fiftyforms.services.files.File - -trait AppState - extends connectors.DetailPageConnector.AppState - with connectors.DetailParametruPageConnector.AppState - with connectors.DetailKriteriaPageConnector.AppState - with pages.detail.UpravDukaz.State: - def users: EventStream[List[UserInfo]] - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - -class MockAppState(implicit owner: Owner, router: Router[Page]) - extends AppState: - - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonDecoder[UserFunction] = DeriveJsonDecoder.gen - given JsonDecoder[UserContract] = DeriveJsonDecoder.gen - given JsonDecoder[UserInfo] = DeriveJsonDecoder.gen - - given JsonDecoder[ParameterCriteria] = DeriveJsonDecoder.gen - given JsonDecoder[Parameter] = DeriveJsonDecoder.gen - - private val actions = EventBus[Action]() - private val (parametersStream, pushParameters) = - EventStream.withCallback[List[Parameter]] - private val (usersStream, pushUsers) = - EventStream.withCallback[List[UserInfo]] - private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] - private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] - - private val mockData: List[UserInfo] = - mockUsers - .asInstanceOf[js.Dictionary[js.Object]] - .values - // TODO: is there a more efficient way to parse from JS object directly? - .map { o => - val parsed = JSON.stringify(o).fromJson[UserInfo] - parsed.left.foreach(org.scalajs.dom.console.log(_)) - parsed - } - .collect { case Right(u) => - u - } - .toList - - private val mockParameters: List[Parameter] = - pdbParams - .asInstanceOf[js.Dictionary[js.Object]] - .values - .map(o => JSON.stringify(o).fromJson[Parameter]) - .collect { case Right(p) => p } - .toList - - // TODO: Extract to separate event handler - actions.events.foreach { - case FetchDirectory => pushUsers(mockData) - case FetchUserDetails(osc) => - mockData.find(_.personalNumber == osc).foreach { o => - pushDetails(o) - router.replaceState(Page.Detail(o)) - } - case FetchParameters(osc) => - pushParameters(mockParameters) - case FetchParameter(osc, paramId) => - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(Page.DetailParametru(o, p)) - case FetchParameterCriteria(osc, paramId, critId, page) => - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - c <- p.criteria.find(_.id == critId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(page(o, p, c)) - case NavigateTo(page) => router.pushState(page) - case FetchAvailableFiles(osc) => - pushFiles( - List( - File("https://tc163.cmi.cz/here", "Example file") - ) - ) - } - - override def users: EventStream[List[UserInfo]] = - usersStream.debugWithName("users") - - override def details: EventStream[UserInfo] = - detailsStream.debugWithName("details") - - override def parameters: EventStream[List[Parameter]] = - parametersStream.debugWithName("parameters") - - override def availableFiles: EventStream[List[File]] = - filesStream.debugWithName("available files") - - override def actionBus: Observer[Action] = - actions.writer.debugWithName("actions writer") diff --git a/app/src/main/scala/mdr/pdb/app/Main.scala b/app/src/main/scala/mdr/pdb/app/Main.scala new file mode 100644 index 0000000..081b4ee --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/Main.scala @@ -0,0 +1,103 @@ +package mdr.pdb.app + +import scala.scalajs.js.annotation.JSExportTopLevel +import scala.scalajs.js.annotation.JSExport +import scala.scalajs.js.annotation.JSImport +import scala.scalajs.js +import org.scalajs.dom +import com.raquo.laminar.api.L.{*, given} +import scala.scalajs.js.Date +import com.raquo.waypoint.Router +import com.raquo.waypoint.SplitRender +import mdr.pdb.app.services.DataFetcher +import scala.scalajs.js.JSON +import zio.json._ +import mdr.pdb.UserInfo +import mdr.pdb.app.state.AppState + +@js.native +@JSImport("stylesheets/main.css", JSImport.Namespace) +object Css extends js.Any + +@js.native +@JSImport("data/users.json", JSImport.Default) +object mockUsers extends js.Object + +@js.native +@JSImport("params/pdb-params.json", JSImport.Default) +object pdbParams extends js.Object + +@JSExportTopLevel("app") +object Main: + + @JSExport + def main(args: Array[String]): Unit = + import Routes.given + onLoad { + setupAirstream() + val appContainer = dom.document.querySelector("#app") + val _ = + render( + appContainer, + renderPage(state.MockAppState(using unsafeWindowOwner, router)) + ) + } + + private def onLoad(f: => Unit): Unit = + documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) + + private def setupAirstream()(using router: Router[Page]): Unit = + AirstreamError.registerUnhandledErrorCallback(err => + router.forcePage( + Page.UnhandledError( + Some(err.getClass.getName), // TODO: Fill only in dev mode + Some(err.getMessage) + ) + ) + ) + + def renderPage(state: AppState)(using + router: Router[Page] + ): HtmlElement = + val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) + .collectSignal[Page.Detail]( + connectors + .DetailPageConnector(state)(_) + .apply + ) + .collectSignal[Page.DetailParametru]( + connectors + .DetailParametruPageConnector(state)(_) + .apply + ) + .collectSignal[Page.DetailKriteria]( + connectors + .DetailKriteriaPageConnector(state)(_) + .apply + ) + .collectSignal[Page.UpravDukazKriteria]( + pages.detail.UpravDukaz.Connector(state)(_).apply + ) + .collectStatic(Page.Dashboard)( + connectors.DashboardPageConnector(state.actionBus).apply + ) + .collect[Page.NotFound](pg => + pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) + ) + .collect[Page.UnhandledError](pg => + pages.errors + .UnhandledErrorPage( + pages.errors.UnhandledErrorPage + .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), + state.actionBus + ) + ) + .collectStatic(Page.Directory)( + connectors + .DirectoryPageConnector(state.users, state.actionBus) + .apply + ) + div(cls := "h-full", child <-- pageSplitter.$view) + + // Pull in the stylesheet + val css: Css.type = Css diff --git a/app/src/main/scala/mdr/pdb/app/Routes.scala b/app/src/main/scala/mdr/pdb/app/Routes.scala new file mode 100644 index 0000000..2cc860c --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/Routes.scala @@ -0,0 +1,198 @@ +package mdr.pdb.app + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.* +import org.scalajs.dom +import zio.json.{*, given} +import mdr.pdb.OsobniCislo + +import scala.scalajs.js +import mdr.pdb.UserInfo +import mdr.pdb.Parameter +import mdr.pdb.ParameterCriteria +import mdr.pdb.app.Page.Titled + +// enum is not working with Waypoints' SplitRender collectStatic +sealed abstract class Page( + val id: String, + val title: String, + val parent: Option[Page] +) { + val path: Vector[Page] = + parent match + case None => Vector(this) + case Some(p) => p.path :+ this + + val isRoot: Boolean = parent.isEmpty +} + +object Page: + + case class Titled[V](value: V, title: Option[String] = None): + val show: String = title.getOrElse(value.toString) + + case object Directory extends Page("directory", "Adresář", None) + + case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) + + case class Detail(osobniCislo: Titled[OsobniCislo]) + extends Page("user", osobniCislo.show, Some(Directory)) + + object Detail { + def apply(o: UserInfo): Detail = Detail( + Titled(o.personalNumber, Some(o.name)) + ) + } + + case class DetailParametru( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String] + ) extends Page( + "parameter", + parametr.show, + Some(Detail(osobniCislo)) + ) + + object DetailParametru { + def apply(o: UserInfo, p: Parameter): DetailParametru = + DetailParametru( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)) + ) + } + + case class DetailKriteria( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String], + kriterium: Titled[String] + ) extends Page( + "criteria", + kriterium.show, + Some(DetailParametru(osobniCislo, parametr)) + ) + + object DetailKriteria { + def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = + DetailKriteria( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)), + Titled(k.id, Some(k.id)) + ) + } + + case class UpravDukazKriteria( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String], + kriterium: Titled[String] + ) extends Page( + "addProof", + "Důkaz", + Some(DetailKriteria(osobniCislo, parametr, kriterium)) + ) + + object UpravDukazKriteria { + def apply( + o: UserInfo, + p: Parameter, + k: ParameterCriteria + ): UpravDukazKriteria = + UpravDukazKriteria( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)), + Titled(k.id, Some(k.id)) + ) + } + + case class NotFound(url: String) extends Page("404", "404", Some(Directory)) + + case class UnhandledError( + errorName: Option[String], + errorMessage: Option[String] + ) extends Page("500", "Unexpected error", Some(Directory)) + +object Routes: + given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) + given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) + given [V: JsonEncoder]: JsonEncoder[Titled[V]] = + DeriveJsonEncoder.gen[Titled[V]] + given [V: JsonDecoder]: JsonDecoder[Titled[V]] = + DeriveJsonDecoder.gen[Titled[V]] + given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] + given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] + + val base = + js.`import`.meta.env.BASE_URL + .asInstanceOf[String] + "app" + + val homePage: Page = Page.Directory + + given router: Router[Page] = Router[Page]( + routes = List( + Route.static(homePage, root / endOfSegments, basePath = base), + Route.static( + Page.Dashboard, + root / "dashboard" / endOfSegments, + basePath = base + ), + Route[Page.Detail, String]( + encode = _.osobniCislo.value.toString, + decode = osc => Page.Detail(Titled(OsobniCislo(osc))), + root / "osoba" / segment[String] / endOfSegments, + basePath = base + ), + Route[Page.DetailParametru, (String, String)]( + encode = p => (p.osobniCislo.value.toString, p.parametr.value), + decode = + p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / endOfSegments, + basePath = base + ), + Route[Page.DetailKriteria, (String, String, String)]( + encode = p => + ( + p.osobniCislo.value.toString, + p.parametr.value, + p.kriterium.value.replaceAll("\\.", "--") + ), + decode = p => + Page.DetailKriteria( + Titled(OsobniCislo(p._1)), + Titled(p._2), + Titled(p._3.replaceAll("--", ".")) + ), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / "kriterium" / segment[String] / endOfSegments, + basePath = base + ), + Route[Page.UpravDukazKriteria, (String, String, String)]( + encode = p => + ( + p.osobniCislo.value.toString, + p.parametr.value, + p.kriterium.value.replaceAll("\\.", "--") + ), + decode = p => + Page.UpravDukazKriteria( + Titled(OsobniCislo(p._1)), + Titled(p._2), + Titled(p._3.replaceAll("--", ".")) + ), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / "kriterium" / segment[String] / "edit" / endOfSegments, + basePath = base + ) + ), + serializePage = _.toJson, + deserializePage = _.fromJson[Page] + .fold(s => throw IllegalStateException(s), identity), + getPageTitle = _.title, + routeFallback = url => Page.NotFound(url), + deserializeFallback = _ => Page.Dashboard + )( + $popStateEvent = windowEvents.onPopState, + owner = unsafeWindowOwner + ) diff --git a/app/src/main/scala/mdr/pdb/app/actions.scala b/app/src/main/scala/mdr/pdb/app/actions.scala new file mode 100644 index 0000000..d078b3a --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/actions.scala @@ -0,0 +1,21 @@ +package mdr.pdb.app + +import mdr.pdb.OsobniCislo +import mdr.pdb.UserInfo +import mdr.pdb.Parameter +import mdr.pdb.ParameterCriteria + +sealed trait Action + +case object FetchDirectory extends Action +case class FetchUserDetails(osc: OsobniCislo) extends Action +case class FetchParameters(osc: OsobniCislo) extends Action +case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action +case class FetchParameterCriteria( + osc: OsobniCislo, + paramId: String, + critId: String, + page: (UserInfo, Parameter, ParameterCriteria) => Page +) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action +case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/mdr/pdb/app/components/AppPage.scala new file mode 100644 index 0000000..130c817 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/AppPage.scala @@ -0,0 +1,82 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.Page +import mdr.pdb.{UserProfile, UserInfo, OsobniCislo} +import com.raquo.waypoint.Router +import mdr.pdb.app.Action +import mdr.pdb.app.NavigateTo +import mdr.pdb.UserFunction + +object AppPage: + // TODO: pages by logged in user + val pages: List[Page] = List(Page.Directory, Page.Dashboard) + + import NavigationBar.{Logo, MenuItem} + + val logo = Logo( + "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", + "Workflow" + ) + + // TODO: menu items by user profile + val userMenu = + List( + MenuItem("Your Profile"), + MenuItem("Settings"), + MenuItem("Sign out") + ) + + // TODO: load user profile + val $userProfile = Var( + UserProfile( + "tom", + UserInfo( + OsobniCislo("1031"), + "tom", + "Tom", + "Cook", + None, + None, + Some("tom@example.com"), + Some("+420 222 866 180"), + Some( + UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") + ), + Nil, + None + ) + ) + ) + + val $userInfo = $userProfile.signal.map(_.userInfo) + + type ViewModel = Option[HtmlElement] + def apply( + actionBus: Observer[Action] + )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using + router: Router[Page] + ): HtmlElement = + PageLayout(actionBus)( + $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => + PageLayout.ViewModel( + NavigationBar.ViewModel( + u, + pages.map(p => + NavigationBar.Link( + () => + PageLink( + p, + actionBus + ), + p == cp + ) + ), + userMenu, + logo + ), + c + ) + ), + mods + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala new file mode 100644 index 0000000..9375398 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala @@ -0,0 +1,143 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.tailwind.list.IconText.ViewModel +import fiftyforms.ui.components.tailwind.Icons +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.Page + +import com.raquo.laminar.api.L.{*, given} +import io.laminext.syntax.core.{*, given} +import mdr.pdb.app.Action + +object Breadcrumbs: + + private def slash = + import svg.{*, given} + svg( + cls := "flex-shrink-0 h-5 w-5 text-gray-300", + xmlns := "http://www.w3.org/2000/svg", + fill := "currentColor", + viewBox := "0 0 20 20", + ariaHidden := true, + path( + d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" + ) + ) + + def backIcon = + Icons.solid + .`arrow-narrow-left`() + .amend( + svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" + ) + + object Link: + case class ViewModel( + page: Page, + icon: Option[SvgElement], + extraClasses: String, + text: String, + textClasses: Option[String] = None + ) + + val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" + + def shortHome(p: Page) = ViewModel( + p, + Some(backIcon), + "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", + "Zpět na úvodní stránku", + None + ) + + def fullHome(p: Page) = ViewModel( + p, + Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), + "text-gray-400 hover:text-gray-500", + "Domů", + Some("sr-only") + ) + + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + PageLink + .container($m.map(_.page), actionBus) + .amend( + cls <-- $m.map(_.extraClasses), + child.maybe <-- $m.map(_.icon), + span( + cls <-- $m.map(_.textClasses.getOrElse("")), + child.text <-- $m.map(_.text) + ) + ) + + object FullBreadcrumbs: + type ViewModel = Page + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + ol( + role := "list", + cls := "flex items-center space-x-4", + children <-- $m.map(_.path).split(_.id)((_, _, $p) => + li( + div( + cls := "flex items-center", + child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), + Link( + $p.map(p => + if (p.isRoot) then Link.fullHome(p) + else + Link.ViewModel( + p, + None, + s"ml-4 max-w-xs truncate ${Link.baseClasses}", + p.title + ) + ), + actionBus + ) + ) + ) + ) + ) + + object ShortBreadcrumbs: + type ViewModel = Page + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + Link( + $m.map { p => + val target = p.parent.getOrElse(p) + if target.isRoot then Link.shortHome(target) + else + Link.ViewModel( + target, + Some(backIcon), + "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", + s"Zpět na ${target.title}" + ) + }, + actionBus + ) + + def apply(actionBus: Observer[Action])(using + router: Router[Page] + ): HtmlElement = + nav( + cls := "flex", + aria.label := "Breadcrumb", + div( + cls := "flex sm:hidden", + ShortBreadcrumbs(router.$currentPage, actionBus) + ), + div( + cls := "hidden sm:block", + FullBreadcrumbs(router.$currentPage, actionBus) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala new file mode 100644 index 0000000..4bb8b8d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala @@ -0,0 +1,242 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent +import fiftyforms.ui.components.tailwind.Icons +import fiftyforms.ui.components.tailwind.Avatar +import mdr.pdb.UserInfo +import io.laminext.syntax.core.* + +object NavigationBar: + + case class Logo(img: String, name: String) + case class Link(a: () => Anchor, active: Boolean) + case class MenuItem(title: String) + + case class ViewModel( + userInfo: UserInfo, + pages: List[Link], + userMenu: List[MenuItem], + logo: Logo + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + val $userInfo = $m.map(_.userInfo) + val mobileMenuOpen = Var(false) + + val desktopOnly = cls("hidden md:block") + val mobileOnly = cls("md:hidden") + + inline def avatarImage(size: Int = 8) = + Avatar($userInfo.map(_.img)).avatarImage(size) + + def notificationButton = button( + tpe := "button", + cls := List( + "bg-indigo-600", + "focus:outline-none", + "focus:ring-2", + "focus:ring-offset-2", + "focus:ring-offset-indigo-600", + "focus:ring-white", + "hover:text-white", + "p-1", + "rounded-full", + "text-indigo-200" + ), + span(cls := "sr-only", "View notifications"), + Icons.outline.bell() + ) + + def userProfile: HtmlElement = + val menuOpen = Var(false) + + def menuItem(item: MenuItem, idx: Int): HtmlElement = + a( + href := "#", + cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", + role := "menuitem", + tabIndex := -1, + idAttr := s"user-menu-item-$idx", + item.title + ) + + div( + cls := "ml-3 relative", + div( + button( + tpe := "button", + cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", + idAttr := "user-menu-button", + aria.expanded <-- menuOpen.signal, + aria.hasPopup := true, + span(cls := "sr-only", "Open user menu"), + child <-- avatarImage(), + onClick.preventDefault.mapTo( + !menuOpen.now() + ) --> menuOpen.writer + ) + ), + /* + * */ + div( + cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", + cls <-- menuOpen.signal.map { o => + if (o) "md:block" else "md:hidden" + }, + role := "menu", + aria.orientation := "vertical", + aria.labelledBy := "user-menu-button", + tabIndex := -1, + // : keyboard navigation + children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) + ) + ) + + def mobileProfile = + def menuItem(item: MenuItem): HtmlElement = + a( + href := "#", + cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", + item.title + ) + + div( + cls := "pt-4 pb-3 border-t border-indigo-700", + div( + cls := "flex items-center px-5", + div( + cls := "flex-shrink-0", + child <-- avatarImage(10) + ), + div( + cls := "ml-3", + div( + cls := "text-base font-medium text-white", + child.text <-- $userInfo.map(_.name) + ), + child.maybe <-- $userInfo.map( + _.email.map(e => + div( + cls := "text-sm font-medium text-indigo-300", + e + ) + ) + ) + ), + notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) + ), + div( + cls := "mt-3 px-2 space-y-1", + children <-- $m.map(_.userMenu.map(menuItem)) + ) + ) + + def pageLink(page: Link): Anchor = + page + .a() + .amend( + cls := "text-white px-3 py-2 rounded-md text-sm font-medium", + cls := Seq( + "bg-indigo-700" -> page.active, + "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active + ), + ariaCurrent := (if page.active then "page" else "false") + ) + + def logoImg: Image = + img( + cls := "h-8 w-8", + src <-- $m.map(_.logo.img), + alt <-- $m.map(_.logo.name) + ) + + def mobileMenuButton = button( + tpe := "button", + cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", + aria.controls := "mobile-menu", + aria.expanded <-- mobileMenuOpen.signal, + span(cls := "sr-only", "Open main menu"), + Icons.outline + .menu() + .amend( + svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") + ), + Icons.outline + .x() + .amend( + svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") + ), + onClick.preventDefault.mapTo( + !mobileMenuOpen.now() + ) --> mobileMenuOpen.writer + ) + + def navBarLeft = + div( + cls := "flex items-center", + div(cls := "flex-shrink-0", logoImg), + div( + desktopOnly, + div( + cls := "ml-10 flex items-baseline space-x-4", + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) + ) + ) + ) + + def navBarRight = + div( + desktopOnly, + div( + cls := "ml-4 flex items-center md:ml-6", + notificationButton, + userProfile + ) + ) + + def navBarMobile = + div( + cls := "-mr-2 flex", + mobileOnly, + mobileMenuButton + ) + + def navBar = + div( + cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", + div( + cls := "flex items-center justify-between h-16", + navBarLeft, + navBarRight, + navBarMobile + ) + ) + + def mobileMenu = + div( + mobileOnly, + cls <-- mobileMenuOpen.signal.map { o => + if (o) "block" else "hidden" + }, + idAttr := "mobile-menu", + div( + cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) + ), + mobileProfile + ) + + nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala new file mode 100644 index 0000000..c4cb10d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala @@ -0,0 +1,19 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.Page +import com.raquo.waypoint.Router +import mdr.pdb.app.Action + +object PageHeader: + def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = + header( + cls := "bg-white shadow-sm", + div( + cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", + h1( + cls := "text-lg leading-6 font-semibold text-gray-900", + Breadcrumbs(actionBus) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala new file mode 100644 index 0000000..aae4539 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala @@ -0,0 +1,30 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.Page +import mdr.pdb.app.Action +import fiftyforms.ui.components.tailwind.Loading +import io.laminext.syntax.core.* + +object PageLayout: + case class ViewModel( + navigation: NavigationBar.ViewModel, + content: Option[HtmlElement] + ) + def apply(actionBus: Observer[Action])( + $m: Signal[ViewModel], + mods: Modifier[HtmlElement]* + )(using router: Router[Page]): HtmlElement = + val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) + div( + cls := "h-full flex flex-col", + NavigationBar($m.map(_.navigation)), + child.maybe <-- router.$currentPage.map(_.isRoot) + .switch(None, Some(PageHeader(actionBus))), + main( + cls := "flex-grow-1 overflow-y-auto", + mods, + child <-- $maybeContent.map(_.getOrElse(Loading)) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/mdr/pdb/app/components/PageLink.scala new file mode 100644 index 0000000..e6e7052 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageLink.scala @@ -0,0 +1,40 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.LinkSupport.* +import mdr.pdb.app.Page +import mdr.pdb.app.Action +import com.raquo.waypoint.Router +import mdr.pdb.app.NavigateTo + +object PageLink: + type ViewModel = Page + def apply($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = + a(mods($m, actions), child.text <-- $m.map(_.title)) + + def apply(m: ViewModel, actions: Observer[Action])(using + router: Router[Page] + ): Anchor = apply(Val(m), actions) + + def container($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = a(mods($m, actions)) + + def container(m: ViewModel, actions: Observer[Action])(using + Router[Page] + ): Anchor = container(Val(m), actions) + + private def mods($m: Signal[Page], actions: Observer[Action])(using + router: Router[Page] + ): Modifier[Anchor] = + Seq( + href <-- $m.map(router.absoluteUrlForPage).recover { case _ => + Some("invalid url") + }, + composeEvents(onClick.noKeyMod.preventDefault)( + _.sample($m) + .map(NavigateTo.apply) + ) --> actions + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala new file mode 100644 index 0000000..7682772 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala @@ -0,0 +1,13 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.pages.dashboard.DashboardPage +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage + +class DashboardPageConnector(actionBus: Observer[Action])(using + router: Router[Page] +): + def apply: HtmlElement = + AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala new file mode 100644 index 0000000..76b9133 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala @@ -0,0 +1,76 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.Parameter +import mdr.pdb.UserInfo +import mdr.pdb.app.pages.detail.DetailParametruPage +import pages.detail.DetailKriteriaPage +import pages.detail.components.DukazyKriteria +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.ParameterCriteria + +object DetailKriteriaPageConnector { + trait AppState { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + } +} + +case class DetailKriteriaPageConnector( + state: DetailKriteriaPageConnector.AppState +)( + $page: Signal[Page.DetailKriteria] +)(using Router[Page]): + val $paramChangeSignal = + $page.splitOne(p => + (p.osobniCislo.value, p.parametr.value, p.kriterium.value) + )((x, _, _) => x) + val $pageChangeSignal = + $paramChangeSignal.map( + FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) + ) + + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + val $merged = + $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => + for { + da <- d + pa <- p + pb <- pa.find(_.id == pc._2) + ka <- pb.criteria.find(_.id == pc._3) + } yield (da, pb, ka) + ) + + def apply: HtmlElement = + AppPage(state.actionBus)( + $merged.map(_.map(buildModel)) + .split(_ => ())((_, s, $s) => + DetailKriteriaPage($s)(state.actionBus.contramap { + case DukazyKriteria.Add => + NavigateTo( + Page.UpravDukazKriteria( + Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), + Page.Titled(s.parametr.id, Some(s.parametr.nazev)), + Page.Titled(s.kriterium.id, Some(s.kriterium.id)) + ) + ) + }) + ), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: Parameter, + k: ParameterCriteria + ): DetailKriteriaPage.ViewModel = + DetailKriteriaPage.ViewModel( + o.toDetailOsoby, + p.toParametr(_ => a()), + k.toKriterium(_ => a()) + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DetailPageConnector.scala new file mode 100644 index 0000000..df3a6b7 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DetailPageConnector.scala @@ -0,0 +1,57 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.UserInfo +import pages.detail.DetailPage +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.pages.detail.components.DetailOsoby +import mdr.pdb.Parameter +import mdr.pdb.app.pages.detail.components.SeznamParametru +import fiftyforms.ui.components.tailwind.Color + +object DetailPageConnector { + trait AppState { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + } +} + +case class DetailPageConnector(state: DetailPageConnector.AppState)( + $page: Signal[Page.Detail] +)(using router: Router[Page]): + val $oscChangeSignal = $page.splitOne(_.osobniCislo.value)((osc, _, _) => osc) + val $pageChangeSignal = + $oscChangeSignal.flatMap(osc => + EventStream.fromSeq(Seq(FetchUserDetails(osc), FetchParameters(osc))) + ) + // TODO: filter the value based on the current osc + // OSC change will fetch new data, but still + // - we need to be sure that what we got is really what we ought to display + // - we want to display stale data accordingly (at least with loading indicator) + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + def apply: HtmlElement = + AppPage(state.actionBus)( + $data.combineWithFn($params)(_ zip _) + .map(_.map(buildModel)) + .split(_ => ())((_, _, s) => DetailPage(s)), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: List[Parameter] + ): DetailPage.ViewModel = + DetailPage.ViewModel( + o.toDetailOsoby, + p.map( + _.toParametr(param => + PageLink.container(Page.DetailParametru(o, param), state.actionBus) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DetailParametruPageConnector.scala new file mode 100644 index 0000000..fa7193d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DetailParametruPageConnector.scala @@ -0,0 +1,67 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.Parameter +import mdr.pdb.UserInfo +import mdr.pdb.app.pages.detail.DetailParametruPage +import pages.detail.DetailParametruPage +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.components.PageLink + +object DetailParametruPageConnector { + trait AppState { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + } +} + +case class DetailParametruPageConnector( + state: DetailParametruPageConnector.AppState +)( + $page: Signal[Page.DetailParametru] +)(using router: Router[Page]): + val $paramChangeSignal = + $page.splitOne(p => (p.osobniCislo.value, p.parametr.value))((x, _, _) => x) + val $pageChangeSignal = + $paramChangeSignal.map(FetchParameter(_, _)) + + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + val $merged = + $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => + for { + da <- d + pa <- p + pb <- pa.find(_.id == pc._2) + } yield (da, pb) + ) + + def apply: HtmlElement = + AppPage(state.actionBus)( + $merged.map(_.map(buildModel)) + .split(_ => ())((_, _, s) => DetailParametruPage(s)), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: Parameter + ): DetailParametruPage.ViewModel = + DetailParametruPage.ViewModel( + o.toDetailOsoby, + p.toParametr(p => + PageLink.container(Page.DetailParametru(o, p), state.actionBus) + ), + p.criteria.map( + _.toKriterium { c => + PageLink.container( + Page.DetailKriteria(o, p, c), + state.actionBus + ) + } + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DirectoryPageConnector.scala new file mode 100644 index 0000000..ab2c045 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DirectoryPageConnector.scala @@ -0,0 +1,34 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.UserInfo +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.components.AppPage + +case class DirectoryPageConnector( + $input: EventStream[List[UserInfo]], + actionBus: Observer[Action] +)(using router: Router[Page]): + val $data = $input.startWithNone + val $actionSignal = EventStream.fromValue(FetchDirectory) + + def apply: HtmlElement = + AppPage(actionBus)( + $data.split(_ => ())((_, _, s) => + pages.directory.DirectoryPage( + s.map( + _.map( + _.toUserRow(u => + PageLink.container( + Page.Detail(Page.Titled(u.personalNumber)), + actionBus + ) + ) + ) + ) + ) + ), + $actionSignal --> actionBus + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/mdr/pdb/app/connectors/conversions.scala new file mode 100644 index 0000000..f581796 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/conversions.scala @@ -0,0 +1,68 @@ +package mdr.pdb.app.connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.UserInfo +import mdr.pdb.app.pages.detail.components.DetailOsoby +import mdr.pdb.Parameter +import mdr.pdb.app.pages.detail.components.SeznamParametru +import mdr.pdb.ParameterCriteria +import mdr.pdb.app.pages.detail.components.SeznamKriterii +import mdr.pdb.app.pages.directory.components.UserRow +import mdr.pdb.app.pages.detail.components.DetailParametru +import mdr.pdb.app.pages.detail.components.DetailKriteria +import fiftyforms.ui.components.tailwind.Color + +extension (o: UserInfo) + def toDetailOsoby: DetailOsoby.ViewModel = + DetailOsoby.ViewModel( + o.personalNumber, + o.name, + o.email, + o.phone, + o.img, + o.mainFunction.map(f => + DetailOsoby.Funkce.ViewModel(f.name, f.dept, f.ou) + ), + o.userContracts.headOption.map(c => + DetailOsoby.PracovniPomer.ViewModel(c.rel, c.startDate, c.endDate) + ) + ) + +extension (param: Parameter) + def toParametr(container: Parameter => Anchor): DetailParametru.ViewModel = + DetailParametru.ViewModel( + id = param.id, + nazev = param.name, + popis = param.description, + status = "Nesplněno", + statusColor = Color.red, + a = container(param) + ) + +extension (crit: ParameterCriteria) + def toKriterium( + container: ParameterCriteria => Anchor + ): DetailKriteria.ViewModel = + DetailKriteria.ViewModel( + nazev = crit.criteriumText, + kapitola = crit.chapterId, + bod = crit.itemId, + status = "Nesplněno", + statusColor = Color.red, + splneno = false, + dukazy = Nil, + container = container(crit) + ) + +extension (user: UserInfo) + def toUserRow( + container: UserInfo => HtmlElement = (_: UserInfo) => div() + ): UserRow.ViewModel = + UserRow.ViewModel( + osobniCislo = user.personalNumber.toString, + celeJmeno = user.name, + prijmeni = user.surname, + hlavniFunkce = user.mainFunction.map(_.name), + img = user.img, + container = () => container(user) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/dashboard/DashboardPage.scala b/app/src/main/scala/mdr/pdb/app/pages/dashboard/DashboardPage.scala new file mode 100644 index 0000000..0eb8f7a --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/dashboard/DashboardPage.scala @@ -0,0 +1,11 @@ +package mdr.pdb.app.pages.dashboard + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.Page + +object DashboardPage: + + def render: HtmlElement = + div("Dashboard page") diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala new file mode 100644 index 0000000..ff3a25f --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala @@ -0,0 +1,31 @@ +package mdr.pdb.app.pages.detail + +import com.raquo.laminar.api.L.{*, given} + +import components._ + +object DetailKriteriaPage: + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametr: DetailParametru.ViewModel, + kriterium: DetailKriteria.ViewModel + ) + + def apply($m: Signal[ViewModel])( + action: Observer[DukazyKriteria.Action] + ): HtmlElement = + div( + cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + div( + DetailOsoby.header($m.map(_.osoba)), + DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") + ), + div( + DetailKriteria($m.map(_.kriterium)), + DukazyKriteria($m.map(_.kriterium.dukazy))(action) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/DetailPage.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailPage.scala new file mode 100644 index 0000000..b591cc9 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailPage.scala @@ -0,0 +1,25 @@ +package mdr.pdb.app.pages.detail + +import com.raquo.laminar.api.L.{*, given} + +import components._ +import mdr.pdb.app.Page +import com.raquo.waypoint.Router +import mdr.pdb.app.Action + +object DetailPage: + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametry: SeznamParametru.ViewModel + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + DetailOsoby($m.map(_.osoba)), + SeznamParametru($m.map(_.parametry)) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/DetailParametruPage.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailParametruPage.scala new file mode 100644 index 0000000..3322279 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailParametruPage.scala @@ -0,0 +1,24 @@ +package mdr.pdb.app.pages.detail + +import com.raquo.laminar.api.L.{*, given} + +import components._ + +object DetailParametruPage: + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametr: DetailParametru.ViewModel, + kriteria: SeznamKriterii.ViewModel + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + DetailOsoby.header($m.map(_.osoba)), + DetailParametru($m.map(_.parametr)), + SeznamKriterii($m.map(_.kriteria)) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala new file mode 100644 index 0000000..b58618a --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala @@ -0,0 +1,107 @@ +package mdr.pdb +package app +package pages.detail + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.pages.detail.components.UpravDukazForm +import fiftyforms.services.files.File + +object UpravDukaz: + + type ThisPage = Page.UpravDukazKriteria + type PageKey = (OsobniCislo, String, String) + + trait State { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def availableFiles: EventStream[List[File]] + def actionBus: Observer[Action] + } + + def keyOfPage(page: ThisPage): PageKey = + (page.osobniCislo.value, page.parametr.value, page.kriterium.value) + + def onChangeAction(key: PageKey): Action = + FetchParameterCriteria( + key._1, + key._2, + key._3, + Page.UpravDukazKriteria(_, _, _) + ) + + class Connector(state: State)($page: Signal[ThisPage])(using Router[Page]): + val $paramChangeSignal = $page.splitOne(keyOfPage)((x, _, _) => x) + val $pageChangeSignal = $paramChangeSignal.map(onChangeAction) + + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + val $merged = + $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => + for { + da <- d + pa <- p + pb <- pa.find(_.id == pc._2) + ka <- pb.criteria.find(_.id == pc._3) + } yield (da, pb, ka) + ) + + def apply: HtmlElement = + AppPage(state.actionBus)( + $merged.split(_ => ())((_, s, $s) => + PageComponent( + $s.map(buildModel), + state.availableFiles, + state.actionBus.contramap { + case UpravDukazForm.Cancelled => + NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) + case UpravDukazForm.AvailableFilesRequested => + FetchAvailableFiles(s._1.personalNumber) + } + ) + ), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: Parameter, + k: ParameterCriteria + ): PageComponent.ViewModel = + import connectors.* + PageComponent.ViewModel( + o.toDetailOsoby, + p.toParametr(_ => a()), + k.toKriterium(_ => a()) + ) + + object PageComponent: + import components.* + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametr: DetailParametru.ViewModel, + kriterium: DetailKriteria.ViewModel + ) + + def apply( + $m: Signal[ViewModel], + availableFilesStream: EventStream[List[File]], + events: Observer[UpravDukazForm.Event] + ): HtmlElement = + div( + cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + div( + DetailOsoby.header($m.map(_.osoba)), + DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") + ), + div( + DetailKriteria($m.map(_.kriterium)), + UpravDukazForm(availableFilesStream)(events) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailKriteria.scala new file mode 100644 index 0000000..a3ab634 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailKriteria.scala @@ -0,0 +1,37 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.CustomAttrs +import fiftyforms.ui.components.tailwind.Icons +import java.time.LocalDate +import fiftyforms.ui.components.tailwind.Color + +object DetailKriteria: + case class ViewModel( + nazev: String, + kapitola: String, + bod: String, + status: String, + statusColor: Color, + splneno: Boolean, + dukazy: List[DukazKriteria.ViewModel], + container: HtmlElement = div() + ) { + val id = s"${kapitola}${bod}" + } + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "pb-5 border-b border-gray-200", + h3( + cls := "text-xl leading-6 font-bold text-gray-900", + child.text <-- $m.map(_.nazev) + ), + h3( + cls := "mt-2 max-w-4xl text-sm text-gray-500", + "Kapitola ", + child.text <-- $m.map(_.kapitola), + " bod ", + child <-- $m.map(_.bod) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailOsoby.scala new file mode 100644 index 0000000..0fe2ba5 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailOsoby.scala @@ -0,0 +1,100 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.OsobniCislo + +import java.time.LocalDate +import fiftyforms.ui.components.tailwind.Avatar + +object DetailOsoby: + + object Funkce: + case class ViewModel( + nazev: String, + stredisko: String, + voj: String + ) + + def render($m: Signal[ViewModel]): HtmlElement = + p( + cls := "text-sm font-medium text-gray-500", + child.text <-- $m.map(_.nazev), + span( + cls := "hidden md:inline", + " @ ", + child.text <-- $m.map(_.stredisko), + ", ", + child.text <-- $m.map(_.voj) + ) + ) + + object PracovniPomer: + case class ViewModel( + druh: String, + pocatek: LocalDate, + konec: Option[LocalDate] + ) + + def render($m: Signal[ViewModel]): HtmlElement = + import fiftyforms.ui.components.tailwind.CustomAttrs.datetime + p( + cls := "text-sm font-medium text-gray-500", + child.text <-- $m.map(_.druh), + " od ", + time( + datetime <-- $m.map(_.pocatek.toString), + child.text <-- $m.map(_.pocatek.toString) + ) + ) + + case class ViewModel( + osobniCislo: OsobniCislo, + jmeno: String, + email: Option[String], + telefon: Option[String], + img: Option[String], + hlavniFunkce: Option[Funkce.ViewModel], + pracovniPomer: Option[PracovniPomer.ViewModel] + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "md:flex md:items-center md:justify-between md:space-x-4", + div( + cls := "flex items-start space-x-4", + div( + cls := "flex-shrink-0", + Avatar($m.map(_.img)).avatar(16) + ), + div( + h1( + cls := "text-2xl font-bold text-gray-900", + child.text <-- $m.map(_.jmeno) + ), + child.maybe <-- $m.map(_.hlavniFunkce).split(_ => ())((_, _, d) => + Funkce.render(d) + ), + child.maybe <-- $m.map(_.pracovniPomer).split(_ => ())((_, _, d) => + PracovniPomer.render(d) + ) + ) + ) + ) + + def header($m: Signal[ViewModel]): HtmlElement = + div( + cls := "md:flex md:items-center md:justify-between md:space-x-5", + div( + cls := "flex items-start space-x-4", + div( + cls := "flex-shrink-0", + Avatar($m.map(_.img)).avatar(8) + ), + div( + h1( + cls := "text-lg font-medium text-gray-800", + child.text <-- $m.map(_.jmeno) + ) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailParametru.scala new file mode 100644 index 0000000..6310dde --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailParametru.scala @@ -0,0 +1,34 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.Color + +object DetailParametru: + case class ViewModel( + id: String, + nazev: String, + popis: String, + status: String, + statusColor: Color, + a: Anchor + ) + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "pb-5 border-b border-gray-200", + h2( + cls := "text-2xl leading-6 font-medium text-gray-900", + child.text <-- $m.map(_.nazev) + ), + p( + cls := "mt-2 max-w-4xl text-sm text-gray-500", + child.text <-- $m.map(_.popis) + ) + ) + + def header($m: Signal[ViewModel]): HtmlElement = + div( + h2( + cls := "text-lg leading-6 font-medium text-gray-600", + child.text <-- $m.map(_.nazev) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazKriteria.scala new file mode 100644 index 0000000..c292919 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazKriteria.scala @@ -0,0 +1,177 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import java.time.LocalDate +import fiftyforms.ui.components.tailwind.CustomAttrs + +object DukazKriteria: + case class Osoba(osobniCislo: String, jmeno: String) + case class Udalost(osoba: Osoba, datum: LocalDate) + case class Dokument( + url: String, + nazev: String, + pridal: Udalost, + odebral: Option[Udalost] + ) + case class ViewModel( + dokumenty: List[Dokument], + autorizoval: Option[Udalost], + platiDo: Option[LocalDate], + poznámka: Option[String] + ) + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "bg-white shadow overflow-hidden sm:rounded-lg", + div( + cls := "px-4 py-5 sm:px-6", + h3( + cls := "text-lg leading-6 font-medium text-gray-900", + """Applicant Information""" + ), + p( + cls := "mt-1 max-w-2xl text-sm text-gray-500", + """Personal details and application.""" + ) + ), + div( + cls := "border-t border-gray-200 px-4 py-5 sm:px-6", + dl( + cls := "grid grid-cols-1 gap-x-4 gap-y-8 sm:grid-cols-2", + div( + cls := "sm:col-span-1", + dt( + cls := "text-sm font-medium text-gray-500", + """Full name""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """Margot Foster""" + ) + ), + div( + cls := "sm:col-span-1", + dt( + cls := "text-sm font-medium text-gray-500", + """Application for""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """Backend Developer""" + ) + ), + div( + cls := "sm:col-span-1", + dt( + cls := "text-sm font-medium text-gray-500", + """Email address""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """margotfoster@example.com""" + ) + ), + div( + cls := "sm:col-span-1", + dt( + cls := "text-sm font-medium text-gray-500", + """Salary expectation""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """$120,000""" + ) + ), + div( + cls := "sm:col-span-2", + dt( + cls := "text-sm font-medium text-gray-500", + """About""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """Fugiat ipsum ipsum deserunt culpa aute sint do nostrud anim incididunt cillum culpa consequat. Excepteur qui ipsum aliquip consequat sint. Sit id mollit nulla mollit nostrud in ea officia proident. Irure nostrud pariatur mollit ad adipisicing reprehenderit deserunt qui eu.""" + ) + ), + div( + cls := "sm:col-span-2", + dt( + cls := "text-sm font-medium text-gray-500", + """Attachments""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + ul( + role := "list", + cls := "border border-gray-200 rounded-md divide-y divide-gray-200", + li( + cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", + div( + cls := "w-0 flex-1 flex items-center", { + import svg.* + import CustomAttrs.svg.ariaHidden + svg( + cls := "flex-shrink-0 h-5 w-5 text-gray-400", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + ariaHidden := true, + path( + fillRule := "evenodd", + d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", + clipRule := "evenodd" + ) + ) + }, + span( + cls := "ml-2 flex-1 w-0 truncate", + """resume_back_end_developer.pdf""" + ) + ), + div( + cls := "ml-4 flex-shrink-0", + a( + href := "#", + cls := "font-medium text-indigo-600 hover:text-indigo-500", + """Download""" + ) + ) + ), + li( + cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", + div( + cls := "w-0 flex-1 flex items-center", { + import svg.* + import CustomAttrs.svg.ariaHidden + svg( + cls := "flex-shrink-0 h-5 w-5 text-gray-400", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + ariaHidden := true, + path( + fillRule := "evenodd", + d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", + clipRule := "evenodd" + ) + ) + }, + span( + cls := "ml-2 flex-1 w-0 truncate", + """coverletter_back_end_developer.pdf""" + ) + ), + div( + cls := "ml-4 flex-shrink-0", + a( + href := "#", + cls := "font-medium text-indigo-600 hover:text-indigo-500", + """Download""" + ) + ) + ) + ) + ) + ) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala new file mode 100644 index 0000000..d604860 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala @@ -0,0 +1,35 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.Icons + +object DukazyKriteria: + sealed trait Action + case object Add extends Action + + type ViewModel = List[DukazKriteria.ViewModel] + + def apply($m: Signal[ViewModel])(actions: Observer[Action]): HtmlElement = + div( + child.maybe <-- $m.map(m => + if (m.isEmpty) then Some(prazdnyDukaz(actions)) + else None + ), + children <-- $m.map(_.zipWithIndex).split(_._2)((_, _, $s) => + DukazKriteria($s.map(_._1)) + ) + ) + + private def prazdnyDukaz(actions: Observer[Action]): HtmlElement = + button( + tpe := "button", + onClick.preventDefault.mapTo(Add) --> actions, + cls := "relative block w-full border-2 border-gray-300 border-dashed rounded-lg p-12 text-center hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + Icons.outline + .`document-add`(12) + .amend(svg.cls := "mx-auto text-gray-400"), + span( + cls := "mt-2 block text-sm font-medium text-gray-900", + "Přidat důkaz" + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala new file mode 100644 index 0000000..7e77a7b --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala @@ -0,0 +1,34 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.Color +import fiftyforms.ui.components.tailwind.list.StackedList +import fiftyforms.ui.components.tailwind.list.ListRow +import fiftyforms.ui.components.tailwind.list.RowTag +import fiftyforms.ui.components.tailwind.list.RowNext +import java.time.LocalDate + +object SeznamKriterii: + type ViewModel = List[DetailKriteria.ViewModel] + + private val kritList = new StackedList[DetailKriteria.ViewModel] + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "bg-white shadow overflow-hidden sm:rounded-md", + kritList($m, _.id) { $i => + $i.map { i => + ListRow.ViewModel( + title = i.nazev, + topRight = RowTag.render( + $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) + ), + bottomLeft = + p(cls := "text-sm text-gray-500", s"${i.kapitola}${i.bod}"), + bottomRight = emptyNode, + farRight = RowNext.render, + containerElement = i.container + ) + } + } + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamParametru.scala new file mode 100644 index 0000000..0760b32 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamParametru.scala @@ -0,0 +1,37 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.list.{ + StackedList, + ListRow, + RowTag, + PropList, + IconText, + RowNext +} +import fiftyforms.ui.components.tailwind.Color +import fiftyforms.ui.components.tailwind.LinkSupport.* + +object SeznamParametru: + type ViewModel = List[DetailParametru.ViewModel] + + private val parametrList = new StackedList[DetailParametru.ViewModel] + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "bg-white shadow overflow-hidden sm:rounded-md", + parametrList($m, _.id) { $i => + $i.map { i => + ListRow.ViewModel( + title = i.nazev, + topRight = RowTag.render( + $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) + ), + bottomLeft = emptyNode, + bottomRight = emptyNode, + farRight = RowNext.render, + containerElement = i.a + ) + } + } + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala new file mode 100644 index 0000000..28753c9 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala @@ -0,0 +1,101 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import io.laminext.syntax.core.* +import fiftyforms.ui.components.tailwind.CustomAttrs +import fiftyforms.ui.components.tailwind.form.* +import org.scalajs.dom +import com.raquo.laminar.nodes.ReactiveHtmlElement +import fiftyforms.services.files.components.tailwind.FilePicker +import mdr.pdb.frontend.AutorizujDukaz +import mdr.pdb.frontend.DocumentRef +import fiftyforms.services.files.File + +object UpravDukazForm: + sealed trait Event + case object Cancelled extends Event + case object AvailableFilesRequested extends Event + def apply(availableFilesStream: EventStream[List[File]])( + updates: Observer[Event] + ): HtmlElement = + val (filesStream, filesObserver) = + EventStream.withObserver[FilePicker.Event] + val files = Var[List[File]](Nil) + def submitButtons: HtmlElement = + div( + cls := "pt-5", + div( + cls := "flex justify-end", + button( + tpe := "button", + cls := "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + "Zrušit", + onClick.mapTo(Cancelled) --> updates + ), + button( + tpe := "submit", + cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + disabled <-- files.signal.map(_.isEmpty), + "Autorizovat" + ) + ) + ) + + div( + filesStream.collect { case FilePicker.SelectionUpdated(files) => + files.to(List) + } --> files.writer, + filesStream.collect { case FilePicker.AvailableFilesRequested => + AvailableFilesRequested + } --> updates, + cls := "bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6", + Form( + Form.Body( + Form.Section( + FormHeader( + FormHeader.ViewModel( + "Doložení kritéria", + "Sestavte doklady poskytující důkaz kritéria a potvrďte odesláním formuláře. Případné limitace či jiné relevantní údaje vepište do pole Komentář." + ) + ), + FormFields( + FormRow( + "dokumenty", + "Dokumenty", + FilePicker(files.signal, availableFilesStream)(filesObserver) + .amend(idAttr := "dokumenty", cls("max-w-lg")) + ).toHtml, + FormRow( + "platnost", + "Platnost", + input( + idAttr := "platnost", + name := "platnost", + tpe := "date", + autoComplete := "date", + cls := "block max-w-lg w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" + ) + ).toHtml, + FormRow( + "komentar", + "Komentář", + div( + cls := "mt-1 sm:mt-0 sm:col-span-2", + textArea( + idAttr := "komentar", + name := "about", + rows := 3, + cls := "max-w-lg shadow-sm block w-full focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border border-gray-300 rounded-md" + ), + p( + cls := "mt-2 text-sm text-gray-500", + "Doplňte prosím potřebné informace související s doložením kritéria, včetně případných limitací." + ) + ) + ).toHtml + ) + ) + ), + submitButtons + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/directory/DirectoryPage.scala b/app/src/main/scala/mdr/pdb/app/pages/directory/DirectoryPage.scala new file mode 100644 index 0000000..7a72ca3 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/directory/DirectoryPage.scala @@ -0,0 +1,40 @@ +package mdr.pdb.app.pages.directory + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.UserInfo + +import components._ + +object DirectoryPage: + + type ViewModel = List[UserRow.ViewModel] + + def apply($m: Signal[ViewModel]): HtmlElement = + val (actionsStream, actionObserver) = + EventStream.withObserver[SearchForm.Action] + val $filter = actionsStream + .collect { + case SearchForm.ClearFilter => None + case SearchForm.SetFilter(t) => Some(t) + } + .startWith(None) + val byLetter = for { + d <- $m + f <- $filter + } yield for { + (letter, users) <- d + .filter { user => + f match + case None => true + case Some(t) => user.search.contains(t) + } + .groupBy(_.prijmeni.head) + .to(List) + .sortBy(_._1) + } yield (letter.toString, users.sortBy(_.prijmeni)) + + div( + cls := "h-full max-w-7xl mx-auto order-first flex flex-col flex-shrink-0", + SearchForm(actionObserver), + Directory(byLetter) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/directory/components/Directory.scala b/app/src/main/scala/mdr/pdb/app/pages/directory/components/Directory.scala new file mode 100644 index 0000000..f7d6f99 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/directory/components/Directory.scala @@ -0,0 +1,37 @@ +package mdr.pdb.app.pages.directory.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.UserInfo + +object Directory: + + object Header: + type ViewModel = String + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500", + h3(child.text <-- $m) + ) + + object UserList: + type ViewModel = List[UserRow.ViewModel] + def apply($m: Signal[ViewModel]): HtmlElement = + ul( + role := "list", + cls := "relative z-0 divide-y divide-gray-200", + children <-- $m.split(_.osobniCislo)((_, _, s) => UserRow(s)) + ) + + type ViewModel = List[(String, List[UserRow.ViewModel])] + def apply($m: Signal[ViewModel]): HtmlElement = + nav( + cls := "flex-1 min-h-0 overflow-y-auto", + aria.label := "Directory", + children <-- $m.split(_._1)((_, _, s) => + div( + cls := "relative", + Header(s.map(_._1)), + UserList(s.map(_._2)) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/directory/components/SearchForm.scala b/app/src/main/scala/mdr/pdb/app/pages/directory/components/SearchForm.scala new file mode 100644 index 0000000..fb9ecc4 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/directory/components/SearchForm.scala @@ -0,0 +1,54 @@ +package mdr.pdb.app.pages.directory.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.Icons + +object SearchForm: + sealed trait Action + case object SubmitFilter extends Action + sealed trait FilterAction extends Action + case object ClearFilter extends FilterAction + case class SetFilter(value: String) extends FilterAction + + def apply(actions: Observer[Action]): HtmlElement = + div( + cls := "px-6 pt-4 pb-4", + form( + cls := "flex space-x-4", + onSubmit.mapTo(SubmitFilter) --> actions, + div( + cls := "flex-1 min-w-0", + label( + forId := "search", + cls := "sr-only", + "Hledat" + ), + div( + cls := "relative rounded-md shadow-sm", + div( + cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", + Icons.solid.search().amend(svg.cls := "text-gray-400") + ), + input( + tpe := "search", + name := "search", + idAttr := "search", + cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", + placeholder := "Hledat", + composeEvents(onInput.mapToValue.setAsValue.map(SetFilter(_)))( + _.throttle(500) + ) --> actions + ) + ) + ), + button( + tpe := "submit", + cls := "inline-flex justify-center px-3.5 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500", + Icons.solid.filter().amend(svg.cls := "text-gray-400"), + span( + cls := "sr-only", + "Hledat" + ) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/directory/components/UserRow.scala b/app/src/main/scala/mdr/pdb/app/pages/directory/components/UserRow.scala new file mode 100644 index 0000000..4aa1672 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/directory/components/UserRow.scala @@ -0,0 +1,52 @@ +package mdr.pdb.app.pages.directory.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.airstream.core.Signal +import fiftyforms.ui.components.tailwind.Avatar + +object UserRow: + case class ViewModel( + osobniCislo: String, + celeJmeno: String, + prijmeni: String, + hlavniFunkce: Option[String], + img: Option[String], + container: () => HtmlElement = () => div() + ) { + val search = prijmeni.toLowerCase + } + + def apply($m: Signal[ViewModel]): HtmlElement = + inline def avatarImage = + Avatar($m.map(_.img)).avatarImage(10) + + li( + div( + cls := "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500", + div( + cls := "flex-shrink-0", + child <-- avatarImage + ), + div( + cls := "flex-1 min-w-0", + child <-- $m.map { o => + o.container() + .amend( + cls := "focus:outline-none", + span( + cls := "absolute inset-0", + aria.hidden := true + ), + p( + cls := "text-sm font-medium text-gray-900", + o.celeJmeno + ), + p( + cls := "text-sm text-gray-500 truncate", + o.hlavniFunkce + ) + ) + } + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/errors/ErrorPage.scala b/app/src/main/scala/mdr/pdb/app/pages/errors/ErrorPage.scala new file mode 100644 index 0000000..6b195ef --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/errors/ErrorPage.scala @@ -0,0 +1,99 @@ +package mdr.pdb.app.pages.errors + +import com.raquo.domtypes.generic.codecs.StringAsIsCodec +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.Page +import mdr.pdb.app.Action +import com.raquo.waypoint.Router + +object ErrorPage: + case class ViewModel( + homePage: Page, + errorName: String, + title: String, + subTitle: String + ) + + def apply(m: ViewModel, actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + val ViewModel(homePage, errorName, title, subTitle) = m + div( + cls := "min-h-full pt-16 pb-12 flex flex-col bg-white", + main( + cls := "flex-grow flex flex-col justify-center max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", + div( + cls := "flex-shrink-0 flex justify-center", + a( + href := "/", + cls := "inline-flex", + span( + cls := "sr-only", + """Workflow""" + ), + img( + cls := "h-12 w-auto", + src := "https://tailwindui.com/img/logos/workflow-mark.svg?color=indigo&shade=600", + alt := "" + ) + ) + ), + div( + cls := "py-16", + div( + cls := "text-center", + p( + cls := "text-sm font-semibold text-indigo-600 uppercase tracking-wide", + errorName + ), + h1( + cls := "mt-2 text-4xl font-extrabold text-gray-900 tracking-tight sm:text-5xl", + title + ), + p( + cls := "mt-2 text-base text-gray-500", + subTitle + ), + div( + cls := "mt-6", + PageLink + .container(homePage, actionBus) + .amend( + cls := "text-base font-medium text-indigo-600 hover:text-indigo-500", + """Go back home""" + ) + ) + ) + ) + ), + footer( + cls := "flex-shrink-0 max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", + nav( + cls := "flex justify-center space-x-4", + a( + href := "#", + cls := "text-sm font-medium text-gray-500 hover:text-gray-600", + """Contact Support""" + ), + span( + cls := "inline-block border-l border-gray-300", + aria.hidden := true + ), + a( + href := "#", + cls := "text-sm font-medium text-gray-500 hover:text-gray-600", + """Status""" + ), + span( + cls := "inline-block border-l border-gray-300", + aria.hidden := true + ), + a( + href := "#", + cls := "text-sm font-medium text-gray-500 hover:text-gray-600", + """Twitter""" + ) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/errors/NotFoundPage.scala b/app/src/main/scala/mdr/pdb/app/pages/errors/NotFoundPage.scala new file mode 100644 index 0000000..97760e1 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/errors/NotFoundPage.scala @@ -0,0 +1,21 @@ +package mdr.pdb.app.pages.errors + +import com.raquo.domtypes.generic.codecs.StringAsIsCodec +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.Page +import mdr.pdb.app.Action + +object NotFoundPage: + def apply(homePage: Page, url: String, actionBus: Observer[Action])(using + router: Router[Page] + ): HtmlElement = + ErrorPage( + ErrorPage.ViewModel( + homePage, + "404 error", + "Page not found.", + s"Sorry, but page $url doesn't exist." + ), + actionBus + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala b/app/src/main/scala/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala new file mode 100644 index 0000000..77b0af8 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala @@ -0,0 +1,32 @@ +package mdr.pdb.app.pages.errors + +import com.raquo.domtypes.generic.codecs.StringAsIsCodec +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.Page +import mdr.pdb.app.Action + +object UnhandledErrorPage: + + case class ViewModel( + homePage: Page, + errorName: Option[String], + errorMessage: Option[String] + ) + + def apply(m: ViewModel, actionBus: Observer[Action])(using + router: Router[Page] + ): HtmlElement = + ErrorPage( + ErrorPage.ViewModel( + m.homePage, + "Unexpected error occurred", + m.errorName.getOrElse( + "Uh oh!" + ), // TODO: translations, better text than uh oh + m.errorMessage.getOrElse( + "This wasn't supposed to happen! Please try again." + ) + ), + actionBus + ) diff --git a/app/src/main/scala/mdr/pdb/app/services/DataFetcher.scala b/app/src/main/scala/mdr/pdb/app/services/DataFetcher.scala new file mode 100644 index 0000000..134c221 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/services/DataFetcher.scala @@ -0,0 +1,6 @@ +package mdr.pdb.app.services + +import com.raquo.airstream.core.Observer + +trait DataFetcher[K, A]: + def fetch(id: K, sink: Observer[A]): Unit diff --git a/app/src/main/scala/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/mdr/pdb/app/state/AppState.scala new file mode 100644 index 0000000..e44b2df --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/state/AppState.scala @@ -0,0 +1,120 @@ +package mdr.pdb.app +package state + +import com.raquo.airstream.core.EventStream +import mdr.pdb.{UserInfo, OsobniCislo} +import com.raquo.airstream.core.Observer +import scala.scalajs.js +import scala.scalajs.js.JSON +import zio.json._ +import com.raquo.airstream.eventbus.EventBus +import com.raquo.airstream.ownership.Owner +import com.raquo.waypoint.Router +import mdr.pdb.Parameter +import mdr.pdb.ParameterCriteria +import mdr.pdb.UserFunction +import mdr.pdb.UserContract +import fiftyforms.services.files.File + +trait AppState + extends connectors.DetailPageConnector.AppState + with connectors.DetailParametruPageConnector.AppState + with connectors.DetailKriteriaPageConnector.AppState + with pages.detail.UpravDukaz.State: + def users: EventStream[List[UserInfo]] + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + +class MockAppState(implicit owner: Owner, router: Router[Page]) + extends AppState: + + given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) + given JsonDecoder[UserFunction] = DeriveJsonDecoder.gen + given JsonDecoder[UserContract] = DeriveJsonDecoder.gen + given JsonDecoder[UserInfo] = DeriveJsonDecoder.gen + + given JsonDecoder[ParameterCriteria] = DeriveJsonDecoder.gen + given JsonDecoder[Parameter] = DeriveJsonDecoder.gen + + private val actions = EventBus[Action]() + private val (parametersStream, pushParameters) = + EventStream.withCallback[List[Parameter]] + private val (usersStream, pushUsers) = + EventStream.withCallback[List[UserInfo]] + private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] + private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] + + private val mockData: List[UserInfo] = + mockUsers + .asInstanceOf[js.Dictionary[js.Object]] + .values + // TODO: is there a more efficient way to parse from JS object directly? + .map { o => + val parsed = JSON.stringify(o).fromJson[UserInfo] + parsed.left.foreach(org.scalajs.dom.console.log(_)) + parsed + } + .collect { case Right(u) => + u + } + .toList + + private val mockParameters: List[Parameter] = + pdbParams + .asInstanceOf[js.Dictionary[js.Object]] + .values + .map(o => JSON.stringify(o).fromJson[Parameter]) + .collect { case Right(p) => p } + .toList + + // TODO: Extract to separate event handler + actions.events.foreach { + case FetchDirectory => pushUsers(mockData) + case FetchUserDetails(osc) => + mockData.find(_.personalNumber == osc).foreach { o => + pushDetails(o) + router.replaceState(Page.Detail(o)) + } + case FetchParameters(osc) => + pushParameters(mockParameters) + case FetchParameter(osc, paramId) => + for + o <- mockData.find(_.personalNumber == osc) + p <- mockParameters.find(_.id == paramId) + do + pushDetails(o) + pushParameters(mockParameters) + router.replaceState(Page.DetailParametru(o, p)) + case FetchParameterCriteria(osc, paramId, critId, page) => + for + o <- mockData.find(_.personalNumber == osc) + p <- mockParameters.find(_.id == paramId) + c <- p.criteria.find(_.id == critId) + do + pushDetails(o) + pushParameters(mockParameters) + router.replaceState(page(o, p, c)) + case NavigateTo(page) => router.pushState(page) + case FetchAvailableFiles(osc) => + pushFiles( + List( + File("https://tc163.cmi.cz/here", "Example file") + ) + ) + } + + override def users: EventStream[List[UserInfo]] = + usersStream.debugWithName("users") + + override def details: EventStream[UserInfo] = + detailsStream.debugWithName("details") + + override def parameters: EventStream[List[Parameter]] = + parametersStream.debugWithName("parameters") + + override def availableFiles: EventStream[List[File]] = + filesStream.debugWithName("available files") + + override def actionBus: Observer[Action] = + actions.writer.debugWithName("actions writer") diff --git a/build.sbt b/build.sbt index 2afc879..0daf796 100644 --- a/build.sbt +++ b/build.sbt @@ -11,6 +11,12 @@ lazy val core = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Pure) .in(file("core")) + .settings( + IWDeps.useZIO(Test), + IWDeps.tapirCore, + IWDeps.tapirZIO, + IWDeps.tapirZIOJson + ) lazy val ui = (project in file("ui")) .enablePlugins(ScalaJSPlugin) diff --git a/core/src/main/scala/cz/e_bs/cmi/mdr/pdb/Parameter.scala b/core/src/main/scala/cz/e_bs/cmi/mdr/pdb/Parameter.scala deleted file mode 100644 index 9d68240..0000000 --- a/core/src/main/scala/cz/e_bs/cmi/mdr/pdb/Parameter.scala +++ /dev/null @@ -1,27 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb - -import java.math.BigInteger -import java.security.MessageDigest - -object ParameterCriteria { - type Id = String -} - -case class ParameterCriteria( - chapterId: String, - itemId: String, - criteriumText: String -) { - val id: ParameterCriteria.Id = s"${chapterId}${itemId}" -} - -object Parameter { - type Id = String -} - -case class Parameter( - id: Parameter.Id, - name: String, - description: String, - criteria: List[ParameterCriteria] -) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala deleted file mode 100644 index a3b00af..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala +++ /dev/null @@ -1,103 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import scala.scalajs.js.annotation.JSExportTopLevel -import scala.scalajs.js.annotation.JSExport -import scala.scalajs.js.annotation.JSImport -import scala.scalajs.js -import org.scalajs.dom -import com.raquo.laminar.api.L.{*, given} -import scala.scalajs.js.Date -import com.raquo.waypoint.Router -import com.raquo.waypoint.SplitRender -import cz.e_bs.cmi.mdr.pdb.app.services.DataFetcher -import scala.scalajs.js.JSON -import zio.json._ -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.state.AppState - -@js.native -@JSImport("stylesheets/main.css", JSImport.Namespace) -object Css extends js.Any - -@js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native -@JSImport("params/pdb-params.json", JSImport.Default) -object pdbParams extends js.Object - -@JSExportTopLevel("app") -object Main: - - @JSExport - def main(args: Array[String]): Unit = - import Routes.given - onLoad { - setupAirstream() - val appContainer = dom.document.querySelector("#app") - val _ = - render( - appContainer, - renderPage(state.MockAppState(using unsafeWindowOwner, router)) - ) - } - - private def onLoad(f: => Unit): Unit = - documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) - - private def setupAirstream()(using router: Router[Page]): Unit = - AirstreamError.registerUnhandledErrorCallback(err => - router.forcePage( - Page.UnhandledError( - Some(err.getClass.getName), // TODO: Fill only in dev mode - Some(err.getMessage) - ) - ) - ) - - def renderPage(state: AppState)(using - router: Router[Page] - ): HtmlElement = - val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) - .collectSignal[Page.Detail]( - connectors - .DetailPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailParametru]( - connectors - .DetailParametruPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailKriteria]( - connectors - .DetailKriteriaPageConnector(state)(_) - .apply - ) - .collectSignal[Page.UpravDukazKriteria]( - pages.detail.UpravDukaz.Connector(state)(_).apply - ) - .collectStatic(Page.Dashboard)( - connectors.DashboardPageConnector(state.actionBus).apply - ) - .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) - ) - .collect[Page.UnhandledError](pg => - pages.errors - .UnhandledErrorPage( - pages.errors.UnhandledErrorPage - .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), - state.actionBus - ) - ) - .collectStatic(Page.Directory)( - connectors - .DirectoryPageConnector(state.users, state.actionBus) - .apply - ) - div(cls := "h-full", child <-- pageSplitter.$view) - - // Pull in the stylesheet - val css: Css.type = Css diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala deleted file mode 100644 index 3bbc8c0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ /dev/null @@ -1,198 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.* -import org.scalajs.dom -import zio.json.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import scala.scalajs.js -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.Page.Titled - -// enum is not working with Waypoints' SplitRender collectStatic -sealed abstract class Page( - val id: String, - val title: String, - val parent: Option[Page] -) { - val path: Vector[Page] = - parent match - case None => Vector(this) - case Some(p) => p.path :+ this - - val isRoot: Boolean = parent.isEmpty -} - -object Page: - - case class Titled[V](value: V, title: Option[String] = None): - val show: String = title.getOrElse(value.toString) - - case object Directory extends Page("directory", "Adresář", None) - - case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) - - case class Detail(osobniCislo: Titled[OsobniCislo]) - extends Page("user", osobniCislo.show, Some(Directory)) - - object Detail { - def apply(o: UserInfo): Detail = Detail( - Titled(o.personalNumber, Some(o.name)) - ) - } - - case class DetailParametru( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String] - ) extends Page( - "parameter", - parametr.show, - Some(Detail(osobniCislo)) - ) - - object DetailParametru { - def apply(o: UserInfo, p: Parameter): DetailParametru = - DetailParametru( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)) - ) - } - - case class DetailKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "criteria", - kriterium.show, - Some(DetailParametru(osobniCislo, parametr)) - ) - - object DetailKriteria { - def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = - DetailKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class UpravDukazKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "addProof", - "Důkaz", - Some(DetailKriteria(osobniCislo, parametr, kriterium)) - ) - - object UpravDukazKriteria { - def apply( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): UpravDukazKriteria = - UpravDukazKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class NotFound(url: String) extends Page("404", "404", Some(Directory)) - - case class UnhandledError( - errorName: Option[String], - errorMessage: Option[String] - ) extends Page("500", "Unexpected error", Some(Directory)) - -object Routes: - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) - given [V: JsonEncoder]: JsonEncoder[Titled[V]] = - DeriveJsonEncoder.gen[Titled[V]] - given [V: JsonDecoder]: JsonDecoder[Titled[V]] = - DeriveJsonDecoder.gen[Titled[V]] - given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] - given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] - - val base = - js.`import`.meta.env.BASE_URL - .asInstanceOf[String] + "app" - - val homePage: Page = Page.Directory - - given router: Router[Page] = Router[Page]( - routes = List( - Route.static(homePage, root / endOfSegments, basePath = base), - Route.static( - Page.Dashboard, - root / "dashboard" / endOfSegments, - basePath = base - ), - Route[Page.Detail, String]( - encode = _.osobniCislo.value.toString, - decode = osc => Page.Detail(Titled(OsobniCislo(osc))), - root / "osoba" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.DetailParametru, (String, String)]( - encode = p => (p.osobniCislo.value.toString, p.parametr.value), - decode = - p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / endOfSegments, - basePath = base - ), - Route[Page.DetailKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.DetailKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.UpravDukazKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.UpravDukazKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / "edit" / endOfSegments, - basePath = base - ) - ), - serializePage = _.toJson, - deserializePage = _.fromJson[Page] - .fold(s => throw IllegalStateException(s), identity), - getPageTitle = _.title, - routeFallback = url => Page.NotFound(url), - deserializeFallback = _ => Page.Dashboard - )( - $popStateEvent = windowEvents.onPopState, - owner = unsafeWindowOwner - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala deleted file mode 100644 index b24b82b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -sealed trait Action - -case object FetchDirectory extends Action -case class FetchUserDetails(osc: OsobniCislo) extends Action -case class FetchParameters(osc: OsobniCislo) extends Action -case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action -case class FetchParameterCriteria( - osc: OsobniCislo, - paramId: String, - critId: String, - page: (UserInfo, Parameter, ParameterCriteria) => Page -) extends Action -case class FetchAvailableFiles(osc: OsobniCislo) extends Action -case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala deleted file mode 100644 index c637d35..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala +++ /dev/null @@ -1,82 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.{UserProfile, UserInfo, OsobniCislo} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo -import cz.e_bs.cmi.mdr.pdb.UserFunction - -object AppPage: - // TODO: pages by logged in user - val pages: List[Page] = List(Page.Directory, Page.Dashboard) - - import NavigationBar.{Logo, MenuItem} - - val logo = Logo( - "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", - "Workflow" - ) - - // TODO: menu items by user profile - val userMenu = - List( - MenuItem("Your Profile"), - MenuItem("Settings"), - MenuItem("Sign out") - ) - - // TODO: load user profile - val $userProfile = Var( - UserProfile( - "tom", - UserInfo( - OsobniCislo("1031"), - "tom", - "Tom", - "Cook", - None, - None, - Some("tom@example.com"), - Some("+420 222 866 180"), - Some( - UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") - ), - Nil, - None - ) - ) - ) - - val $userInfo = $userProfile.signal.map(_.userInfo) - - type ViewModel = Option[HtmlElement] - def apply( - actionBus: Observer[Action] - )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using - router: Router[Page] - ): HtmlElement = - PageLayout(actionBus)( - $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => - PageLayout.ViewModel( - NavigationBar.ViewModel( - u, - pages.map(p => - NavigationBar.Link( - () => - PageLink( - p, - actionBus - ), - p == cp - ) - ), - userMenu, - logo - ), - c - ) - ), - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala deleted file mode 100644 index bf473c6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ /dev/null @@ -1,143 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden -import fiftyforms.ui.components.tailwind.list.IconText.ViewModel -import fiftyforms.ui.components.tailwind.Icons -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Action - -object Breadcrumbs: - - private def slash = - import svg.{*, given} - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-300", - xmlns := "http://www.w3.org/2000/svg", - fill := "currentColor", - viewBox := "0 0 20 20", - ariaHidden := true, - path( - d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" - ) - ) - - def backIcon = - Icons.solid - .`arrow-narrow-left`() - .amend( - svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" - ) - - object Link: - case class ViewModel( - page: Page, - icon: Option[SvgElement], - extraClasses: String, - text: String, - textClasses: Option[String] = None - ) - - val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" - - def shortHome(p: Page) = ViewModel( - p, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - "Zpět na úvodní stránku", - None - ) - - def fullHome(p: Page) = ViewModel( - p, - Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), - "text-gray-400 hover:text-gray-500", - "Domů", - Some("sr-only") - ) - - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - PageLink - .container($m.map(_.page), actionBus) - .amend( - cls <-- $m.map(_.extraClasses), - child.maybe <-- $m.map(_.icon), - span( - cls <-- $m.map(_.textClasses.getOrElse("")), - child.text <-- $m.map(_.text) - ) - ) - - object FullBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - ol( - role := "list", - cls := "flex items-center space-x-4", - children <-- $m.map(_.path).split(_.id)((_, _, $p) => - li( - div( - cls := "flex items-center", - child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), - Link( - $p.map(p => - if (p.isRoot) then Link.fullHome(p) - else - Link.ViewModel( - p, - None, - s"ml-4 max-w-xs truncate ${Link.baseClasses}", - p.title - ) - ), - actionBus - ) - ) - ) - ) - ) - - object ShortBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - Link( - $m.map { p => - val target = p.parent.getOrElse(p) - if target.isRoot then Link.shortHome(target) - else - Link.ViewModel( - target, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - s"Zpět na ${target.title}" - ) - }, - actionBus - ) - - def apply(actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - nav( - cls := "flex", - aria.label := "Breadcrumb", - div( - cls := "flex sm:hidden", - ShortBreadcrumbs(router.$currentPage, actionBus) - ), - div( - cls := "hidden sm:block", - FullBreadcrumbs(router.$currentPage, actionBus) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala deleted file mode 100644 index 0db2047..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ /dev/null @@ -1,242 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent -import fiftyforms.ui.components.tailwind.Icons -import fiftyforms.ui.components.tailwind.Avatar -import cz.e_bs.cmi.mdr.pdb.UserInfo -import io.laminext.syntax.core.* - -object NavigationBar: - - case class Logo(img: String, name: String) - case class Link(a: () => Anchor, active: Boolean) - case class MenuItem(title: String) - - case class ViewModel( - userInfo: UserInfo, - pages: List[Link], - userMenu: List[MenuItem], - logo: Logo - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - val $userInfo = $m.map(_.userInfo) - val mobileMenuOpen = Var(false) - - val desktopOnly = cls("hidden md:block") - val mobileOnly = cls("md:hidden") - - inline def avatarImage(size: Int = 8) = - Avatar($userInfo.map(_.img)).avatarImage(size) - - def notificationButton = button( - tpe := "button", - cls := List( - "bg-indigo-600", - "focus:outline-none", - "focus:ring-2", - "focus:ring-offset-2", - "focus:ring-offset-indigo-600", - "focus:ring-white", - "hover:text-white", - "p-1", - "rounded-full", - "text-indigo-200" - ), - span(cls := "sr-only", "View notifications"), - Icons.outline.bell() - ) - - def userProfile: HtmlElement = - val menuOpen = Var(false) - - def menuItem(item: MenuItem, idx: Int): HtmlElement = - a( - href := "#", - cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", - role := "menuitem", - tabIndex := -1, - idAttr := s"user-menu-item-$idx", - item.title - ) - - div( - cls := "ml-3 relative", - div( - button( - tpe := "button", - cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - idAttr := "user-menu-button", - aria.expanded <-- menuOpen.signal, - aria.hasPopup := true, - span(cls := "sr-only", "Open user menu"), - child <-- avatarImage(), - onClick.preventDefault.mapTo( - !menuOpen.now() - ) --> menuOpen.writer - ) - ), - /* - * */ - div( - cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", - cls <-- menuOpen.signal.map { o => - if (o) "md:block" else "md:hidden" - }, - role := "menu", - aria.orientation := "vertical", - aria.labelledBy := "user-menu-button", - tabIndex := -1, - // : keyboard navigation - children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) - ) - ) - - def mobileProfile = - def menuItem(item: MenuItem): HtmlElement = - a( - href := "#", - cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", - item.title - ) - - div( - cls := "pt-4 pb-3 border-t border-indigo-700", - div( - cls := "flex items-center px-5", - div( - cls := "flex-shrink-0", - child <-- avatarImage(10) - ), - div( - cls := "ml-3", - div( - cls := "text-base font-medium text-white", - child.text <-- $userInfo.map(_.name) - ), - child.maybe <-- $userInfo.map( - _.email.map(e => - div( - cls := "text-sm font-medium text-indigo-300", - e - ) - ) - ) - ), - notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) - ), - div( - cls := "mt-3 px-2 space-y-1", - children <-- $m.map(_.userMenu.map(menuItem)) - ) - ) - - def pageLink(page: Link): Anchor = - page - .a() - .amend( - cls := "text-white px-3 py-2 rounded-md text-sm font-medium", - cls := Seq( - "bg-indigo-700" -> page.active, - "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active - ), - ariaCurrent := (if page.active then "page" else "false") - ) - - def logoImg: Image = - img( - cls := "h-8 w-8", - src <-- $m.map(_.logo.img), - alt <-- $m.map(_.logo.name) - ) - - def mobileMenuButton = button( - tpe := "button", - cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - aria.controls := "mobile-menu", - aria.expanded <-- mobileMenuOpen.signal, - span(cls := "sr-only", "Open main menu"), - Icons.outline - .menu() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") - ), - Icons.outline - .x() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") - ), - onClick.preventDefault.mapTo( - !mobileMenuOpen.now() - ) --> mobileMenuOpen.writer - ) - - def navBarLeft = - div( - cls := "flex items-center", - div(cls := "flex-shrink-0", logoImg), - div( - desktopOnly, - div( - cls := "ml-10 flex items-baseline space-x-4", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ) - ) - ) - - def navBarRight = - div( - desktopOnly, - div( - cls := "ml-4 flex items-center md:ml-6", - notificationButton, - userProfile - ) - ) - - def navBarMobile = - div( - cls := "-mr-2 flex", - mobileOnly, - mobileMenuButton - ) - - def navBar = - div( - cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex items-center justify-between h-16", - navBarLeft, - navBarRight, - navBarMobile - ) - ) - - def mobileMenu = - div( - mobileOnly, - cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }, - idAttr := "mobile-menu", - div( - cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ), - mobileProfile - ) - - nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala deleted file mode 100644 index 75339df..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object PageHeader: - def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = - header( - cls := "bg-white shadow-sm", - div( - cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", - h1( - cls := "text-lg leading-6 font-semibold text-gray-900", - Breadcrumbs(actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala deleted file mode 100644 index 5096463..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ /dev/null @@ -1,30 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import fiftyforms.ui.components.tailwind.Loading -import io.laminext.syntax.core.* - -object PageLayout: - case class ViewModel( - navigation: NavigationBar.ViewModel, - content: Option[HtmlElement] - ) - def apply(actionBus: Observer[Action])( - $m: Signal[ViewModel], - mods: Modifier[HtmlElement]* - )(using router: Router[Page]): HtmlElement = - val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) - div( - cls := "h-full flex flex-col", - NavigationBar($m.map(_.navigation)), - child.maybe <-- router.$currentPage.map(_.isRoot) - .switch(None, Some(PageHeader(actionBus))), - main( - cls := "flex-grow-1 overflow-y-auto", - mods, - child <-- $maybeContent.map(_.getOrElse(Loading)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala deleted file mode 100644 index 62d820f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.LinkSupport.* -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo - -object PageLink: - type ViewModel = Page - def apply($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = - a(mods($m, actions), child.text <-- $m.map(_.title)) - - def apply(m: ViewModel, actions: Observer[Action])(using - router: Router[Page] - ): Anchor = apply(Val(m), actions) - - def container($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = a(mods($m, actions)) - - def container(m: ViewModel, actions: Observer[Action])(using - Router[Page] - ): Anchor = container(Val(m), actions) - - private def mods($m: Signal[Page], actions: Observer[Action])(using - router: Router[Page] - ): Modifier[Anchor] = - Seq( - href <-- $m.map(router.absoluteUrlForPage).recover { case _ => - Some("invalid url") - }, - composeEvents(onClick.noKeyMod.preventDefault)( - _.sample($m) - .map(NavigateTo.apply) - ) --> actions - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala deleted file mode 100644 index 87f825a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.pages.dashboard.DashboardPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -class DashboardPageConnector(actionBus: Observer[Action])(using - router: Router[Page] -): - def apply: HtmlElement = - AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala deleted file mode 100644 index e9b0f71..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala +++ /dev/null @@ -1,76 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailKriteriaPage -import pages.detail.components.DukazyKriteria -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -object DetailKriteriaPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailKriteriaPageConnector( - state: DetailKriteriaPageConnector.AppState -)( - $page: Signal[Page.DetailKriteria] -)(using Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => - (p.osobniCislo.value, p.parametr.value, p.kriterium.value) - )((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map( - FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) - ) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, s, $s) => - DetailKriteriaPage($s)(state.actionBus.contramap { - case DukazyKriteria.Add => - NavigateTo( - Page.UpravDukazKriteria( - Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), - Page.Titled(s.parametr.id, Some(s.parametr.nazev)), - Page.Titled(s.kriterium.id, Some(s.kriterium.id)) - ) - ) - }) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): DetailKriteriaPage.ViewModel = - DetailKriteriaPage.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala deleted file mode 100644 index 4d8024f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ /dev/null @@ -1,57 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import pages.detail.DetailPage -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import fiftyforms.ui.components.tailwind.Color - -object DetailPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailPageConnector(state: DetailPageConnector.AppState)( - $page: Signal[Page.Detail] -)(using router: Router[Page]): - val $oscChangeSignal = $page.splitOne(_.osobniCislo.value)((osc, _, _) => osc) - val $pageChangeSignal = - $oscChangeSignal.flatMap(osc => - EventStream.fromSeq(Seq(FetchUserDetails(osc), FetchParameters(osc))) - ) - // TODO: filter the value based on the current osc - // OSC change will fetch new data, but still - // - we need to be sure that what we got is really what we ought to display - // - we want to display stale data accordingly (at least with loading indicator) - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - def apply: HtmlElement = - AppPage(state.actionBus)( - $data.combineWithFn($params)(_ zip _) - .map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: List[Parameter] - ): DetailPage.ViewModel = - DetailPage.ViewModel( - o.toDetailOsoby, - p.map( - _.toParametr(param => - PageLink.container(Page.DetailParametru(o, param), state.actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala deleted file mode 100644 index fdaa7f8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailParametruPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink - -object DetailParametruPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailParametruPageConnector( - state: DetailParametruPageConnector.AppState -)( - $page: Signal[Page.DetailParametru] -)(using router: Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => (p.osobniCislo.value, p.parametr.value))((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map(FetchParameter(_, _)) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - } yield (da, pb) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailParametruPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter - ): DetailParametruPage.ViewModel = - DetailParametruPage.ViewModel( - o.toDetailOsoby, - p.toParametr(p => - PageLink.container(Page.DetailParametru(o, p), state.actionBus) - ), - p.criteria.map( - _.toKriterium { c => - PageLink.container( - Page.DetailKriteria(o, p, c), - state.actionBus - ) - } - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala deleted file mode 100644 index 049aa22..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -case class DirectoryPageConnector( - $input: EventStream[List[UserInfo]], - actionBus: Observer[Action] -)(using router: Router[Page]): - val $data = $input.startWithNone - val $actionSignal = EventStream.fromValue(FetchDirectory) - - def apply: HtmlElement = - AppPage(actionBus)( - $data.split(_ => ())((_, _, s) => - pages.directory.DirectoryPage( - s.map( - _.map( - _.toUserRow(u => - PageLink.container( - Page.Detail(Page.Titled(u.personalNumber)), - actionBus - ) - ) - ) - ) - ) - ), - $actionSignal --> actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala deleted file mode 100644 index 88e7450..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ /dev/null @@ -1,68 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii -import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria -import fiftyforms.ui.components.tailwind.Color - -extension (o: UserInfo) - def toDetailOsoby: DetailOsoby.ViewModel = - DetailOsoby.ViewModel( - o.personalNumber, - o.name, - o.email, - o.phone, - o.img, - o.mainFunction.map(f => - DetailOsoby.Funkce.ViewModel(f.name, f.dept, f.ou) - ), - o.userContracts.headOption.map(c => - DetailOsoby.PracovniPomer.ViewModel(c.rel, c.startDate, c.endDate) - ) - ) - -extension (param: Parameter) - def toParametr(container: Parameter => Anchor): DetailParametru.ViewModel = - DetailParametru.ViewModel( - id = param.id, - nazev = param.name, - popis = param.description, - status = "Nesplněno", - statusColor = Color.red, - a = container(param) - ) - -extension (crit: ParameterCriteria) - def toKriterium( - container: ParameterCriteria => Anchor - ): DetailKriteria.ViewModel = - DetailKriteria.ViewModel( - nazev = crit.criteriumText, - kapitola = crit.chapterId, - bod = crit.itemId, - status = "Nesplněno", - statusColor = Color.red, - splneno = false, - dukazy = Nil, - container = container(crit) - ) - -extension (user: UserInfo) - def toUserRow( - container: UserInfo => HtmlElement = (_: UserInfo) => div() - ): UserRow.ViewModel = - UserRow.ViewModel( - osobniCislo = user.personalNumber.toString, - celeJmeno = user.name, - prijmeni = user.surname, - hlavniFunkce = user.mainFunction.map(_.name), - img = user.img, - container = () => container(user) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala deleted file mode 100644 index 7d5b2c1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.dashboard - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.Page - -object DashboardPage: - - def render: HtmlElement = - div("Dashboard page") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala deleted file mode 100644 index 9ef0698..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala +++ /dev/null @@ -1,31 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailKriteriaPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply($m: Signal[ViewModel])( - action: Observer[DukazyKriteria.Action] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - DukazyKriteria($m.map(_.kriterium.dukazy))(action) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala deleted file mode 100644 index 3b316e1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object DetailPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametry: SeznamParametru.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby($m.map(_.osoba)), - SeznamParametru($m.map(_.parametry)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala deleted file mode 100644 index 17224cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala +++ /dev/null @@ -1,24 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailParametruPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriteria: SeznamKriterii.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby.header($m.map(_.osoba)), - DetailParametru($m.map(_.parametr)), - SeznamKriterii($m.map(_.kriteria)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala deleted file mode 100644 index dc912fa..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ /dev/null @@ -1,107 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb -package app -package pages.detail - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.UpravDukazForm -import fiftyforms.services.files.File - -object UpravDukaz: - - type ThisPage = Page.UpravDukazKriteria - type PageKey = (OsobniCislo, String, String) - - trait State { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def availableFiles: EventStream[List[File]] - def actionBus: Observer[Action] - } - - def keyOfPage(page: ThisPage): PageKey = - (page.osobniCislo.value, page.parametr.value, page.kriterium.value) - - def onChangeAction(key: PageKey): Action = - FetchParameterCriteria( - key._1, - key._2, - key._3, - Page.UpravDukazKriteria(_, _, _) - ) - - class Connector(state: State)($page: Signal[ThisPage])(using Router[Page]): - val $paramChangeSignal = $page.splitOne(keyOfPage)((x, _, _) => x) - val $pageChangeSignal = $paramChangeSignal.map(onChangeAction) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.split(_ => ())((_, s, $s) => - PageComponent( - $s.map(buildModel), - state.availableFiles, - state.actionBus.contramap { - case UpravDukazForm.Cancelled => - NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) - case UpravDukazForm.AvailableFilesRequested => - FetchAvailableFiles(s._1.personalNumber) - } - ) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): PageComponent.ViewModel = - import connectors.* - PageComponent.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) - - object PageComponent: - import components.* - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply( - $m: Signal[ViewModel], - availableFilesStream: EventStream[List[File]], - events: Observer[UpravDukazForm.Event] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - UpravDukazForm(availableFilesStream)(events) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala deleted file mode 100644 index ca311b0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.Icons -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Color - -object DetailKriteria: - case class ViewModel( - nazev: String, - kapitola: String, - bod: String, - status: String, - statusColor: Color, - splneno: Boolean, - dukazy: List[DukazKriteria.ViewModel], - container: HtmlElement = div() - ) { - val id = s"${kapitola}${bod}" - } - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h3( - cls := "text-xl leading-6 font-bold text-gray-900", - child.text <-- $m.map(_.nazev) - ), - h3( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - "Kapitola ", - child.text <-- $m.map(_.kapitola), - " bod ", - child <-- $m.map(_.bod) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala deleted file mode 100644 index ae90290..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala +++ /dev/null @@ -1,100 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Avatar - -object DetailOsoby: - - object Funkce: - case class ViewModel( - nazev: String, - stredisko: String, - voj: String - ) - - def render($m: Signal[ViewModel]): HtmlElement = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.nazev), - span( - cls := "hidden md:inline", - " @ ", - child.text <-- $m.map(_.stredisko), - ", ", - child.text <-- $m.map(_.voj) - ) - ) - - object PracovniPomer: - case class ViewModel( - druh: String, - pocatek: LocalDate, - konec: Option[LocalDate] - ) - - def render($m: Signal[ViewModel]): HtmlElement = - import fiftyforms.ui.components.tailwind.CustomAttrs.datetime - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.druh), - " od ", - time( - datetime <-- $m.map(_.pocatek.toString), - child.text <-- $m.map(_.pocatek.toString) - ) - ) - - case class ViewModel( - osobniCislo: OsobniCislo, - jmeno: String, - email: Option[String], - telefon: Option[String], - img: Option[String], - hlavniFunkce: Option[Funkce.ViewModel], - pracovniPomer: Option[PracovniPomer.ViewModel] - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-4", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(16) - ), - div( - h1( - cls := "text-2xl font-bold text-gray-900", - child.text <-- $m.map(_.jmeno) - ), - child.maybe <-- $m.map(_.hlavniFunkce).split(_ => ())((_, _, d) => - Funkce.render(d) - ), - child.maybe <-- $m.map(_.pracovniPomer).split(_ => ())((_, _, d) => - PracovniPomer.render(d) - ) - ) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-5", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(8) - ), - div( - h1( - cls := "text-lg font-medium text-gray-800", - child.text <-- $m.map(_.jmeno) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala deleted file mode 100644 index e11cb62..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color - -object DetailParametru: - case class ViewModel( - id: String, - nazev: String, - popis: String, - status: String, - statusColor: Color, - a: Anchor - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h2( - cls := "text-2xl leading-6 font-medium text-gray-900", - child.text <-- $m.map(_.nazev) - ), - p( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - child.text <-- $m.map(_.popis) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - h2( - cls := "text-lg leading-6 font-medium text-gray-600", - child.text <-- $m.map(_.nazev) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala deleted file mode 100644 index a00ede8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala +++ /dev/null @@ -1,177 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.CustomAttrs - -object DukazKriteria: - case class Osoba(osobniCislo: String, jmeno: String) - case class Udalost(osoba: Osoba, datum: LocalDate) - case class Dokument( - url: String, - nazev: String, - pridal: Udalost, - odebral: Option[Udalost] - ) - case class ViewModel( - dokumenty: List[Dokument], - autorizoval: Option[Udalost], - platiDo: Option[LocalDate], - poznámka: Option[String] - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-lg", - div( - cls := "px-4 py-5 sm:px-6", - h3( - cls := "text-lg leading-6 font-medium text-gray-900", - """Applicant Information""" - ), - p( - cls := "mt-1 max-w-2xl text-sm text-gray-500", - """Personal details and application.""" - ) - ), - div( - cls := "border-t border-gray-200 px-4 py-5 sm:px-6", - dl( - cls := "grid grid-cols-1 gap-x-4 gap-y-8 sm:grid-cols-2", - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Full name""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Margot Foster""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Application for""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Backend Developer""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Email address""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """margotfoster@example.com""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Salary expectation""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """$120,000""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """About""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Fugiat ipsum ipsum deserunt culpa aute sint do nostrud anim incididunt cillum culpa consequat. Excepteur qui ipsum aliquip consequat sint. Sit id mollit nulla mollit nostrud in ea officia proident. Irure nostrud pariatur mollit ad adipisicing reprehenderit deserunt qui eu.""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """Attachments""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - ul( - role := "list", - cls := "border border-gray-200 rounded-md divide-y divide-gray-200", - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """resume_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ), - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """coverletter_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala deleted file mode 100644 index 22f134f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala +++ /dev/null @@ -1,35 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object DukazyKriteria: - sealed trait Action - case object Add extends Action - - type ViewModel = List[DukazKriteria.ViewModel] - - def apply($m: Signal[ViewModel])(actions: Observer[Action]): HtmlElement = - div( - child.maybe <-- $m.map(m => - if (m.isEmpty) then Some(prazdnyDukaz(actions)) - else None - ), - children <-- $m.map(_.zipWithIndex).split(_._2)((_, _, $s) => - DukazKriteria($s.map(_._1)) - ) - ) - - private def prazdnyDukaz(actions: Observer[Action]): HtmlElement = - button( - tpe := "button", - onClick.preventDefault.mapTo(Add) --> actions, - cls := "relative block w-full border-2 border-gray-300 border-dashed rounded-lg p-12 text-center hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - Icons.outline - .`document-add`(12) - .amend(svg.cls := "mx-auto text-gray-400"), - span( - cls := "mt-2 block text-sm font-medium text-gray-900", - "Přidat důkaz" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala deleted file mode 100644 index f628697..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.list.StackedList -import fiftyforms.ui.components.tailwind.list.ListRow -import fiftyforms.ui.components.tailwind.list.RowTag -import fiftyforms.ui.components.tailwind.list.RowNext -import java.time.LocalDate - -object SeznamKriterii: - type ViewModel = List[DetailKriteria.ViewModel] - - private val kritList = new StackedList[DetailKriteria.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - kritList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = - p(cls := "text-sm text-gray-500", s"${i.kapitola}${i.bod}"), - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.container - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala deleted file mode 100644 index 2bfe4bc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.list.{ - StackedList, - ListRow, - RowTag, - PropList, - IconText, - RowNext -} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.LinkSupport.* - -object SeznamParametru: - type ViewModel = List[DetailParametru.ViewModel] - - private val parametrList = new StackedList[DetailParametru.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - parametrList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = emptyNode, - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.a - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala deleted file mode 100644 index f048e9d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ /dev/null @@ -1,101 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.form.* -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement -import fiftyforms.services.files.components.tailwind.FilePicker -import cz.e_bs.cmi.mdr.pdb.frontend.AutorizujDukaz -import cz.e_bs.cmi.mdr.pdb.frontend.DocumentRef -import fiftyforms.services.files.File - -object UpravDukazForm: - sealed trait Event - case object Cancelled extends Event - case object AvailableFilesRequested extends Event - def apply(availableFilesStream: EventStream[List[File]])( - updates: Observer[Event] - ): HtmlElement = - val (filesStream, filesObserver) = - EventStream.withObserver[FilePicker.Event] - val files = Var[List[File]](Nil) - def submitButtons: HtmlElement = - div( - cls := "pt-5", - div( - cls := "flex justify-end", - button( - tpe := "button", - cls := "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zrušit", - onClick.mapTo(Cancelled) --> updates - ), - button( - tpe := "submit", - cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - disabled <-- files.signal.map(_.isEmpty), - "Autorizovat" - ) - ) - ) - - div( - filesStream.collect { case FilePicker.SelectionUpdated(files) => - files.to(List) - } --> files.writer, - filesStream.collect { case FilePicker.AvailableFilesRequested => - AvailableFilesRequested - } --> updates, - cls := "bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6", - Form( - Form.Body( - Form.Section( - FormHeader( - FormHeader.ViewModel( - "Doložení kritéria", - "Sestavte doklady poskytující důkaz kritéria a potvrďte odesláním formuláře. Případné limitace či jiné relevantní údaje vepište do pole Komentář." - ) - ), - FormFields( - FormRow( - "dokumenty", - "Dokumenty", - FilePicker(files.signal, availableFilesStream)(filesObserver) - .amend(idAttr := "dokumenty", cls("max-w-lg")) - ).toHtml, - FormRow( - "platnost", - "Platnost", - input( - idAttr := "platnost", - name := "platnost", - tpe := "date", - autoComplete := "date", - cls := "block max-w-lg w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" - ) - ).toHtml, - FormRow( - "komentar", - "Komentář", - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - textArea( - idAttr := "komentar", - name := "about", - rows := 3, - cls := "max-w-lg shadow-sm block w-full focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border border-gray-300 rounded-md" - ), - p( - cls := "mt-2 text-sm text-gray-500", - "Doplňte prosím potřebné informace související s doložením kritéria, včetně případných limitací." - ) - ) - ).toHtml - ) - ) - ), - submitButtons - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala deleted file mode 100644 index 41033f6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -import components._ - -object DirectoryPage: - - type ViewModel = List[UserRow.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - val (actionsStream, actionObserver) = - EventStream.withObserver[SearchForm.Action] - val $filter = actionsStream - .collect { - case SearchForm.ClearFilter => None - case SearchForm.SetFilter(t) => Some(t) - } - .startWith(None) - val byLetter = for { - d <- $m - f <- $filter - } yield for { - (letter, users) <- d - .filter { user => - f match - case None => true - case Some(t) => user.search.contains(t) - } - .groupBy(_.prijmeni.head) - .to(List) - .sortBy(_._1) - } yield (letter.toString, users.sortBy(_.prijmeni)) - - div( - cls := "h-full max-w-7xl mx-auto order-first flex flex-col flex-shrink-0", - SearchForm(actionObserver), - Directory(byLetter) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala deleted file mode 100644 index 73f915d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -object Directory: - - object Header: - type ViewModel = String - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500", - h3(child.text <-- $m) - ) - - object UserList: - type ViewModel = List[UserRow.ViewModel] - def apply($m: Signal[ViewModel]): HtmlElement = - ul( - role := "list", - cls := "relative z-0 divide-y divide-gray-200", - children <-- $m.split(_.osobniCislo)((_, _, s) => UserRow(s)) - ) - - type ViewModel = List[(String, List[UserRow.ViewModel])] - def apply($m: Signal[ViewModel]): HtmlElement = - nav( - cls := "flex-1 min-h-0 overflow-y-auto", - aria.label := "Directory", - children <-- $m.split(_._1)((_, _, s) => - div( - cls := "relative", - Header(s.map(_._1)), - UserList(s.map(_._2)) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala deleted file mode 100644 index e182910..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala +++ /dev/null @@ -1,54 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object SearchForm: - sealed trait Action - case object SubmitFilter extends Action - sealed trait FilterAction extends Action - case object ClearFilter extends FilterAction - case class SetFilter(value: String) extends FilterAction - - def apply(actions: Observer[Action]): HtmlElement = - div( - cls := "px-6 pt-4 pb-4", - form( - cls := "flex space-x-4", - onSubmit.mapTo(SubmitFilter) --> actions, - div( - cls := "flex-1 min-w-0", - label( - forId := "search", - cls := "sr-only", - "Hledat" - ), - div( - cls := "relative rounded-md shadow-sm", - div( - cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", - Icons.solid.search().amend(svg.cls := "text-gray-400") - ), - input( - tpe := "search", - name := "search", - idAttr := "search", - cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", - placeholder := "Hledat", - composeEvents(onInput.mapToValue.setAsValue.map(SetFilter(_)))( - _.throttle(500) - ) --> actions - ) - ) - ), - button( - tpe := "submit", - cls := "inline-flex justify-center px-3.5 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500", - Icons.solid.filter().amend(svg.cls := "text-gray-400"), - span( - cls := "sr-only", - "Hledat" - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala deleted file mode 100644 index 82f7723..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala +++ /dev/null @@ -1,52 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.airstream.core.Signal -import fiftyforms.ui.components.tailwind.Avatar - -object UserRow: - case class ViewModel( - osobniCislo: String, - celeJmeno: String, - prijmeni: String, - hlavniFunkce: Option[String], - img: Option[String], - container: () => HtmlElement = () => div() - ) { - val search = prijmeni.toLowerCase - } - - def apply($m: Signal[ViewModel]): HtmlElement = - inline def avatarImage = - Avatar($m.map(_.img)).avatarImage(10) - - li( - div( - cls := "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500", - div( - cls := "flex-shrink-0", - child <-- avatarImage - ), - div( - cls := "flex-1 min-w-0", - child <-- $m.map { o => - o.container() - .amend( - cls := "focus:outline-none", - span( - cls := "absolute inset-0", - aria.hidden := true - ), - p( - cls := "text-sm font-medium text-gray-900", - o.celeJmeno - ), - p( - cls := "text-sm text-gray-500 truncate", - o.hlavniFunkce - ) - ) - } - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala deleted file mode 100644 index 467c5f5..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala +++ /dev/null @@ -1,99 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router - -object ErrorPage: - case class ViewModel( - homePage: Page, - errorName: String, - title: String, - subTitle: String - ) - - def apply(m: ViewModel, actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - val ViewModel(homePage, errorName, title, subTitle) = m - div( - cls := "min-h-full pt-16 pb-12 flex flex-col bg-white", - main( - cls := "flex-grow flex flex-col justify-center max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex-shrink-0 flex justify-center", - a( - href := "/", - cls := "inline-flex", - span( - cls := "sr-only", - """Workflow""" - ), - img( - cls := "h-12 w-auto", - src := "https://tailwindui.com/img/logos/workflow-mark.svg?color=indigo&shade=600", - alt := "" - ) - ) - ), - div( - cls := "py-16", - div( - cls := "text-center", - p( - cls := "text-sm font-semibold text-indigo-600 uppercase tracking-wide", - errorName - ), - h1( - cls := "mt-2 text-4xl font-extrabold text-gray-900 tracking-tight sm:text-5xl", - title - ), - p( - cls := "mt-2 text-base text-gray-500", - subTitle - ), - div( - cls := "mt-6", - PageLink - .container(homePage, actionBus) - .amend( - cls := "text-base font-medium text-indigo-600 hover:text-indigo-500", - """Go back home""" - ) - ) - ) - ) - ), - footer( - cls := "flex-shrink-0 max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", - nav( - cls := "flex justify-center space-x-4", - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Contact Support""" - ), - span( - cls := "inline-block border-l border-gray-300", - aria.hidden := true - ), - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Status""" - ), - span( - cls := "inline-block border-l border-gray-300", - aria.hidden := true - ), - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Twitter""" - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala deleted file mode 100644 index bd9baec..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action - -object NotFoundPage: - def apply(homePage: Page, url: String, actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - ErrorPage( - ErrorPage.ViewModel( - homePage, - "404 error", - "Page not found.", - s"Sorry, but page $url doesn't exist." - ), - actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala deleted file mode 100644 index 476fe3c..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action - -object UnhandledErrorPage: - - case class ViewModel( - homePage: Page, - errorName: Option[String], - errorMessage: Option[String] - ) - - def apply(m: ViewModel, actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - ErrorPage( - ErrorPage.ViewModel( - m.homePage, - "Unexpected error occurred", - m.errorName.getOrElse( - "Uh oh!" - ), // TODO: translations, better text than uh oh - m.errorMessage.getOrElse( - "This wasn't supposed to happen! Please try again." - ) - ), - actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala deleted file mode 100644 index 25f059f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala +++ /dev/null @@ -1,6 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.services - -import com.raquo.airstream.core.Observer - -trait DataFetcher[K, A]: - def fetch(id: K, sink: Observer[A]): Unit diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala deleted file mode 100644 index a469830..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala +++ /dev/null @@ -1,120 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package state - -import com.raquo.airstream.core.EventStream -import cz.e_bs.cmi.mdr.pdb.{UserInfo, OsobniCislo} -import com.raquo.airstream.core.Observer -import scala.scalajs.js -import scala.scalajs.js.JSON -import zio.json._ -import com.raquo.airstream.eventbus.EventBus -import com.raquo.airstream.ownership.Owner -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.UserFunction -import cz.e_bs.cmi.mdr.pdb.UserContract -import fiftyforms.services.files.File - -trait AppState - extends connectors.DetailPageConnector.AppState - with connectors.DetailParametruPageConnector.AppState - with connectors.DetailKriteriaPageConnector.AppState - with pages.detail.UpravDukaz.State: - def users: EventStream[List[UserInfo]] - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - -class MockAppState(implicit owner: Owner, router: Router[Page]) - extends AppState: - - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonDecoder[UserFunction] = DeriveJsonDecoder.gen - given JsonDecoder[UserContract] = DeriveJsonDecoder.gen - given JsonDecoder[UserInfo] = DeriveJsonDecoder.gen - - given JsonDecoder[ParameterCriteria] = DeriveJsonDecoder.gen - given JsonDecoder[Parameter] = DeriveJsonDecoder.gen - - private val actions = EventBus[Action]() - private val (parametersStream, pushParameters) = - EventStream.withCallback[List[Parameter]] - private val (usersStream, pushUsers) = - EventStream.withCallback[List[UserInfo]] - private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] - private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] - - private val mockData: List[UserInfo] = - mockUsers - .asInstanceOf[js.Dictionary[js.Object]] - .values - // TODO: is there a more efficient way to parse from JS object directly? - .map { o => - val parsed = JSON.stringify(o).fromJson[UserInfo] - parsed.left.foreach(org.scalajs.dom.console.log(_)) - parsed - } - .collect { case Right(u) => - u - } - .toList - - private val mockParameters: List[Parameter] = - pdbParams - .asInstanceOf[js.Dictionary[js.Object]] - .values - .map(o => JSON.stringify(o).fromJson[Parameter]) - .collect { case Right(p) => p } - .toList - - // TODO: Extract to separate event handler - actions.events.foreach { - case FetchDirectory => pushUsers(mockData) - case FetchUserDetails(osc) => - mockData.find(_.personalNumber == osc).foreach { o => - pushDetails(o) - router.replaceState(Page.Detail(o)) - } - case FetchParameters(osc) => - pushParameters(mockParameters) - case FetchParameter(osc, paramId) => - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(Page.DetailParametru(o, p)) - case FetchParameterCriteria(osc, paramId, critId, page) => - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - c <- p.criteria.find(_.id == critId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(page(o, p, c)) - case NavigateTo(page) => router.pushState(page) - case FetchAvailableFiles(osc) => - pushFiles( - List( - File("https://tc163.cmi.cz/here", "Example file") - ) - ) - } - - override def users: EventStream[List[UserInfo]] = - usersStream.debugWithName("users") - - override def details: EventStream[UserInfo] = - detailsStream.debugWithName("details") - - override def parameters: EventStream[List[Parameter]] = - parametersStream.debugWithName("parameters") - - override def availableFiles: EventStream[List[File]] = - filesStream.debugWithName("available files") - - override def actionBus: Observer[Action] = - actions.writer.debugWithName("actions writer") diff --git a/app/src/main/scala/mdr/pdb/app/Main.scala b/app/src/main/scala/mdr/pdb/app/Main.scala new file mode 100644 index 0000000..081b4ee --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/Main.scala @@ -0,0 +1,103 @@ +package mdr.pdb.app + +import scala.scalajs.js.annotation.JSExportTopLevel +import scala.scalajs.js.annotation.JSExport +import scala.scalajs.js.annotation.JSImport +import scala.scalajs.js +import org.scalajs.dom +import com.raquo.laminar.api.L.{*, given} +import scala.scalajs.js.Date +import com.raquo.waypoint.Router +import com.raquo.waypoint.SplitRender +import mdr.pdb.app.services.DataFetcher +import scala.scalajs.js.JSON +import zio.json._ +import mdr.pdb.UserInfo +import mdr.pdb.app.state.AppState + +@js.native +@JSImport("stylesheets/main.css", JSImport.Namespace) +object Css extends js.Any + +@js.native +@JSImport("data/users.json", JSImport.Default) +object mockUsers extends js.Object + +@js.native +@JSImport("params/pdb-params.json", JSImport.Default) +object pdbParams extends js.Object + +@JSExportTopLevel("app") +object Main: + + @JSExport + def main(args: Array[String]): Unit = + import Routes.given + onLoad { + setupAirstream() + val appContainer = dom.document.querySelector("#app") + val _ = + render( + appContainer, + renderPage(state.MockAppState(using unsafeWindowOwner, router)) + ) + } + + private def onLoad(f: => Unit): Unit = + documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) + + private def setupAirstream()(using router: Router[Page]): Unit = + AirstreamError.registerUnhandledErrorCallback(err => + router.forcePage( + Page.UnhandledError( + Some(err.getClass.getName), // TODO: Fill only in dev mode + Some(err.getMessage) + ) + ) + ) + + def renderPage(state: AppState)(using + router: Router[Page] + ): HtmlElement = + val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) + .collectSignal[Page.Detail]( + connectors + .DetailPageConnector(state)(_) + .apply + ) + .collectSignal[Page.DetailParametru]( + connectors + .DetailParametruPageConnector(state)(_) + .apply + ) + .collectSignal[Page.DetailKriteria]( + connectors + .DetailKriteriaPageConnector(state)(_) + .apply + ) + .collectSignal[Page.UpravDukazKriteria]( + pages.detail.UpravDukaz.Connector(state)(_).apply + ) + .collectStatic(Page.Dashboard)( + connectors.DashboardPageConnector(state.actionBus).apply + ) + .collect[Page.NotFound](pg => + pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) + ) + .collect[Page.UnhandledError](pg => + pages.errors + .UnhandledErrorPage( + pages.errors.UnhandledErrorPage + .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), + state.actionBus + ) + ) + .collectStatic(Page.Directory)( + connectors + .DirectoryPageConnector(state.users, state.actionBus) + .apply + ) + div(cls := "h-full", child <-- pageSplitter.$view) + + // Pull in the stylesheet + val css: Css.type = Css diff --git a/app/src/main/scala/mdr/pdb/app/Routes.scala b/app/src/main/scala/mdr/pdb/app/Routes.scala new file mode 100644 index 0000000..2cc860c --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/Routes.scala @@ -0,0 +1,198 @@ +package mdr.pdb.app + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.* +import org.scalajs.dom +import zio.json.{*, given} +import mdr.pdb.OsobniCislo + +import scala.scalajs.js +import mdr.pdb.UserInfo +import mdr.pdb.Parameter +import mdr.pdb.ParameterCriteria +import mdr.pdb.app.Page.Titled + +// enum is not working with Waypoints' SplitRender collectStatic +sealed abstract class Page( + val id: String, + val title: String, + val parent: Option[Page] +) { + val path: Vector[Page] = + parent match + case None => Vector(this) + case Some(p) => p.path :+ this + + val isRoot: Boolean = parent.isEmpty +} + +object Page: + + case class Titled[V](value: V, title: Option[String] = None): + val show: String = title.getOrElse(value.toString) + + case object Directory extends Page("directory", "Adresář", None) + + case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) + + case class Detail(osobniCislo: Titled[OsobniCislo]) + extends Page("user", osobniCislo.show, Some(Directory)) + + object Detail { + def apply(o: UserInfo): Detail = Detail( + Titled(o.personalNumber, Some(o.name)) + ) + } + + case class DetailParametru( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String] + ) extends Page( + "parameter", + parametr.show, + Some(Detail(osobniCislo)) + ) + + object DetailParametru { + def apply(o: UserInfo, p: Parameter): DetailParametru = + DetailParametru( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)) + ) + } + + case class DetailKriteria( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String], + kriterium: Titled[String] + ) extends Page( + "criteria", + kriterium.show, + Some(DetailParametru(osobniCislo, parametr)) + ) + + object DetailKriteria { + def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = + DetailKriteria( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)), + Titled(k.id, Some(k.id)) + ) + } + + case class UpravDukazKriteria( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String], + kriterium: Titled[String] + ) extends Page( + "addProof", + "Důkaz", + Some(DetailKriteria(osobniCislo, parametr, kriterium)) + ) + + object UpravDukazKriteria { + def apply( + o: UserInfo, + p: Parameter, + k: ParameterCriteria + ): UpravDukazKriteria = + UpravDukazKriteria( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)), + Titled(k.id, Some(k.id)) + ) + } + + case class NotFound(url: String) extends Page("404", "404", Some(Directory)) + + case class UnhandledError( + errorName: Option[String], + errorMessage: Option[String] + ) extends Page("500", "Unexpected error", Some(Directory)) + +object Routes: + given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) + given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) + given [V: JsonEncoder]: JsonEncoder[Titled[V]] = + DeriveJsonEncoder.gen[Titled[V]] + given [V: JsonDecoder]: JsonDecoder[Titled[V]] = + DeriveJsonDecoder.gen[Titled[V]] + given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] + given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] + + val base = + js.`import`.meta.env.BASE_URL + .asInstanceOf[String] + "app" + + val homePage: Page = Page.Directory + + given router: Router[Page] = Router[Page]( + routes = List( + Route.static(homePage, root / endOfSegments, basePath = base), + Route.static( + Page.Dashboard, + root / "dashboard" / endOfSegments, + basePath = base + ), + Route[Page.Detail, String]( + encode = _.osobniCislo.value.toString, + decode = osc => Page.Detail(Titled(OsobniCislo(osc))), + root / "osoba" / segment[String] / endOfSegments, + basePath = base + ), + Route[Page.DetailParametru, (String, String)]( + encode = p => (p.osobniCislo.value.toString, p.parametr.value), + decode = + p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / endOfSegments, + basePath = base + ), + Route[Page.DetailKriteria, (String, String, String)]( + encode = p => + ( + p.osobniCislo.value.toString, + p.parametr.value, + p.kriterium.value.replaceAll("\\.", "--") + ), + decode = p => + Page.DetailKriteria( + Titled(OsobniCislo(p._1)), + Titled(p._2), + Titled(p._3.replaceAll("--", ".")) + ), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / "kriterium" / segment[String] / endOfSegments, + basePath = base + ), + Route[Page.UpravDukazKriteria, (String, String, String)]( + encode = p => + ( + p.osobniCislo.value.toString, + p.parametr.value, + p.kriterium.value.replaceAll("\\.", "--") + ), + decode = p => + Page.UpravDukazKriteria( + Titled(OsobniCislo(p._1)), + Titled(p._2), + Titled(p._3.replaceAll("--", ".")) + ), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / "kriterium" / segment[String] / "edit" / endOfSegments, + basePath = base + ) + ), + serializePage = _.toJson, + deserializePage = _.fromJson[Page] + .fold(s => throw IllegalStateException(s), identity), + getPageTitle = _.title, + routeFallback = url => Page.NotFound(url), + deserializeFallback = _ => Page.Dashboard + )( + $popStateEvent = windowEvents.onPopState, + owner = unsafeWindowOwner + ) diff --git a/app/src/main/scala/mdr/pdb/app/actions.scala b/app/src/main/scala/mdr/pdb/app/actions.scala new file mode 100644 index 0000000..d078b3a --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/actions.scala @@ -0,0 +1,21 @@ +package mdr.pdb.app + +import mdr.pdb.OsobniCislo +import mdr.pdb.UserInfo +import mdr.pdb.Parameter +import mdr.pdb.ParameterCriteria + +sealed trait Action + +case object FetchDirectory extends Action +case class FetchUserDetails(osc: OsobniCislo) extends Action +case class FetchParameters(osc: OsobniCislo) extends Action +case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action +case class FetchParameterCriteria( + osc: OsobniCislo, + paramId: String, + critId: String, + page: (UserInfo, Parameter, ParameterCriteria) => Page +) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action +case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/mdr/pdb/app/components/AppPage.scala new file mode 100644 index 0000000..130c817 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/AppPage.scala @@ -0,0 +1,82 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.Page +import mdr.pdb.{UserProfile, UserInfo, OsobniCislo} +import com.raquo.waypoint.Router +import mdr.pdb.app.Action +import mdr.pdb.app.NavigateTo +import mdr.pdb.UserFunction + +object AppPage: + // TODO: pages by logged in user + val pages: List[Page] = List(Page.Directory, Page.Dashboard) + + import NavigationBar.{Logo, MenuItem} + + val logo = Logo( + "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", + "Workflow" + ) + + // TODO: menu items by user profile + val userMenu = + List( + MenuItem("Your Profile"), + MenuItem("Settings"), + MenuItem("Sign out") + ) + + // TODO: load user profile + val $userProfile = Var( + UserProfile( + "tom", + UserInfo( + OsobniCislo("1031"), + "tom", + "Tom", + "Cook", + None, + None, + Some("tom@example.com"), + Some("+420 222 866 180"), + Some( + UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") + ), + Nil, + None + ) + ) + ) + + val $userInfo = $userProfile.signal.map(_.userInfo) + + type ViewModel = Option[HtmlElement] + def apply( + actionBus: Observer[Action] + )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using + router: Router[Page] + ): HtmlElement = + PageLayout(actionBus)( + $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => + PageLayout.ViewModel( + NavigationBar.ViewModel( + u, + pages.map(p => + NavigationBar.Link( + () => + PageLink( + p, + actionBus + ), + p == cp + ) + ), + userMenu, + logo + ), + c + ) + ), + mods + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala new file mode 100644 index 0000000..9375398 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala @@ -0,0 +1,143 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.tailwind.list.IconText.ViewModel +import fiftyforms.ui.components.tailwind.Icons +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.Page + +import com.raquo.laminar.api.L.{*, given} +import io.laminext.syntax.core.{*, given} +import mdr.pdb.app.Action + +object Breadcrumbs: + + private def slash = + import svg.{*, given} + svg( + cls := "flex-shrink-0 h-5 w-5 text-gray-300", + xmlns := "http://www.w3.org/2000/svg", + fill := "currentColor", + viewBox := "0 0 20 20", + ariaHidden := true, + path( + d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" + ) + ) + + def backIcon = + Icons.solid + .`arrow-narrow-left`() + .amend( + svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" + ) + + object Link: + case class ViewModel( + page: Page, + icon: Option[SvgElement], + extraClasses: String, + text: String, + textClasses: Option[String] = None + ) + + val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" + + def shortHome(p: Page) = ViewModel( + p, + Some(backIcon), + "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", + "Zpět na úvodní stránku", + None + ) + + def fullHome(p: Page) = ViewModel( + p, + Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), + "text-gray-400 hover:text-gray-500", + "Domů", + Some("sr-only") + ) + + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + PageLink + .container($m.map(_.page), actionBus) + .amend( + cls <-- $m.map(_.extraClasses), + child.maybe <-- $m.map(_.icon), + span( + cls <-- $m.map(_.textClasses.getOrElse("")), + child.text <-- $m.map(_.text) + ) + ) + + object FullBreadcrumbs: + type ViewModel = Page + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + ol( + role := "list", + cls := "flex items-center space-x-4", + children <-- $m.map(_.path).split(_.id)((_, _, $p) => + li( + div( + cls := "flex items-center", + child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), + Link( + $p.map(p => + if (p.isRoot) then Link.fullHome(p) + else + Link.ViewModel( + p, + None, + s"ml-4 max-w-xs truncate ${Link.baseClasses}", + p.title + ) + ), + actionBus + ) + ) + ) + ) + ) + + object ShortBreadcrumbs: + type ViewModel = Page + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + Link( + $m.map { p => + val target = p.parent.getOrElse(p) + if target.isRoot then Link.shortHome(target) + else + Link.ViewModel( + target, + Some(backIcon), + "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", + s"Zpět na ${target.title}" + ) + }, + actionBus + ) + + def apply(actionBus: Observer[Action])(using + router: Router[Page] + ): HtmlElement = + nav( + cls := "flex", + aria.label := "Breadcrumb", + div( + cls := "flex sm:hidden", + ShortBreadcrumbs(router.$currentPage, actionBus) + ), + div( + cls := "hidden sm:block", + FullBreadcrumbs(router.$currentPage, actionBus) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala new file mode 100644 index 0000000..4bb8b8d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala @@ -0,0 +1,242 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent +import fiftyforms.ui.components.tailwind.Icons +import fiftyforms.ui.components.tailwind.Avatar +import mdr.pdb.UserInfo +import io.laminext.syntax.core.* + +object NavigationBar: + + case class Logo(img: String, name: String) + case class Link(a: () => Anchor, active: Boolean) + case class MenuItem(title: String) + + case class ViewModel( + userInfo: UserInfo, + pages: List[Link], + userMenu: List[MenuItem], + logo: Logo + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + val $userInfo = $m.map(_.userInfo) + val mobileMenuOpen = Var(false) + + val desktopOnly = cls("hidden md:block") + val mobileOnly = cls("md:hidden") + + inline def avatarImage(size: Int = 8) = + Avatar($userInfo.map(_.img)).avatarImage(size) + + def notificationButton = button( + tpe := "button", + cls := List( + "bg-indigo-600", + "focus:outline-none", + "focus:ring-2", + "focus:ring-offset-2", + "focus:ring-offset-indigo-600", + "focus:ring-white", + "hover:text-white", + "p-1", + "rounded-full", + "text-indigo-200" + ), + span(cls := "sr-only", "View notifications"), + Icons.outline.bell() + ) + + def userProfile: HtmlElement = + val menuOpen = Var(false) + + def menuItem(item: MenuItem, idx: Int): HtmlElement = + a( + href := "#", + cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", + role := "menuitem", + tabIndex := -1, + idAttr := s"user-menu-item-$idx", + item.title + ) + + div( + cls := "ml-3 relative", + div( + button( + tpe := "button", + cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", + idAttr := "user-menu-button", + aria.expanded <-- menuOpen.signal, + aria.hasPopup := true, + span(cls := "sr-only", "Open user menu"), + child <-- avatarImage(), + onClick.preventDefault.mapTo( + !menuOpen.now() + ) --> menuOpen.writer + ) + ), + /* + * */ + div( + cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", + cls <-- menuOpen.signal.map { o => + if (o) "md:block" else "md:hidden" + }, + role := "menu", + aria.orientation := "vertical", + aria.labelledBy := "user-menu-button", + tabIndex := -1, + // : keyboard navigation + children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) + ) + ) + + def mobileProfile = + def menuItem(item: MenuItem): HtmlElement = + a( + href := "#", + cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", + item.title + ) + + div( + cls := "pt-4 pb-3 border-t border-indigo-700", + div( + cls := "flex items-center px-5", + div( + cls := "flex-shrink-0", + child <-- avatarImage(10) + ), + div( + cls := "ml-3", + div( + cls := "text-base font-medium text-white", + child.text <-- $userInfo.map(_.name) + ), + child.maybe <-- $userInfo.map( + _.email.map(e => + div( + cls := "text-sm font-medium text-indigo-300", + e + ) + ) + ) + ), + notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) + ), + div( + cls := "mt-3 px-2 space-y-1", + children <-- $m.map(_.userMenu.map(menuItem)) + ) + ) + + def pageLink(page: Link): Anchor = + page + .a() + .amend( + cls := "text-white px-3 py-2 rounded-md text-sm font-medium", + cls := Seq( + "bg-indigo-700" -> page.active, + "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active + ), + ariaCurrent := (if page.active then "page" else "false") + ) + + def logoImg: Image = + img( + cls := "h-8 w-8", + src <-- $m.map(_.logo.img), + alt <-- $m.map(_.logo.name) + ) + + def mobileMenuButton = button( + tpe := "button", + cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", + aria.controls := "mobile-menu", + aria.expanded <-- mobileMenuOpen.signal, + span(cls := "sr-only", "Open main menu"), + Icons.outline + .menu() + .amend( + svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") + ), + Icons.outline + .x() + .amend( + svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") + ), + onClick.preventDefault.mapTo( + !mobileMenuOpen.now() + ) --> mobileMenuOpen.writer + ) + + def navBarLeft = + div( + cls := "flex items-center", + div(cls := "flex-shrink-0", logoImg), + div( + desktopOnly, + div( + cls := "ml-10 flex items-baseline space-x-4", + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) + ) + ) + ) + + def navBarRight = + div( + desktopOnly, + div( + cls := "ml-4 flex items-center md:ml-6", + notificationButton, + userProfile + ) + ) + + def navBarMobile = + div( + cls := "-mr-2 flex", + mobileOnly, + mobileMenuButton + ) + + def navBar = + div( + cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", + div( + cls := "flex items-center justify-between h-16", + navBarLeft, + navBarRight, + navBarMobile + ) + ) + + def mobileMenu = + div( + mobileOnly, + cls <-- mobileMenuOpen.signal.map { o => + if (o) "block" else "hidden" + }, + idAttr := "mobile-menu", + div( + cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) + ), + mobileProfile + ) + + nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala new file mode 100644 index 0000000..c4cb10d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala @@ -0,0 +1,19 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.Page +import com.raquo.waypoint.Router +import mdr.pdb.app.Action + +object PageHeader: + def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = + header( + cls := "bg-white shadow-sm", + div( + cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", + h1( + cls := "text-lg leading-6 font-semibold text-gray-900", + Breadcrumbs(actionBus) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala new file mode 100644 index 0000000..aae4539 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala @@ -0,0 +1,30 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.Page +import mdr.pdb.app.Action +import fiftyforms.ui.components.tailwind.Loading +import io.laminext.syntax.core.* + +object PageLayout: + case class ViewModel( + navigation: NavigationBar.ViewModel, + content: Option[HtmlElement] + ) + def apply(actionBus: Observer[Action])( + $m: Signal[ViewModel], + mods: Modifier[HtmlElement]* + )(using router: Router[Page]): HtmlElement = + val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) + div( + cls := "h-full flex flex-col", + NavigationBar($m.map(_.navigation)), + child.maybe <-- router.$currentPage.map(_.isRoot) + .switch(None, Some(PageHeader(actionBus))), + main( + cls := "flex-grow-1 overflow-y-auto", + mods, + child <-- $maybeContent.map(_.getOrElse(Loading)) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/mdr/pdb/app/components/PageLink.scala new file mode 100644 index 0000000..e6e7052 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageLink.scala @@ -0,0 +1,40 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.LinkSupport.* +import mdr.pdb.app.Page +import mdr.pdb.app.Action +import com.raquo.waypoint.Router +import mdr.pdb.app.NavigateTo + +object PageLink: + type ViewModel = Page + def apply($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = + a(mods($m, actions), child.text <-- $m.map(_.title)) + + def apply(m: ViewModel, actions: Observer[Action])(using + router: Router[Page] + ): Anchor = apply(Val(m), actions) + + def container($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = a(mods($m, actions)) + + def container(m: ViewModel, actions: Observer[Action])(using + Router[Page] + ): Anchor = container(Val(m), actions) + + private def mods($m: Signal[Page], actions: Observer[Action])(using + router: Router[Page] + ): Modifier[Anchor] = + Seq( + href <-- $m.map(router.absoluteUrlForPage).recover { case _ => + Some("invalid url") + }, + composeEvents(onClick.noKeyMod.preventDefault)( + _.sample($m) + .map(NavigateTo.apply) + ) --> actions + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala new file mode 100644 index 0000000..7682772 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala @@ -0,0 +1,13 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.pages.dashboard.DashboardPage +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage + +class DashboardPageConnector(actionBus: Observer[Action])(using + router: Router[Page] +): + def apply: HtmlElement = + AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala new file mode 100644 index 0000000..76b9133 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala @@ -0,0 +1,76 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.Parameter +import mdr.pdb.UserInfo +import mdr.pdb.app.pages.detail.DetailParametruPage +import pages.detail.DetailKriteriaPage +import pages.detail.components.DukazyKriteria +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.ParameterCriteria + +object DetailKriteriaPageConnector { + trait AppState { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + } +} + +case class DetailKriteriaPageConnector( + state: DetailKriteriaPageConnector.AppState +)( + $page: Signal[Page.DetailKriteria] +)(using Router[Page]): + val $paramChangeSignal = + $page.splitOne(p => + (p.osobniCislo.value, p.parametr.value, p.kriterium.value) + )((x, _, _) => x) + val $pageChangeSignal = + $paramChangeSignal.map( + FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) + ) + + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + val $merged = + $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => + for { + da <- d + pa <- p + pb <- pa.find(_.id == pc._2) + ka <- pb.criteria.find(_.id == pc._3) + } yield (da, pb, ka) + ) + + def apply: HtmlElement = + AppPage(state.actionBus)( + $merged.map(_.map(buildModel)) + .split(_ => ())((_, s, $s) => + DetailKriteriaPage($s)(state.actionBus.contramap { + case DukazyKriteria.Add => + NavigateTo( + Page.UpravDukazKriteria( + Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), + Page.Titled(s.parametr.id, Some(s.parametr.nazev)), + Page.Titled(s.kriterium.id, Some(s.kriterium.id)) + ) + ) + }) + ), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: Parameter, + k: ParameterCriteria + ): DetailKriteriaPage.ViewModel = + DetailKriteriaPage.ViewModel( + o.toDetailOsoby, + p.toParametr(_ => a()), + k.toKriterium(_ => a()) + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DetailPageConnector.scala new file mode 100644 index 0000000..df3a6b7 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DetailPageConnector.scala @@ -0,0 +1,57 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.UserInfo +import pages.detail.DetailPage +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.pages.detail.components.DetailOsoby +import mdr.pdb.Parameter +import mdr.pdb.app.pages.detail.components.SeznamParametru +import fiftyforms.ui.components.tailwind.Color + +object DetailPageConnector { + trait AppState { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + } +} + +case class DetailPageConnector(state: DetailPageConnector.AppState)( + $page: Signal[Page.Detail] +)(using router: Router[Page]): + val $oscChangeSignal = $page.splitOne(_.osobniCislo.value)((osc, _, _) => osc) + val $pageChangeSignal = + $oscChangeSignal.flatMap(osc => + EventStream.fromSeq(Seq(FetchUserDetails(osc), FetchParameters(osc))) + ) + // TODO: filter the value based on the current osc + // OSC change will fetch new data, but still + // - we need to be sure that what we got is really what we ought to display + // - we want to display stale data accordingly (at least with loading indicator) + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + def apply: HtmlElement = + AppPage(state.actionBus)( + $data.combineWithFn($params)(_ zip _) + .map(_.map(buildModel)) + .split(_ => ())((_, _, s) => DetailPage(s)), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: List[Parameter] + ): DetailPage.ViewModel = + DetailPage.ViewModel( + o.toDetailOsoby, + p.map( + _.toParametr(param => + PageLink.container(Page.DetailParametru(o, param), state.actionBus) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DetailParametruPageConnector.scala new file mode 100644 index 0000000..fa7193d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DetailParametruPageConnector.scala @@ -0,0 +1,67 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.Parameter +import mdr.pdb.UserInfo +import mdr.pdb.app.pages.detail.DetailParametruPage +import pages.detail.DetailParametruPage +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.components.PageLink + +object DetailParametruPageConnector { + trait AppState { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + } +} + +case class DetailParametruPageConnector( + state: DetailParametruPageConnector.AppState +)( + $page: Signal[Page.DetailParametru] +)(using router: Router[Page]): + val $paramChangeSignal = + $page.splitOne(p => (p.osobniCislo.value, p.parametr.value))((x, _, _) => x) + val $pageChangeSignal = + $paramChangeSignal.map(FetchParameter(_, _)) + + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + val $merged = + $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => + for { + da <- d + pa <- p + pb <- pa.find(_.id == pc._2) + } yield (da, pb) + ) + + def apply: HtmlElement = + AppPage(state.actionBus)( + $merged.map(_.map(buildModel)) + .split(_ => ())((_, _, s) => DetailParametruPage(s)), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: Parameter + ): DetailParametruPage.ViewModel = + DetailParametruPage.ViewModel( + o.toDetailOsoby, + p.toParametr(p => + PageLink.container(Page.DetailParametru(o, p), state.actionBus) + ), + p.criteria.map( + _.toKriterium { c => + PageLink.container( + Page.DetailKriteria(o, p, c), + state.actionBus + ) + } + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DirectoryPageConnector.scala new file mode 100644 index 0000000..ab2c045 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DirectoryPageConnector.scala @@ -0,0 +1,34 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.UserInfo +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.components.AppPage + +case class DirectoryPageConnector( + $input: EventStream[List[UserInfo]], + actionBus: Observer[Action] +)(using router: Router[Page]): + val $data = $input.startWithNone + val $actionSignal = EventStream.fromValue(FetchDirectory) + + def apply: HtmlElement = + AppPage(actionBus)( + $data.split(_ => ())((_, _, s) => + pages.directory.DirectoryPage( + s.map( + _.map( + _.toUserRow(u => + PageLink.container( + Page.Detail(Page.Titled(u.personalNumber)), + actionBus + ) + ) + ) + ) + ) + ), + $actionSignal --> actionBus + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/mdr/pdb/app/connectors/conversions.scala new file mode 100644 index 0000000..f581796 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/conversions.scala @@ -0,0 +1,68 @@ +package mdr.pdb.app.connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.UserInfo +import mdr.pdb.app.pages.detail.components.DetailOsoby +import mdr.pdb.Parameter +import mdr.pdb.app.pages.detail.components.SeznamParametru +import mdr.pdb.ParameterCriteria +import mdr.pdb.app.pages.detail.components.SeznamKriterii +import mdr.pdb.app.pages.directory.components.UserRow +import mdr.pdb.app.pages.detail.components.DetailParametru +import mdr.pdb.app.pages.detail.components.DetailKriteria +import fiftyforms.ui.components.tailwind.Color + +extension (o: UserInfo) + def toDetailOsoby: DetailOsoby.ViewModel = + DetailOsoby.ViewModel( + o.personalNumber, + o.name, + o.email, + o.phone, + o.img, + o.mainFunction.map(f => + DetailOsoby.Funkce.ViewModel(f.name, f.dept, f.ou) + ), + o.userContracts.headOption.map(c => + DetailOsoby.PracovniPomer.ViewModel(c.rel, c.startDate, c.endDate) + ) + ) + +extension (param: Parameter) + def toParametr(container: Parameter => Anchor): DetailParametru.ViewModel = + DetailParametru.ViewModel( + id = param.id, + nazev = param.name, + popis = param.description, + status = "Nesplněno", + statusColor = Color.red, + a = container(param) + ) + +extension (crit: ParameterCriteria) + def toKriterium( + container: ParameterCriteria => Anchor + ): DetailKriteria.ViewModel = + DetailKriteria.ViewModel( + nazev = crit.criteriumText, + kapitola = crit.chapterId, + bod = crit.itemId, + status = "Nesplněno", + statusColor = Color.red, + splneno = false, + dukazy = Nil, + container = container(crit) + ) + +extension (user: UserInfo) + def toUserRow( + container: UserInfo => HtmlElement = (_: UserInfo) => div() + ): UserRow.ViewModel = + UserRow.ViewModel( + osobniCislo = user.personalNumber.toString, + celeJmeno = user.name, + prijmeni = user.surname, + hlavniFunkce = user.mainFunction.map(_.name), + img = user.img, + container = () => container(user) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/dashboard/DashboardPage.scala b/app/src/main/scala/mdr/pdb/app/pages/dashboard/DashboardPage.scala new file mode 100644 index 0000000..0eb8f7a --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/dashboard/DashboardPage.scala @@ -0,0 +1,11 @@ +package mdr.pdb.app.pages.dashboard + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.Page + +object DashboardPage: + + def render: HtmlElement = + div("Dashboard page") diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala new file mode 100644 index 0000000..ff3a25f --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala @@ -0,0 +1,31 @@ +package mdr.pdb.app.pages.detail + +import com.raquo.laminar.api.L.{*, given} + +import components._ + +object DetailKriteriaPage: + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametr: DetailParametru.ViewModel, + kriterium: DetailKriteria.ViewModel + ) + + def apply($m: Signal[ViewModel])( + action: Observer[DukazyKriteria.Action] + ): HtmlElement = + div( + cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + div( + DetailOsoby.header($m.map(_.osoba)), + DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") + ), + div( + DetailKriteria($m.map(_.kriterium)), + DukazyKriteria($m.map(_.kriterium.dukazy))(action) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/DetailPage.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailPage.scala new file mode 100644 index 0000000..b591cc9 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailPage.scala @@ -0,0 +1,25 @@ +package mdr.pdb.app.pages.detail + +import com.raquo.laminar.api.L.{*, given} + +import components._ +import mdr.pdb.app.Page +import com.raquo.waypoint.Router +import mdr.pdb.app.Action + +object DetailPage: + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametry: SeznamParametru.ViewModel + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + DetailOsoby($m.map(_.osoba)), + SeznamParametru($m.map(_.parametry)) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/DetailParametruPage.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailParametruPage.scala new file mode 100644 index 0000000..3322279 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailParametruPage.scala @@ -0,0 +1,24 @@ +package mdr.pdb.app.pages.detail + +import com.raquo.laminar.api.L.{*, given} + +import components._ + +object DetailParametruPage: + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametr: DetailParametru.ViewModel, + kriteria: SeznamKriterii.ViewModel + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + DetailOsoby.header($m.map(_.osoba)), + DetailParametru($m.map(_.parametr)), + SeznamKriterii($m.map(_.kriteria)) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala new file mode 100644 index 0000000..b58618a --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala @@ -0,0 +1,107 @@ +package mdr.pdb +package app +package pages.detail + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.pages.detail.components.UpravDukazForm +import fiftyforms.services.files.File + +object UpravDukaz: + + type ThisPage = Page.UpravDukazKriteria + type PageKey = (OsobniCislo, String, String) + + trait State { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def availableFiles: EventStream[List[File]] + def actionBus: Observer[Action] + } + + def keyOfPage(page: ThisPage): PageKey = + (page.osobniCislo.value, page.parametr.value, page.kriterium.value) + + def onChangeAction(key: PageKey): Action = + FetchParameterCriteria( + key._1, + key._2, + key._3, + Page.UpravDukazKriteria(_, _, _) + ) + + class Connector(state: State)($page: Signal[ThisPage])(using Router[Page]): + val $paramChangeSignal = $page.splitOne(keyOfPage)((x, _, _) => x) + val $pageChangeSignal = $paramChangeSignal.map(onChangeAction) + + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + val $merged = + $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => + for { + da <- d + pa <- p + pb <- pa.find(_.id == pc._2) + ka <- pb.criteria.find(_.id == pc._3) + } yield (da, pb, ka) + ) + + def apply: HtmlElement = + AppPage(state.actionBus)( + $merged.split(_ => ())((_, s, $s) => + PageComponent( + $s.map(buildModel), + state.availableFiles, + state.actionBus.contramap { + case UpravDukazForm.Cancelled => + NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) + case UpravDukazForm.AvailableFilesRequested => + FetchAvailableFiles(s._1.personalNumber) + } + ) + ), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: Parameter, + k: ParameterCriteria + ): PageComponent.ViewModel = + import connectors.* + PageComponent.ViewModel( + o.toDetailOsoby, + p.toParametr(_ => a()), + k.toKriterium(_ => a()) + ) + + object PageComponent: + import components.* + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametr: DetailParametru.ViewModel, + kriterium: DetailKriteria.ViewModel + ) + + def apply( + $m: Signal[ViewModel], + availableFilesStream: EventStream[List[File]], + events: Observer[UpravDukazForm.Event] + ): HtmlElement = + div( + cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + div( + DetailOsoby.header($m.map(_.osoba)), + DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") + ), + div( + DetailKriteria($m.map(_.kriterium)), + UpravDukazForm(availableFilesStream)(events) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailKriteria.scala new file mode 100644 index 0000000..a3ab634 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailKriteria.scala @@ -0,0 +1,37 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.CustomAttrs +import fiftyforms.ui.components.tailwind.Icons +import java.time.LocalDate +import fiftyforms.ui.components.tailwind.Color + +object DetailKriteria: + case class ViewModel( + nazev: String, + kapitola: String, + bod: String, + status: String, + statusColor: Color, + splneno: Boolean, + dukazy: List[DukazKriteria.ViewModel], + container: HtmlElement = div() + ) { + val id = s"${kapitola}${bod}" + } + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "pb-5 border-b border-gray-200", + h3( + cls := "text-xl leading-6 font-bold text-gray-900", + child.text <-- $m.map(_.nazev) + ), + h3( + cls := "mt-2 max-w-4xl text-sm text-gray-500", + "Kapitola ", + child.text <-- $m.map(_.kapitola), + " bod ", + child <-- $m.map(_.bod) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailOsoby.scala new file mode 100644 index 0000000..0fe2ba5 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailOsoby.scala @@ -0,0 +1,100 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.OsobniCislo + +import java.time.LocalDate +import fiftyforms.ui.components.tailwind.Avatar + +object DetailOsoby: + + object Funkce: + case class ViewModel( + nazev: String, + stredisko: String, + voj: String + ) + + def render($m: Signal[ViewModel]): HtmlElement = + p( + cls := "text-sm font-medium text-gray-500", + child.text <-- $m.map(_.nazev), + span( + cls := "hidden md:inline", + " @ ", + child.text <-- $m.map(_.stredisko), + ", ", + child.text <-- $m.map(_.voj) + ) + ) + + object PracovniPomer: + case class ViewModel( + druh: String, + pocatek: LocalDate, + konec: Option[LocalDate] + ) + + def render($m: Signal[ViewModel]): HtmlElement = + import fiftyforms.ui.components.tailwind.CustomAttrs.datetime + p( + cls := "text-sm font-medium text-gray-500", + child.text <-- $m.map(_.druh), + " od ", + time( + datetime <-- $m.map(_.pocatek.toString), + child.text <-- $m.map(_.pocatek.toString) + ) + ) + + case class ViewModel( + osobniCislo: OsobniCislo, + jmeno: String, + email: Option[String], + telefon: Option[String], + img: Option[String], + hlavniFunkce: Option[Funkce.ViewModel], + pracovniPomer: Option[PracovniPomer.ViewModel] + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "md:flex md:items-center md:justify-between md:space-x-4", + div( + cls := "flex items-start space-x-4", + div( + cls := "flex-shrink-0", + Avatar($m.map(_.img)).avatar(16) + ), + div( + h1( + cls := "text-2xl font-bold text-gray-900", + child.text <-- $m.map(_.jmeno) + ), + child.maybe <-- $m.map(_.hlavniFunkce).split(_ => ())((_, _, d) => + Funkce.render(d) + ), + child.maybe <-- $m.map(_.pracovniPomer).split(_ => ())((_, _, d) => + PracovniPomer.render(d) + ) + ) + ) + ) + + def header($m: Signal[ViewModel]): HtmlElement = + div( + cls := "md:flex md:items-center md:justify-between md:space-x-5", + div( + cls := "flex items-start space-x-4", + div( + cls := "flex-shrink-0", + Avatar($m.map(_.img)).avatar(8) + ), + div( + h1( + cls := "text-lg font-medium text-gray-800", + child.text <-- $m.map(_.jmeno) + ) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailParametru.scala new file mode 100644 index 0000000..6310dde --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailParametru.scala @@ -0,0 +1,34 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.Color + +object DetailParametru: + case class ViewModel( + id: String, + nazev: String, + popis: String, + status: String, + statusColor: Color, + a: Anchor + ) + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "pb-5 border-b border-gray-200", + h2( + cls := "text-2xl leading-6 font-medium text-gray-900", + child.text <-- $m.map(_.nazev) + ), + p( + cls := "mt-2 max-w-4xl text-sm text-gray-500", + child.text <-- $m.map(_.popis) + ) + ) + + def header($m: Signal[ViewModel]): HtmlElement = + div( + h2( + cls := "text-lg leading-6 font-medium text-gray-600", + child.text <-- $m.map(_.nazev) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazKriteria.scala new file mode 100644 index 0000000..c292919 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazKriteria.scala @@ -0,0 +1,177 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import java.time.LocalDate +import fiftyforms.ui.components.tailwind.CustomAttrs + +object DukazKriteria: + case class Osoba(osobniCislo: String, jmeno: String) + case class Udalost(osoba: Osoba, datum: LocalDate) + case class Dokument( + url: String, + nazev: String, + pridal: Udalost, + odebral: Option[Udalost] + ) + case class ViewModel( + dokumenty: List[Dokument], + autorizoval: Option[Udalost], + platiDo: Option[LocalDate], + poznámka: Option[String] + ) + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "bg-white shadow overflow-hidden sm:rounded-lg", + div( + cls := "px-4 py-5 sm:px-6", + h3( + cls := "text-lg leading-6 font-medium text-gray-900", + """Applicant Information""" + ), + p( + cls := "mt-1 max-w-2xl text-sm text-gray-500", + """Personal details and application.""" + ) + ), + div( + cls := "border-t border-gray-200 px-4 py-5 sm:px-6", + dl( + cls := "grid grid-cols-1 gap-x-4 gap-y-8 sm:grid-cols-2", + div( + cls := "sm:col-span-1", + dt( + cls := "text-sm font-medium text-gray-500", + """Full name""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """Margot Foster""" + ) + ), + div( + cls := "sm:col-span-1", + dt( + cls := "text-sm font-medium text-gray-500", + """Application for""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """Backend Developer""" + ) + ), + div( + cls := "sm:col-span-1", + dt( + cls := "text-sm font-medium text-gray-500", + """Email address""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """margotfoster@example.com""" + ) + ), + div( + cls := "sm:col-span-1", + dt( + cls := "text-sm font-medium text-gray-500", + """Salary expectation""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """$120,000""" + ) + ), + div( + cls := "sm:col-span-2", + dt( + cls := "text-sm font-medium text-gray-500", + """About""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """Fugiat ipsum ipsum deserunt culpa aute sint do nostrud anim incididunt cillum culpa consequat. Excepteur qui ipsum aliquip consequat sint. Sit id mollit nulla mollit nostrud in ea officia proident. Irure nostrud pariatur mollit ad adipisicing reprehenderit deserunt qui eu.""" + ) + ), + div( + cls := "sm:col-span-2", + dt( + cls := "text-sm font-medium text-gray-500", + """Attachments""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + ul( + role := "list", + cls := "border border-gray-200 rounded-md divide-y divide-gray-200", + li( + cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", + div( + cls := "w-0 flex-1 flex items-center", { + import svg.* + import CustomAttrs.svg.ariaHidden + svg( + cls := "flex-shrink-0 h-5 w-5 text-gray-400", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + ariaHidden := true, + path( + fillRule := "evenodd", + d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", + clipRule := "evenodd" + ) + ) + }, + span( + cls := "ml-2 flex-1 w-0 truncate", + """resume_back_end_developer.pdf""" + ) + ), + div( + cls := "ml-4 flex-shrink-0", + a( + href := "#", + cls := "font-medium text-indigo-600 hover:text-indigo-500", + """Download""" + ) + ) + ), + li( + cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", + div( + cls := "w-0 flex-1 flex items-center", { + import svg.* + import CustomAttrs.svg.ariaHidden + svg( + cls := "flex-shrink-0 h-5 w-5 text-gray-400", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + ariaHidden := true, + path( + fillRule := "evenodd", + d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", + clipRule := "evenodd" + ) + ) + }, + span( + cls := "ml-2 flex-1 w-0 truncate", + """coverletter_back_end_developer.pdf""" + ) + ), + div( + cls := "ml-4 flex-shrink-0", + a( + href := "#", + cls := "font-medium text-indigo-600 hover:text-indigo-500", + """Download""" + ) + ) + ) + ) + ) + ) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala new file mode 100644 index 0000000..d604860 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala @@ -0,0 +1,35 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.Icons + +object DukazyKriteria: + sealed trait Action + case object Add extends Action + + type ViewModel = List[DukazKriteria.ViewModel] + + def apply($m: Signal[ViewModel])(actions: Observer[Action]): HtmlElement = + div( + child.maybe <-- $m.map(m => + if (m.isEmpty) then Some(prazdnyDukaz(actions)) + else None + ), + children <-- $m.map(_.zipWithIndex).split(_._2)((_, _, $s) => + DukazKriteria($s.map(_._1)) + ) + ) + + private def prazdnyDukaz(actions: Observer[Action]): HtmlElement = + button( + tpe := "button", + onClick.preventDefault.mapTo(Add) --> actions, + cls := "relative block w-full border-2 border-gray-300 border-dashed rounded-lg p-12 text-center hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + Icons.outline + .`document-add`(12) + .amend(svg.cls := "mx-auto text-gray-400"), + span( + cls := "mt-2 block text-sm font-medium text-gray-900", + "Přidat důkaz" + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala new file mode 100644 index 0000000..7e77a7b --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala @@ -0,0 +1,34 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.Color +import fiftyforms.ui.components.tailwind.list.StackedList +import fiftyforms.ui.components.tailwind.list.ListRow +import fiftyforms.ui.components.tailwind.list.RowTag +import fiftyforms.ui.components.tailwind.list.RowNext +import java.time.LocalDate + +object SeznamKriterii: + type ViewModel = List[DetailKriteria.ViewModel] + + private val kritList = new StackedList[DetailKriteria.ViewModel] + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "bg-white shadow overflow-hidden sm:rounded-md", + kritList($m, _.id) { $i => + $i.map { i => + ListRow.ViewModel( + title = i.nazev, + topRight = RowTag.render( + $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) + ), + bottomLeft = + p(cls := "text-sm text-gray-500", s"${i.kapitola}${i.bod}"), + bottomRight = emptyNode, + farRight = RowNext.render, + containerElement = i.container + ) + } + } + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamParametru.scala new file mode 100644 index 0000000..0760b32 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamParametru.scala @@ -0,0 +1,37 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.list.{ + StackedList, + ListRow, + RowTag, + PropList, + IconText, + RowNext +} +import fiftyforms.ui.components.tailwind.Color +import fiftyforms.ui.components.tailwind.LinkSupport.* + +object SeznamParametru: + type ViewModel = List[DetailParametru.ViewModel] + + private val parametrList = new StackedList[DetailParametru.ViewModel] + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "bg-white shadow overflow-hidden sm:rounded-md", + parametrList($m, _.id) { $i => + $i.map { i => + ListRow.ViewModel( + title = i.nazev, + topRight = RowTag.render( + $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) + ), + bottomLeft = emptyNode, + bottomRight = emptyNode, + farRight = RowNext.render, + containerElement = i.a + ) + } + } + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala new file mode 100644 index 0000000..28753c9 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala @@ -0,0 +1,101 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import io.laminext.syntax.core.* +import fiftyforms.ui.components.tailwind.CustomAttrs +import fiftyforms.ui.components.tailwind.form.* +import org.scalajs.dom +import com.raquo.laminar.nodes.ReactiveHtmlElement +import fiftyforms.services.files.components.tailwind.FilePicker +import mdr.pdb.frontend.AutorizujDukaz +import mdr.pdb.frontend.DocumentRef +import fiftyforms.services.files.File + +object UpravDukazForm: + sealed trait Event + case object Cancelled extends Event + case object AvailableFilesRequested extends Event + def apply(availableFilesStream: EventStream[List[File]])( + updates: Observer[Event] + ): HtmlElement = + val (filesStream, filesObserver) = + EventStream.withObserver[FilePicker.Event] + val files = Var[List[File]](Nil) + def submitButtons: HtmlElement = + div( + cls := "pt-5", + div( + cls := "flex justify-end", + button( + tpe := "button", + cls := "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + "Zrušit", + onClick.mapTo(Cancelled) --> updates + ), + button( + tpe := "submit", + cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + disabled <-- files.signal.map(_.isEmpty), + "Autorizovat" + ) + ) + ) + + div( + filesStream.collect { case FilePicker.SelectionUpdated(files) => + files.to(List) + } --> files.writer, + filesStream.collect { case FilePicker.AvailableFilesRequested => + AvailableFilesRequested + } --> updates, + cls := "bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6", + Form( + Form.Body( + Form.Section( + FormHeader( + FormHeader.ViewModel( + "Doložení kritéria", + "Sestavte doklady poskytující důkaz kritéria a potvrďte odesláním formuláře. Případné limitace či jiné relevantní údaje vepište do pole Komentář." + ) + ), + FormFields( + FormRow( + "dokumenty", + "Dokumenty", + FilePicker(files.signal, availableFilesStream)(filesObserver) + .amend(idAttr := "dokumenty", cls("max-w-lg")) + ).toHtml, + FormRow( + "platnost", + "Platnost", + input( + idAttr := "platnost", + name := "platnost", + tpe := "date", + autoComplete := "date", + cls := "block max-w-lg w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" + ) + ).toHtml, + FormRow( + "komentar", + "Komentář", + div( + cls := "mt-1 sm:mt-0 sm:col-span-2", + textArea( + idAttr := "komentar", + name := "about", + rows := 3, + cls := "max-w-lg shadow-sm block w-full focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border border-gray-300 rounded-md" + ), + p( + cls := "mt-2 text-sm text-gray-500", + "Doplňte prosím potřebné informace související s doložením kritéria, včetně případných limitací." + ) + ) + ).toHtml + ) + ) + ), + submitButtons + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/directory/DirectoryPage.scala b/app/src/main/scala/mdr/pdb/app/pages/directory/DirectoryPage.scala new file mode 100644 index 0000000..7a72ca3 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/directory/DirectoryPage.scala @@ -0,0 +1,40 @@ +package mdr.pdb.app.pages.directory + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.UserInfo + +import components._ + +object DirectoryPage: + + type ViewModel = List[UserRow.ViewModel] + + def apply($m: Signal[ViewModel]): HtmlElement = + val (actionsStream, actionObserver) = + EventStream.withObserver[SearchForm.Action] + val $filter = actionsStream + .collect { + case SearchForm.ClearFilter => None + case SearchForm.SetFilter(t) => Some(t) + } + .startWith(None) + val byLetter = for { + d <- $m + f <- $filter + } yield for { + (letter, users) <- d + .filter { user => + f match + case None => true + case Some(t) => user.search.contains(t) + } + .groupBy(_.prijmeni.head) + .to(List) + .sortBy(_._1) + } yield (letter.toString, users.sortBy(_.prijmeni)) + + div( + cls := "h-full max-w-7xl mx-auto order-first flex flex-col flex-shrink-0", + SearchForm(actionObserver), + Directory(byLetter) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/directory/components/Directory.scala b/app/src/main/scala/mdr/pdb/app/pages/directory/components/Directory.scala new file mode 100644 index 0000000..f7d6f99 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/directory/components/Directory.scala @@ -0,0 +1,37 @@ +package mdr.pdb.app.pages.directory.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.UserInfo + +object Directory: + + object Header: + type ViewModel = String + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500", + h3(child.text <-- $m) + ) + + object UserList: + type ViewModel = List[UserRow.ViewModel] + def apply($m: Signal[ViewModel]): HtmlElement = + ul( + role := "list", + cls := "relative z-0 divide-y divide-gray-200", + children <-- $m.split(_.osobniCislo)((_, _, s) => UserRow(s)) + ) + + type ViewModel = List[(String, List[UserRow.ViewModel])] + def apply($m: Signal[ViewModel]): HtmlElement = + nav( + cls := "flex-1 min-h-0 overflow-y-auto", + aria.label := "Directory", + children <-- $m.split(_._1)((_, _, s) => + div( + cls := "relative", + Header(s.map(_._1)), + UserList(s.map(_._2)) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/directory/components/SearchForm.scala b/app/src/main/scala/mdr/pdb/app/pages/directory/components/SearchForm.scala new file mode 100644 index 0000000..fb9ecc4 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/directory/components/SearchForm.scala @@ -0,0 +1,54 @@ +package mdr.pdb.app.pages.directory.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.Icons + +object SearchForm: + sealed trait Action + case object SubmitFilter extends Action + sealed trait FilterAction extends Action + case object ClearFilter extends FilterAction + case class SetFilter(value: String) extends FilterAction + + def apply(actions: Observer[Action]): HtmlElement = + div( + cls := "px-6 pt-4 pb-4", + form( + cls := "flex space-x-4", + onSubmit.mapTo(SubmitFilter) --> actions, + div( + cls := "flex-1 min-w-0", + label( + forId := "search", + cls := "sr-only", + "Hledat" + ), + div( + cls := "relative rounded-md shadow-sm", + div( + cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", + Icons.solid.search().amend(svg.cls := "text-gray-400") + ), + input( + tpe := "search", + name := "search", + idAttr := "search", + cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", + placeholder := "Hledat", + composeEvents(onInput.mapToValue.setAsValue.map(SetFilter(_)))( + _.throttle(500) + ) --> actions + ) + ) + ), + button( + tpe := "submit", + cls := "inline-flex justify-center px-3.5 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500", + Icons.solid.filter().amend(svg.cls := "text-gray-400"), + span( + cls := "sr-only", + "Hledat" + ) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/directory/components/UserRow.scala b/app/src/main/scala/mdr/pdb/app/pages/directory/components/UserRow.scala new file mode 100644 index 0000000..4aa1672 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/directory/components/UserRow.scala @@ -0,0 +1,52 @@ +package mdr.pdb.app.pages.directory.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.airstream.core.Signal +import fiftyforms.ui.components.tailwind.Avatar + +object UserRow: + case class ViewModel( + osobniCislo: String, + celeJmeno: String, + prijmeni: String, + hlavniFunkce: Option[String], + img: Option[String], + container: () => HtmlElement = () => div() + ) { + val search = prijmeni.toLowerCase + } + + def apply($m: Signal[ViewModel]): HtmlElement = + inline def avatarImage = + Avatar($m.map(_.img)).avatarImage(10) + + li( + div( + cls := "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500", + div( + cls := "flex-shrink-0", + child <-- avatarImage + ), + div( + cls := "flex-1 min-w-0", + child <-- $m.map { o => + o.container() + .amend( + cls := "focus:outline-none", + span( + cls := "absolute inset-0", + aria.hidden := true + ), + p( + cls := "text-sm font-medium text-gray-900", + o.celeJmeno + ), + p( + cls := "text-sm text-gray-500 truncate", + o.hlavniFunkce + ) + ) + } + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/errors/ErrorPage.scala b/app/src/main/scala/mdr/pdb/app/pages/errors/ErrorPage.scala new file mode 100644 index 0000000..6b195ef --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/errors/ErrorPage.scala @@ -0,0 +1,99 @@ +package mdr.pdb.app.pages.errors + +import com.raquo.domtypes.generic.codecs.StringAsIsCodec +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.Page +import mdr.pdb.app.Action +import com.raquo.waypoint.Router + +object ErrorPage: + case class ViewModel( + homePage: Page, + errorName: String, + title: String, + subTitle: String + ) + + def apply(m: ViewModel, actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + val ViewModel(homePage, errorName, title, subTitle) = m + div( + cls := "min-h-full pt-16 pb-12 flex flex-col bg-white", + main( + cls := "flex-grow flex flex-col justify-center max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", + div( + cls := "flex-shrink-0 flex justify-center", + a( + href := "/", + cls := "inline-flex", + span( + cls := "sr-only", + """Workflow""" + ), + img( + cls := "h-12 w-auto", + src := "https://tailwindui.com/img/logos/workflow-mark.svg?color=indigo&shade=600", + alt := "" + ) + ) + ), + div( + cls := "py-16", + div( + cls := "text-center", + p( + cls := "text-sm font-semibold text-indigo-600 uppercase tracking-wide", + errorName + ), + h1( + cls := "mt-2 text-4xl font-extrabold text-gray-900 tracking-tight sm:text-5xl", + title + ), + p( + cls := "mt-2 text-base text-gray-500", + subTitle + ), + div( + cls := "mt-6", + PageLink + .container(homePage, actionBus) + .amend( + cls := "text-base font-medium text-indigo-600 hover:text-indigo-500", + """Go back home""" + ) + ) + ) + ) + ), + footer( + cls := "flex-shrink-0 max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", + nav( + cls := "flex justify-center space-x-4", + a( + href := "#", + cls := "text-sm font-medium text-gray-500 hover:text-gray-600", + """Contact Support""" + ), + span( + cls := "inline-block border-l border-gray-300", + aria.hidden := true + ), + a( + href := "#", + cls := "text-sm font-medium text-gray-500 hover:text-gray-600", + """Status""" + ), + span( + cls := "inline-block border-l border-gray-300", + aria.hidden := true + ), + a( + href := "#", + cls := "text-sm font-medium text-gray-500 hover:text-gray-600", + """Twitter""" + ) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/errors/NotFoundPage.scala b/app/src/main/scala/mdr/pdb/app/pages/errors/NotFoundPage.scala new file mode 100644 index 0000000..97760e1 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/errors/NotFoundPage.scala @@ -0,0 +1,21 @@ +package mdr.pdb.app.pages.errors + +import com.raquo.domtypes.generic.codecs.StringAsIsCodec +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.Page +import mdr.pdb.app.Action + +object NotFoundPage: + def apply(homePage: Page, url: String, actionBus: Observer[Action])(using + router: Router[Page] + ): HtmlElement = + ErrorPage( + ErrorPage.ViewModel( + homePage, + "404 error", + "Page not found.", + s"Sorry, but page $url doesn't exist." + ), + actionBus + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala b/app/src/main/scala/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala new file mode 100644 index 0000000..77b0af8 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala @@ -0,0 +1,32 @@ +package mdr.pdb.app.pages.errors + +import com.raquo.domtypes.generic.codecs.StringAsIsCodec +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.Page +import mdr.pdb.app.Action + +object UnhandledErrorPage: + + case class ViewModel( + homePage: Page, + errorName: Option[String], + errorMessage: Option[String] + ) + + def apply(m: ViewModel, actionBus: Observer[Action])(using + router: Router[Page] + ): HtmlElement = + ErrorPage( + ErrorPage.ViewModel( + m.homePage, + "Unexpected error occurred", + m.errorName.getOrElse( + "Uh oh!" + ), // TODO: translations, better text than uh oh + m.errorMessage.getOrElse( + "This wasn't supposed to happen! Please try again." + ) + ), + actionBus + ) diff --git a/app/src/main/scala/mdr/pdb/app/services/DataFetcher.scala b/app/src/main/scala/mdr/pdb/app/services/DataFetcher.scala new file mode 100644 index 0000000..134c221 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/services/DataFetcher.scala @@ -0,0 +1,6 @@ +package mdr.pdb.app.services + +import com.raquo.airstream.core.Observer + +trait DataFetcher[K, A]: + def fetch(id: K, sink: Observer[A]): Unit diff --git a/app/src/main/scala/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/mdr/pdb/app/state/AppState.scala new file mode 100644 index 0000000..e44b2df --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/state/AppState.scala @@ -0,0 +1,120 @@ +package mdr.pdb.app +package state + +import com.raquo.airstream.core.EventStream +import mdr.pdb.{UserInfo, OsobniCislo} +import com.raquo.airstream.core.Observer +import scala.scalajs.js +import scala.scalajs.js.JSON +import zio.json._ +import com.raquo.airstream.eventbus.EventBus +import com.raquo.airstream.ownership.Owner +import com.raquo.waypoint.Router +import mdr.pdb.Parameter +import mdr.pdb.ParameterCriteria +import mdr.pdb.UserFunction +import mdr.pdb.UserContract +import fiftyforms.services.files.File + +trait AppState + extends connectors.DetailPageConnector.AppState + with connectors.DetailParametruPageConnector.AppState + with connectors.DetailKriteriaPageConnector.AppState + with pages.detail.UpravDukaz.State: + def users: EventStream[List[UserInfo]] + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + +class MockAppState(implicit owner: Owner, router: Router[Page]) + extends AppState: + + given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) + given JsonDecoder[UserFunction] = DeriveJsonDecoder.gen + given JsonDecoder[UserContract] = DeriveJsonDecoder.gen + given JsonDecoder[UserInfo] = DeriveJsonDecoder.gen + + given JsonDecoder[ParameterCriteria] = DeriveJsonDecoder.gen + given JsonDecoder[Parameter] = DeriveJsonDecoder.gen + + private val actions = EventBus[Action]() + private val (parametersStream, pushParameters) = + EventStream.withCallback[List[Parameter]] + private val (usersStream, pushUsers) = + EventStream.withCallback[List[UserInfo]] + private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] + private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] + + private val mockData: List[UserInfo] = + mockUsers + .asInstanceOf[js.Dictionary[js.Object]] + .values + // TODO: is there a more efficient way to parse from JS object directly? + .map { o => + val parsed = JSON.stringify(o).fromJson[UserInfo] + parsed.left.foreach(org.scalajs.dom.console.log(_)) + parsed + } + .collect { case Right(u) => + u + } + .toList + + private val mockParameters: List[Parameter] = + pdbParams + .asInstanceOf[js.Dictionary[js.Object]] + .values + .map(o => JSON.stringify(o).fromJson[Parameter]) + .collect { case Right(p) => p } + .toList + + // TODO: Extract to separate event handler + actions.events.foreach { + case FetchDirectory => pushUsers(mockData) + case FetchUserDetails(osc) => + mockData.find(_.personalNumber == osc).foreach { o => + pushDetails(o) + router.replaceState(Page.Detail(o)) + } + case FetchParameters(osc) => + pushParameters(mockParameters) + case FetchParameter(osc, paramId) => + for + o <- mockData.find(_.personalNumber == osc) + p <- mockParameters.find(_.id == paramId) + do + pushDetails(o) + pushParameters(mockParameters) + router.replaceState(Page.DetailParametru(o, p)) + case FetchParameterCriteria(osc, paramId, critId, page) => + for + o <- mockData.find(_.personalNumber == osc) + p <- mockParameters.find(_.id == paramId) + c <- p.criteria.find(_.id == critId) + do + pushDetails(o) + pushParameters(mockParameters) + router.replaceState(page(o, p, c)) + case NavigateTo(page) => router.pushState(page) + case FetchAvailableFiles(osc) => + pushFiles( + List( + File("https://tc163.cmi.cz/here", "Example file") + ) + ) + } + + override def users: EventStream[List[UserInfo]] = + usersStream.debugWithName("users") + + override def details: EventStream[UserInfo] = + detailsStream.debugWithName("details") + + override def parameters: EventStream[List[Parameter]] = + parametersStream.debugWithName("parameters") + + override def availableFiles: EventStream[List[File]] = + filesStream.debugWithName("available files") + + override def actionBus: Observer[Action] = + actions.writer.debugWithName("actions writer") diff --git a/build.sbt b/build.sbt index 2afc879..0daf796 100644 --- a/build.sbt +++ b/build.sbt @@ -11,6 +11,12 @@ lazy val core = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Pure) .in(file("core")) + .settings( + IWDeps.useZIO(Test), + IWDeps.tapirCore, + IWDeps.tapirZIO, + IWDeps.tapirZIOJson + ) lazy val ui = (project in file("ui")) .enablePlugins(ScalaJSPlugin) diff --git a/core/src/main/scala/cz/e_bs/cmi/mdr/pdb/Parameter.scala b/core/src/main/scala/cz/e_bs/cmi/mdr/pdb/Parameter.scala deleted file mode 100644 index 9d68240..0000000 --- a/core/src/main/scala/cz/e_bs/cmi/mdr/pdb/Parameter.scala +++ /dev/null @@ -1,27 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb - -import java.math.BigInteger -import java.security.MessageDigest - -object ParameterCriteria { - type Id = String -} - -case class ParameterCriteria( - chapterId: String, - itemId: String, - criteriumText: String -) { - val id: ParameterCriteria.Id = s"${chapterId}${itemId}" -} - -object Parameter { - type Id = String -} - -case class Parameter( - id: Parameter.Id, - name: String, - description: String, - criteria: List[ParameterCriteria] -) diff --git a/core/src/main/scala/cz/e_bs/cmi/mdr/pdb/UserProfile.scala b/core/src/main/scala/cz/e_bs/cmi/mdr/pdb/UserProfile.scala deleted file mode 100644 index 455a445..0000000 --- a/core/src/main/scala/cz/e_bs/cmi/mdr/pdb/UserProfile.scala +++ /dev/null @@ -1,44 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb - -import java.time.LocalDate - -opaque type OsobniCislo = String - -object OsobniCislo: - // TODO: validation - def apply(osc: String): OsobniCislo = osc - -extension (osc: OsobniCislo) def toString: String = osc - -case class UserContract( - rel: String, - startDate: LocalDate, - endDate: Option[LocalDate] -) -case class UserFunction(name: String, dept: String, ou: String) - -case class UserInfo( - personalNumber: OsobniCislo, - username: String, - givenName: String, - surname: String, - titlesBeforeName: Option[String] = None, - titlesAfterName: Option[String] = None, - email: Option[String] = None, - phone: Option[String] = None, - mainFunction: Option[UserFunction] = None, - userContracts: List[UserContract] = Nil, - img: Option[String] = None -) { - val name = - List( - Some( - List(titlesBeforeName, Some(givenName), Some(surname)).flatten.mkString( - " " - ) - ), - titlesAfterName - ).flatten.mkString(", ") -} - -case class UserProfile(username: String, userInfo: UserInfo) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala deleted file mode 100644 index a3b00af..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala +++ /dev/null @@ -1,103 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import scala.scalajs.js.annotation.JSExportTopLevel -import scala.scalajs.js.annotation.JSExport -import scala.scalajs.js.annotation.JSImport -import scala.scalajs.js -import org.scalajs.dom -import com.raquo.laminar.api.L.{*, given} -import scala.scalajs.js.Date -import com.raquo.waypoint.Router -import com.raquo.waypoint.SplitRender -import cz.e_bs.cmi.mdr.pdb.app.services.DataFetcher -import scala.scalajs.js.JSON -import zio.json._ -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.state.AppState - -@js.native -@JSImport("stylesheets/main.css", JSImport.Namespace) -object Css extends js.Any - -@js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native -@JSImport("params/pdb-params.json", JSImport.Default) -object pdbParams extends js.Object - -@JSExportTopLevel("app") -object Main: - - @JSExport - def main(args: Array[String]): Unit = - import Routes.given - onLoad { - setupAirstream() - val appContainer = dom.document.querySelector("#app") - val _ = - render( - appContainer, - renderPage(state.MockAppState(using unsafeWindowOwner, router)) - ) - } - - private def onLoad(f: => Unit): Unit = - documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) - - private def setupAirstream()(using router: Router[Page]): Unit = - AirstreamError.registerUnhandledErrorCallback(err => - router.forcePage( - Page.UnhandledError( - Some(err.getClass.getName), // TODO: Fill only in dev mode - Some(err.getMessage) - ) - ) - ) - - def renderPage(state: AppState)(using - router: Router[Page] - ): HtmlElement = - val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) - .collectSignal[Page.Detail]( - connectors - .DetailPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailParametru]( - connectors - .DetailParametruPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailKriteria]( - connectors - .DetailKriteriaPageConnector(state)(_) - .apply - ) - .collectSignal[Page.UpravDukazKriteria]( - pages.detail.UpravDukaz.Connector(state)(_).apply - ) - .collectStatic(Page.Dashboard)( - connectors.DashboardPageConnector(state.actionBus).apply - ) - .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) - ) - .collect[Page.UnhandledError](pg => - pages.errors - .UnhandledErrorPage( - pages.errors.UnhandledErrorPage - .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), - state.actionBus - ) - ) - .collectStatic(Page.Directory)( - connectors - .DirectoryPageConnector(state.users, state.actionBus) - .apply - ) - div(cls := "h-full", child <-- pageSplitter.$view) - - // Pull in the stylesheet - val css: Css.type = Css diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala deleted file mode 100644 index 3bbc8c0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ /dev/null @@ -1,198 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.* -import org.scalajs.dom -import zio.json.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import scala.scalajs.js -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.Page.Titled - -// enum is not working with Waypoints' SplitRender collectStatic -sealed abstract class Page( - val id: String, - val title: String, - val parent: Option[Page] -) { - val path: Vector[Page] = - parent match - case None => Vector(this) - case Some(p) => p.path :+ this - - val isRoot: Boolean = parent.isEmpty -} - -object Page: - - case class Titled[V](value: V, title: Option[String] = None): - val show: String = title.getOrElse(value.toString) - - case object Directory extends Page("directory", "Adresář", None) - - case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) - - case class Detail(osobniCislo: Titled[OsobniCislo]) - extends Page("user", osobniCislo.show, Some(Directory)) - - object Detail { - def apply(o: UserInfo): Detail = Detail( - Titled(o.personalNumber, Some(o.name)) - ) - } - - case class DetailParametru( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String] - ) extends Page( - "parameter", - parametr.show, - Some(Detail(osobniCislo)) - ) - - object DetailParametru { - def apply(o: UserInfo, p: Parameter): DetailParametru = - DetailParametru( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)) - ) - } - - case class DetailKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "criteria", - kriterium.show, - Some(DetailParametru(osobniCislo, parametr)) - ) - - object DetailKriteria { - def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = - DetailKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class UpravDukazKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "addProof", - "Důkaz", - Some(DetailKriteria(osobniCislo, parametr, kriterium)) - ) - - object UpravDukazKriteria { - def apply( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): UpravDukazKriteria = - UpravDukazKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class NotFound(url: String) extends Page("404", "404", Some(Directory)) - - case class UnhandledError( - errorName: Option[String], - errorMessage: Option[String] - ) extends Page("500", "Unexpected error", Some(Directory)) - -object Routes: - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) - given [V: JsonEncoder]: JsonEncoder[Titled[V]] = - DeriveJsonEncoder.gen[Titled[V]] - given [V: JsonDecoder]: JsonDecoder[Titled[V]] = - DeriveJsonDecoder.gen[Titled[V]] - given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] - given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] - - val base = - js.`import`.meta.env.BASE_URL - .asInstanceOf[String] + "app" - - val homePage: Page = Page.Directory - - given router: Router[Page] = Router[Page]( - routes = List( - Route.static(homePage, root / endOfSegments, basePath = base), - Route.static( - Page.Dashboard, - root / "dashboard" / endOfSegments, - basePath = base - ), - Route[Page.Detail, String]( - encode = _.osobniCislo.value.toString, - decode = osc => Page.Detail(Titled(OsobniCislo(osc))), - root / "osoba" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.DetailParametru, (String, String)]( - encode = p => (p.osobniCislo.value.toString, p.parametr.value), - decode = - p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / endOfSegments, - basePath = base - ), - Route[Page.DetailKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.DetailKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.UpravDukazKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.UpravDukazKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / "edit" / endOfSegments, - basePath = base - ) - ), - serializePage = _.toJson, - deserializePage = _.fromJson[Page] - .fold(s => throw IllegalStateException(s), identity), - getPageTitle = _.title, - routeFallback = url => Page.NotFound(url), - deserializeFallback = _ => Page.Dashboard - )( - $popStateEvent = windowEvents.onPopState, - owner = unsafeWindowOwner - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala deleted file mode 100644 index b24b82b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -sealed trait Action - -case object FetchDirectory extends Action -case class FetchUserDetails(osc: OsobniCislo) extends Action -case class FetchParameters(osc: OsobniCislo) extends Action -case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action -case class FetchParameterCriteria( - osc: OsobniCislo, - paramId: String, - critId: String, - page: (UserInfo, Parameter, ParameterCriteria) => Page -) extends Action -case class FetchAvailableFiles(osc: OsobniCislo) extends Action -case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala deleted file mode 100644 index c637d35..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala +++ /dev/null @@ -1,82 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.{UserProfile, UserInfo, OsobniCislo} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo -import cz.e_bs.cmi.mdr.pdb.UserFunction - -object AppPage: - // TODO: pages by logged in user - val pages: List[Page] = List(Page.Directory, Page.Dashboard) - - import NavigationBar.{Logo, MenuItem} - - val logo = Logo( - "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", - "Workflow" - ) - - // TODO: menu items by user profile - val userMenu = - List( - MenuItem("Your Profile"), - MenuItem("Settings"), - MenuItem("Sign out") - ) - - // TODO: load user profile - val $userProfile = Var( - UserProfile( - "tom", - UserInfo( - OsobniCislo("1031"), - "tom", - "Tom", - "Cook", - None, - None, - Some("tom@example.com"), - Some("+420 222 866 180"), - Some( - UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") - ), - Nil, - None - ) - ) - ) - - val $userInfo = $userProfile.signal.map(_.userInfo) - - type ViewModel = Option[HtmlElement] - def apply( - actionBus: Observer[Action] - )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using - router: Router[Page] - ): HtmlElement = - PageLayout(actionBus)( - $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => - PageLayout.ViewModel( - NavigationBar.ViewModel( - u, - pages.map(p => - NavigationBar.Link( - () => - PageLink( - p, - actionBus - ), - p == cp - ) - ), - userMenu, - logo - ), - c - ) - ), - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala deleted file mode 100644 index bf473c6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ /dev/null @@ -1,143 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden -import fiftyforms.ui.components.tailwind.list.IconText.ViewModel -import fiftyforms.ui.components.tailwind.Icons -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Action - -object Breadcrumbs: - - private def slash = - import svg.{*, given} - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-300", - xmlns := "http://www.w3.org/2000/svg", - fill := "currentColor", - viewBox := "0 0 20 20", - ariaHidden := true, - path( - d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" - ) - ) - - def backIcon = - Icons.solid - .`arrow-narrow-left`() - .amend( - svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" - ) - - object Link: - case class ViewModel( - page: Page, - icon: Option[SvgElement], - extraClasses: String, - text: String, - textClasses: Option[String] = None - ) - - val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" - - def shortHome(p: Page) = ViewModel( - p, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - "Zpět na úvodní stránku", - None - ) - - def fullHome(p: Page) = ViewModel( - p, - Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), - "text-gray-400 hover:text-gray-500", - "Domů", - Some("sr-only") - ) - - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - PageLink - .container($m.map(_.page), actionBus) - .amend( - cls <-- $m.map(_.extraClasses), - child.maybe <-- $m.map(_.icon), - span( - cls <-- $m.map(_.textClasses.getOrElse("")), - child.text <-- $m.map(_.text) - ) - ) - - object FullBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - ol( - role := "list", - cls := "flex items-center space-x-4", - children <-- $m.map(_.path).split(_.id)((_, _, $p) => - li( - div( - cls := "flex items-center", - child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), - Link( - $p.map(p => - if (p.isRoot) then Link.fullHome(p) - else - Link.ViewModel( - p, - None, - s"ml-4 max-w-xs truncate ${Link.baseClasses}", - p.title - ) - ), - actionBus - ) - ) - ) - ) - ) - - object ShortBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - Link( - $m.map { p => - val target = p.parent.getOrElse(p) - if target.isRoot then Link.shortHome(target) - else - Link.ViewModel( - target, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - s"Zpět na ${target.title}" - ) - }, - actionBus - ) - - def apply(actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - nav( - cls := "flex", - aria.label := "Breadcrumb", - div( - cls := "flex sm:hidden", - ShortBreadcrumbs(router.$currentPage, actionBus) - ), - div( - cls := "hidden sm:block", - FullBreadcrumbs(router.$currentPage, actionBus) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala deleted file mode 100644 index 0db2047..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ /dev/null @@ -1,242 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent -import fiftyforms.ui.components.tailwind.Icons -import fiftyforms.ui.components.tailwind.Avatar -import cz.e_bs.cmi.mdr.pdb.UserInfo -import io.laminext.syntax.core.* - -object NavigationBar: - - case class Logo(img: String, name: String) - case class Link(a: () => Anchor, active: Boolean) - case class MenuItem(title: String) - - case class ViewModel( - userInfo: UserInfo, - pages: List[Link], - userMenu: List[MenuItem], - logo: Logo - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - val $userInfo = $m.map(_.userInfo) - val mobileMenuOpen = Var(false) - - val desktopOnly = cls("hidden md:block") - val mobileOnly = cls("md:hidden") - - inline def avatarImage(size: Int = 8) = - Avatar($userInfo.map(_.img)).avatarImage(size) - - def notificationButton = button( - tpe := "button", - cls := List( - "bg-indigo-600", - "focus:outline-none", - "focus:ring-2", - "focus:ring-offset-2", - "focus:ring-offset-indigo-600", - "focus:ring-white", - "hover:text-white", - "p-1", - "rounded-full", - "text-indigo-200" - ), - span(cls := "sr-only", "View notifications"), - Icons.outline.bell() - ) - - def userProfile: HtmlElement = - val menuOpen = Var(false) - - def menuItem(item: MenuItem, idx: Int): HtmlElement = - a( - href := "#", - cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", - role := "menuitem", - tabIndex := -1, - idAttr := s"user-menu-item-$idx", - item.title - ) - - div( - cls := "ml-3 relative", - div( - button( - tpe := "button", - cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - idAttr := "user-menu-button", - aria.expanded <-- menuOpen.signal, - aria.hasPopup := true, - span(cls := "sr-only", "Open user menu"), - child <-- avatarImage(), - onClick.preventDefault.mapTo( - !menuOpen.now() - ) --> menuOpen.writer - ) - ), - /* - * */ - div( - cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", - cls <-- menuOpen.signal.map { o => - if (o) "md:block" else "md:hidden" - }, - role := "menu", - aria.orientation := "vertical", - aria.labelledBy := "user-menu-button", - tabIndex := -1, - // : keyboard navigation - children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) - ) - ) - - def mobileProfile = - def menuItem(item: MenuItem): HtmlElement = - a( - href := "#", - cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", - item.title - ) - - div( - cls := "pt-4 pb-3 border-t border-indigo-700", - div( - cls := "flex items-center px-5", - div( - cls := "flex-shrink-0", - child <-- avatarImage(10) - ), - div( - cls := "ml-3", - div( - cls := "text-base font-medium text-white", - child.text <-- $userInfo.map(_.name) - ), - child.maybe <-- $userInfo.map( - _.email.map(e => - div( - cls := "text-sm font-medium text-indigo-300", - e - ) - ) - ) - ), - notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) - ), - div( - cls := "mt-3 px-2 space-y-1", - children <-- $m.map(_.userMenu.map(menuItem)) - ) - ) - - def pageLink(page: Link): Anchor = - page - .a() - .amend( - cls := "text-white px-3 py-2 rounded-md text-sm font-medium", - cls := Seq( - "bg-indigo-700" -> page.active, - "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active - ), - ariaCurrent := (if page.active then "page" else "false") - ) - - def logoImg: Image = - img( - cls := "h-8 w-8", - src <-- $m.map(_.logo.img), - alt <-- $m.map(_.logo.name) - ) - - def mobileMenuButton = button( - tpe := "button", - cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - aria.controls := "mobile-menu", - aria.expanded <-- mobileMenuOpen.signal, - span(cls := "sr-only", "Open main menu"), - Icons.outline - .menu() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") - ), - Icons.outline - .x() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") - ), - onClick.preventDefault.mapTo( - !mobileMenuOpen.now() - ) --> mobileMenuOpen.writer - ) - - def navBarLeft = - div( - cls := "flex items-center", - div(cls := "flex-shrink-0", logoImg), - div( - desktopOnly, - div( - cls := "ml-10 flex items-baseline space-x-4", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ) - ) - ) - - def navBarRight = - div( - desktopOnly, - div( - cls := "ml-4 flex items-center md:ml-6", - notificationButton, - userProfile - ) - ) - - def navBarMobile = - div( - cls := "-mr-2 flex", - mobileOnly, - mobileMenuButton - ) - - def navBar = - div( - cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex items-center justify-between h-16", - navBarLeft, - navBarRight, - navBarMobile - ) - ) - - def mobileMenu = - div( - mobileOnly, - cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }, - idAttr := "mobile-menu", - div( - cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ), - mobileProfile - ) - - nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala deleted file mode 100644 index 75339df..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object PageHeader: - def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = - header( - cls := "bg-white shadow-sm", - div( - cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", - h1( - cls := "text-lg leading-6 font-semibold text-gray-900", - Breadcrumbs(actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala deleted file mode 100644 index 5096463..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ /dev/null @@ -1,30 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import fiftyforms.ui.components.tailwind.Loading -import io.laminext.syntax.core.* - -object PageLayout: - case class ViewModel( - navigation: NavigationBar.ViewModel, - content: Option[HtmlElement] - ) - def apply(actionBus: Observer[Action])( - $m: Signal[ViewModel], - mods: Modifier[HtmlElement]* - )(using router: Router[Page]): HtmlElement = - val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) - div( - cls := "h-full flex flex-col", - NavigationBar($m.map(_.navigation)), - child.maybe <-- router.$currentPage.map(_.isRoot) - .switch(None, Some(PageHeader(actionBus))), - main( - cls := "flex-grow-1 overflow-y-auto", - mods, - child <-- $maybeContent.map(_.getOrElse(Loading)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala deleted file mode 100644 index 62d820f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.LinkSupport.* -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo - -object PageLink: - type ViewModel = Page - def apply($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = - a(mods($m, actions), child.text <-- $m.map(_.title)) - - def apply(m: ViewModel, actions: Observer[Action])(using - router: Router[Page] - ): Anchor = apply(Val(m), actions) - - def container($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = a(mods($m, actions)) - - def container(m: ViewModel, actions: Observer[Action])(using - Router[Page] - ): Anchor = container(Val(m), actions) - - private def mods($m: Signal[Page], actions: Observer[Action])(using - router: Router[Page] - ): Modifier[Anchor] = - Seq( - href <-- $m.map(router.absoluteUrlForPage).recover { case _ => - Some("invalid url") - }, - composeEvents(onClick.noKeyMod.preventDefault)( - _.sample($m) - .map(NavigateTo.apply) - ) --> actions - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala deleted file mode 100644 index 87f825a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.pages.dashboard.DashboardPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -class DashboardPageConnector(actionBus: Observer[Action])(using - router: Router[Page] -): - def apply: HtmlElement = - AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala deleted file mode 100644 index e9b0f71..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala +++ /dev/null @@ -1,76 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailKriteriaPage -import pages.detail.components.DukazyKriteria -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -object DetailKriteriaPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailKriteriaPageConnector( - state: DetailKriteriaPageConnector.AppState -)( - $page: Signal[Page.DetailKriteria] -)(using Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => - (p.osobniCislo.value, p.parametr.value, p.kriterium.value) - )((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map( - FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) - ) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, s, $s) => - DetailKriteriaPage($s)(state.actionBus.contramap { - case DukazyKriteria.Add => - NavigateTo( - Page.UpravDukazKriteria( - Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), - Page.Titled(s.parametr.id, Some(s.parametr.nazev)), - Page.Titled(s.kriterium.id, Some(s.kriterium.id)) - ) - ) - }) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): DetailKriteriaPage.ViewModel = - DetailKriteriaPage.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala deleted file mode 100644 index 4d8024f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ /dev/null @@ -1,57 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import pages.detail.DetailPage -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import fiftyforms.ui.components.tailwind.Color - -object DetailPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailPageConnector(state: DetailPageConnector.AppState)( - $page: Signal[Page.Detail] -)(using router: Router[Page]): - val $oscChangeSignal = $page.splitOne(_.osobniCislo.value)((osc, _, _) => osc) - val $pageChangeSignal = - $oscChangeSignal.flatMap(osc => - EventStream.fromSeq(Seq(FetchUserDetails(osc), FetchParameters(osc))) - ) - // TODO: filter the value based on the current osc - // OSC change will fetch new data, but still - // - we need to be sure that what we got is really what we ought to display - // - we want to display stale data accordingly (at least with loading indicator) - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - def apply: HtmlElement = - AppPage(state.actionBus)( - $data.combineWithFn($params)(_ zip _) - .map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: List[Parameter] - ): DetailPage.ViewModel = - DetailPage.ViewModel( - o.toDetailOsoby, - p.map( - _.toParametr(param => - PageLink.container(Page.DetailParametru(o, param), state.actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala deleted file mode 100644 index fdaa7f8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailParametruPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink - -object DetailParametruPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailParametruPageConnector( - state: DetailParametruPageConnector.AppState -)( - $page: Signal[Page.DetailParametru] -)(using router: Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => (p.osobniCislo.value, p.parametr.value))((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map(FetchParameter(_, _)) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - } yield (da, pb) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailParametruPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter - ): DetailParametruPage.ViewModel = - DetailParametruPage.ViewModel( - o.toDetailOsoby, - p.toParametr(p => - PageLink.container(Page.DetailParametru(o, p), state.actionBus) - ), - p.criteria.map( - _.toKriterium { c => - PageLink.container( - Page.DetailKriteria(o, p, c), - state.actionBus - ) - } - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala deleted file mode 100644 index 049aa22..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -case class DirectoryPageConnector( - $input: EventStream[List[UserInfo]], - actionBus: Observer[Action] -)(using router: Router[Page]): - val $data = $input.startWithNone - val $actionSignal = EventStream.fromValue(FetchDirectory) - - def apply: HtmlElement = - AppPage(actionBus)( - $data.split(_ => ())((_, _, s) => - pages.directory.DirectoryPage( - s.map( - _.map( - _.toUserRow(u => - PageLink.container( - Page.Detail(Page.Titled(u.personalNumber)), - actionBus - ) - ) - ) - ) - ) - ), - $actionSignal --> actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala deleted file mode 100644 index 88e7450..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ /dev/null @@ -1,68 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii -import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria -import fiftyforms.ui.components.tailwind.Color - -extension (o: UserInfo) - def toDetailOsoby: DetailOsoby.ViewModel = - DetailOsoby.ViewModel( - o.personalNumber, - o.name, - o.email, - o.phone, - o.img, - o.mainFunction.map(f => - DetailOsoby.Funkce.ViewModel(f.name, f.dept, f.ou) - ), - o.userContracts.headOption.map(c => - DetailOsoby.PracovniPomer.ViewModel(c.rel, c.startDate, c.endDate) - ) - ) - -extension (param: Parameter) - def toParametr(container: Parameter => Anchor): DetailParametru.ViewModel = - DetailParametru.ViewModel( - id = param.id, - nazev = param.name, - popis = param.description, - status = "Nesplněno", - statusColor = Color.red, - a = container(param) - ) - -extension (crit: ParameterCriteria) - def toKriterium( - container: ParameterCriteria => Anchor - ): DetailKriteria.ViewModel = - DetailKriteria.ViewModel( - nazev = crit.criteriumText, - kapitola = crit.chapterId, - bod = crit.itemId, - status = "Nesplněno", - statusColor = Color.red, - splneno = false, - dukazy = Nil, - container = container(crit) - ) - -extension (user: UserInfo) - def toUserRow( - container: UserInfo => HtmlElement = (_: UserInfo) => div() - ): UserRow.ViewModel = - UserRow.ViewModel( - osobniCislo = user.personalNumber.toString, - celeJmeno = user.name, - prijmeni = user.surname, - hlavniFunkce = user.mainFunction.map(_.name), - img = user.img, - container = () => container(user) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala deleted file mode 100644 index 7d5b2c1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.dashboard - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.Page - -object DashboardPage: - - def render: HtmlElement = - div("Dashboard page") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala deleted file mode 100644 index 9ef0698..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala +++ /dev/null @@ -1,31 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailKriteriaPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply($m: Signal[ViewModel])( - action: Observer[DukazyKriteria.Action] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - DukazyKriteria($m.map(_.kriterium.dukazy))(action) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala deleted file mode 100644 index 3b316e1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object DetailPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametry: SeznamParametru.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby($m.map(_.osoba)), - SeznamParametru($m.map(_.parametry)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala deleted file mode 100644 index 17224cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala +++ /dev/null @@ -1,24 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailParametruPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriteria: SeznamKriterii.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby.header($m.map(_.osoba)), - DetailParametru($m.map(_.parametr)), - SeznamKriterii($m.map(_.kriteria)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala deleted file mode 100644 index dc912fa..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ /dev/null @@ -1,107 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb -package app -package pages.detail - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.UpravDukazForm -import fiftyforms.services.files.File - -object UpravDukaz: - - type ThisPage = Page.UpravDukazKriteria - type PageKey = (OsobniCislo, String, String) - - trait State { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def availableFiles: EventStream[List[File]] - def actionBus: Observer[Action] - } - - def keyOfPage(page: ThisPage): PageKey = - (page.osobniCislo.value, page.parametr.value, page.kriterium.value) - - def onChangeAction(key: PageKey): Action = - FetchParameterCriteria( - key._1, - key._2, - key._3, - Page.UpravDukazKriteria(_, _, _) - ) - - class Connector(state: State)($page: Signal[ThisPage])(using Router[Page]): - val $paramChangeSignal = $page.splitOne(keyOfPage)((x, _, _) => x) - val $pageChangeSignal = $paramChangeSignal.map(onChangeAction) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.split(_ => ())((_, s, $s) => - PageComponent( - $s.map(buildModel), - state.availableFiles, - state.actionBus.contramap { - case UpravDukazForm.Cancelled => - NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) - case UpravDukazForm.AvailableFilesRequested => - FetchAvailableFiles(s._1.personalNumber) - } - ) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): PageComponent.ViewModel = - import connectors.* - PageComponent.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) - - object PageComponent: - import components.* - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply( - $m: Signal[ViewModel], - availableFilesStream: EventStream[List[File]], - events: Observer[UpravDukazForm.Event] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - UpravDukazForm(availableFilesStream)(events) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala deleted file mode 100644 index ca311b0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.Icons -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Color - -object DetailKriteria: - case class ViewModel( - nazev: String, - kapitola: String, - bod: String, - status: String, - statusColor: Color, - splneno: Boolean, - dukazy: List[DukazKriteria.ViewModel], - container: HtmlElement = div() - ) { - val id = s"${kapitola}${bod}" - } - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h3( - cls := "text-xl leading-6 font-bold text-gray-900", - child.text <-- $m.map(_.nazev) - ), - h3( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - "Kapitola ", - child.text <-- $m.map(_.kapitola), - " bod ", - child <-- $m.map(_.bod) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala deleted file mode 100644 index ae90290..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala +++ /dev/null @@ -1,100 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Avatar - -object DetailOsoby: - - object Funkce: - case class ViewModel( - nazev: String, - stredisko: String, - voj: String - ) - - def render($m: Signal[ViewModel]): HtmlElement = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.nazev), - span( - cls := "hidden md:inline", - " @ ", - child.text <-- $m.map(_.stredisko), - ", ", - child.text <-- $m.map(_.voj) - ) - ) - - object PracovniPomer: - case class ViewModel( - druh: String, - pocatek: LocalDate, - konec: Option[LocalDate] - ) - - def render($m: Signal[ViewModel]): HtmlElement = - import fiftyforms.ui.components.tailwind.CustomAttrs.datetime - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.druh), - " od ", - time( - datetime <-- $m.map(_.pocatek.toString), - child.text <-- $m.map(_.pocatek.toString) - ) - ) - - case class ViewModel( - osobniCislo: OsobniCislo, - jmeno: String, - email: Option[String], - telefon: Option[String], - img: Option[String], - hlavniFunkce: Option[Funkce.ViewModel], - pracovniPomer: Option[PracovniPomer.ViewModel] - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-4", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(16) - ), - div( - h1( - cls := "text-2xl font-bold text-gray-900", - child.text <-- $m.map(_.jmeno) - ), - child.maybe <-- $m.map(_.hlavniFunkce).split(_ => ())((_, _, d) => - Funkce.render(d) - ), - child.maybe <-- $m.map(_.pracovniPomer).split(_ => ())((_, _, d) => - PracovniPomer.render(d) - ) - ) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-5", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(8) - ), - div( - h1( - cls := "text-lg font-medium text-gray-800", - child.text <-- $m.map(_.jmeno) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala deleted file mode 100644 index e11cb62..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color - -object DetailParametru: - case class ViewModel( - id: String, - nazev: String, - popis: String, - status: String, - statusColor: Color, - a: Anchor - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h2( - cls := "text-2xl leading-6 font-medium text-gray-900", - child.text <-- $m.map(_.nazev) - ), - p( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - child.text <-- $m.map(_.popis) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - h2( - cls := "text-lg leading-6 font-medium text-gray-600", - child.text <-- $m.map(_.nazev) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala deleted file mode 100644 index a00ede8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala +++ /dev/null @@ -1,177 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.CustomAttrs - -object DukazKriteria: - case class Osoba(osobniCislo: String, jmeno: String) - case class Udalost(osoba: Osoba, datum: LocalDate) - case class Dokument( - url: String, - nazev: String, - pridal: Udalost, - odebral: Option[Udalost] - ) - case class ViewModel( - dokumenty: List[Dokument], - autorizoval: Option[Udalost], - platiDo: Option[LocalDate], - poznámka: Option[String] - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-lg", - div( - cls := "px-4 py-5 sm:px-6", - h3( - cls := "text-lg leading-6 font-medium text-gray-900", - """Applicant Information""" - ), - p( - cls := "mt-1 max-w-2xl text-sm text-gray-500", - """Personal details and application.""" - ) - ), - div( - cls := "border-t border-gray-200 px-4 py-5 sm:px-6", - dl( - cls := "grid grid-cols-1 gap-x-4 gap-y-8 sm:grid-cols-2", - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Full name""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Margot Foster""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Application for""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Backend Developer""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Email address""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """margotfoster@example.com""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Salary expectation""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """$120,000""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """About""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Fugiat ipsum ipsum deserunt culpa aute sint do nostrud anim incididunt cillum culpa consequat. Excepteur qui ipsum aliquip consequat sint. Sit id mollit nulla mollit nostrud in ea officia proident. Irure nostrud pariatur mollit ad adipisicing reprehenderit deserunt qui eu.""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """Attachments""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - ul( - role := "list", - cls := "border border-gray-200 rounded-md divide-y divide-gray-200", - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """resume_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ), - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """coverletter_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala deleted file mode 100644 index 22f134f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala +++ /dev/null @@ -1,35 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object DukazyKriteria: - sealed trait Action - case object Add extends Action - - type ViewModel = List[DukazKriteria.ViewModel] - - def apply($m: Signal[ViewModel])(actions: Observer[Action]): HtmlElement = - div( - child.maybe <-- $m.map(m => - if (m.isEmpty) then Some(prazdnyDukaz(actions)) - else None - ), - children <-- $m.map(_.zipWithIndex).split(_._2)((_, _, $s) => - DukazKriteria($s.map(_._1)) - ) - ) - - private def prazdnyDukaz(actions: Observer[Action]): HtmlElement = - button( - tpe := "button", - onClick.preventDefault.mapTo(Add) --> actions, - cls := "relative block w-full border-2 border-gray-300 border-dashed rounded-lg p-12 text-center hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - Icons.outline - .`document-add`(12) - .amend(svg.cls := "mx-auto text-gray-400"), - span( - cls := "mt-2 block text-sm font-medium text-gray-900", - "Přidat důkaz" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala deleted file mode 100644 index f628697..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.list.StackedList -import fiftyforms.ui.components.tailwind.list.ListRow -import fiftyforms.ui.components.tailwind.list.RowTag -import fiftyforms.ui.components.tailwind.list.RowNext -import java.time.LocalDate - -object SeznamKriterii: - type ViewModel = List[DetailKriteria.ViewModel] - - private val kritList = new StackedList[DetailKriteria.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - kritList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = - p(cls := "text-sm text-gray-500", s"${i.kapitola}${i.bod}"), - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.container - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala deleted file mode 100644 index 2bfe4bc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.list.{ - StackedList, - ListRow, - RowTag, - PropList, - IconText, - RowNext -} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.LinkSupport.* - -object SeznamParametru: - type ViewModel = List[DetailParametru.ViewModel] - - private val parametrList = new StackedList[DetailParametru.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - parametrList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = emptyNode, - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.a - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala deleted file mode 100644 index f048e9d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ /dev/null @@ -1,101 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.form.* -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement -import fiftyforms.services.files.components.tailwind.FilePicker -import cz.e_bs.cmi.mdr.pdb.frontend.AutorizujDukaz -import cz.e_bs.cmi.mdr.pdb.frontend.DocumentRef -import fiftyforms.services.files.File - -object UpravDukazForm: - sealed trait Event - case object Cancelled extends Event - case object AvailableFilesRequested extends Event - def apply(availableFilesStream: EventStream[List[File]])( - updates: Observer[Event] - ): HtmlElement = - val (filesStream, filesObserver) = - EventStream.withObserver[FilePicker.Event] - val files = Var[List[File]](Nil) - def submitButtons: HtmlElement = - div( - cls := "pt-5", - div( - cls := "flex justify-end", - button( - tpe := "button", - cls := "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zrušit", - onClick.mapTo(Cancelled) --> updates - ), - button( - tpe := "submit", - cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - disabled <-- files.signal.map(_.isEmpty), - "Autorizovat" - ) - ) - ) - - div( - filesStream.collect { case FilePicker.SelectionUpdated(files) => - files.to(List) - } --> files.writer, - filesStream.collect { case FilePicker.AvailableFilesRequested => - AvailableFilesRequested - } --> updates, - cls := "bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6", - Form( - Form.Body( - Form.Section( - FormHeader( - FormHeader.ViewModel( - "Doložení kritéria", - "Sestavte doklady poskytující důkaz kritéria a potvrďte odesláním formuláře. Případné limitace či jiné relevantní údaje vepište do pole Komentář." - ) - ), - FormFields( - FormRow( - "dokumenty", - "Dokumenty", - FilePicker(files.signal, availableFilesStream)(filesObserver) - .amend(idAttr := "dokumenty", cls("max-w-lg")) - ).toHtml, - FormRow( - "platnost", - "Platnost", - input( - idAttr := "platnost", - name := "platnost", - tpe := "date", - autoComplete := "date", - cls := "block max-w-lg w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" - ) - ).toHtml, - FormRow( - "komentar", - "Komentář", - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - textArea( - idAttr := "komentar", - name := "about", - rows := 3, - cls := "max-w-lg shadow-sm block w-full focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border border-gray-300 rounded-md" - ), - p( - cls := "mt-2 text-sm text-gray-500", - "Doplňte prosím potřebné informace související s doložením kritéria, včetně případných limitací." - ) - ) - ).toHtml - ) - ) - ), - submitButtons - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala deleted file mode 100644 index 41033f6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -import components._ - -object DirectoryPage: - - type ViewModel = List[UserRow.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - val (actionsStream, actionObserver) = - EventStream.withObserver[SearchForm.Action] - val $filter = actionsStream - .collect { - case SearchForm.ClearFilter => None - case SearchForm.SetFilter(t) => Some(t) - } - .startWith(None) - val byLetter = for { - d <- $m - f <- $filter - } yield for { - (letter, users) <- d - .filter { user => - f match - case None => true - case Some(t) => user.search.contains(t) - } - .groupBy(_.prijmeni.head) - .to(List) - .sortBy(_._1) - } yield (letter.toString, users.sortBy(_.prijmeni)) - - div( - cls := "h-full max-w-7xl mx-auto order-first flex flex-col flex-shrink-0", - SearchForm(actionObserver), - Directory(byLetter) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala deleted file mode 100644 index 73f915d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -object Directory: - - object Header: - type ViewModel = String - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500", - h3(child.text <-- $m) - ) - - object UserList: - type ViewModel = List[UserRow.ViewModel] - def apply($m: Signal[ViewModel]): HtmlElement = - ul( - role := "list", - cls := "relative z-0 divide-y divide-gray-200", - children <-- $m.split(_.osobniCislo)((_, _, s) => UserRow(s)) - ) - - type ViewModel = List[(String, List[UserRow.ViewModel])] - def apply($m: Signal[ViewModel]): HtmlElement = - nav( - cls := "flex-1 min-h-0 overflow-y-auto", - aria.label := "Directory", - children <-- $m.split(_._1)((_, _, s) => - div( - cls := "relative", - Header(s.map(_._1)), - UserList(s.map(_._2)) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala deleted file mode 100644 index e182910..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala +++ /dev/null @@ -1,54 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object SearchForm: - sealed trait Action - case object SubmitFilter extends Action - sealed trait FilterAction extends Action - case object ClearFilter extends FilterAction - case class SetFilter(value: String) extends FilterAction - - def apply(actions: Observer[Action]): HtmlElement = - div( - cls := "px-6 pt-4 pb-4", - form( - cls := "flex space-x-4", - onSubmit.mapTo(SubmitFilter) --> actions, - div( - cls := "flex-1 min-w-0", - label( - forId := "search", - cls := "sr-only", - "Hledat" - ), - div( - cls := "relative rounded-md shadow-sm", - div( - cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", - Icons.solid.search().amend(svg.cls := "text-gray-400") - ), - input( - tpe := "search", - name := "search", - idAttr := "search", - cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", - placeholder := "Hledat", - composeEvents(onInput.mapToValue.setAsValue.map(SetFilter(_)))( - _.throttle(500) - ) --> actions - ) - ) - ), - button( - tpe := "submit", - cls := "inline-flex justify-center px-3.5 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500", - Icons.solid.filter().amend(svg.cls := "text-gray-400"), - span( - cls := "sr-only", - "Hledat" - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala deleted file mode 100644 index 82f7723..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala +++ /dev/null @@ -1,52 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.airstream.core.Signal -import fiftyforms.ui.components.tailwind.Avatar - -object UserRow: - case class ViewModel( - osobniCislo: String, - celeJmeno: String, - prijmeni: String, - hlavniFunkce: Option[String], - img: Option[String], - container: () => HtmlElement = () => div() - ) { - val search = prijmeni.toLowerCase - } - - def apply($m: Signal[ViewModel]): HtmlElement = - inline def avatarImage = - Avatar($m.map(_.img)).avatarImage(10) - - li( - div( - cls := "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500", - div( - cls := "flex-shrink-0", - child <-- avatarImage - ), - div( - cls := "flex-1 min-w-0", - child <-- $m.map { o => - o.container() - .amend( - cls := "focus:outline-none", - span( - cls := "absolute inset-0", - aria.hidden := true - ), - p( - cls := "text-sm font-medium text-gray-900", - o.celeJmeno - ), - p( - cls := "text-sm text-gray-500 truncate", - o.hlavniFunkce - ) - ) - } - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala deleted file mode 100644 index 467c5f5..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala +++ /dev/null @@ -1,99 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router - -object ErrorPage: - case class ViewModel( - homePage: Page, - errorName: String, - title: String, - subTitle: String - ) - - def apply(m: ViewModel, actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - val ViewModel(homePage, errorName, title, subTitle) = m - div( - cls := "min-h-full pt-16 pb-12 flex flex-col bg-white", - main( - cls := "flex-grow flex flex-col justify-center max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex-shrink-0 flex justify-center", - a( - href := "/", - cls := "inline-flex", - span( - cls := "sr-only", - """Workflow""" - ), - img( - cls := "h-12 w-auto", - src := "https://tailwindui.com/img/logos/workflow-mark.svg?color=indigo&shade=600", - alt := "" - ) - ) - ), - div( - cls := "py-16", - div( - cls := "text-center", - p( - cls := "text-sm font-semibold text-indigo-600 uppercase tracking-wide", - errorName - ), - h1( - cls := "mt-2 text-4xl font-extrabold text-gray-900 tracking-tight sm:text-5xl", - title - ), - p( - cls := "mt-2 text-base text-gray-500", - subTitle - ), - div( - cls := "mt-6", - PageLink - .container(homePage, actionBus) - .amend( - cls := "text-base font-medium text-indigo-600 hover:text-indigo-500", - """Go back home""" - ) - ) - ) - ) - ), - footer( - cls := "flex-shrink-0 max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", - nav( - cls := "flex justify-center space-x-4", - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Contact Support""" - ), - span( - cls := "inline-block border-l border-gray-300", - aria.hidden := true - ), - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Status""" - ), - span( - cls := "inline-block border-l border-gray-300", - aria.hidden := true - ), - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Twitter""" - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala deleted file mode 100644 index bd9baec..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action - -object NotFoundPage: - def apply(homePage: Page, url: String, actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - ErrorPage( - ErrorPage.ViewModel( - homePage, - "404 error", - "Page not found.", - s"Sorry, but page $url doesn't exist." - ), - actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala deleted file mode 100644 index 476fe3c..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action - -object UnhandledErrorPage: - - case class ViewModel( - homePage: Page, - errorName: Option[String], - errorMessage: Option[String] - ) - - def apply(m: ViewModel, actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - ErrorPage( - ErrorPage.ViewModel( - m.homePage, - "Unexpected error occurred", - m.errorName.getOrElse( - "Uh oh!" - ), // TODO: translations, better text than uh oh - m.errorMessage.getOrElse( - "This wasn't supposed to happen! Please try again." - ) - ), - actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala deleted file mode 100644 index 25f059f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala +++ /dev/null @@ -1,6 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.services - -import com.raquo.airstream.core.Observer - -trait DataFetcher[K, A]: - def fetch(id: K, sink: Observer[A]): Unit diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala deleted file mode 100644 index a469830..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala +++ /dev/null @@ -1,120 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package state - -import com.raquo.airstream.core.EventStream -import cz.e_bs.cmi.mdr.pdb.{UserInfo, OsobniCislo} -import com.raquo.airstream.core.Observer -import scala.scalajs.js -import scala.scalajs.js.JSON -import zio.json._ -import com.raquo.airstream.eventbus.EventBus -import com.raquo.airstream.ownership.Owner -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.UserFunction -import cz.e_bs.cmi.mdr.pdb.UserContract -import fiftyforms.services.files.File - -trait AppState - extends connectors.DetailPageConnector.AppState - with connectors.DetailParametruPageConnector.AppState - with connectors.DetailKriteriaPageConnector.AppState - with pages.detail.UpravDukaz.State: - def users: EventStream[List[UserInfo]] - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - -class MockAppState(implicit owner: Owner, router: Router[Page]) - extends AppState: - - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonDecoder[UserFunction] = DeriveJsonDecoder.gen - given JsonDecoder[UserContract] = DeriveJsonDecoder.gen - given JsonDecoder[UserInfo] = DeriveJsonDecoder.gen - - given JsonDecoder[ParameterCriteria] = DeriveJsonDecoder.gen - given JsonDecoder[Parameter] = DeriveJsonDecoder.gen - - private val actions = EventBus[Action]() - private val (parametersStream, pushParameters) = - EventStream.withCallback[List[Parameter]] - private val (usersStream, pushUsers) = - EventStream.withCallback[List[UserInfo]] - private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] - private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] - - private val mockData: List[UserInfo] = - mockUsers - .asInstanceOf[js.Dictionary[js.Object]] - .values - // TODO: is there a more efficient way to parse from JS object directly? - .map { o => - val parsed = JSON.stringify(o).fromJson[UserInfo] - parsed.left.foreach(org.scalajs.dom.console.log(_)) - parsed - } - .collect { case Right(u) => - u - } - .toList - - private val mockParameters: List[Parameter] = - pdbParams - .asInstanceOf[js.Dictionary[js.Object]] - .values - .map(o => JSON.stringify(o).fromJson[Parameter]) - .collect { case Right(p) => p } - .toList - - // TODO: Extract to separate event handler - actions.events.foreach { - case FetchDirectory => pushUsers(mockData) - case FetchUserDetails(osc) => - mockData.find(_.personalNumber == osc).foreach { o => - pushDetails(o) - router.replaceState(Page.Detail(o)) - } - case FetchParameters(osc) => - pushParameters(mockParameters) - case FetchParameter(osc, paramId) => - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(Page.DetailParametru(o, p)) - case FetchParameterCriteria(osc, paramId, critId, page) => - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - c <- p.criteria.find(_.id == critId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(page(o, p, c)) - case NavigateTo(page) => router.pushState(page) - case FetchAvailableFiles(osc) => - pushFiles( - List( - File("https://tc163.cmi.cz/here", "Example file") - ) - ) - } - - override def users: EventStream[List[UserInfo]] = - usersStream.debugWithName("users") - - override def details: EventStream[UserInfo] = - detailsStream.debugWithName("details") - - override def parameters: EventStream[List[Parameter]] = - parametersStream.debugWithName("parameters") - - override def availableFiles: EventStream[List[File]] = - filesStream.debugWithName("available files") - - override def actionBus: Observer[Action] = - actions.writer.debugWithName("actions writer") diff --git a/app/src/main/scala/mdr/pdb/app/Main.scala b/app/src/main/scala/mdr/pdb/app/Main.scala new file mode 100644 index 0000000..081b4ee --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/Main.scala @@ -0,0 +1,103 @@ +package mdr.pdb.app + +import scala.scalajs.js.annotation.JSExportTopLevel +import scala.scalajs.js.annotation.JSExport +import scala.scalajs.js.annotation.JSImport +import scala.scalajs.js +import org.scalajs.dom +import com.raquo.laminar.api.L.{*, given} +import scala.scalajs.js.Date +import com.raquo.waypoint.Router +import com.raquo.waypoint.SplitRender +import mdr.pdb.app.services.DataFetcher +import scala.scalajs.js.JSON +import zio.json._ +import mdr.pdb.UserInfo +import mdr.pdb.app.state.AppState + +@js.native +@JSImport("stylesheets/main.css", JSImport.Namespace) +object Css extends js.Any + +@js.native +@JSImport("data/users.json", JSImport.Default) +object mockUsers extends js.Object + +@js.native +@JSImport("params/pdb-params.json", JSImport.Default) +object pdbParams extends js.Object + +@JSExportTopLevel("app") +object Main: + + @JSExport + def main(args: Array[String]): Unit = + import Routes.given + onLoad { + setupAirstream() + val appContainer = dom.document.querySelector("#app") + val _ = + render( + appContainer, + renderPage(state.MockAppState(using unsafeWindowOwner, router)) + ) + } + + private def onLoad(f: => Unit): Unit = + documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) + + private def setupAirstream()(using router: Router[Page]): Unit = + AirstreamError.registerUnhandledErrorCallback(err => + router.forcePage( + Page.UnhandledError( + Some(err.getClass.getName), // TODO: Fill only in dev mode + Some(err.getMessage) + ) + ) + ) + + def renderPage(state: AppState)(using + router: Router[Page] + ): HtmlElement = + val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) + .collectSignal[Page.Detail]( + connectors + .DetailPageConnector(state)(_) + .apply + ) + .collectSignal[Page.DetailParametru]( + connectors + .DetailParametruPageConnector(state)(_) + .apply + ) + .collectSignal[Page.DetailKriteria]( + connectors + .DetailKriteriaPageConnector(state)(_) + .apply + ) + .collectSignal[Page.UpravDukazKriteria]( + pages.detail.UpravDukaz.Connector(state)(_).apply + ) + .collectStatic(Page.Dashboard)( + connectors.DashboardPageConnector(state.actionBus).apply + ) + .collect[Page.NotFound](pg => + pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) + ) + .collect[Page.UnhandledError](pg => + pages.errors + .UnhandledErrorPage( + pages.errors.UnhandledErrorPage + .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), + state.actionBus + ) + ) + .collectStatic(Page.Directory)( + connectors + .DirectoryPageConnector(state.users, state.actionBus) + .apply + ) + div(cls := "h-full", child <-- pageSplitter.$view) + + // Pull in the stylesheet + val css: Css.type = Css diff --git a/app/src/main/scala/mdr/pdb/app/Routes.scala b/app/src/main/scala/mdr/pdb/app/Routes.scala new file mode 100644 index 0000000..2cc860c --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/Routes.scala @@ -0,0 +1,198 @@ +package mdr.pdb.app + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.* +import org.scalajs.dom +import zio.json.{*, given} +import mdr.pdb.OsobniCislo + +import scala.scalajs.js +import mdr.pdb.UserInfo +import mdr.pdb.Parameter +import mdr.pdb.ParameterCriteria +import mdr.pdb.app.Page.Titled + +// enum is not working with Waypoints' SplitRender collectStatic +sealed abstract class Page( + val id: String, + val title: String, + val parent: Option[Page] +) { + val path: Vector[Page] = + parent match + case None => Vector(this) + case Some(p) => p.path :+ this + + val isRoot: Boolean = parent.isEmpty +} + +object Page: + + case class Titled[V](value: V, title: Option[String] = None): + val show: String = title.getOrElse(value.toString) + + case object Directory extends Page("directory", "Adresář", None) + + case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) + + case class Detail(osobniCislo: Titled[OsobniCislo]) + extends Page("user", osobniCislo.show, Some(Directory)) + + object Detail { + def apply(o: UserInfo): Detail = Detail( + Titled(o.personalNumber, Some(o.name)) + ) + } + + case class DetailParametru( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String] + ) extends Page( + "parameter", + parametr.show, + Some(Detail(osobniCislo)) + ) + + object DetailParametru { + def apply(o: UserInfo, p: Parameter): DetailParametru = + DetailParametru( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)) + ) + } + + case class DetailKriteria( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String], + kriterium: Titled[String] + ) extends Page( + "criteria", + kriterium.show, + Some(DetailParametru(osobniCislo, parametr)) + ) + + object DetailKriteria { + def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = + DetailKriteria( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)), + Titled(k.id, Some(k.id)) + ) + } + + case class UpravDukazKriteria( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String], + kriterium: Titled[String] + ) extends Page( + "addProof", + "Důkaz", + Some(DetailKriteria(osobniCislo, parametr, kriterium)) + ) + + object UpravDukazKriteria { + def apply( + o: UserInfo, + p: Parameter, + k: ParameterCriteria + ): UpravDukazKriteria = + UpravDukazKriteria( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)), + Titled(k.id, Some(k.id)) + ) + } + + case class NotFound(url: String) extends Page("404", "404", Some(Directory)) + + case class UnhandledError( + errorName: Option[String], + errorMessage: Option[String] + ) extends Page("500", "Unexpected error", Some(Directory)) + +object Routes: + given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) + given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) + given [V: JsonEncoder]: JsonEncoder[Titled[V]] = + DeriveJsonEncoder.gen[Titled[V]] + given [V: JsonDecoder]: JsonDecoder[Titled[V]] = + DeriveJsonDecoder.gen[Titled[V]] + given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] + given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] + + val base = + js.`import`.meta.env.BASE_URL + .asInstanceOf[String] + "app" + + val homePage: Page = Page.Directory + + given router: Router[Page] = Router[Page]( + routes = List( + Route.static(homePage, root / endOfSegments, basePath = base), + Route.static( + Page.Dashboard, + root / "dashboard" / endOfSegments, + basePath = base + ), + Route[Page.Detail, String]( + encode = _.osobniCislo.value.toString, + decode = osc => Page.Detail(Titled(OsobniCislo(osc))), + root / "osoba" / segment[String] / endOfSegments, + basePath = base + ), + Route[Page.DetailParametru, (String, String)]( + encode = p => (p.osobniCislo.value.toString, p.parametr.value), + decode = + p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / endOfSegments, + basePath = base + ), + Route[Page.DetailKriteria, (String, String, String)]( + encode = p => + ( + p.osobniCislo.value.toString, + p.parametr.value, + p.kriterium.value.replaceAll("\\.", "--") + ), + decode = p => + Page.DetailKriteria( + Titled(OsobniCislo(p._1)), + Titled(p._2), + Titled(p._3.replaceAll("--", ".")) + ), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / "kriterium" / segment[String] / endOfSegments, + basePath = base + ), + Route[Page.UpravDukazKriteria, (String, String, String)]( + encode = p => + ( + p.osobniCislo.value.toString, + p.parametr.value, + p.kriterium.value.replaceAll("\\.", "--") + ), + decode = p => + Page.UpravDukazKriteria( + Titled(OsobniCislo(p._1)), + Titled(p._2), + Titled(p._3.replaceAll("--", ".")) + ), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / "kriterium" / segment[String] / "edit" / endOfSegments, + basePath = base + ) + ), + serializePage = _.toJson, + deserializePage = _.fromJson[Page] + .fold(s => throw IllegalStateException(s), identity), + getPageTitle = _.title, + routeFallback = url => Page.NotFound(url), + deserializeFallback = _ => Page.Dashboard + )( + $popStateEvent = windowEvents.onPopState, + owner = unsafeWindowOwner + ) diff --git a/app/src/main/scala/mdr/pdb/app/actions.scala b/app/src/main/scala/mdr/pdb/app/actions.scala new file mode 100644 index 0000000..d078b3a --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/actions.scala @@ -0,0 +1,21 @@ +package mdr.pdb.app + +import mdr.pdb.OsobniCislo +import mdr.pdb.UserInfo +import mdr.pdb.Parameter +import mdr.pdb.ParameterCriteria + +sealed trait Action + +case object FetchDirectory extends Action +case class FetchUserDetails(osc: OsobniCislo) extends Action +case class FetchParameters(osc: OsobniCislo) extends Action +case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action +case class FetchParameterCriteria( + osc: OsobniCislo, + paramId: String, + critId: String, + page: (UserInfo, Parameter, ParameterCriteria) => Page +) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action +case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/mdr/pdb/app/components/AppPage.scala new file mode 100644 index 0000000..130c817 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/AppPage.scala @@ -0,0 +1,82 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.Page +import mdr.pdb.{UserProfile, UserInfo, OsobniCislo} +import com.raquo.waypoint.Router +import mdr.pdb.app.Action +import mdr.pdb.app.NavigateTo +import mdr.pdb.UserFunction + +object AppPage: + // TODO: pages by logged in user + val pages: List[Page] = List(Page.Directory, Page.Dashboard) + + import NavigationBar.{Logo, MenuItem} + + val logo = Logo( + "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", + "Workflow" + ) + + // TODO: menu items by user profile + val userMenu = + List( + MenuItem("Your Profile"), + MenuItem("Settings"), + MenuItem("Sign out") + ) + + // TODO: load user profile + val $userProfile = Var( + UserProfile( + "tom", + UserInfo( + OsobniCislo("1031"), + "tom", + "Tom", + "Cook", + None, + None, + Some("tom@example.com"), + Some("+420 222 866 180"), + Some( + UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") + ), + Nil, + None + ) + ) + ) + + val $userInfo = $userProfile.signal.map(_.userInfo) + + type ViewModel = Option[HtmlElement] + def apply( + actionBus: Observer[Action] + )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using + router: Router[Page] + ): HtmlElement = + PageLayout(actionBus)( + $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => + PageLayout.ViewModel( + NavigationBar.ViewModel( + u, + pages.map(p => + NavigationBar.Link( + () => + PageLink( + p, + actionBus + ), + p == cp + ) + ), + userMenu, + logo + ), + c + ) + ), + mods + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala new file mode 100644 index 0000000..9375398 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala @@ -0,0 +1,143 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.tailwind.list.IconText.ViewModel +import fiftyforms.ui.components.tailwind.Icons +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.Page + +import com.raquo.laminar.api.L.{*, given} +import io.laminext.syntax.core.{*, given} +import mdr.pdb.app.Action + +object Breadcrumbs: + + private def slash = + import svg.{*, given} + svg( + cls := "flex-shrink-0 h-5 w-5 text-gray-300", + xmlns := "http://www.w3.org/2000/svg", + fill := "currentColor", + viewBox := "0 0 20 20", + ariaHidden := true, + path( + d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" + ) + ) + + def backIcon = + Icons.solid + .`arrow-narrow-left`() + .amend( + svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" + ) + + object Link: + case class ViewModel( + page: Page, + icon: Option[SvgElement], + extraClasses: String, + text: String, + textClasses: Option[String] = None + ) + + val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" + + def shortHome(p: Page) = ViewModel( + p, + Some(backIcon), + "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", + "Zpět na úvodní stránku", + None + ) + + def fullHome(p: Page) = ViewModel( + p, + Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), + "text-gray-400 hover:text-gray-500", + "Domů", + Some("sr-only") + ) + + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + PageLink + .container($m.map(_.page), actionBus) + .amend( + cls <-- $m.map(_.extraClasses), + child.maybe <-- $m.map(_.icon), + span( + cls <-- $m.map(_.textClasses.getOrElse("")), + child.text <-- $m.map(_.text) + ) + ) + + object FullBreadcrumbs: + type ViewModel = Page + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + ol( + role := "list", + cls := "flex items-center space-x-4", + children <-- $m.map(_.path).split(_.id)((_, _, $p) => + li( + div( + cls := "flex items-center", + child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), + Link( + $p.map(p => + if (p.isRoot) then Link.fullHome(p) + else + Link.ViewModel( + p, + None, + s"ml-4 max-w-xs truncate ${Link.baseClasses}", + p.title + ) + ), + actionBus + ) + ) + ) + ) + ) + + object ShortBreadcrumbs: + type ViewModel = Page + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + Link( + $m.map { p => + val target = p.parent.getOrElse(p) + if target.isRoot then Link.shortHome(target) + else + Link.ViewModel( + target, + Some(backIcon), + "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", + s"Zpět na ${target.title}" + ) + }, + actionBus + ) + + def apply(actionBus: Observer[Action])(using + router: Router[Page] + ): HtmlElement = + nav( + cls := "flex", + aria.label := "Breadcrumb", + div( + cls := "flex sm:hidden", + ShortBreadcrumbs(router.$currentPage, actionBus) + ), + div( + cls := "hidden sm:block", + FullBreadcrumbs(router.$currentPage, actionBus) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala new file mode 100644 index 0000000..4bb8b8d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala @@ -0,0 +1,242 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent +import fiftyforms.ui.components.tailwind.Icons +import fiftyforms.ui.components.tailwind.Avatar +import mdr.pdb.UserInfo +import io.laminext.syntax.core.* + +object NavigationBar: + + case class Logo(img: String, name: String) + case class Link(a: () => Anchor, active: Boolean) + case class MenuItem(title: String) + + case class ViewModel( + userInfo: UserInfo, + pages: List[Link], + userMenu: List[MenuItem], + logo: Logo + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + val $userInfo = $m.map(_.userInfo) + val mobileMenuOpen = Var(false) + + val desktopOnly = cls("hidden md:block") + val mobileOnly = cls("md:hidden") + + inline def avatarImage(size: Int = 8) = + Avatar($userInfo.map(_.img)).avatarImage(size) + + def notificationButton = button( + tpe := "button", + cls := List( + "bg-indigo-600", + "focus:outline-none", + "focus:ring-2", + "focus:ring-offset-2", + "focus:ring-offset-indigo-600", + "focus:ring-white", + "hover:text-white", + "p-1", + "rounded-full", + "text-indigo-200" + ), + span(cls := "sr-only", "View notifications"), + Icons.outline.bell() + ) + + def userProfile: HtmlElement = + val menuOpen = Var(false) + + def menuItem(item: MenuItem, idx: Int): HtmlElement = + a( + href := "#", + cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", + role := "menuitem", + tabIndex := -1, + idAttr := s"user-menu-item-$idx", + item.title + ) + + div( + cls := "ml-3 relative", + div( + button( + tpe := "button", + cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", + idAttr := "user-menu-button", + aria.expanded <-- menuOpen.signal, + aria.hasPopup := true, + span(cls := "sr-only", "Open user menu"), + child <-- avatarImage(), + onClick.preventDefault.mapTo( + !menuOpen.now() + ) --> menuOpen.writer + ) + ), + /* + * */ + div( + cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", + cls <-- menuOpen.signal.map { o => + if (o) "md:block" else "md:hidden" + }, + role := "menu", + aria.orientation := "vertical", + aria.labelledBy := "user-menu-button", + tabIndex := -1, + // : keyboard navigation + children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) + ) + ) + + def mobileProfile = + def menuItem(item: MenuItem): HtmlElement = + a( + href := "#", + cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", + item.title + ) + + div( + cls := "pt-4 pb-3 border-t border-indigo-700", + div( + cls := "flex items-center px-5", + div( + cls := "flex-shrink-0", + child <-- avatarImage(10) + ), + div( + cls := "ml-3", + div( + cls := "text-base font-medium text-white", + child.text <-- $userInfo.map(_.name) + ), + child.maybe <-- $userInfo.map( + _.email.map(e => + div( + cls := "text-sm font-medium text-indigo-300", + e + ) + ) + ) + ), + notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) + ), + div( + cls := "mt-3 px-2 space-y-1", + children <-- $m.map(_.userMenu.map(menuItem)) + ) + ) + + def pageLink(page: Link): Anchor = + page + .a() + .amend( + cls := "text-white px-3 py-2 rounded-md text-sm font-medium", + cls := Seq( + "bg-indigo-700" -> page.active, + "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active + ), + ariaCurrent := (if page.active then "page" else "false") + ) + + def logoImg: Image = + img( + cls := "h-8 w-8", + src <-- $m.map(_.logo.img), + alt <-- $m.map(_.logo.name) + ) + + def mobileMenuButton = button( + tpe := "button", + cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", + aria.controls := "mobile-menu", + aria.expanded <-- mobileMenuOpen.signal, + span(cls := "sr-only", "Open main menu"), + Icons.outline + .menu() + .amend( + svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") + ), + Icons.outline + .x() + .amend( + svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") + ), + onClick.preventDefault.mapTo( + !mobileMenuOpen.now() + ) --> mobileMenuOpen.writer + ) + + def navBarLeft = + div( + cls := "flex items-center", + div(cls := "flex-shrink-0", logoImg), + div( + desktopOnly, + div( + cls := "ml-10 flex items-baseline space-x-4", + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) + ) + ) + ) + + def navBarRight = + div( + desktopOnly, + div( + cls := "ml-4 flex items-center md:ml-6", + notificationButton, + userProfile + ) + ) + + def navBarMobile = + div( + cls := "-mr-2 flex", + mobileOnly, + mobileMenuButton + ) + + def navBar = + div( + cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", + div( + cls := "flex items-center justify-between h-16", + navBarLeft, + navBarRight, + navBarMobile + ) + ) + + def mobileMenu = + div( + mobileOnly, + cls <-- mobileMenuOpen.signal.map { o => + if (o) "block" else "hidden" + }, + idAttr := "mobile-menu", + div( + cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) + ), + mobileProfile + ) + + nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala new file mode 100644 index 0000000..c4cb10d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala @@ -0,0 +1,19 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.Page +import com.raquo.waypoint.Router +import mdr.pdb.app.Action + +object PageHeader: + def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = + header( + cls := "bg-white shadow-sm", + div( + cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", + h1( + cls := "text-lg leading-6 font-semibold text-gray-900", + Breadcrumbs(actionBus) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala new file mode 100644 index 0000000..aae4539 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala @@ -0,0 +1,30 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.Page +import mdr.pdb.app.Action +import fiftyforms.ui.components.tailwind.Loading +import io.laminext.syntax.core.* + +object PageLayout: + case class ViewModel( + navigation: NavigationBar.ViewModel, + content: Option[HtmlElement] + ) + def apply(actionBus: Observer[Action])( + $m: Signal[ViewModel], + mods: Modifier[HtmlElement]* + )(using router: Router[Page]): HtmlElement = + val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) + div( + cls := "h-full flex flex-col", + NavigationBar($m.map(_.navigation)), + child.maybe <-- router.$currentPage.map(_.isRoot) + .switch(None, Some(PageHeader(actionBus))), + main( + cls := "flex-grow-1 overflow-y-auto", + mods, + child <-- $maybeContent.map(_.getOrElse(Loading)) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/mdr/pdb/app/components/PageLink.scala new file mode 100644 index 0000000..e6e7052 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageLink.scala @@ -0,0 +1,40 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.LinkSupport.* +import mdr.pdb.app.Page +import mdr.pdb.app.Action +import com.raquo.waypoint.Router +import mdr.pdb.app.NavigateTo + +object PageLink: + type ViewModel = Page + def apply($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = + a(mods($m, actions), child.text <-- $m.map(_.title)) + + def apply(m: ViewModel, actions: Observer[Action])(using + router: Router[Page] + ): Anchor = apply(Val(m), actions) + + def container($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = a(mods($m, actions)) + + def container(m: ViewModel, actions: Observer[Action])(using + Router[Page] + ): Anchor = container(Val(m), actions) + + private def mods($m: Signal[Page], actions: Observer[Action])(using + router: Router[Page] + ): Modifier[Anchor] = + Seq( + href <-- $m.map(router.absoluteUrlForPage).recover { case _ => + Some("invalid url") + }, + composeEvents(onClick.noKeyMod.preventDefault)( + _.sample($m) + .map(NavigateTo.apply) + ) --> actions + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala new file mode 100644 index 0000000..7682772 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala @@ -0,0 +1,13 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.pages.dashboard.DashboardPage +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage + +class DashboardPageConnector(actionBus: Observer[Action])(using + router: Router[Page] +): + def apply: HtmlElement = + AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala new file mode 100644 index 0000000..76b9133 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala @@ -0,0 +1,76 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.Parameter +import mdr.pdb.UserInfo +import mdr.pdb.app.pages.detail.DetailParametruPage +import pages.detail.DetailKriteriaPage +import pages.detail.components.DukazyKriteria +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.ParameterCriteria + +object DetailKriteriaPageConnector { + trait AppState { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + } +} + +case class DetailKriteriaPageConnector( + state: DetailKriteriaPageConnector.AppState +)( + $page: Signal[Page.DetailKriteria] +)(using Router[Page]): + val $paramChangeSignal = + $page.splitOne(p => + (p.osobniCislo.value, p.parametr.value, p.kriterium.value) + )((x, _, _) => x) + val $pageChangeSignal = + $paramChangeSignal.map( + FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) + ) + + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + val $merged = + $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => + for { + da <- d + pa <- p + pb <- pa.find(_.id == pc._2) + ka <- pb.criteria.find(_.id == pc._3) + } yield (da, pb, ka) + ) + + def apply: HtmlElement = + AppPage(state.actionBus)( + $merged.map(_.map(buildModel)) + .split(_ => ())((_, s, $s) => + DetailKriteriaPage($s)(state.actionBus.contramap { + case DukazyKriteria.Add => + NavigateTo( + Page.UpravDukazKriteria( + Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), + Page.Titled(s.parametr.id, Some(s.parametr.nazev)), + Page.Titled(s.kriterium.id, Some(s.kriterium.id)) + ) + ) + }) + ), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: Parameter, + k: ParameterCriteria + ): DetailKriteriaPage.ViewModel = + DetailKriteriaPage.ViewModel( + o.toDetailOsoby, + p.toParametr(_ => a()), + k.toKriterium(_ => a()) + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DetailPageConnector.scala new file mode 100644 index 0000000..df3a6b7 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DetailPageConnector.scala @@ -0,0 +1,57 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.UserInfo +import pages.detail.DetailPage +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.pages.detail.components.DetailOsoby +import mdr.pdb.Parameter +import mdr.pdb.app.pages.detail.components.SeznamParametru +import fiftyforms.ui.components.tailwind.Color + +object DetailPageConnector { + trait AppState { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + } +} + +case class DetailPageConnector(state: DetailPageConnector.AppState)( + $page: Signal[Page.Detail] +)(using router: Router[Page]): + val $oscChangeSignal = $page.splitOne(_.osobniCislo.value)((osc, _, _) => osc) + val $pageChangeSignal = + $oscChangeSignal.flatMap(osc => + EventStream.fromSeq(Seq(FetchUserDetails(osc), FetchParameters(osc))) + ) + // TODO: filter the value based on the current osc + // OSC change will fetch new data, but still + // - we need to be sure that what we got is really what we ought to display + // - we want to display stale data accordingly (at least with loading indicator) + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + def apply: HtmlElement = + AppPage(state.actionBus)( + $data.combineWithFn($params)(_ zip _) + .map(_.map(buildModel)) + .split(_ => ())((_, _, s) => DetailPage(s)), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: List[Parameter] + ): DetailPage.ViewModel = + DetailPage.ViewModel( + o.toDetailOsoby, + p.map( + _.toParametr(param => + PageLink.container(Page.DetailParametru(o, param), state.actionBus) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DetailParametruPageConnector.scala new file mode 100644 index 0000000..fa7193d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DetailParametruPageConnector.scala @@ -0,0 +1,67 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.Parameter +import mdr.pdb.UserInfo +import mdr.pdb.app.pages.detail.DetailParametruPage +import pages.detail.DetailParametruPage +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.components.PageLink + +object DetailParametruPageConnector { + trait AppState { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + } +} + +case class DetailParametruPageConnector( + state: DetailParametruPageConnector.AppState +)( + $page: Signal[Page.DetailParametru] +)(using router: Router[Page]): + val $paramChangeSignal = + $page.splitOne(p => (p.osobniCislo.value, p.parametr.value))((x, _, _) => x) + val $pageChangeSignal = + $paramChangeSignal.map(FetchParameter(_, _)) + + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + val $merged = + $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => + for { + da <- d + pa <- p + pb <- pa.find(_.id == pc._2) + } yield (da, pb) + ) + + def apply: HtmlElement = + AppPage(state.actionBus)( + $merged.map(_.map(buildModel)) + .split(_ => ())((_, _, s) => DetailParametruPage(s)), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: Parameter + ): DetailParametruPage.ViewModel = + DetailParametruPage.ViewModel( + o.toDetailOsoby, + p.toParametr(p => + PageLink.container(Page.DetailParametru(o, p), state.actionBus) + ), + p.criteria.map( + _.toKriterium { c => + PageLink.container( + Page.DetailKriteria(o, p, c), + state.actionBus + ) + } + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DirectoryPageConnector.scala new file mode 100644 index 0000000..ab2c045 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DirectoryPageConnector.scala @@ -0,0 +1,34 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.UserInfo +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.components.AppPage + +case class DirectoryPageConnector( + $input: EventStream[List[UserInfo]], + actionBus: Observer[Action] +)(using router: Router[Page]): + val $data = $input.startWithNone + val $actionSignal = EventStream.fromValue(FetchDirectory) + + def apply: HtmlElement = + AppPage(actionBus)( + $data.split(_ => ())((_, _, s) => + pages.directory.DirectoryPage( + s.map( + _.map( + _.toUserRow(u => + PageLink.container( + Page.Detail(Page.Titled(u.personalNumber)), + actionBus + ) + ) + ) + ) + ) + ), + $actionSignal --> actionBus + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/mdr/pdb/app/connectors/conversions.scala new file mode 100644 index 0000000..f581796 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/conversions.scala @@ -0,0 +1,68 @@ +package mdr.pdb.app.connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.UserInfo +import mdr.pdb.app.pages.detail.components.DetailOsoby +import mdr.pdb.Parameter +import mdr.pdb.app.pages.detail.components.SeznamParametru +import mdr.pdb.ParameterCriteria +import mdr.pdb.app.pages.detail.components.SeznamKriterii +import mdr.pdb.app.pages.directory.components.UserRow +import mdr.pdb.app.pages.detail.components.DetailParametru +import mdr.pdb.app.pages.detail.components.DetailKriteria +import fiftyforms.ui.components.tailwind.Color + +extension (o: UserInfo) + def toDetailOsoby: DetailOsoby.ViewModel = + DetailOsoby.ViewModel( + o.personalNumber, + o.name, + o.email, + o.phone, + o.img, + o.mainFunction.map(f => + DetailOsoby.Funkce.ViewModel(f.name, f.dept, f.ou) + ), + o.userContracts.headOption.map(c => + DetailOsoby.PracovniPomer.ViewModel(c.rel, c.startDate, c.endDate) + ) + ) + +extension (param: Parameter) + def toParametr(container: Parameter => Anchor): DetailParametru.ViewModel = + DetailParametru.ViewModel( + id = param.id, + nazev = param.name, + popis = param.description, + status = "Nesplněno", + statusColor = Color.red, + a = container(param) + ) + +extension (crit: ParameterCriteria) + def toKriterium( + container: ParameterCriteria => Anchor + ): DetailKriteria.ViewModel = + DetailKriteria.ViewModel( + nazev = crit.criteriumText, + kapitola = crit.chapterId, + bod = crit.itemId, + status = "Nesplněno", + statusColor = Color.red, + splneno = false, + dukazy = Nil, + container = container(crit) + ) + +extension (user: UserInfo) + def toUserRow( + container: UserInfo => HtmlElement = (_: UserInfo) => div() + ): UserRow.ViewModel = + UserRow.ViewModel( + osobniCislo = user.personalNumber.toString, + celeJmeno = user.name, + prijmeni = user.surname, + hlavniFunkce = user.mainFunction.map(_.name), + img = user.img, + container = () => container(user) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/dashboard/DashboardPage.scala b/app/src/main/scala/mdr/pdb/app/pages/dashboard/DashboardPage.scala new file mode 100644 index 0000000..0eb8f7a --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/dashboard/DashboardPage.scala @@ -0,0 +1,11 @@ +package mdr.pdb.app.pages.dashboard + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.Page + +object DashboardPage: + + def render: HtmlElement = + div("Dashboard page") diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala new file mode 100644 index 0000000..ff3a25f --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala @@ -0,0 +1,31 @@ +package mdr.pdb.app.pages.detail + +import com.raquo.laminar.api.L.{*, given} + +import components._ + +object DetailKriteriaPage: + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametr: DetailParametru.ViewModel, + kriterium: DetailKriteria.ViewModel + ) + + def apply($m: Signal[ViewModel])( + action: Observer[DukazyKriteria.Action] + ): HtmlElement = + div( + cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + div( + DetailOsoby.header($m.map(_.osoba)), + DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") + ), + div( + DetailKriteria($m.map(_.kriterium)), + DukazyKriteria($m.map(_.kriterium.dukazy))(action) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/DetailPage.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailPage.scala new file mode 100644 index 0000000..b591cc9 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailPage.scala @@ -0,0 +1,25 @@ +package mdr.pdb.app.pages.detail + +import com.raquo.laminar.api.L.{*, given} + +import components._ +import mdr.pdb.app.Page +import com.raquo.waypoint.Router +import mdr.pdb.app.Action + +object DetailPage: + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametry: SeznamParametru.ViewModel + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + DetailOsoby($m.map(_.osoba)), + SeznamParametru($m.map(_.parametry)) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/DetailParametruPage.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailParametruPage.scala new file mode 100644 index 0000000..3322279 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailParametruPage.scala @@ -0,0 +1,24 @@ +package mdr.pdb.app.pages.detail + +import com.raquo.laminar.api.L.{*, given} + +import components._ + +object DetailParametruPage: + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametr: DetailParametru.ViewModel, + kriteria: SeznamKriterii.ViewModel + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + DetailOsoby.header($m.map(_.osoba)), + DetailParametru($m.map(_.parametr)), + SeznamKriterii($m.map(_.kriteria)) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala new file mode 100644 index 0000000..b58618a --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala @@ -0,0 +1,107 @@ +package mdr.pdb +package app +package pages.detail + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.pages.detail.components.UpravDukazForm +import fiftyforms.services.files.File + +object UpravDukaz: + + type ThisPage = Page.UpravDukazKriteria + type PageKey = (OsobniCislo, String, String) + + trait State { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def availableFiles: EventStream[List[File]] + def actionBus: Observer[Action] + } + + def keyOfPage(page: ThisPage): PageKey = + (page.osobniCislo.value, page.parametr.value, page.kriterium.value) + + def onChangeAction(key: PageKey): Action = + FetchParameterCriteria( + key._1, + key._2, + key._3, + Page.UpravDukazKriteria(_, _, _) + ) + + class Connector(state: State)($page: Signal[ThisPage])(using Router[Page]): + val $paramChangeSignal = $page.splitOne(keyOfPage)((x, _, _) => x) + val $pageChangeSignal = $paramChangeSignal.map(onChangeAction) + + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + val $merged = + $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => + for { + da <- d + pa <- p + pb <- pa.find(_.id == pc._2) + ka <- pb.criteria.find(_.id == pc._3) + } yield (da, pb, ka) + ) + + def apply: HtmlElement = + AppPage(state.actionBus)( + $merged.split(_ => ())((_, s, $s) => + PageComponent( + $s.map(buildModel), + state.availableFiles, + state.actionBus.contramap { + case UpravDukazForm.Cancelled => + NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) + case UpravDukazForm.AvailableFilesRequested => + FetchAvailableFiles(s._1.personalNumber) + } + ) + ), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: Parameter, + k: ParameterCriteria + ): PageComponent.ViewModel = + import connectors.* + PageComponent.ViewModel( + o.toDetailOsoby, + p.toParametr(_ => a()), + k.toKriterium(_ => a()) + ) + + object PageComponent: + import components.* + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametr: DetailParametru.ViewModel, + kriterium: DetailKriteria.ViewModel + ) + + def apply( + $m: Signal[ViewModel], + availableFilesStream: EventStream[List[File]], + events: Observer[UpravDukazForm.Event] + ): HtmlElement = + div( + cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + div( + DetailOsoby.header($m.map(_.osoba)), + DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") + ), + div( + DetailKriteria($m.map(_.kriterium)), + UpravDukazForm(availableFilesStream)(events) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailKriteria.scala new file mode 100644 index 0000000..a3ab634 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailKriteria.scala @@ -0,0 +1,37 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.CustomAttrs +import fiftyforms.ui.components.tailwind.Icons +import java.time.LocalDate +import fiftyforms.ui.components.tailwind.Color + +object DetailKriteria: + case class ViewModel( + nazev: String, + kapitola: String, + bod: String, + status: String, + statusColor: Color, + splneno: Boolean, + dukazy: List[DukazKriteria.ViewModel], + container: HtmlElement = div() + ) { + val id = s"${kapitola}${bod}" + } + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "pb-5 border-b border-gray-200", + h3( + cls := "text-xl leading-6 font-bold text-gray-900", + child.text <-- $m.map(_.nazev) + ), + h3( + cls := "mt-2 max-w-4xl text-sm text-gray-500", + "Kapitola ", + child.text <-- $m.map(_.kapitola), + " bod ", + child <-- $m.map(_.bod) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailOsoby.scala new file mode 100644 index 0000000..0fe2ba5 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailOsoby.scala @@ -0,0 +1,100 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.OsobniCislo + +import java.time.LocalDate +import fiftyforms.ui.components.tailwind.Avatar + +object DetailOsoby: + + object Funkce: + case class ViewModel( + nazev: String, + stredisko: String, + voj: String + ) + + def render($m: Signal[ViewModel]): HtmlElement = + p( + cls := "text-sm font-medium text-gray-500", + child.text <-- $m.map(_.nazev), + span( + cls := "hidden md:inline", + " @ ", + child.text <-- $m.map(_.stredisko), + ", ", + child.text <-- $m.map(_.voj) + ) + ) + + object PracovniPomer: + case class ViewModel( + druh: String, + pocatek: LocalDate, + konec: Option[LocalDate] + ) + + def render($m: Signal[ViewModel]): HtmlElement = + import fiftyforms.ui.components.tailwind.CustomAttrs.datetime + p( + cls := "text-sm font-medium text-gray-500", + child.text <-- $m.map(_.druh), + " od ", + time( + datetime <-- $m.map(_.pocatek.toString), + child.text <-- $m.map(_.pocatek.toString) + ) + ) + + case class ViewModel( + osobniCislo: OsobniCislo, + jmeno: String, + email: Option[String], + telefon: Option[String], + img: Option[String], + hlavniFunkce: Option[Funkce.ViewModel], + pracovniPomer: Option[PracovniPomer.ViewModel] + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "md:flex md:items-center md:justify-between md:space-x-4", + div( + cls := "flex items-start space-x-4", + div( + cls := "flex-shrink-0", + Avatar($m.map(_.img)).avatar(16) + ), + div( + h1( + cls := "text-2xl font-bold text-gray-900", + child.text <-- $m.map(_.jmeno) + ), + child.maybe <-- $m.map(_.hlavniFunkce).split(_ => ())((_, _, d) => + Funkce.render(d) + ), + child.maybe <-- $m.map(_.pracovniPomer).split(_ => ())((_, _, d) => + PracovniPomer.render(d) + ) + ) + ) + ) + + def header($m: Signal[ViewModel]): HtmlElement = + div( + cls := "md:flex md:items-center md:justify-between md:space-x-5", + div( + cls := "flex items-start space-x-4", + div( + cls := "flex-shrink-0", + Avatar($m.map(_.img)).avatar(8) + ), + div( + h1( + cls := "text-lg font-medium text-gray-800", + child.text <-- $m.map(_.jmeno) + ) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailParametru.scala new file mode 100644 index 0000000..6310dde --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailParametru.scala @@ -0,0 +1,34 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.Color + +object DetailParametru: + case class ViewModel( + id: String, + nazev: String, + popis: String, + status: String, + statusColor: Color, + a: Anchor + ) + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "pb-5 border-b border-gray-200", + h2( + cls := "text-2xl leading-6 font-medium text-gray-900", + child.text <-- $m.map(_.nazev) + ), + p( + cls := "mt-2 max-w-4xl text-sm text-gray-500", + child.text <-- $m.map(_.popis) + ) + ) + + def header($m: Signal[ViewModel]): HtmlElement = + div( + h2( + cls := "text-lg leading-6 font-medium text-gray-600", + child.text <-- $m.map(_.nazev) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazKriteria.scala new file mode 100644 index 0000000..c292919 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazKriteria.scala @@ -0,0 +1,177 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import java.time.LocalDate +import fiftyforms.ui.components.tailwind.CustomAttrs + +object DukazKriteria: + case class Osoba(osobniCislo: String, jmeno: String) + case class Udalost(osoba: Osoba, datum: LocalDate) + case class Dokument( + url: String, + nazev: String, + pridal: Udalost, + odebral: Option[Udalost] + ) + case class ViewModel( + dokumenty: List[Dokument], + autorizoval: Option[Udalost], + platiDo: Option[LocalDate], + poznámka: Option[String] + ) + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "bg-white shadow overflow-hidden sm:rounded-lg", + div( + cls := "px-4 py-5 sm:px-6", + h3( + cls := "text-lg leading-6 font-medium text-gray-900", + """Applicant Information""" + ), + p( + cls := "mt-1 max-w-2xl text-sm text-gray-500", + """Personal details and application.""" + ) + ), + div( + cls := "border-t border-gray-200 px-4 py-5 sm:px-6", + dl( + cls := "grid grid-cols-1 gap-x-4 gap-y-8 sm:grid-cols-2", + div( + cls := "sm:col-span-1", + dt( + cls := "text-sm font-medium text-gray-500", + """Full name""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """Margot Foster""" + ) + ), + div( + cls := "sm:col-span-1", + dt( + cls := "text-sm font-medium text-gray-500", + """Application for""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """Backend Developer""" + ) + ), + div( + cls := "sm:col-span-1", + dt( + cls := "text-sm font-medium text-gray-500", + """Email address""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """margotfoster@example.com""" + ) + ), + div( + cls := "sm:col-span-1", + dt( + cls := "text-sm font-medium text-gray-500", + """Salary expectation""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """$120,000""" + ) + ), + div( + cls := "sm:col-span-2", + dt( + cls := "text-sm font-medium text-gray-500", + """About""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """Fugiat ipsum ipsum deserunt culpa aute sint do nostrud anim incididunt cillum culpa consequat. Excepteur qui ipsum aliquip consequat sint. Sit id mollit nulla mollit nostrud in ea officia proident. Irure nostrud pariatur mollit ad adipisicing reprehenderit deserunt qui eu.""" + ) + ), + div( + cls := "sm:col-span-2", + dt( + cls := "text-sm font-medium text-gray-500", + """Attachments""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + ul( + role := "list", + cls := "border border-gray-200 rounded-md divide-y divide-gray-200", + li( + cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", + div( + cls := "w-0 flex-1 flex items-center", { + import svg.* + import CustomAttrs.svg.ariaHidden + svg( + cls := "flex-shrink-0 h-5 w-5 text-gray-400", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + ariaHidden := true, + path( + fillRule := "evenodd", + d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", + clipRule := "evenodd" + ) + ) + }, + span( + cls := "ml-2 flex-1 w-0 truncate", + """resume_back_end_developer.pdf""" + ) + ), + div( + cls := "ml-4 flex-shrink-0", + a( + href := "#", + cls := "font-medium text-indigo-600 hover:text-indigo-500", + """Download""" + ) + ) + ), + li( + cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", + div( + cls := "w-0 flex-1 flex items-center", { + import svg.* + import CustomAttrs.svg.ariaHidden + svg( + cls := "flex-shrink-0 h-5 w-5 text-gray-400", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + ariaHidden := true, + path( + fillRule := "evenodd", + d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", + clipRule := "evenodd" + ) + ) + }, + span( + cls := "ml-2 flex-1 w-0 truncate", + """coverletter_back_end_developer.pdf""" + ) + ), + div( + cls := "ml-4 flex-shrink-0", + a( + href := "#", + cls := "font-medium text-indigo-600 hover:text-indigo-500", + """Download""" + ) + ) + ) + ) + ) + ) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala new file mode 100644 index 0000000..d604860 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala @@ -0,0 +1,35 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.Icons + +object DukazyKriteria: + sealed trait Action + case object Add extends Action + + type ViewModel = List[DukazKriteria.ViewModel] + + def apply($m: Signal[ViewModel])(actions: Observer[Action]): HtmlElement = + div( + child.maybe <-- $m.map(m => + if (m.isEmpty) then Some(prazdnyDukaz(actions)) + else None + ), + children <-- $m.map(_.zipWithIndex).split(_._2)((_, _, $s) => + DukazKriteria($s.map(_._1)) + ) + ) + + private def prazdnyDukaz(actions: Observer[Action]): HtmlElement = + button( + tpe := "button", + onClick.preventDefault.mapTo(Add) --> actions, + cls := "relative block w-full border-2 border-gray-300 border-dashed rounded-lg p-12 text-center hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + Icons.outline + .`document-add`(12) + .amend(svg.cls := "mx-auto text-gray-400"), + span( + cls := "mt-2 block text-sm font-medium text-gray-900", + "Přidat důkaz" + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala new file mode 100644 index 0000000..7e77a7b --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala @@ -0,0 +1,34 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.Color +import fiftyforms.ui.components.tailwind.list.StackedList +import fiftyforms.ui.components.tailwind.list.ListRow +import fiftyforms.ui.components.tailwind.list.RowTag +import fiftyforms.ui.components.tailwind.list.RowNext +import java.time.LocalDate + +object SeznamKriterii: + type ViewModel = List[DetailKriteria.ViewModel] + + private val kritList = new StackedList[DetailKriteria.ViewModel] + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "bg-white shadow overflow-hidden sm:rounded-md", + kritList($m, _.id) { $i => + $i.map { i => + ListRow.ViewModel( + title = i.nazev, + topRight = RowTag.render( + $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) + ), + bottomLeft = + p(cls := "text-sm text-gray-500", s"${i.kapitola}${i.bod}"), + bottomRight = emptyNode, + farRight = RowNext.render, + containerElement = i.container + ) + } + } + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamParametru.scala new file mode 100644 index 0000000..0760b32 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamParametru.scala @@ -0,0 +1,37 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.list.{ + StackedList, + ListRow, + RowTag, + PropList, + IconText, + RowNext +} +import fiftyforms.ui.components.tailwind.Color +import fiftyforms.ui.components.tailwind.LinkSupport.* + +object SeznamParametru: + type ViewModel = List[DetailParametru.ViewModel] + + private val parametrList = new StackedList[DetailParametru.ViewModel] + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "bg-white shadow overflow-hidden sm:rounded-md", + parametrList($m, _.id) { $i => + $i.map { i => + ListRow.ViewModel( + title = i.nazev, + topRight = RowTag.render( + $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) + ), + bottomLeft = emptyNode, + bottomRight = emptyNode, + farRight = RowNext.render, + containerElement = i.a + ) + } + } + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala new file mode 100644 index 0000000..28753c9 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala @@ -0,0 +1,101 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import io.laminext.syntax.core.* +import fiftyforms.ui.components.tailwind.CustomAttrs +import fiftyforms.ui.components.tailwind.form.* +import org.scalajs.dom +import com.raquo.laminar.nodes.ReactiveHtmlElement +import fiftyforms.services.files.components.tailwind.FilePicker +import mdr.pdb.frontend.AutorizujDukaz +import mdr.pdb.frontend.DocumentRef +import fiftyforms.services.files.File + +object UpravDukazForm: + sealed trait Event + case object Cancelled extends Event + case object AvailableFilesRequested extends Event + def apply(availableFilesStream: EventStream[List[File]])( + updates: Observer[Event] + ): HtmlElement = + val (filesStream, filesObserver) = + EventStream.withObserver[FilePicker.Event] + val files = Var[List[File]](Nil) + def submitButtons: HtmlElement = + div( + cls := "pt-5", + div( + cls := "flex justify-end", + button( + tpe := "button", + cls := "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + "Zrušit", + onClick.mapTo(Cancelled) --> updates + ), + button( + tpe := "submit", + cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + disabled <-- files.signal.map(_.isEmpty), + "Autorizovat" + ) + ) + ) + + div( + filesStream.collect { case FilePicker.SelectionUpdated(files) => + files.to(List) + } --> files.writer, + filesStream.collect { case FilePicker.AvailableFilesRequested => + AvailableFilesRequested + } --> updates, + cls := "bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6", + Form( + Form.Body( + Form.Section( + FormHeader( + FormHeader.ViewModel( + "Doložení kritéria", + "Sestavte doklady poskytující důkaz kritéria a potvrďte odesláním formuláře. Případné limitace či jiné relevantní údaje vepište do pole Komentář." + ) + ), + FormFields( + FormRow( + "dokumenty", + "Dokumenty", + FilePicker(files.signal, availableFilesStream)(filesObserver) + .amend(idAttr := "dokumenty", cls("max-w-lg")) + ).toHtml, + FormRow( + "platnost", + "Platnost", + input( + idAttr := "platnost", + name := "platnost", + tpe := "date", + autoComplete := "date", + cls := "block max-w-lg w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" + ) + ).toHtml, + FormRow( + "komentar", + "Komentář", + div( + cls := "mt-1 sm:mt-0 sm:col-span-2", + textArea( + idAttr := "komentar", + name := "about", + rows := 3, + cls := "max-w-lg shadow-sm block w-full focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border border-gray-300 rounded-md" + ), + p( + cls := "mt-2 text-sm text-gray-500", + "Doplňte prosím potřebné informace související s doložením kritéria, včetně případných limitací." + ) + ) + ).toHtml + ) + ) + ), + submitButtons + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/directory/DirectoryPage.scala b/app/src/main/scala/mdr/pdb/app/pages/directory/DirectoryPage.scala new file mode 100644 index 0000000..7a72ca3 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/directory/DirectoryPage.scala @@ -0,0 +1,40 @@ +package mdr.pdb.app.pages.directory + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.UserInfo + +import components._ + +object DirectoryPage: + + type ViewModel = List[UserRow.ViewModel] + + def apply($m: Signal[ViewModel]): HtmlElement = + val (actionsStream, actionObserver) = + EventStream.withObserver[SearchForm.Action] + val $filter = actionsStream + .collect { + case SearchForm.ClearFilter => None + case SearchForm.SetFilter(t) => Some(t) + } + .startWith(None) + val byLetter = for { + d <- $m + f <- $filter + } yield for { + (letter, users) <- d + .filter { user => + f match + case None => true + case Some(t) => user.search.contains(t) + } + .groupBy(_.prijmeni.head) + .to(List) + .sortBy(_._1) + } yield (letter.toString, users.sortBy(_.prijmeni)) + + div( + cls := "h-full max-w-7xl mx-auto order-first flex flex-col flex-shrink-0", + SearchForm(actionObserver), + Directory(byLetter) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/directory/components/Directory.scala b/app/src/main/scala/mdr/pdb/app/pages/directory/components/Directory.scala new file mode 100644 index 0000000..f7d6f99 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/directory/components/Directory.scala @@ -0,0 +1,37 @@ +package mdr.pdb.app.pages.directory.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.UserInfo + +object Directory: + + object Header: + type ViewModel = String + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500", + h3(child.text <-- $m) + ) + + object UserList: + type ViewModel = List[UserRow.ViewModel] + def apply($m: Signal[ViewModel]): HtmlElement = + ul( + role := "list", + cls := "relative z-0 divide-y divide-gray-200", + children <-- $m.split(_.osobniCislo)((_, _, s) => UserRow(s)) + ) + + type ViewModel = List[(String, List[UserRow.ViewModel])] + def apply($m: Signal[ViewModel]): HtmlElement = + nav( + cls := "flex-1 min-h-0 overflow-y-auto", + aria.label := "Directory", + children <-- $m.split(_._1)((_, _, s) => + div( + cls := "relative", + Header(s.map(_._1)), + UserList(s.map(_._2)) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/directory/components/SearchForm.scala b/app/src/main/scala/mdr/pdb/app/pages/directory/components/SearchForm.scala new file mode 100644 index 0000000..fb9ecc4 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/directory/components/SearchForm.scala @@ -0,0 +1,54 @@ +package mdr.pdb.app.pages.directory.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.Icons + +object SearchForm: + sealed trait Action + case object SubmitFilter extends Action + sealed trait FilterAction extends Action + case object ClearFilter extends FilterAction + case class SetFilter(value: String) extends FilterAction + + def apply(actions: Observer[Action]): HtmlElement = + div( + cls := "px-6 pt-4 pb-4", + form( + cls := "flex space-x-4", + onSubmit.mapTo(SubmitFilter) --> actions, + div( + cls := "flex-1 min-w-0", + label( + forId := "search", + cls := "sr-only", + "Hledat" + ), + div( + cls := "relative rounded-md shadow-sm", + div( + cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", + Icons.solid.search().amend(svg.cls := "text-gray-400") + ), + input( + tpe := "search", + name := "search", + idAttr := "search", + cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", + placeholder := "Hledat", + composeEvents(onInput.mapToValue.setAsValue.map(SetFilter(_)))( + _.throttle(500) + ) --> actions + ) + ) + ), + button( + tpe := "submit", + cls := "inline-flex justify-center px-3.5 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500", + Icons.solid.filter().amend(svg.cls := "text-gray-400"), + span( + cls := "sr-only", + "Hledat" + ) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/directory/components/UserRow.scala b/app/src/main/scala/mdr/pdb/app/pages/directory/components/UserRow.scala new file mode 100644 index 0000000..4aa1672 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/directory/components/UserRow.scala @@ -0,0 +1,52 @@ +package mdr.pdb.app.pages.directory.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.airstream.core.Signal +import fiftyforms.ui.components.tailwind.Avatar + +object UserRow: + case class ViewModel( + osobniCislo: String, + celeJmeno: String, + prijmeni: String, + hlavniFunkce: Option[String], + img: Option[String], + container: () => HtmlElement = () => div() + ) { + val search = prijmeni.toLowerCase + } + + def apply($m: Signal[ViewModel]): HtmlElement = + inline def avatarImage = + Avatar($m.map(_.img)).avatarImage(10) + + li( + div( + cls := "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500", + div( + cls := "flex-shrink-0", + child <-- avatarImage + ), + div( + cls := "flex-1 min-w-0", + child <-- $m.map { o => + o.container() + .amend( + cls := "focus:outline-none", + span( + cls := "absolute inset-0", + aria.hidden := true + ), + p( + cls := "text-sm font-medium text-gray-900", + o.celeJmeno + ), + p( + cls := "text-sm text-gray-500 truncate", + o.hlavniFunkce + ) + ) + } + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/errors/ErrorPage.scala b/app/src/main/scala/mdr/pdb/app/pages/errors/ErrorPage.scala new file mode 100644 index 0000000..6b195ef --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/errors/ErrorPage.scala @@ -0,0 +1,99 @@ +package mdr.pdb.app.pages.errors + +import com.raquo.domtypes.generic.codecs.StringAsIsCodec +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.Page +import mdr.pdb.app.Action +import com.raquo.waypoint.Router + +object ErrorPage: + case class ViewModel( + homePage: Page, + errorName: String, + title: String, + subTitle: String + ) + + def apply(m: ViewModel, actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + val ViewModel(homePage, errorName, title, subTitle) = m + div( + cls := "min-h-full pt-16 pb-12 flex flex-col bg-white", + main( + cls := "flex-grow flex flex-col justify-center max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", + div( + cls := "flex-shrink-0 flex justify-center", + a( + href := "/", + cls := "inline-flex", + span( + cls := "sr-only", + """Workflow""" + ), + img( + cls := "h-12 w-auto", + src := "https://tailwindui.com/img/logos/workflow-mark.svg?color=indigo&shade=600", + alt := "" + ) + ) + ), + div( + cls := "py-16", + div( + cls := "text-center", + p( + cls := "text-sm font-semibold text-indigo-600 uppercase tracking-wide", + errorName + ), + h1( + cls := "mt-2 text-4xl font-extrabold text-gray-900 tracking-tight sm:text-5xl", + title + ), + p( + cls := "mt-2 text-base text-gray-500", + subTitle + ), + div( + cls := "mt-6", + PageLink + .container(homePage, actionBus) + .amend( + cls := "text-base font-medium text-indigo-600 hover:text-indigo-500", + """Go back home""" + ) + ) + ) + ) + ), + footer( + cls := "flex-shrink-0 max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", + nav( + cls := "flex justify-center space-x-4", + a( + href := "#", + cls := "text-sm font-medium text-gray-500 hover:text-gray-600", + """Contact Support""" + ), + span( + cls := "inline-block border-l border-gray-300", + aria.hidden := true + ), + a( + href := "#", + cls := "text-sm font-medium text-gray-500 hover:text-gray-600", + """Status""" + ), + span( + cls := "inline-block border-l border-gray-300", + aria.hidden := true + ), + a( + href := "#", + cls := "text-sm font-medium text-gray-500 hover:text-gray-600", + """Twitter""" + ) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/errors/NotFoundPage.scala b/app/src/main/scala/mdr/pdb/app/pages/errors/NotFoundPage.scala new file mode 100644 index 0000000..97760e1 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/errors/NotFoundPage.scala @@ -0,0 +1,21 @@ +package mdr.pdb.app.pages.errors + +import com.raquo.domtypes.generic.codecs.StringAsIsCodec +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.Page +import mdr.pdb.app.Action + +object NotFoundPage: + def apply(homePage: Page, url: String, actionBus: Observer[Action])(using + router: Router[Page] + ): HtmlElement = + ErrorPage( + ErrorPage.ViewModel( + homePage, + "404 error", + "Page not found.", + s"Sorry, but page $url doesn't exist." + ), + actionBus + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala b/app/src/main/scala/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala new file mode 100644 index 0000000..77b0af8 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala @@ -0,0 +1,32 @@ +package mdr.pdb.app.pages.errors + +import com.raquo.domtypes.generic.codecs.StringAsIsCodec +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.Page +import mdr.pdb.app.Action + +object UnhandledErrorPage: + + case class ViewModel( + homePage: Page, + errorName: Option[String], + errorMessage: Option[String] + ) + + def apply(m: ViewModel, actionBus: Observer[Action])(using + router: Router[Page] + ): HtmlElement = + ErrorPage( + ErrorPage.ViewModel( + m.homePage, + "Unexpected error occurred", + m.errorName.getOrElse( + "Uh oh!" + ), // TODO: translations, better text than uh oh + m.errorMessage.getOrElse( + "This wasn't supposed to happen! Please try again." + ) + ), + actionBus + ) diff --git a/app/src/main/scala/mdr/pdb/app/services/DataFetcher.scala b/app/src/main/scala/mdr/pdb/app/services/DataFetcher.scala new file mode 100644 index 0000000..134c221 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/services/DataFetcher.scala @@ -0,0 +1,6 @@ +package mdr.pdb.app.services + +import com.raquo.airstream.core.Observer + +trait DataFetcher[K, A]: + def fetch(id: K, sink: Observer[A]): Unit diff --git a/app/src/main/scala/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/mdr/pdb/app/state/AppState.scala new file mode 100644 index 0000000..e44b2df --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/state/AppState.scala @@ -0,0 +1,120 @@ +package mdr.pdb.app +package state + +import com.raquo.airstream.core.EventStream +import mdr.pdb.{UserInfo, OsobniCislo} +import com.raquo.airstream.core.Observer +import scala.scalajs.js +import scala.scalajs.js.JSON +import zio.json._ +import com.raquo.airstream.eventbus.EventBus +import com.raquo.airstream.ownership.Owner +import com.raquo.waypoint.Router +import mdr.pdb.Parameter +import mdr.pdb.ParameterCriteria +import mdr.pdb.UserFunction +import mdr.pdb.UserContract +import fiftyforms.services.files.File + +trait AppState + extends connectors.DetailPageConnector.AppState + with connectors.DetailParametruPageConnector.AppState + with connectors.DetailKriteriaPageConnector.AppState + with pages.detail.UpravDukaz.State: + def users: EventStream[List[UserInfo]] + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + +class MockAppState(implicit owner: Owner, router: Router[Page]) + extends AppState: + + given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) + given JsonDecoder[UserFunction] = DeriveJsonDecoder.gen + given JsonDecoder[UserContract] = DeriveJsonDecoder.gen + given JsonDecoder[UserInfo] = DeriveJsonDecoder.gen + + given JsonDecoder[ParameterCriteria] = DeriveJsonDecoder.gen + given JsonDecoder[Parameter] = DeriveJsonDecoder.gen + + private val actions = EventBus[Action]() + private val (parametersStream, pushParameters) = + EventStream.withCallback[List[Parameter]] + private val (usersStream, pushUsers) = + EventStream.withCallback[List[UserInfo]] + private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] + private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] + + private val mockData: List[UserInfo] = + mockUsers + .asInstanceOf[js.Dictionary[js.Object]] + .values + // TODO: is there a more efficient way to parse from JS object directly? + .map { o => + val parsed = JSON.stringify(o).fromJson[UserInfo] + parsed.left.foreach(org.scalajs.dom.console.log(_)) + parsed + } + .collect { case Right(u) => + u + } + .toList + + private val mockParameters: List[Parameter] = + pdbParams + .asInstanceOf[js.Dictionary[js.Object]] + .values + .map(o => JSON.stringify(o).fromJson[Parameter]) + .collect { case Right(p) => p } + .toList + + // TODO: Extract to separate event handler + actions.events.foreach { + case FetchDirectory => pushUsers(mockData) + case FetchUserDetails(osc) => + mockData.find(_.personalNumber == osc).foreach { o => + pushDetails(o) + router.replaceState(Page.Detail(o)) + } + case FetchParameters(osc) => + pushParameters(mockParameters) + case FetchParameter(osc, paramId) => + for + o <- mockData.find(_.personalNumber == osc) + p <- mockParameters.find(_.id == paramId) + do + pushDetails(o) + pushParameters(mockParameters) + router.replaceState(Page.DetailParametru(o, p)) + case FetchParameterCriteria(osc, paramId, critId, page) => + for + o <- mockData.find(_.personalNumber == osc) + p <- mockParameters.find(_.id == paramId) + c <- p.criteria.find(_.id == critId) + do + pushDetails(o) + pushParameters(mockParameters) + router.replaceState(page(o, p, c)) + case NavigateTo(page) => router.pushState(page) + case FetchAvailableFiles(osc) => + pushFiles( + List( + File("https://tc163.cmi.cz/here", "Example file") + ) + ) + } + + override def users: EventStream[List[UserInfo]] = + usersStream.debugWithName("users") + + override def details: EventStream[UserInfo] = + detailsStream.debugWithName("details") + + override def parameters: EventStream[List[Parameter]] = + parametersStream.debugWithName("parameters") + + override def availableFiles: EventStream[List[File]] = + filesStream.debugWithName("available files") + + override def actionBus: Observer[Action] = + actions.writer.debugWithName("actions writer") diff --git a/build.sbt b/build.sbt index 2afc879..0daf796 100644 --- a/build.sbt +++ b/build.sbt @@ -11,6 +11,12 @@ lazy val core = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Pure) .in(file("core")) + .settings( + IWDeps.useZIO(Test), + IWDeps.tapirCore, + IWDeps.tapirZIO, + IWDeps.tapirZIOJson + ) lazy val ui = (project in file("ui")) .enablePlugins(ScalaJSPlugin) diff --git a/core/src/main/scala/cz/e_bs/cmi/mdr/pdb/Parameter.scala b/core/src/main/scala/cz/e_bs/cmi/mdr/pdb/Parameter.scala deleted file mode 100644 index 9d68240..0000000 --- a/core/src/main/scala/cz/e_bs/cmi/mdr/pdb/Parameter.scala +++ /dev/null @@ -1,27 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb - -import java.math.BigInteger -import java.security.MessageDigest - -object ParameterCriteria { - type Id = String -} - -case class ParameterCriteria( - chapterId: String, - itemId: String, - criteriumText: String -) { - val id: ParameterCriteria.Id = s"${chapterId}${itemId}" -} - -object Parameter { - type Id = String -} - -case class Parameter( - id: Parameter.Id, - name: String, - description: String, - criteria: List[ParameterCriteria] -) diff --git a/core/src/main/scala/cz/e_bs/cmi/mdr/pdb/UserProfile.scala b/core/src/main/scala/cz/e_bs/cmi/mdr/pdb/UserProfile.scala deleted file mode 100644 index 455a445..0000000 --- a/core/src/main/scala/cz/e_bs/cmi/mdr/pdb/UserProfile.scala +++ /dev/null @@ -1,44 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb - -import java.time.LocalDate - -opaque type OsobniCislo = String - -object OsobniCislo: - // TODO: validation - def apply(osc: String): OsobniCislo = osc - -extension (osc: OsobniCislo) def toString: String = osc - -case class UserContract( - rel: String, - startDate: LocalDate, - endDate: Option[LocalDate] -) -case class UserFunction(name: String, dept: String, ou: String) - -case class UserInfo( - personalNumber: OsobniCislo, - username: String, - givenName: String, - surname: String, - titlesBeforeName: Option[String] = None, - titlesAfterName: Option[String] = None, - email: Option[String] = None, - phone: Option[String] = None, - mainFunction: Option[UserFunction] = None, - userContracts: List[UserContract] = Nil, - img: Option[String] = None -) { - val name = - List( - Some( - List(titlesBeforeName, Some(givenName), Some(surname)).flatten.mkString( - " " - ) - ), - titlesAfterName - ).flatten.mkString(", ") -} - -case class UserProfile(username: String, userInfo: UserInfo) diff --git a/core/src/main/scala/cz/e_bs/cmi/mdr/pdb/frontend/domain.scala b/core/src/main/scala/cz/e_bs/cmi/mdr/pdb/frontend/domain.scala deleted file mode 100644 index 10980c5..0000000 --- a/core/src/main/scala/cz/e_bs/cmi/mdr/pdb/frontend/domain.scala +++ /dev/null @@ -1,17 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb -package frontend - -import java.time.LocalDate -import java.time.Instant - -sealed trait Command - -type DocumentRef = String - -case class AutorizujDukaz( - osoba: OsobniCislo, - parametr: Parameter.Id, - kriterium: ParameterCriteria.Id, - dukaz: List[DocumentRef], - platiDo: Option[LocalDate] -) extends Command diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala deleted file mode 100644 index a3b00af..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala +++ /dev/null @@ -1,103 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import scala.scalajs.js.annotation.JSExportTopLevel -import scala.scalajs.js.annotation.JSExport -import scala.scalajs.js.annotation.JSImport -import scala.scalajs.js -import org.scalajs.dom -import com.raquo.laminar.api.L.{*, given} -import scala.scalajs.js.Date -import com.raquo.waypoint.Router -import com.raquo.waypoint.SplitRender -import cz.e_bs.cmi.mdr.pdb.app.services.DataFetcher -import scala.scalajs.js.JSON -import zio.json._ -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.state.AppState - -@js.native -@JSImport("stylesheets/main.css", JSImport.Namespace) -object Css extends js.Any - -@js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native -@JSImport("params/pdb-params.json", JSImport.Default) -object pdbParams extends js.Object - -@JSExportTopLevel("app") -object Main: - - @JSExport - def main(args: Array[String]): Unit = - import Routes.given - onLoad { - setupAirstream() - val appContainer = dom.document.querySelector("#app") - val _ = - render( - appContainer, - renderPage(state.MockAppState(using unsafeWindowOwner, router)) - ) - } - - private def onLoad(f: => Unit): Unit = - documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) - - private def setupAirstream()(using router: Router[Page]): Unit = - AirstreamError.registerUnhandledErrorCallback(err => - router.forcePage( - Page.UnhandledError( - Some(err.getClass.getName), // TODO: Fill only in dev mode - Some(err.getMessage) - ) - ) - ) - - def renderPage(state: AppState)(using - router: Router[Page] - ): HtmlElement = - val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) - .collectSignal[Page.Detail]( - connectors - .DetailPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailParametru]( - connectors - .DetailParametruPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailKriteria]( - connectors - .DetailKriteriaPageConnector(state)(_) - .apply - ) - .collectSignal[Page.UpravDukazKriteria]( - pages.detail.UpravDukaz.Connector(state)(_).apply - ) - .collectStatic(Page.Dashboard)( - connectors.DashboardPageConnector(state.actionBus).apply - ) - .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) - ) - .collect[Page.UnhandledError](pg => - pages.errors - .UnhandledErrorPage( - pages.errors.UnhandledErrorPage - .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), - state.actionBus - ) - ) - .collectStatic(Page.Directory)( - connectors - .DirectoryPageConnector(state.users, state.actionBus) - .apply - ) - div(cls := "h-full", child <-- pageSplitter.$view) - - // Pull in the stylesheet - val css: Css.type = Css diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala deleted file mode 100644 index 3bbc8c0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ /dev/null @@ -1,198 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.* -import org.scalajs.dom -import zio.json.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import scala.scalajs.js -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.Page.Titled - -// enum is not working with Waypoints' SplitRender collectStatic -sealed abstract class Page( - val id: String, - val title: String, - val parent: Option[Page] -) { - val path: Vector[Page] = - parent match - case None => Vector(this) - case Some(p) => p.path :+ this - - val isRoot: Boolean = parent.isEmpty -} - -object Page: - - case class Titled[V](value: V, title: Option[String] = None): - val show: String = title.getOrElse(value.toString) - - case object Directory extends Page("directory", "Adresář", None) - - case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) - - case class Detail(osobniCislo: Titled[OsobniCislo]) - extends Page("user", osobniCislo.show, Some(Directory)) - - object Detail { - def apply(o: UserInfo): Detail = Detail( - Titled(o.personalNumber, Some(o.name)) - ) - } - - case class DetailParametru( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String] - ) extends Page( - "parameter", - parametr.show, - Some(Detail(osobniCislo)) - ) - - object DetailParametru { - def apply(o: UserInfo, p: Parameter): DetailParametru = - DetailParametru( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)) - ) - } - - case class DetailKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "criteria", - kriterium.show, - Some(DetailParametru(osobniCislo, parametr)) - ) - - object DetailKriteria { - def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = - DetailKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class UpravDukazKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "addProof", - "Důkaz", - Some(DetailKriteria(osobniCislo, parametr, kriterium)) - ) - - object UpravDukazKriteria { - def apply( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): UpravDukazKriteria = - UpravDukazKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class NotFound(url: String) extends Page("404", "404", Some(Directory)) - - case class UnhandledError( - errorName: Option[String], - errorMessage: Option[String] - ) extends Page("500", "Unexpected error", Some(Directory)) - -object Routes: - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) - given [V: JsonEncoder]: JsonEncoder[Titled[V]] = - DeriveJsonEncoder.gen[Titled[V]] - given [V: JsonDecoder]: JsonDecoder[Titled[V]] = - DeriveJsonDecoder.gen[Titled[V]] - given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] - given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] - - val base = - js.`import`.meta.env.BASE_URL - .asInstanceOf[String] + "app" - - val homePage: Page = Page.Directory - - given router: Router[Page] = Router[Page]( - routes = List( - Route.static(homePage, root / endOfSegments, basePath = base), - Route.static( - Page.Dashboard, - root / "dashboard" / endOfSegments, - basePath = base - ), - Route[Page.Detail, String]( - encode = _.osobniCislo.value.toString, - decode = osc => Page.Detail(Titled(OsobniCislo(osc))), - root / "osoba" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.DetailParametru, (String, String)]( - encode = p => (p.osobniCislo.value.toString, p.parametr.value), - decode = - p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / endOfSegments, - basePath = base - ), - Route[Page.DetailKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.DetailKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.UpravDukazKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.UpravDukazKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / "edit" / endOfSegments, - basePath = base - ) - ), - serializePage = _.toJson, - deserializePage = _.fromJson[Page] - .fold(s => throw IllegalStateException(s), identity), - getPageTitle = _.title, - routeFallback = url => Page.NotFound(url), - deserializeFallback = _ => Page.Dashboard - )( - $popStateEvent = windowEvents.onPopState, - owner = unsafeWindowOwner - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala deleted file mode 100644 index b24b82b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -sealed trait Action - -case object FetchDirectory extends Action -case class FetchUserDetails(osc: OsobniCislo) extends Action -case class FetchParameters(osc: OsobniCislo) extends Action -case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action -case class FetchParameterCriteria( - osc: OsobniCislo, - paramId: String, - critId: String, - page: (UserInfo, Parameter, ParameterCriteria) => Page -) extends Action -case class FetchAvailableFiles(osc: OsobniCislo) extends Action -case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala deleted file mode 100644 index c637d35..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala +++ /dev/null @@ -1,82 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.{UserProfile, UserInfo, OsobniCislo} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo -import cz.e_bs.cmi.mdr.pdb.UserFunction - -object AppPage: - // TODO: pages by logged in user - val pages: List[Page] = List(Page.Directory, Page.Dashboard) - - import NavigationBar.{Logo, MenuItem} - - val logo = Logo( - "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", - "Workflow" - ) - - // TODO: menu items by user profile - val userMenu = - List( - MenuItem("Your Profile"), - MenuItem("Settings"), - MenuItem("Sign out") - ) - - // TODO: load user profile - val $userProfile = Var( - UserProfile( - "tom", - UserInfo( - OsobniCislo("1031"), - "tom", - "Tom", - "Cook", - None, - None, - Some("tom@example.com"), - Some("+420 222 866 180"), - Some( - UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") - ), - Nil, - None - ) - ) - ) - - val $userInfo = $userProfile.signal.map(_.userInfo) - - type ViewModel = Option[HtmlElement] - def apply( - actionBus: Observer[Action] - )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using - router: Router[Page] - ): HtmlElement = - PageLayout(actionBus)( - $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => - PageLayout.ViewModel( - NavigationBar.ViewModel( - u, - pages.map(p => - NavigationBar.Link( - () => - PageLink( - p, - actionBus - ), - p == cp - ) - ), - userMenu, - logo - ), - c - ) - ), - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala deleted file mode 100644 index bf473c6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ /dev/null @@ -1,143 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden -import fiftyforms.ui.components.tailwind.list.IconText.ViewModel -import fiftyforms.ui.components.tailwind.Icons -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Action - -object Breadcrumbs: - - private def slash = - import svg.{*, given} - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-300", - xmlns := "http://www.w3.org/2000/svg", - fill := "currentColor", - viewBox := "0 0 20 20", - ariaHidden := true, - path( - d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" - ) - ) - - def backIcon = - Icons.solid - .`arrow-narrow-left`() - .amend( - svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" - ) - - object Link: - case class ViewModel( - page: Page, - icon: Option[SvgElement], - extraClasses: String, - text: String, - textClasses: Option[String] = None - ) - - val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" - - def shortHome(p: Page) = ViewModel( - p, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - "Zpět na úvodní stránku", - None - ) - - def fullHome(p: Page) = ViewModel( - p, - Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), - "text-gray-400 hover:text-gray-500", - "Domů", - Some("sr-only") - ) - - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - PageLink - .container($m.map(_.page), actionBus) - .amend( - cls <-- $m.map(_.extraClasses), - child.maybe <-- $m.map(_.icon), - span( - cls <-- $m.map(_.textClasses.getOrElse("")), - child.text <-- $m.map(_.text) - ) - ) - - object FullBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - ol( - role := "list", - cls := "flex items-center space-x-4", - children <-- $m.map(_.path).split(_.id)((_, _, $p) => - li( - div( - cls := "flex items-center", - child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), - Link( - $p.map(p => - if (p.isRoot) then Link.fullHome(p) - else - Link.ViewModel( - p, - None, - s"ml-4 max-w-xs truncate ${Link.baseClasses}", - p.title - ) - ), - actionBus - ) - ) - ) - ) - ) - - object ShortBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - Link( - $m.map { p => - val target = p.parent.getOrElse(p) - if target.isRoot then Link.shortHome(target) - else - Link.ViewModel( - target, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - s"Zpět na ${target.title}" - ) - }, - actionBus - ) - - def apply(actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - nav( - cls := "flex", - aria.label := "Breadcrumb", - div( - cls := "flex sm:hidden", - ShortBreadcrumbs(router.$currentPage, actionBus) - ), - div( - cls := "hidden sm:block", - FullBreadcrumbs(router.$currentPage, actionBus) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala deleted file mode 100644 index 0db2047..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ /dev/null @@ -1,242 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent -import fiftyforms.ui.components.tailwind.Icons -import fiftyforms.ui.components.tailwind.Avatar -import cz.e_bs.cmi.mdr.pdb.UserInfo -import io.laminext.syntax.core.* - -object NavigationBar: - - case class Logo(img: String, name: String) - case class Link(a: () => Anchor, active: Boolean) - case class MenuItem(title: String) - - case class ViewModel( - userInfo: UserInfo, - pages: List[Link], - userMenu: List[MenuItem], - logo: Logo - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - val $userInfo = $m.map(_.userInfo) - val mobileMenuOpen = Var(false) - - val desktopOnly = cls("hidden md:block") - val mobileOnly = cls("md:hidden") - - inline def avatarImage(size: Int = 8) = - Avatar($userInfo.map(_.img)).avatarImage(size) - - def notificationButton = button( - tpe := "button", - cls := List( - "bg-indigo-600", - "focus:outline-none", - "focus:ring-2", - "focus:ring-offset-2", - "focus:ring-offset-indigo-600", - "focus:ring-white", - "hover:text-white", - "p-1", - "rounded-full", - "text-indigo-200" - ), - span(cls := "sr-only", "View notifications"), - Icons.outline.bell() - ) - - def userProfile: HtmlElement = - val menuOpen = Var(false) - - def menuItem(item: MenuItem, idx: Int): HtmlElement = - a( - href := "#", - cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", - role := "menuitem", - tabIndex := -1, - idAttr := s"user-menu-item-$idx", - item.title - ) - - div( - cls := "ml-3 relative", - div( - button( - tpe := "button", - cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - idAttr := "user-menu-button", - aria.expanded <-- menuOpen.signal, - aria.hasPopup := true, - span(cls := "sr-only", "Open user menu"), - child <-- avatarImage(), - onClick.preventDefault.mapTo( - !menuOpen.now() - ) --> menuOpen.writer - ) - ), - /* - * */ - div( - cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", - cls <-- menuOpen.signal.map { o => - if (o) "md:block" else "md:hidden" - }, - role := "menu", - aria.orientation := "vertical", - aria.labelledBy := "user-menu-button", - tabIndex := -1, - // : keyboard navigation - children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) - ) - ) - - def mobileProfile = - def menuItem(item: MenuItem): HtmlElement = - a( - href := "#", - cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", - item.title - ) - - div( - cls := "pt-4 pb-3 border-t border-indigo-700", - div( - cls := "flex items-center px-5", - div( - cls := "flex-shrink-0", - child <-- avatarImage(10) - ), - div( - cls := "ml-3", - div( - cls := "text-base font-medium text-white", - child.text <-- $userInfo.map(_.name) - ), - child.maybe <-- $userInfo.map( - _.email.map(e => - div( - cls := "text-sm font-medium text-indigo-300", - e - ) - ) - ) - ), - notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) - ), - div( - cls := "mt-3 px-2 space-y-1", - children <-- $m.map(_.userMenu.map(menuItem)) - ) - ) - - def pageLink(page: Link): Anchor = - page - .a() - .amend( - cls := "text-white px-3 py-2 rounded-md text-sm font-medium", - cls := Seq( - "bg-indigo-700" -> page.active, - "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active - ), - ariaCurrent := (if page.active then "page" else "false") - ) - - def logoImg: Image = - img( - cls := "h-8 w-8", - src <-- $m.map(_.logo.img), - alt <-- $m.map(_.logo.name) - ) - - def mobileMenuButton = button( - tpe := "button", - cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - aria.controls := "mobile-menu", - aria.expanded <-- mobileMenuOpen.signal, - span(cls := "sr-only", "Open main menu"), - Icons.outline - .menu() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") - ), - Icons.outline - .x() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") - ), - onClick.preventDefault.mapTo( - !mobileMenuOpen.now() - ) --> mobileMenuOpen.writer - ) - - def navBarLeft = - div( - cls := "flex items-center", - div(cls := "flex-shrink-0", logoImg), - div( - desktopOnly, - div( - cls := "ml-10 flex items-baseline space-x-4", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ) - ) - ) - - def navBarRight = - div( - desktopOnly, - div( - cls := "ml-4 flex items-center md:ml-6", - notificationButton, - userProfile - ) - ) - - def navBarMobile = - div( - cls := "-mr-2 flex", - mobileOnly, - mobileMenuButton - ) - - def navBar = - div( - cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex items-center justify-between h-16", - navBarLeft, - navBarRight, - navBarMobile - ) - ) - - def mobileMenu = - div( - mobileOnly, - cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }, - idAttr := "mobile-menu", - div( - cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ), - mobileProfile - ) - - nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala deleted file mode 100644 index 75339df..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object PageHeader: - def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = - header( - cls := "bg-white shadow-sm", - div( - cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", - h1( - cls := "text-lg leading-6 font-semibold text-gray-900", - Breadcrumbs(actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala deleted file mode 100644 index 5096463..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ /dev/null @@ -1,30 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import fiftyforms.ui.components.tailwind.Loading -import io.laminext.syntax.core.* - -object PageLayout: - case class ViewModel( - navigation: NavigationBar.ViewModel, - content: Option[HtmlElement] - ) - def apply(actionBus: Observer[Action])( - $m: Signal[ViewModel], - mods: Modifier[HtmlElement]* - )(using router: Router[Page]): HtmlElement = - val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) - div( - cls := "h-full flex flex-col", - NavigationBar($m.map(_.navigation)), - child.maybe <-- router.$currentPage.map(_.isRoot) - .switch(None, Some(PageHeader(actionBus))), - main( - cls := "flex-grow-1 overflow-y-auto", - mods, - child <-- $maybeContent.map(_.getOrElse(Loading)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala deleted file mode 100644 index 62d820f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.LinkSupport.* -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo - -object PageLink: - type ViewModel = Page - def apply($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = - a(mods($m, actions), child.text <-- $m.map(_.title)) - - def apply(m: ViewModel, actions: Observer[Action])(using - router: Router[Page] - ): Anchor = apply(Val(m), actions) - - def container($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = a(mods($m, actions)) - - def container(m: ViewModel, actions: Observer[Action])(using - Router[Page] - ): Anchor = container(Val(m), actions) - - private def mods($m: Signal[Page], actions: Observer[Action])(using - router: Router[Page] - ): Modifier[Anchor] = - Seq( - href <-- $m.map(router.absoluteUrlForPage).recover { case _ => - Some("invalid url") - }, - composeEvents(onClick.noKeyMod.preventDefault)( - _.sample($m) - .map(NavigateTo.apply) - ) --> actions - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala deleted file mode 100644 index 87f825a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.pages.dashboard.DashboardPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -class DashboardPageConnector(actionBus: Observer[Action])(using - router: Router[Page] -): - def apply: HtmlElement = - AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala deleted file mode 100644 index e9b0f71..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala +++ /dev/null @@ -1,76 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailKriteriaPage -import pages.detail.components.DukazyKriteria -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -object DetailKriteriaPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailKriteriaPageConnector( - state: DetailKriteriaPageConnector.AppState -)( - $page: Signal[Page.DetailKriteria] -)(using Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => - (p.osobniCislo.value, p.parametr.value, p.kriterium.value) - )((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map( - FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) - ) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, s, $s) => - DetailKriteriaPage($s)(state.actionBus.contramap { - case DukazyKriteria.Add => - NavigateTo( - Page.UpravDukazKriteria( - Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), - Page.Titled(s.parametr.id, Some(s.parametr.nazev)), - Page.Titled(s.kriterium.id, Some(s.kriterium.id)) - ) - ) - }) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): DetailKriteriaPage.ViewModel = - DetailKriteriaPage.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala deleted file mode 100644 index 4d8024f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ /dev/null @@ -1,57 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import pages.detail.DetailPage -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import fiftyforms.ui.components.tailwind.Color - -object DetailPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailPageConnector(state: DetailPageConnector.AppState)( - $page: Signal[Page.Detail] -)(using router: Router[Page]): - val $oscChangeSignal = $page.splitOne(_.osobniCislo.value)((osc, _, _) => osc) - val $pageChangeSignal = - $oscChangeSignal.flatMap(osc => - EventStream.fromSeq(Seq(FetchUserDetails(osc), FetchParameters(osc))) - ) - // TODO: filter the value based on the current osc - // OSC change will fetch new data, but still - // - we need to be sure that what we got is really what we ought to display - // - we want to display stale data accordingly (at least with loading indicator) - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - def apply: HtmlElement = - AppPage(state.actionBus)( - $data.combineWithFn($params)(_ zip _) - .map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: List[Parameter] - ): DetailPage.ViewModel = - DetailPage.ViewModel( - o.toDetailOsoby, - p.map( - _.toParametr(param => - PageLink.container(Page.DetailParametru(o, param), state.actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala deleted file mode 100644 index fdaa7f8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailParametruPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink - -object DetailParametruPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailParametruPageConnector( - state: DetailParametruPageConnector.AppState -)( - $page: Signal[Page.DetailParametru] -)(using router: Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => (p.osobniCislo.value, p.parametr.value))((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map(FetchParameter(_, _)) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - } yield (da, pb) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailParametruPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter - ): DetailParametruPage.ViewModel = - DetailParametruPage.ViewModel( - o.toDetailOsoby, - p.toParametr(p => - PageLink.container(Page.DetailParametru(o, p), state.actionBus) - ), - p.criteria.map( - _.toKriterium { c => - PageLink.container( - Page.DetailKriteria(o, p, c), - state.actionBus - ) - } - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala deleted file mode 100644 index 049aa22..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -case class DirectoryPageConnector( - $input: EventStream[List[UserInfo]], - actionBus: Observer[Action] -)(using router: Router[Page]): - val $data = $input.startWithNone - val $actionSignal = EventStream.fromValue(FetchDirectory) - - def apply: HtmlElement = - AppPage(actionBus)( - $data.split(_ => ())((_, _, s) => - pages.directory.DirectoryPage( - s.map( - _.map( - _.toUserRow(u => - PageLink.container( - Page.Detail(Page.Titled(u.personalNumber)), - actionBus - ) - ) - ) - ) - ) - ), - $actionSignal --> actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala deleted file mode 100644 index 88e7450..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ /dev/null @@ -1,68 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii -import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria -import fiftyforms.ui.components.tailwind.Color - -extension (o: UserInfo) - def toDetailOsoby: DetailOsoby.ViewModel = - DetailOsoby.ViewModel( - o.personalNumber, - o.name, - o.email, - o.phone, - o.img, - o.mainFunction.map(f => - DetailOsoby.Funkce.ViewModel(f.name, f.dept, f.ou) - ), - o.userContracts.headOption.map(c => - DetailOsoby.PracovniPomer.ViewModel(c.rel, c.startDate, c.endDate) - ) - ) - -extension (param: Parameter) - def toParametr(container: Parameter => Anchor): DetailParametru.ViewModel = - DetailParametru.ViewModel( - id = param.id, - nazev = param.name, - popis = param.description, - status = "Nesplněno", - statusColor = Color.red, - a = container(param) - ) - -extension (crit: ParameterCriteria) - def toKriterium( - container: ParameterCriteria => Anchor - ): DetailKriteria.ViewModel = - DetailKriteria.ViewModel( - nazev = crit.criteriumText, - kapitola = crit.chapterId, - bod = crit.itemId, - status = "Nesplněno", - statusColor = Color.red, - splneno = false, - dukazy = Nil, - container = container(crit) - ) - -extension (user: UserInfo) - def toUserRow( - container: UserInfo => HtmlElement = (_: UserInfo) => div() - ): UserRow.ViewModel = - UserRow.ViewModel( - osobniCislo = user.personalNumber.toString, - celeJmeno = user.name, - prijmeni = user.surname, - hlavniFunkce = user.mainFunction.map(_.name), - img = user.img, - container = () => container(user) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala deleted file mode 100644 index 7d5b2c1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.dashboard - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.Page - -object DashboardPage: - - def render: HtmlElement = - div("Dashboard page") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala deleted file mode 100644 index 9ef0698..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala +++ /dev/null @@ -1,31 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailKriteriaPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply($m: Signal[ViewModel])( - action: Observer[DukazyKriteria.Action] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - DukazyKriteria($m.map(_.kriterium.dukazy))(action) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala deleted file mode 100644 index 3b316e1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object DetailPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametry: SeznamParametru.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby($m.map(_.osoba)), - SeznamParametru($m.map(_.parametry)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala deleted file mode 100644 index 17224cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala +++ /dev/null @@ -1,24 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailParametruPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriteria: SeznamKriterii.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby.header($m.map(_.osoba)), - DetailParametru($m.map(_.parametr)), - SeznamKriterii($m.map(_.kriteria)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala deleted file mode 100644 index dc912fa..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ /dev/null @@ -1,107 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb -package app -package pages.detail - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.UpravDukazForm -import fiftyforms.services.files.File - -object UpravDukaz: - - type ThisPage = Page.UpravDukazKriteria - type PageKey = (OsobniCislo, String, String) - - trait State { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def availableFiles: EventStream[List[File]] - def actionBus: Observer[Action] - } - - def keyOfPage(page: ThisPage): PageKey = - (page.osobniCislo.value, page.parametr.value, page.kriterium.value) - - def onChangeAction(key: PageKey): Action = - FetchParameterCriteria( - key._1, - key._2, - key._3, - Page.UpravDukazKriteria(_, _, _) - ) - - class Connector(state: State)($page: Signal[ThisPage])(using Router[Page]): - val $paramChangeSignal = $page.splitOne(keyOfPage)((x, _, _) => x) - val $pageChangeSignal = $paramChangeSignal.map(onChangeAction) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.split(_ => ())((_, s, $s) => - PageComponent( - $s.map(buildModel), - state.availableFiles, - state.actionBus.contramap { - case UpravDukazForm.Cancelled => - NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) - case UpravDukazForm.AvailableFilesRequested => - FetchAvailableFiles(s._1.personalNumber) - } - ) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): PageComponent.ViewModel = - import connectors.* - PageComponent.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) - - object PageComponent: - import components.* - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply( - $m: Signal[ViewModel], - availableFilesStream: EventStream[List[File]], - events: Observer[UpravDukazForm.Event] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - UpravDukazForm(availableFilesStream)(events) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala deleted file mode 100644 index ca311b0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.Icons -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Color - -object DetailKriteria: - case class ViewModel( - nazev: String, - kapitola: String, - bod: String, - status: String, - statusColor: Color, - splneno: Boolean, - dukazy: List[DukazKriteria.ViewModel], - container: HtmlElement = div() - ) { - val id = s"${kapitola}${bod}" - } - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h3( - cls := "text-xl leading-6 font-bold text-gray-900", - child.text <-- $m.map(_.nazev) - ), - h3( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - "Kapitola ", - child.text <-- $m.map(_.kapitola), - " bod ", - child <-- $m.map(_.bod) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala deleted file mode 100644 index ae90290..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala +++ /dev/null @@ -1,100 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Avatar - -object DetailOsoby: - - object Funkce: - case class ViewModel( - nazev: String, - stredisko: String, - voj: String - ) - - def render($m: Signal[ViewModel]): HtmlElement = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.nazev), - span( - cls := "hidden md:inline", - " @ ", - child.text <-- $m.map(_.stredisko), - ", ", - child.text <-- $m.map(_.voj) - ) - ) - - object PracovniPomer: - case class ViewModel( - druh: String, - pocatek: LocalDate, - konec: Option[LocalDate] - ) - - def render($m: Signal[ViewModel]): HtmlElement = - import fiftyforms.ui.components.tailwind.CustomAttrs.datetime - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.druh), - " od ", - time( - datetime <-- $m.map(_.pocatek.toString), - child.text <-- $m.map(_.pocatek.toString) - ) - ) - - case class ViewModel( - osobniCislo: OsobniCislo, - jmeno: String, - email: Option[String], - telefon: Option[String], - img: Option[String], - hlavniFunkce: Option[Funkce.ViewModel], - pracovniPomer: Option[PracovniPomer.ViewModel] - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-4", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(16) - ), - div( - h1( - cls := "text-2xl font-bold text-gray-900", - child.text <-- $m.map(_.jmeno) - ), - child.maybe <-- $m.map(_.hlavniFunkce).split(_ => ())((_, _, d) => - Funkce.render(d) - ), - child.maybe <-- $m.map(_.pracovniPomer).split(_ => ())((_, _, d) => - PracovniPomer.render(d) - ) - ) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-5", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(8) - ), - div( - h1( - cls := "text-lg font-medium text-gray-800", - child.text <-- $m.map(_.jmeno) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala deleted file mode 100644 index e11cb62..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color - -object DetailParametru: - case class ViewModel( - id: String, - nazev: String, - popis: String, - status: String, - statusColor: Color, - a: Anchor - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h2( - cls := "text-2xl leading-6 font-medium text-gray-900", - child.text <-- $m.map(_.nazev) - ), - p( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - child.text <-- $m.map(_.popis) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - h2( - cls := "text-lg leading-6 font-medium text-gray-600", - child.text <-- $m.map(_.nazev) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala deleted file mode 100644 index a00ede8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala +++ /dev/null @@ -1,177 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.CustomAttrs - -object DukazKriteria: - case class Osoba(osobniCislo: String, jmeno: String) - case class Udalost(osoba: Osoba, datum: LocalDate) - case class Dokument( - url: String, - nazev: String, - pridal: Udalost, - odebral: Option[Udalost] - ) - case class ViewModel( - dokumenty: List[Dokument], - autorizoval: Option[Udalost], - platiDo: Option[LocalDate], - poznámka: Option[String] - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-lg", - div( - cls := "px-4 py-5 sm:px-6", - h3( - cls := "text-lg leading-6 font-medium text-gray-900", - """Applicant Information""" - ), - p( - cls := "mt-1 max-w-2xl text-sm text-gray-500", - """Personal details and application.""" - ) - ), - div( - cls := "border-t border-gray-200 px-4 py-5 sm:px-6", - dl( - cls := "grid grid-cols-1 gap-x-4 gap-y-8 sm:grid-cols-2", - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Full name""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Margot Foster""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Application for""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Backend Developer""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Email address""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """margotfoster@example.com""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Salary expectation""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """$120,000""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """About""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Fugiat ipsum ipsum deserunt culpa aute sint do nostrud anim incididunt cillum culpa consequat. Excepteur qui ipsum aliquip consequat sint. Sit id mollit nulla mollit nostrud in ea officia proident. Irure nostrud pariatur mollit ad adipisicing reprehenderit deserunt qui eu.""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """Attachments""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - ul( - role := "list", - cls := "border border-gray-200 rounded-md divide-y divide-gray-200", - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """resume_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ), - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """coverletter_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala deleted file mode 100644 index 22f134f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala +++ /dev/null @@ -1,35 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object DukazyKriteria: - sealed trait Action - case object Add extends Action - - type ViewModel = List[DukazKriteria.ViewModel] - - def apply($m: Signal[ViewModel])(actions: Observer[Action]): HtmlElement = - div( - child.maybe <-- $m.map(m => - if (m.isEmpty) then Some(prazdnyDukaz(actions)) - else None - ), - children <-- $m.map(_.zipWithIndex).split(_._2)((_, _, $s) => - DukazKriteria($s.map(_._1)) - ) - ) - - private def prazdnyDukaz(actions: Observer[Action]): HtmlElement = - button( - tpe := "button", - onClick.preventDefault.mapTo(Add) --> actions, - cls := "relative block w-full border-2 border-gray-300 border-dashed rounded-lg p-12 text-center hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - Icons.outline - .`document-add`(12) - .amend(svg.cls := "mx-auto text-gray-400"), - span( - cls := "mt-2 block text-sm font-medium text-gray-900", - "Přidat důkaz" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala deleted file mode 100644 index f628697..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.list.StackedList -import fiftyforms.ui.components.tailwind.list.ListRow -import fiftyforms.ui.components.tailwind.list.RowTag -import fiftyforms.ui.components.tailwind.list.RowNext -import java.time.LocalDate - -object SeznamKriterii: - type ViewModel = List[DetailKriteria.ViewModel] - - private val kritList = new StackedList[DetailKriteria.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - kritList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = - p(cls := "text-sm text-gray-500", s"${i.kapitola}${i.bod}"), - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.container - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala deleted file mode 100644 index 2bfe4bc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.list.{ - StackedList, - ListRow, - RowTag, - PropList, - IconText, - RowNext -} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.LinkSupport.* - -object SeznamParametru: - type ViewModel = List[DetailParametru.ViewModel] - - private val parametrList = new StackedList[DetailParametru.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - parametrList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = emptyNode, - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.a - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala deleted file mode 100644 index f048e9d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ /dev/null @@ -1,101 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.form.* -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement -import fiftyforms.services.files.components.tailwind.FilePicker -import cz.e_bs.cmi.mdr.pdb.frontend.AutorizujDukaz -import cz.e_bs.cmi.mdr.pdb.frontend.DocumentRef -import fiftyforms.services.files.File - -object UpravDukazForm: - sealed trait Event - case object Cancelled extends Event - case object AvailableFilesRequested extends Event - def apply(availableFilesStream: EventStream[List[File]])( - updates: Observer[Event] - ): HtmlElement = - val (filesStream, filesObserver) = - EventStream.withObserver[FilePicker.Event] - val files = Var[List[File]](Nil) - def submitButtons: HtmlElement = - div( - cls := "pt-5", - div( - cls := "flex justify-end", - button( - tpe := "button", - cls := "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zrušit", - onClick.mapTo(Cancelled) --> updates - ), - button( - tpe := "submit", - cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - disabled <-- files.signal.map(_.isEmpty), - "Autorizovat" - ) - ) - ) - - div( - filesStream.collect { case FilePicker.SelectionUpdated(files) => - files.to(List) - } --> files.writer, - filesStream.collect { case FilePicker.AvailableFilesRequested => - AvailableFilesRequested - } --> updates, - cls := "bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6", - Form( - Form.Body( - Form.Section( - FormHeader( - FormHeader.ViewModel( - "Doložení kritéria", - "Sestavte doklady poskytující důkaz kritéria a potvrďte odesláním formuláře. Případné limitace či jiné relevantní údaje vepište do pole Komentář." - ) - ), - FormFields( - FormRow( - "dokumenty", - "Dokumenty", - FilePicker(files.signal, availableFilesStream)(filesObserver) - .amend(idAttr := "dokumenty", cls("max-w-lg")) - ).toHtml, - FormRow( - "platnost", - "Platnost", - input( - idAttr := "platnost", - name := "platnost", - tpe := "date", - autoComplete := "date", - cls := "block max-w-lg w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" - ) - ).toHtml, - FormRow( - "komentar", - "Komentář", - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - textArea( - idAttr := "komentar", - name := "about", - rows := 3, - cls := "max-w-lg shadow-sm block w-full focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border border-gray-300 rounded-md" - ), - p( - cls := "mt-2 text-sm text-gray-500", - "Doplňte prosím potřebné informace související s doložením kritéria, včetně případných limitací." - ) - ) - ).toHtml - ) - ) - ), - submitButtons - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala deleted file mode 100644 index 41033f6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -import components._ - -object DirectoryPage: - - type ViewModel = List[UserRow.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - val (actionsStream, actionObserver) = - EventStream.withObserver[SearchForm.Action] - val $filter = actionsStream - .collect { - case SearchForm.ClearFilter => None - case SearchForm.SetFilter(t) => Some(t) - } - .startWith(None) - val byLetter = for { - d <- $m - f <- $filter - } yield for { - (letter, users) <- d - .filter { user => - f match - case None => true - case Some(t) => user.search.contains(t) - } - .groupBy(_.prijmeni.head) - .to(List) - .sortBy(_._1) - } yield (letter.toString, users.sortBy(_.prijmeni)) - - div( - cls := "h-full max-w-7xl mx-auto order-first flex flex-col flex-shrink-0", - SearchForm(actionObserver), - Directory(byLetter) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala deleted file mode 100644 index 73f915d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -object Directory: - - object Header: - type ViewModel = String - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500", - h3(child.text <-- $m) - ) - - object UserList: - type ViewModel = List[UserRow.ViewModel] - def apply($m: Signal[ViewModel]): HtmlElement = - ul( - role := "list", - cls := "relative z-0 divide-y divide-gray-200", - children <-- $m.split(_.osobniCislo)((_, _, s) => UserRow(s)) - ) - - type ViewModel = List[(String, List[UserRow.ViewModel])] - def apply($m: Signal[ViewModel]): HtmlElement = - nav( - cls := "flex-1 min-h-0 overflow-y-auto", - aria.label := "Directory", - children <-- $m.split(_._1)((_, _, s) => - div( - cls := "relative", - Header(s.map(_._1)), - UserList(s.map(_._2)) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala deleted file mode 100644 index e182910..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala +++ /dev/null @@ -1,54 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object SearchForm: - sealed trait Action - case object SubmitFilter extends Action - sealed trait FilterAction extends Action - case object ClearFilter extends FilterAction - case class SetFilter(value: String) extends FilterAction - - def apply(actions: Observer[Action]): HtmlElement = - div( - cls := "px-6 pt-4 pb-4", - form( - cls := "flex space-x-4", - onSubmit.mapTo(SubmitFilter) --> actions, - div( - cls := "flex-1 min-w-0", - label( - forId := "search", - cls := "sr-only", - "Hledat" - ), - div( - cls := "relative rounded-md shadow-sm", - div( - cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", - Icons.solid.search().amend(svg.cls := "text-gray-400") - ), - input( - tpe := "search", - name := "search", - idAttr := "search", - cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", - placeholder := "Hledat", - composeEvents(onInput.mapToValue.setAsValue.map(SetFilter(_)))( - _.throttle(500) - ) --> actions - ) - ) - ), - button( - tpe := "submit", - cls := "inline-flex justify-center px-3.5 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500", - Icons.solid.filter().amend(svg.cls := "text-gray-400"), - span( - cls := "sr-only", - "Hledat" - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala deleted file mode 100644 index 82f7723..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala +++ /dev/null @@ -1,52 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.airstream.core.Signal -import fiftyforms.ui.components.tailwind.Avatar - -object UserRow: - case class ViewModel( - osobniCislo: String, - celeJmeno: String, - prijmeni: String, - hlavniFunkce: Option[String], - img: Option[String], - container: () => HtmlElement = () => div() - ) { - val search = prijmeni.toLowerCase - } - - def apply($m: Signal[ViewModel]): HtmlElement = - inline def avatarImage = - Avatar($m.map(_.img)).avatarImage(10) - - li( - div( - cls := "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500", - div( - cls := "flex-shrink-0", - child <-- avatarImage - ), - div( - cls := "flex-1 min-w-0", - child <-- $m.map { o => - o.container() - .amend( - cls := "focus:outline-none", - span( - cls := "absolute inset-0", - aria.hidden := true - ), - p( - cls := "text-sm font-medium text-gray-900", - o.celeJmeno - ), - p( - cls := "text-sm text-gray-500 truncate", - o.hlavniFunkce - ) - ) - } - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala deleted file mode 100644 index 467c5f5..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala +++ /dev/null @@ -1,99 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router - -object ErrorPage: - case class ViewModel( - homePage: Page, - errorName: String, - title: String, - subTitle: String - ) - - def apply(m: ViewModel, actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - val ViewModel(homePage, errorName, title, subTitle) = m - div( - cls := "min-h-full pt-16 pb-12 flex flex-col bg-white", - main( - cls := "flex-grow flex flex-col justify-center max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex-shrink-0 flex justify-center", - a( - href := "/", - cls := "inline-flex", - span( - cls := "sr-only", - """Workflow""" - ), - img( - cls := "h-12 w-auto", - src := "https://tailwindui.com/img/logos/workflow-mark.svg?color=indigo&shade=600", - alt := "" - ) - ) - ), - div( - cls := "py-16", - div( - cls := "text-center", - p( - cls := "text-sm font-semibold text-indigo-600 uppercase tracking-wide", - errorName - ), - h1( - cls := "mt-2 text-4xl font-extrabold text-gray-900 tracking-tight sm:text-5xl", - title - ), - p( - cls := "mt-2 text-base text-gray-500", - subTitle - ), - div( - cls := "mt-6", - PageLink - .container(homePage, actionBus) - .amend( - cls := "text-base font-medium text-indigo-600 hover:text-indigo-500", - """Go back home""" - ) - ) - ) - ) - ), - footer( - cls := "flex-shrink-0 max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", - nav( - cls := "flex justify-center space-x-4", - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Contact Support""" - ), - span( - cls := "inline-block border-l border-gray-300", - aria.hidden := true - ), - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Status""" - ), - span( - cls := "inline-block border-l border-gray-300", - aria.hidden := true - ), - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Twitter""" - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala deleted file mode 100644 index bd9baec..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action - -object NotFoundPage: - def apply(homePage: Page, url: String, actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - ErrorPage( - ErrorPage.ViewModel( - homePage, - "404 error", - "Page not found.", - s"Sorry, but page $url doesn't exist." - ), - actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala deleted file mode 100644 index 476fe3c..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action - -object UnhandledErrorPage: - - case class ViewModel( - homePage: Page, - errorName: Option[String], - errorMessage: Option[String] - ) - - def apply(m: ViewModel, actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - ErrorPage( - ErrorPage.ViewModel( - m.homePage, - "Unexpected error occurred", - m.errorName.getOrElse( - "Uh oh!" - ), // TODO: translations, better text than uh oh - m.errorMessage.getOrElse( - "This wasn't supposed to happen! Please try again." - ) - ), - actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala deleted file mode 100644 index 25f059f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala +++ /dev/null @@ -1,6 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.services - -import com.raquo.airstream.core.Observer - -trait DataFetcher[K, A]: - def fetch(id: K, sink: Observer[A]): Unit diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala deleted file mode 100644 index a469830..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala +++ /dev/null @@ -1,120 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package state - -import com.raquo.airstream.core.EventStream -import cz.e_bs.cmi.mdr.pdb.{UserInfo, OsobniCislo} -import com.raquo.airstream.core.Observer -import scala.scalajs.js -import scala.scalajs.js.JSON -import zio.json._ -import com.raquo.airstream.eventbus.EventBus -import com.raquo.airstream.ownership.Owner -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.UserFunction -import cz.e_bs.cmi.mdr.pdb.UserContract -import fiftyforms.services.files.File - -trait AppState - extends connectors.DetailPageConnector.AppState - with connectors.DetailParametruPageConnector.AppState - with connectors.DetailKriteriaPageConnector.AppState - with pages.detail.UpravDukaz.State: - def users: EventStream[List[UserInfo]] - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - -class MockAppState(implicit owner: Owner, router: Router[Page]) - extends AppState: - - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonDecoder[UserFunction] = DeriveJsonDecoder.gen - given JsonDecoder[UserContract] = DeriveJsonDecoder.gen - given JsonDecoder[UserInfo] = DeriveJsonDecoder.gen - - given JsonDecoder[ParameterCriteria] = DeriveJsonDecoder.gen - given JsonDecoder[Parameter] = DeriveJsonDecoder.gen - - private val actions = EventBus[Action]() - private val (parametersStream, pushParameters) = - EventStream.withCallback[List[Parameter]] - private val (usersStream, pushUsers) = - EventStream.withCallback[List[UserInfo]] - private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] - private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] - - private val mockData: List[UserInfo] = - mockUsers - .asInstanceOf[js.Dictionary[js.Object]] - .values - // TODO: is there a more efficient way to parse from JS object directly? - .map { o => - val parsed = JSON.stringify(o).fromJson[UserInfo] - parsed.left.foreach(org.scalajs.dom.console.log(_)) - parsed - } - .collect { case Right(u) => - u - } - .toList - - private val mockParameters: List[Parameter] = - pdbParams - .asInstanceOf[js.Dictionary[js.Object]] - .values - .map(o => JSON.stringify(o).fromJson[Parameter]) - .collect { case Right(p) => p } - .toList - - // TODO: Extract to separate event handler - actions.events.foreach { - case FetchDirectory => pushUsers(mockData) - case FetchUserDetails(osc) => - mockData.find(_.personalNumber == osc).foreach { o => - pushDetails(o) - router.replaceState(Page.Detail(o)) - } - case FetchParameters(osc) => - pushParameters(mockParameters) - case FetchParameter(osc, paramId) => - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(Page.DetailParametru(o, p)) - case FetchParameterCriteria(osc, paramId, critId, page) => - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - c <- p.criteria.find(_.id == critId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(page(o, p, c)) - case NavigateTo(page) => router.pushState(page) - case FetchAvailableFiles(osc) => - pushFiles( - List( - File("https://tc163.cmi.cz/here", "Example file") - ) - ) - } - - override def users: EventStream[List[UserInfo]] = - usersStream.debugWithName("users") - - override def details: EventStream[UserInfo] = - detailsStream.debugWithName("details") - - override def parameters: EventStream[List[Parameter]] = - parametersStream.debugWithName("parameters") - - override def availableFiles: EventStream[List[File]] = - filesStream.debugWithName("available files") - - override def actionBus: Observer[Action] = - actions.writer.debugWithName("actions writer") diff --git a/app/src/main/scala/mdr/pdb/app/Main.scala b/app/src/main/scala/mdr/pdb/app/Main.scala new file mode 100644 index 0000000..081b4ee --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/Main.scala @@ -0,0 +1,103 @@ +package mdr.pdb.app + +import scala.scalajs.js.annotation.JSExportTopLevel +import scala.scalajs.js.annotation.JSExport +import scala.scalajs.js.annotation.JSImport +import scala.scalajs.js +import org.scalajs.dom +import com.raquo.laminar.api.L.{*, given} +import scala.scalajs.js.Date +import com.raquo.waypoint.Router +import com.raquo.waypoint.SplitRender +import mdr.pdb.app.services.DataFetcher +import scala.scalajs.js.JSON +import zio.json._ +import mdr.pdb.UserInfo +import mdr.pdb.app.state.AppState + +@js.native +@JSImport("stylesheets/main.css", JSImport.Namespace) +object Css extends js.Any + +@js.native +@JSImport("data/users.json", JSImport.Default) +object mockUsers extends js.Object + +@js.native +@JSImport("params/pdb-params.json", JSImport.Default) +object pdbParams extends js.Object + +@JSExportTopLevel("app") +object Main: + + @JSExport + def main(args: Array[String]): Unit = + import Routes.given + onLoad { + setupAirstream() + val appContainer = dom.document.querySelector("#app") + val _ = + render( + appContainer, + renderPage(state.MockAppState(using unsafeWindowOwner, router)) + ) + } + + private def onLoad(f: => Unit): Unit = + documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) + + private def setupAirstream()(using router: Router[Page]): Unit = + AirstreamError.registerUnhandledErrorCallback(err => + router.forcePage( + Page.UnhandledError( + Some(err.getClass.getName), // TODO: Fill only in dev mode + Some(err.getMessage) + ) + ) + ) + + def renderPage(state: AppState)(using + router: Router[Page] + ): HtmlElement = + val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) + .collectSignal[Page.Detail]( + connectors + .DetailPageConnector(state)(_) + .apply + ) + .collectSignal[Page.DetailParametru]( + connectors + .DetailParametruPageConnector(state)(_) + .apply + ) + .collectSignal[Page.DetailKriteria]( + connectors + .DetailKriteriaPageConnector(state)(_) + .apply + ) + .collectSignal[Page.UpravDukazKriteria]( + pages.detail.UpravDukaz.Connector(state)(_).apply + ) + .collectStatic(Page.Dashboard)( + connectors.DashboardPageConnector(state.actionBus).apply + ) + .collect[Page.NotFound](pg => + pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) + ) + .collect[Page.UnhandledError](pg => + pages.errors + .UnhandledErrorPage( + pages.errors.UnhandledErrorPage + .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), + state.actionBus + ) + ) + .collectStatic(Page.Directory)( + connectors + .DirectoryPageConnector(state.users, state.actionBus) + .apply + ) + div(cls := "h-full", child <-- pageSplitter.$view) + + // Pull in the stylesheet + val css: Css.type = Css diff --git a/app/src/main/scala/mdr/pdb/app/Routes.scala b/app/src/main/scala/mdr/pdb/app/Routes.scala new file mode 100644 index 0000000..2cc860c --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/Routes.scala @@ -0,0 +1,198 @@ +package mdr.pdb.app + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.* +import org.scalajs.dom +import zio.json.{*, given} +import mdr.pdb.OsobniCislo + +import scala.scalajs.js +import mdr.pdb.UserInfo +import mdr.pdb.Parameter +import mdr.pdb.ParameterCriteria +import mdr.pdb.app.Page.Titled + +// enum is not working with Waypoints' SplitRender collectStatic +sealed abstract class Page( + val id: String, + val title: String, + val parent: Option[Page] +) { + val path: Vector[Page] = + parent match + case None => Vector(this) + case Some(p) => p.path :+ this + + val isRoot: Boolean = parent.isEmpty +} + +object Page: + + case class Titled[V](value: V, title: Option[String] = None): + val show: String = title.getOrElse(value.toString) + + case object Directory extends Page("directory", "Adresář", None) + + case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) + + case class Detail(osobniCislo: Titled[OsobniCislo]) + extends Page("user", osobniCislo.show, Some(Directory)) + + object Detail { + def apply(o: UserInfo): Detail = Detail( + Titled(o.personalNumber, Some(o.name)) + ) + } + + case class DetailParametru( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String] + ) extends Page( + "parameter", + parametr.show, + Some(Detail(osobniCislo)) + ) + + object DetailParametru { + def apply(o: UserInfo, p: Parameter): DetailParametru = + DetailParametru( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)) + ) + } + + case class DetailKriteria( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String], + kriterium: Titled[String] + ) extends Page( + "criteria", + kriterium.show, + Some(DetailParametru(osobniCislo, parametr)) + ) + + object DetailKriteria { + def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = + DetailKriteria( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)), + Titled(k.id, Some(k.id)) + ) + } + + case class UpravDukazKriteria( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String], + kriterium: Titled[String] + ) extends Page( + "addProof", + "Důkaz", + Some(DetailKriteria(osobniCislo, parametr, kriterium)) + ) + + object UpravDukazKriteria { + def apply( + o: UserInfo, + p: Parameter, + k: ParameterCriteria + ): UpravDukazKriteria = + UpravDukazKriteria( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)), + Titled(k.id, Some(k.id)) + ) + } + + case class NotFound(url: String) extends Page("404", "404", Some(Directory)) + + case class UnhandledError( + errorName: Option[String], + errorMessage: Option[String] + ) extends Page("500", "Unexpected error", Some(Directory)) + +object Routes: + given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) + given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) + given [V: JsonEncoder]: JsonEncoder[Titled[V]] = + DeriveJsonEncoder.gen[Titled[V]] + given [V: JsonDecoder]: JsonDecoder[Titled[V]] = + DeriveJsonDecoder.gen[Titled[V]] + given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] + given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] + + val base = + js.`import`.meta.env.BASE_URL + .asInstanceOf[String] + "app" + + val homePage: Page = Page.Directory + + given router: Router[Page] = Router[Page]( + routes = List( + Route.static(homePage, root / endOfSegments, basePath = base), + Route.static( + Page.Dashboard, + root / "dashboard" / endOfSegments, + basePath = base + ), + Route[Page.Detail, String]( + encode = _.osobniCislo.value.toString, + decode = osc => Page.Detail(Titled(OsobniCislo(osc))), + root / "osoba" / segment[String] / endOfSegments, + basePath = base + ), + Route[Page.DetailParametru, (String, String)]( + encode = p => (p.osobniCislo.value.toString, p.parametr.value), + decode = + p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / endOfSegments, + basePath = base + ), + Route[Page.DetailKriteria, (String, String, String)]( + encode = p => + ( + p.osobniCislo.value.toString, + p.parametr.value, + p.kriterium.value.replaceAll("\\.", "--") + ), + decode = p => + Page.DetailKriteria( + Titled(OsobniCislo(p._1)), + Titled(p._2), + Titled(p._3.replaceAll("--", ".")) + ), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / "kriterium" / segment[String] / endOfSegments, + basePath = base + ), + Route[Page.UpravDukazKriteria, (String, String, String)]( + encode = p => + ( + p.osobniCislo.value.toString, + p.parametr.value, + p.kriterium.value.replaceAll("\\.", "--") + ), + decode = p => + Page.UpravDukazKriteria( + Titled(OsobniCislo(p._1)), + Titled(p._2), + Titled(p._3.replaceAll("--", ".")) + ), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / "kriterium" / segment[String] / "edit" / endOfSegments, + basePath = base + ) + ), + serializePage = _.toJson, + deserializePage = _.fromJson[Page] + .fold(s => throw IllegalStateException(s), identity), + getPageTitle = _.title, + routeFallback = url => Page.NotFound(url), + deserializeFallback = _ => Page.Dashboard + )( + $popStateEvent = windowEvents.onPopState, + owner = unsafeWindowOwner + ) diff --git a/app/src/main/scala/mdr/pdb/app/actions.scala b/app/src/main/scala/mdr/pdb/app/actions.scala new file mode 100644 index 0000000..d078b3a --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/actions.scala @@ -0,0 +1,21 @@ +package mdr.pdb.app + +import mdr.pdb.OsobniCislo +import mdr.pdb.UserInfo +import mdr.pdb.Parameter +import mdr.pdb.ParameterCriteria + +sealed trait Action + +case object FetchDirectory extends Action +case class FetchUserDetails(osc: OsobniCislo) extends Action +case class FetchParameters(osc: OsobniCislo) extends Action +case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action +case class FetchParameterCriteria( + osc: OsobniCislo, + paramId: String, + critId: String, + page: (UserInfo, Parameter, ParameterCriteria) => Page +) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action +case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/mdr/pdb/app/components/AppPage.scala new file mode 100644 index 0000000..130c817 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/AppPage.scala @@ -0,0 +1,82 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.Page +import mdr.pdb.{UserProfile, UserInfo, OsobniCislo} +import com.raquo.waypoint.Router +import mdr.pdb.app.Action +import mdr.pdb.app.NavigateTo +import mdr.pdb.UserFunction + +object AppPage: + // TODO: pages by logged in user + val pages: List[Page] = List(Page.Directory, Page.Dashboard) + + import NavigationBar.{Logo, MenuItem} + + val logo = Logo( + "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", + "Workflow" + ) + + // TODO: menu items by user profile + val userMenu = + List( + MenuItem("Your Profile"), + MenuItem("Settings"), + MenuItem("Sign out") + ) + + // TODO: load user profile + val $userProfile = Var( + UserProfile( + "tom", + UserInfo( + OsobniCislo("1031"), + "tom", + "Tom", + "Cook", + None, + None, + Some("tom@example.com"), + Some("+420 222 866 180"), + Some( + UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") + ), + Nil, + None + ) + ) + ) + + val $userInfo = $userProfile.signal.map(_.userInfo) + + type ViewModel = Option[HtmlElement] + def apply( + actionBus: Observer[Action] + )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using + router: Router[Page] + ): HtmlElement = + PageLayout(actionBus)( + $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => + PageLayout.ViewModel( + NavigationBar.ViewModel( + u, + pages.map(p => + NavigationBar.Link( + () => + PageLink( + p, + actionBus + ), + p == cp + ) + ), + userMenu, + logo + ), + c + ) + ), + mods + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala new file mode 100644 index 0000000..9375398 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala @@ -0,0 +1,143 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.tailwind.list.IconText.ViewModel +import fiftyforms.ui.components.tailwind.Icons +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.Page + +import com.raquo.laminar.api.L.{*, given} +import io.laminext.syntax.core.{*, given} +import mdr.pdb.app.Action + +object Breadcrumbs: + + private def slash = + import svg.{*, given} + svg( + cls := "flex-shrink-0 h-5 w-5 text-gray-300", + xmlns := "http://www.w3.org/2000/svg", + fill := "currentColor", + viewBox := "0 0 20 20", + ariaHidden := true, + path( + d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" + ) + ) + + def backIcon = + Icons.solid + .`arrow-narrow-left`() + .amend( + svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" + ) + + object Link: + case class ViewModel( + page: Page, + icon: Option[SvgElement], + extraClasses: String, + text: String, + textClasses: Option[String] = None + ) + + val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" + + def shortHome(p: Page) = ViewModel( + p, + Some(backIcon), + "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", + "Zpět na úvodní stránku", + None + ) + + def fullHome(p: Page) = ViewModel( + p, + Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), + "text-gray-400 hover:text-gray-500", + "Domů", + Some("sr-only") + ) + + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + PageLink + .container($m.map(_.page), actionBus) + .amend( + cls <-- $m.map(_.extraClasses), + child.maybe <-- $m.map(_.icon), + span( + cls <-- $m.map(_.textClasses.getOrElse("")), + child.text <-- $m.map(_.text) + ) + ) + + object FullBreadcrumbs: + type ViewModel = Page + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + ol( + role := "list", + cls := "flex items-center space-x-4", + children <-- $m.map(_.path).split(_.id)((_, _, $p) => + li( + div( + cls := "flex items-center", + child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), + Link( + $p.map(p => + if (p.isRoot) then Link.fullHome(p) + else + Link.ViewModel( + p, + None, + s"ml-4 max-w-xs truncate ${Link.baseClasses}", + p.title + ) + ), + actionBus + ) + ) + ) + ) + ) + + object ShortBreadcrumbs: + type ViewModel = Page + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + Link( + $m.map { p => + val target = p.parent.getOrElse(p) + if target.isRoot then Link.shortHome(target) + else + Link.ViewModel( + target, + Some(backIcon), + "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", + s"Zpět na ${target.title}" + ) + }, + actionBus + ) + + def apply(actionBus: Observer[Action])(using + router: Router[Page] + ): HtmlElement = + nav( + cls := "flex", + aria.label := "Breadcrumb", + div( + cls := "flex sm:hidden", + ShortBreadcrumbs(router.$currentPage, actionBus) + ), + div( + cls := "hidden sm:block", + FullBreadcrumbs(router.$currentPage, actionBus) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala new file mode 100644 index 0000000..4bb8b8d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala @@ -0,0 +1,242 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent +import fiftyforms.ui.components.tailwind.Icons +import fiftyforms.ui.components.tailwind.Avatar +import mdr.pdb.UserInfo +import io.laminext.syntax.core.* + +object NavigationBar: + + case class Logo(img: String, name: String) + case class Link(a: () => Anchor, active: Boolean) + case class MenuItem(title: String) + + case class ViewModel( + userInfo: UserInfo, + pages: List[Link], + userMenu: List[MenuItem], + logo: Logo + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + val $userInfo = $m.map(_.userInfo) + val mobileMenuOpen = Var(false) + + val desktopOnly = cls("hidden md:block") + val mobileOnly = cls("md:hidden") + + inline def avatarImage(size: Int = 8) = + Avatar($userInfo.map(_.img)).avatarImage(size) + + def notificationButton = button( + tpe := "button", + cls := List( + "bg-indigo-600", + "focus:outline-none", + "focus:ring-2", + "focus:ring-offset-2", + "focus:ring-offset-indigo-600", + "focus:ring-white", + "hover:text-white", + "p-1", + "rounded-full", + "text-indigo-200" + ), + span(cls := "sr-only", "View notifications"), + Icons.outline.bell() + ) + + def userProfile: HtmlElement = + val menuOpen = Var(false) + + def menuItem(item: MenuItem, idx: Int): HtmlElement = + a( + href := "#", + cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", + role := "menuitem", + tabIndex := -1, + idAttr := s"user-menu-item-$idx", + item.title + ) + + div( + cls := "ml-3 relative", + div( + button( + tpe := "button", + cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", + idAttr := "user-menu-button", + aria.expanded <-- menuOpen.signal, + aria.hasPopup := true, + span(cls := "sr-only", "Open user menu"), + child <-- avatarImage(), + onClick.preventDefault.mapTo( + !menuOpen.now() + ) --> menuOpen.writer + ) + ), + /* + * */ + div( + cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", + cls <-- menuOpen.signal.map { o => + if (o) "md:block" else "md:hidden" + }, + role := "menu", + aria.orientation := "vertical", + aria.labelledBy := "user-menu-button", + tabIndex := -1, + // : keyboard navigation + children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) + ) + ) + + def mobileProfile = + def menuItem(item: MenuItem): HtmlElement = + a( + href := "#", + cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", + item.title + ) + + div( + cls := "pt-4 pb-3 border-t border-indigo-700", + div( + cls := "flex items-center px-5", + div( + cls := "flex-shrink-0", + child <-- avatarImage(10) + ), + div( + cls := "ml-3", + div( + cls := "text-base font-medium text-white", + child.text <-- $userInfo.map(_.name) + ), + child.maybe <-- $userInfo.map( + _.email.map(e => + div( + cls := "text-sm font-medium text-indigo-300", + e + ) + ) + ) + ), + notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) + ), + div( + cls := "mt-3 px-2 space-y-1", + children <-- $m.map(_.userMenu.map(menuItem)) + ) + ) + + def pageLink(page: Link): Anchor = + page + .a() + .amend( + cls := "text-white px-3 py-2 rounded-md text-sm font-medium", + cls := Seq( + "bg-indigo-700" -> page.active, + "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active + ), + ariaCurrent := (if page.active then "page" else "false") + ) + + def logoImg: Image = + img( + cls := "h-8 w-8", + src <-- $m.map(_.logo.img), + alt <-- $m.map(_.logo.name) + ) + + def mobileMenuButton = button( + tpe := "button", + cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", + aria.controls := "mobile-menu", + aria.expanded <-- mobileMenuOpen.signal, + span(cls := "sr-only", "Open main menu"), + Icons.outline + .menu() + .amend( + svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") + ), + Icons.outline + .x() + .amend( + svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") + ), + onClick.preventDefault.mapTo( + !mobileMenuOpen.now() + ) --> mobileMenuOpen.writer + ) + + def navBarLeft = + div( + cls := "flex items-center", + div(cls := "flex-shrink-0", logoImg), + div( + desktopOnly, + div( + cls := "ml-10 flex items-baseline space-x-4", + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) + ) + ) + ) + + def navBarRight = + div( + desktopOnly, + div( + cls := "ml-4 flex items-center md:ml-6", + notificationButton, + userProfile + ) + ) + + def navBarMobile = + div( + cls := "-mr-2 flex", + mobileOnly, + mobileMenuButton + ) + + def navBar = + div( + cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", + div( + cls := "flex items-center justify-between h-16", + navBarLeft, + navBarRight, + navBarMobile + ) + ) + + def mobileMenu = + div( + mobileOnly, + cls <-- mobileMenuOpen.signal.map { o => + if (o) "block" else "hidden" + }, + idAttr := "mobile-menu", + div( + cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) + ), + mobileProfile + ) + + nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala new file mode 100644 index 0000000..c4cb10d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala @@ -0,0 +1,19 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.Page +import com.raquo.waypoint.Router +import mdr.pdb.app.Action + +object PageHeader: + def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = + header( + cls := "bg-white shadow-sm", + div( + cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", + h1( + cls := "text-lg leading-6 font-semibold text-gray-900", + Breadcrumbs(actionBus) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala new file mode 100644 index 0000000..aae4539 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala @@ -0,0 +1,30 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.Page +import mdr.pdb.app.Action +import fiftyforms.ui.components.tailwind.Loading +import io.laminext.syntax.core.* + +object PageLayout: + case class ViewModel( + navigation: NavigationBar.ViewModel, + content: Option[HtmlElement] + ) + def apply(actionBus: Observer[Action])( + $m: Signal[ViewModel], + mods: Modifier[HtmlElement]* + )(using router: Router[Page]): HtmlElement = + val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) + div( + cls := "h-full flex flex-col", + NavigationBar($m.map(_.navigation)), + child.maybe <-- router.$currentPage.map(_.isRoot) + .switch(None, Some(PageHeader(actionBus))), + main( + cls := "flex-grow-1 overflow-y-auto", + mods, + child <-- $maybeContent.map(_.getOrElse(Loading)) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/mdr/pdb/app/components/PageLink.scala new file mode 100644 index 0000000..e6e7052 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageLink.scala @@ -0,0 +1,40 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.LinkSupport.* +import mdr.pdb.app.Page +import mdr.pdb.app.Action +import com.raquo.waypoint.Router +import mdr.pdb.app.NavigateTo + +object PageLink: + type ViewModel = Page + def apply($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = + a(mods($m, actions), child.text <-- $m.map(_.title)) + + def apply(m: ViewModel, actions: Observer[Action])(using + router: Router[Page] + ): Anchor = apply(Val(m), actions) + + def container($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = a(mods($m, actions)) + + def container(m: ViewModel, actions: Observer[Action])(using + Router[Page] + ): Anchor = container(Val(m), actions) + + private def mods($m: Signal[Page], actions: Observer[Action])(using + router: Router[Page] + ): Modifier[Anchor] = + Seq( + href <-- $m.map(router.absoluteUrlForPage).recover { case _ => + Some("invalid url") + }, + composeEvents(onClick.noKeyMod.preventDefault)( + _.sample($m) + .map(NavigateTo.apply) + ) --> actions + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala new file mode 100644 index 0000000..7682772 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala @@ -0,0 +1,13 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.pages.dashboard.DashboardPage +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage + +class DashboardPageConnector(actionBus: Observer[Action])(using + router: Router[Page] +): + def apply: HtmlElement = + AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala new file mode 100644 index 0000000..76b9133 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala @@ -0,0 +1,76 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.Parameter +import mdr.pdb.UserInfo +import mdr.pdb.app.pages.detail.DetailParametruPage +import pages.detail.DetailKriteriaPage +import pages.detail.components.DukazyKriteria +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.ParameterCriteria + +object DetailKriteriaPageConnector { + trait AppState { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + } +} + +case class DetailKriteriaPageConnector( + state: DetailKriteriaPageConnector.AppState +)( + $page: Signal[Page.DetailKriteria] +)(using Router[Page]): + val $paramChangeSignal = + $page.splitOne(p => + (p.osobniCislo.value, p.parametr.value, p.kriterium.value) + )((x, _, _) => x) + val $pageChangeSignal = + $paramChangeSignal.map( + FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) + ) + + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + val $merged = + $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => + for { + da <- d + pa <- p + pb <- pa.find(_.id == pc._2) + ka <- pb.criteria.find(_.id == pc._3) + } yield (da, pb, ka) + ) + + def apply: HtmlElement = + AppPage(state.actionBus)( + $merged.map(_.map(buildModel)) + .split(_ => ())((_, s, $s) => + DetailKriteriaPage($s)(state.actionBus.contramap { + case DukazyKriteria.Add => + NavigateTo( + Page.UpravDukazKriteria( + Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), + Page.Titled(s.parametr.id, Some(s.parametr.nazev)), + Page.Titled(s.kriterium.id, Some(s.kriterium.id)) + ) + ) + }) + ), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: Parameter, + k: ParameterCriteria + ): DetailKriteriaPage.ViewModel = + DetailKriteriaPage.ViewModel( + o.toDetailOsoby, + p.toParametr(_ => a()), + k.toKriterium(_ => a()) + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DetailPageConnector.scala new file mode 100644 index 0000000..df3a6b7 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DetailPageConnector.scala @@ -0,0 +1,57 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.UserInfo +import pages.detail.DetailPage +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.pages.detail.components.DetailOsoby +import mdr.pdb.Parameter +import mdr.pdb.app.pages.detail.components.SeznamParametru +import fiftyforms.ui.components.tailwind.Color + +object DetailPageConnector { + trait AppState { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + } +} + +case class DetailPageConnector(state: DetailPageConnector.AppState)( + $page: Signal[Page.Detail] +)(using router: Router[Page]): + val $oscChangeSignal = $page.splitOne(_.osobniCislo.value)((osc, _, _) => osc) + val $pageChangeSignal = + $oscChangeSignal.flatMap(osc => + EventStream.fromSeq(Seq(FetchUserDetails(osc), FetchParameters(osc))) + ) + // TODO: filter the value based on the current osc + // OSC change will fetch new data, but still + // - we need to be sure that what we got is really what we ought to display + // - we want to display stale data accordingly (at least with loading indicator) + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + def apply: HtmlElement = + AppPage(state.actionBus)( + $data.combineWithFn($params)(_ zip _) + .map(_.map(buildModel)) + .split(_ => ())((_, _, s) => DetailPage(s)), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: List[Parameter] + ): DetailPage.ViewModel = + DetailPage.ViewModel( + o.toDetailOsoby, + p.map( + _.toParametr(param => + PageLink.container(Page.DetailParametru(o, param), state.actionBus) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DetailParametruPageConnector.scala new file mode 100644 index 0000000..fa7193d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DetailParametruPageConnector.scala @@ -0,0 +1,67 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.Parameter +import mdr.pdb.UserInfo +import mdr.pdb.app.pages.detail.DetailParametruPage +import pages.detail.DetailParametruPage +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.components.PageLink + +object DetailParametruPageConnector { + trait AppState { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + } +} + +case class DetailParametruPageConnector( + state: DetailParametruPageConnector.AppState +)( + $page: Signal[Page.DetailParametru] +)(using router: Router[Page]): + val $paramChangeSignal = + $page.splitOne(p => (p.osobniCislo.value, p.parametr.value))((x, _, _) => x) + val $pageChangeSignal = + $paramChangeSignal.map(FetchParameter(_, _)) + + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + val $merged = + $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => + for { + da <- d + pa <- p + pb <- pa.find(_.id == pc._2) + } yield (da, pb) + ) + + def apply: HtmlElement = + AppPage(state.actionBus)( + $merged.map(_.map(buildModel)) + .split(_ => ())((_, _, s) => DetailParametruPage(s)), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: Parameter + ): DetailParametruPage.ViewModel = + DetailParametruPage.ViewModel( + o.toDetailOsoby, + p.toParametr(p => + PageLink.container(Page.DetailParametru(o, p), state.actionBus) + ), + p.criteria.map( + _.toKriterium { c => + PageLink.container( + Page.DetailKriteria(o, p, c), + state.actionBus + ) + } + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DirectoryPageConnector.scala new file mode 100644 index 0000000..ab2c045 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DirectoryPageConnector.scala @@ -0,0 +1,34 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.UserInfo +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.components.AppPage + +case class DirectoryPageConnector( + $input: EventStream[List[UserInfo]], + actionBus: Observer[Action] +)(using router: Router[Page]): + val $data = $input.startWithNone + val $actionSignal = EventStream.fromValue(FetchDirectory) + + def apply: HtmlElement = + AppPage(actionBus)( + $data.split(_ => ())((_, _, s) => + pages.directory.DirectoryPage( + s.map( + _.map( + _.toUserRow(u => + PageLink.container( + Page.Detail(Page.Titled(u.personalNumber)), + actionBus + ) + ) + ) + ) + ) + ), + $actionSignal --> actionBus + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/mdr/pdb/app/connectors/conversions.scala new file mode 100644 index 0000000..f581796 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/conversions.scala @@ -0,0 +1,68 @@ +package mdr.pdb.app.connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.UserInfo +import mdr.pdb.app.pages.detail.components.DetailOsoby +import mdr.pdb.Parameter +import mdr.pdb.app.pages.detail.components.SeznamParametru +import mdr.pdb.ParameterCriteria +import mdr.pdb.app.pages.detail.components.SeznamKriterii +import mdr.pdb.app.pages.directory.components.UserRow +import mdr.pdb.app.pages.detail.components.DetailParametru +import mdr.pdb.app.pages.detail.components.DetailKriteria +import fiftyforms.ui.components.tailwind.Color + +extension (o: UserInfo) + def toDetailOsoby: DetailOsoby.ViewModel = + DetailOsoby.ViewModel( + o.personalNumber, + o.name, + o.email, + o.phone, + o.img, + o.mainFunction.map(f => + DetailOsoby.Funkce.ViewModel(f.name, f.dept, f.ou) + ), + o.userContracts.headOption.map(c => + DetailOsoby.PracovniPomer.ViewModel(c.rel, c.startDate, c.endDate) + ) + ) + +extension (param: Parameter) + def toParametr(container: Parameter => Anchor): DetailParametru.ViewModel = + DetailParametru.ViewModel( + id = param.id, + nazev = param.name, + popis = param.description, + status = "Nesplněno", + statusColor = Color.red, + a = container(param) + ) + +extension (crit: ParameterCriteria) + def toKriterium( + container: ParameterCriteria => Anchor + ): DetailKriteria.ViewModel = + DetailKriteria.ViewModel( + nazev = crit.criteriumText, + kapitola = crit.chapterId, + bod = crit.itemId, + status = "Nesplněno", + statusColor = Color.red, + splneno = false, + dukazy = Nil, + container = container(crit) + ) + +extension (user: UserInfo) + def toUserRow( + container: UserInfo => HtmlElement = (_: UserInfo) => div() + ): UserRow.ViewModel = + UserRow.ViewModel( + osobniCislo = user.personalNumber.toString, + celeJmeno = user.name, + prijmeni = user.surname, + hlavniFunkce = user.mainFunction.map(_.name), + img = user.img, + container = () => container(user) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/dashboard/DashboardPage.scala b/app/src/main/scala/mdr/pdb/app/pages/dashboard/DashboardPage.scala new file mode 100644 index 0000000..0eb8f7a --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/dashboard/DashboardPage.scala @@ -0,0 +1,11 @@ +package mdr.pdb.app.pages.dashboard + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.Page + +object DashboardPage: + + def render: HtmlElement = + div("Dashboard page") diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala new file mode 100644 index 0000000..ff3a25f --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala @@ -0,0 +1,31 @@ +package mdr.pdb.app.pages.detail + +import com.raquo.laminar.api.L.{*, given} + +import components._ + +object DetailKriteriaPage: + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametr: DetailParametru.ViewModel, + kriterium: DetailKriteria.ViewModel + ) + + def apply($m: Signal[ViewModel])( + action: Observer[DukazyKriteria.Action] + ): HtmlElement = + div( + cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + div( + DetailOsoby.header($m.map(_.osoba)), + DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") + ), + div( + DetailKriteria($m.map(_.kriterium)), + DukazyKriteria($m.map(_.kriterium.dukazy))(action) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/DetailPage.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailPage.scala new file mode 100644 index 0000000..b591cc9 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailPage.scala @@ -0,0 +1,25 @@ +package mdr.pdb.app.pages.detail + +import com.raquo.laminar.api.L.{*, given} + +import components._ +import mdr.pdb.app.Page +import com.raquo.waypoint.Router +import mdr.pdb.app.Action + +object DetailPage: + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametry: SeznamParametru.ViewModel + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + DetailOsoby($m.map(_.osoba)), + SeznamParametru($m.map(_.parametry)) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/DetailParametruPage.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailParametruPage.scala new file mode 100644 index 0000000..3322279 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailParametruPage.scala @@ -0,0 +1,24 @@ +package mdr.pdb.app.pages.detail + +import com.raquo.laminar.api.L.{*, given} + +import components._ + +object DetailParametruPage: + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametr: DetailParametru.ViewModel, + kriteria: SeznamKriterii.ViewModel + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + DetailOsoby.header($m.map(_.osoba)), + DetailParametru($m.map(_.parametr)), + SeznamKriterii($m.map(_.kriteria)) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala new file mode 100644 index 0000000..b58618a --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala @@ -0,0 +1,107 @@ +package mdr.pdb +package app +package pages.detail + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.pages.detail.components.UpravDukazForm +import fiftyforms.services.files.File + +object UpravDukaz: + + type ThisPage = Page.UpravDukazKriteria + type PageKey = (OsobniCislo, String, String) + + trait State { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def availableFiles: EventStream[List[File]] + def actionBus: Observer[Action] + } + + def keyOfPage(page: ThisPage): PageKey = + (page.osobniCislo.value, page.parametr.value, page.kriterium.value) + + def onChangeAction(key: PageKey): Action = + FetchParameterCriteria( + key._1, + key._2, + key._3, + Page.UpravDukazKriteria(_, _, _) + ) + + class Connector(state: State)($page: Signal[ThisPage])(using Router[Page]): + val $paramChangeSignal = $page.splitOne(keyOfPage)((x, _, _) => x) + val $pageChangeSignal = $paramChangeSignal.map(onChangeAction) + + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + val $merged = + $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => + for { + da <- d + pa <- p + pb <- pa.find(_.id == pc._2) + ka <- pb.criteria.find(_.id == pc._3) + } yield (da, pb, ka) + ) + + def apply: HtmlElement = + AppPage(state.actionBus)( + $merged.split(_ => ())((_, s, $s) => + PageComponent( + $s.map(buildModel), + state.availableFiles, + state.actionBus.contramap { + case UpravDukazForm.Cancelled => + NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) + case UpravDukazForm.AvailableFilesRequested => + FetchAvailableFiles(s._1.personalNumber) + } + ) + ), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: Parameter, + k: ParameterCriteria + ): PageComponent.ViewModel = + import connectors.* + PageComponent.ViewModel( + o.toDetailOsoby, + p.toParametr(_ => a()), + k.toKriterium(_ => a()) + ) + + object PageComponent: + import components.* + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametr: DetailParametru.ViewModel, + kriterium: DetailKriteria.ViewModel + ) + + def apply( + $m: Signal[ViewModel], + availableFilesStream: EventStream[List[File]], + events: Observer[UpravDukazForm.Event] + ): HtmlElement = + div( + cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + div( + DetailOsoby.header($m.map(_.osoba)), + DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") + ), + div( + DetailKriteria($m.map(_.kriterium)), + UpravDukazForm(availableFilesStream)(events) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailKriteria.scala new file mode 100644 index 0000000..a3ab634 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailKriteria.scala @@ -0,0 +1,37 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.CustomAttrs +import fiftyforms.ui.components.tailwind.Icons +import java.time.LocalDate +import fiftyforms.ui.components.tailwind.Color + +object DetailKriteria: + case class ViewModel( + nazev: String, + kapitola: String, + bod: String, + status: String, + statusColor: Color, + splneno: Boolean, + dukazy: List[DukazKriteria.ViewModel], + container: HtmlElement = div() + ) { + val id = s"${kapitola}${bod}" + } + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "pb-5 border-b border-gray-200", + h3( + cls := "text-xl leading-6 font-bold text-gray-900", + child.text <-- $m.map(_.nazev) + ), + h3( + cls := "mt-2 max-w-4xl text-sm text-gray-500", + "Kapitola ", + child.text <-- $m.map(_.kapitola), + " bod ", + child <-- $m.map(_.bod) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailOsoby.scala new file mode 100644 index 0000000..0fe2ba5 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailOsoby.scala @@ -0,0 +1,100 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.OsobniCislo + +import java.time.LocalDate +import fiftyforms.ui.components.tailwind.Avatar + +object DetailOsoby: + + object Funkce: + case class ViewModel( + nazev: String, + stredisko: String, + voj: String + ) + + def render($m: Signal[ViewModel]): HtmlElement = + p( + cls := "text-sm font-medium text-gray-500", + child.text <-- $m.map(_.nazev), + span( + cls := "hidden md:inline", + " @ ", + child.text <-- $m.map(_.stredisko), + ", ", + child.text <-- $m.map(_.voj) + ) + ) + + object PracovniPomer: + case class ViewModel( + druh: String, + pocatek: LocalDate, + konec: Option[LocalDate] + ) + + def render($m: Signal[ViewModel]): HtmlElement = + import fiftyforms.ui.components.tailwind.CustomAttrs.datetime + p( + cls := "text-sm font-medium text-gray-500", + child.text <-- $m.map(_.druh), + " od ", + time( + datetime <-- $m.map(_.pocatek.toString), + child.text <-- $m.map(_.pocatek.toString) + ) + ) + + case class ViewModel( + osobniCislo: OsobniCislo, + jmeno: String, + email: Option[String], + telefon: Option[String], + img: Option[String], + hlavniFunkce: Option[Funkce.ViewModel], + pracovniPomer: Option[PracovniPomer.ViewModel] + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "md:flex md:items-center md:justify-between md:space-x-4", + div( + cls := "flex items-start space-x-4", + div( + cls := "flex-shrink-0", + Avatar($m.map(_.img)).avatar(16) + ), + div( + h1( + cls := "text-2xl font-bold text-gray-900", + child.text <-- $m.map(_.jmeno) + ), + child.maybe <-- $m.map(_.hlavniFunkce).split(_ => ())((_, _, d) => + Funkce.render(d) + ), + child.maybe <-- $m.map(_.pracovniPomer).split(_ => ())((_, _, d) => + PracovniPomer.render(d) + ) + ) + ) + ) + + def header($m: Signal[ViewModel]): HtmlElement = + div( + cls := "md:flex md:items-center md:justify-between md:space-x-5", + div( + cls := "flex items-start space-x-4", + div( + cls := "flex-shrink-0", + Avatar($m.map(_.img)).avatar(8) + ), + div( + h1( + cls := "text-lg font-medium text-gray-800", + child.text <-- $m.map(_.jmeno) + ) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailParametru.scala new file mode 100644 index 0000000..6310dde --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailParametru.scala @@ -0,0 +1,34 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.Color + +object DetailParametru: + case class ViewModel( + id: String, + nazev: String, + popis: String, + status: String, + statusColor: Color, + a: Anchor + ) + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "pb-5 border-b border-gray-200", + h2( + cls := "text-2xl leading-6 font-medium text-gray-900", + child.text <-- $m.map(_.nazev) + ), + p( + cls := "mt-2 max-w-4xl text-sm text-gray-500", + child.text <-- $m.map(_.popis) + ) + ) + + def header($m: Signal[ViewModel]): HtmlElement = + div( + h2( + cls := "text-lg leading-6 font-medium text-gray-600", + child.text <-- $m.map(_.nazev) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazKriteria.scala new file mode 100644 index 0000000..c292919 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazKriteria.scala @@ -0,0 +1,177 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import java.time.LocalDate +import fiftyforms.ui.components.tailwind.CustomAttrs + +object DukazKriteria: + case class Osoba(osobniCislo: String, jmeno: String) + case class Udalost(osoba: Osoba, datum: LocalDate) + case class Dokument( + url: String, + nazev: String, + pridal: Udalost, + odebral: Option[Udalost] + ) + case class ViewModel( + dokumenty: List[Dokument], + autorizoval: Option[Udalost], + platiDo: Option[LocalDate], + poznámka: Option[String] + ) + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "bg-white shadow overflow-hidden sm:rounded-lg", + div( + cls := "px-4 py-5 sm:px-6", + h3( + cls := "text-lg leading-6 font-medium text-gray-900", + """Applicant Information""" + ), + p( + cls := "mt-1 max-w-2xl text-sm text-gray-500", + """Personal details and application.""" + ) + ), + div( + cls := "border-t border-gray-200 px-4 py-5 sm:px-6", + dl( + cls := "grid grid-cols-1 gap-x-4 gap-y-8 sm:grid-cols-2", + div( + cls := "sm:col-span-1", + dt( + cls := "text-sm font-medium text-gray-500", + """Full name""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """Margot Foster""" + ) + ), + div( + cls := "sm:col-span-1", + dt( + cls := "text-sm font-medium text-gray-500", + """Application for""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """Backend Developer""" + ) + ), + div( + cls := "sm:col-span-1", + dt( + cls := "text-sm font-medium text-gray-500", + """Email address""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """margotfoster@example.com""" + ) + ), + div( + cls := "sm:col-span-1", + dt( + cls := "text-sm font-medium text-gray-500", + """Salary expectation""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """$120,000""" + ) + ), + div( + cls := "sm:col-span-2", + dt( + cls := "text-sm font-medium text-gray-500", + """About""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """Fugiat ipsum ipsum deserunt culpa aute sint do nostrud anim incididunt cillum culpa consequat. Excepteur qui ipsum aliquip consequat sint. Sit id mollit nulla mollit nostrud in ea officia proident. Irure nostrud pariatur mollit ad adipisicing reprehenderit deserunt qui eu.""" + ) + ), + div( + cls := "sm:col-span-2", + dt( + cls := "text-sm font-medium text-gray-500", + """Attachments""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + ul( + role := "list", + cls := "border border-gray-200 rounded-md divide-y divide-gray-200", + li( + cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", + div( + cls := "w-0 flex-1 flex items-center", { + import svg.* + import CustomAttrs.svg.ariaHidden + svg( + cls := "flex-shrink-0 h-5 w-5 text-gray-400", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + ariaHidden := true, + path( + fillRule := "evenodd", + d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", + clipRule := "evenodd" + ) + ) + }, + span( + cls := "ml-2 flex-1 w-0 truncate", + """resume_back_end_developer.pdf""" + ) + ), + div( + cls := "ml-4 flex-shrink-0", + a( + href := "#", + cls := "font-medium text-indigo-600 hover:text-indigo-500", + """Download""" + ) + ) + ), + li( + cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", + div( + cls := "w-0 flex-1 flex items-center", { + import svg.* + import CustomAttrs.svg.ariaHidden + svg( + cls := "flex-shrink-0 h-5 w-5 text-gray-400", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + ariaHidden := true, + path( + fillRule := "evenodd", + d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", + clipRule := "evenodd" + ) + ) + }, + span( + cls := "ml-2 flex-1 w-0 truncate", + """coverletter_back_end_developer.pdf""" + ) + ), + div( + cls := "ml-4 flex-shrink-0", + a( + href := "#", + cls := "font-medium text-indigo-600 hover:text-indigo-500", + """Download""" + ) + ) + ) + ) + ) + ) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala new file mode 100644 index 0000000..d604860 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala @@ -0,0 +1,35 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.Icons + +object DukazyKriteria: + sealed trait Action + case object Add extends Action + + type ViewModel = List[DukazKriteria.ViewModel] + + def apply($m: Signal[ViewModel])(actions: Observer[Action]): HtmlElement = + div( + child.maybe <-- $m.map(m => + if (m.isEmpty) then Some(prazdnyDukaz(actions)) + else None + ), + children <-- $m.map(_.zipWithIndex).split(_._2)((_, _, $s) => + DukazKriteria($s.map(_._1)) + ) + ) + + private def prazdnyDukaz(actions: Observer[Action]): HtmlElement = + button( + tpe := "button", + onClick.preventDefault.mapTo(Add) --> actions, + cls := "relative block w-full border-2 border-gray-300 border-dashed rounded-lg p-12 text-center hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + Icons.outline + .`document-add`(12) + .amend(svg.cls := "mx-auto text-gray-400"), + span( + cls := "mt-2 block text-sm font-medium text-gray-900", + "Přidat důkaz" + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala new file mode 100644 index 0000000..7e77a7b --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala @@ -0,0 +1,34 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.Color +import fiftyforms.ui.components.tailwind.list.StackedList +import fiftyforms.ui.components.tailwind.list.ListRow +import fiftyforms.ui.components.tailwind.list.RowTag +import fiftyforms.ui.components.tailwind.list.RowNext +import java.time.LocalDate + +object SeznamKriterii: + type ViewModel = List[DetailKriteria.ViewModel] + + private val kritList = new StackedList[DetailKriteria.ViewModel] + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "bg-white shadow overflow-hidden sm:rounded-md", + kritList($m, _.id) { $i => + $i.map { i => + ListRow.ViewModel( + title = i.nazev, + topRight = RowTag.render( + $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) + ), + bottomLeft = + p(cls := "text-sm text-gray-500", s"${i.kapitola}${i.bod}"), + bottomRight = emptyNode, + farRight = RowNext.render, + containerElement = i.container + ) + } + } + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamParametru.scala new file mode 100644 index 0000000..0760b32 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamParametru.scala @@ -0,0 +1,37 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.list.{ + StackedList, + ListRow, + RowTag, + PropList, + IconText, + RowNext +} +import fiftyforms.ui.components.tailwind.Color +import fiftyforms.ui.components.tailwind.LinkSupport.* + +object SeznamParametru: + type ViewModel = List[DetailParametru.ViewModel] + + private val parametrList = new StackedList[DetailParametru.ViewModel] + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "bg-white shadow overflow-hidden sm:rounded-md", + parametrList($m, _.id) { $i => + $i.map { i => + ListRow.ViewModel( + title = i.nazev, + topRight = RowTag.render( + $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) + ), + bottomLeft = emptyNode, + bottomRight = emptyNode, + farRight = RowNext.render, + containerElement = i.a + ) + } + } + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala new file mode 100644 index 0000000..28753c9 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala @@ -0,0 +1,101 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import io.laminext.syntax.core.* +import fiftyforms.ui.components.tailwind.CustomAttrs +import fiftyforms.ui.components.tailwind.form.* +import org.scalajs.dom +import com.raquo.laminar.nodes.ReactiveHtmlElement +import fiftyforms.services.files.components.tailwind.FilePicker +import mdr.pdb.frontend.AutorizujDukaz +import mdr.pdb.frontend.DocumentRef +import fiftyforms.services.files.File + +object UpravDukazForm: + sealed trait Event + case object Cancelled extends Event + case object AvailableFilesRequested extends Event + def apply(availableFilesStream: EventStream[List[File]])( + updates: Observer[Event] + ): HtmlElement = + val (filesStream, filesObserver) = + EventStream.withObserver[FilePicker.Event] + val files = Var[List[File]](Nil) + def submitButtons: HtmlElement = + div( + cls := "pt-5", + div( + cls := "flex justify-end", + button( + tpe := "button", + cls := "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + "Zrušit", + onClick.mapTo(Cancelled) --> updates + ), + button( + tpe := "submit", + cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + disabled <-- files.signal.map(_.isEmpty), + "Autorizovat" + ) + ) + ) + + div( + filesStream.collect { case FilePicker.SelectionUpdated(files) => + files.to(List) + } --> files.writer, + filesStream.collect { case FilePicker.AvailableFilesRequested => + AvailableFilesRequested + } --> updates, + cls := "bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6", + Form( + Form.Body( + Form.Section( + FormHeader( + FormHeader.ViewModel( + "Doložení kritéria", + "Sestavte doklady poskytující důkaz kritéria a potvrďte odesláním formuláře. Případné limitace či jiné relevantní údaje vepište do pole Komentář." + ) + ), + FormFields( + FormRow( + "dokumenty", + "Dokumenty", + FilePicker(files.signal, availableFilesStream)(filesObserver) + .amend(idAttr := "dokumenty", cls("max-w-lg")) + ).toHtml, + FormRow( + "platnost", + "Platnost", + input( + idAttr := "platnost", + name := "platnost", + tpe := "date", + autoComplete := "date", + cls := "block max-w-lg w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" + ) + ).toHtml, + FormRow( + "komentar", + "Komentář", + div( + cls := "mt-1 sm:mt-0 sm:col-span-2", + textArea( + idAttr := "komentar", + name := "about", + rows := 3, + cls := "max-w-lg shadow-sm block w-full focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border border-gray-300 rounded-md" + ), + p( + cls := "mt-2 text-sm text-gray-500", + "Doplňte prosím potřebné informace související s doložením kritéria, včetně případných limitací." + ) + ) + ).toHtml + ) + ) + ), + submitButtons + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/directory/DirectoryPage.scala b/app/src/main/scala/mdr/pdb/app/pages/directory/DirectoryPage.scala new file mode 100644 index 0000000..7a72ca3 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/directory/DirectoryPage.scala @@ -0,0 +1,40 @@ +package mdr.pdb.app.pages.directory + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.UserInfo + +import components._ + +object DirectoryPage: + + type ViewModel = List[UserRow.ViewModel] + + def apply($m: Signal[ViewModel]): HtmlElement = + val (actionsStream, actionObserver) = + EventStream.withObserver[SearchForm.Action] + val $filter = actionsStream + .collect { + case SearchForm.ClearFilter => None + case SearchForm.SetFilter(t) => Some(t) + } + .startWith(None) + val byLetter = for { + d <- $m + f <- $filter + } yield for { + (letter, users) <- d + .filter { user => + f match + case None => true + case Some(t) => user.search.contains(t) + } + .groupBy(_.prijmeni.head) + .to(List) + .sortBy(_._1) + } yield (letter.toString, users.sortBy(_.prijmeni)) + + div( + cls := "h-full max-w-7xl mx-auto order-first flex flex-col flex-shrink-0", + SearchForm(actionObserver), + Directory(byLetter) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/directory/components/Directory.scala b/app/src/main/scala/mdr/pdb/app/pages/directory/components/Directory.scala new file mode 100644 index 0000000..f7d6f99 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/directory/components/Directory.scala @@ -0,0 +1,37 @@ +package mdr.pdb.app.pages.directory.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.UserInfo + +object Directory: + + object Header: + type ViewModel = String + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500", + h3(child.text <-- $m) + ) + + object UserList: + type ViewModel = List[UserRow.ViewModel] + def apply($m: Signal[ViewModel]): HtmlElement = + ul( + role := "list", + cls := "relative z-0 divide-y divide-gray-200", + children <-- $m.split(_.osobniCislo)((_, _, s) => UserRow(s)) + ) + + type ViewModel = List[(String, List[UserRow.ViewModel])] + def apply($m: Signal[ViewModel]): HtmlElement = + nav( + cls := "flex-1 min-h-0 overflow-y-auto", + aria.label := "Directory", + children <-- $m.split(_._1)((_, _, s) => + div( + cls := "relative", + Header(s.map(_._1)), + UserList(s.map(_._2)) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/directory/components/SearchForm.scala b/app/src/main/scala/mdr/pdb/app/pages/directory/components/SearchForm.scala new file mode 100644 index 0000000..fb9ecc4 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/directory/components/SearchForm.scala @@ -0,0 +1,54 @@ +package mdr.pdb.app.pages.directory.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.Icons + +object SearchForm: + sealed trait Action + case object SubmitFilter extends Action + sealed trait FilterAction extends Action + case object ClearFilter extends FilterAction + case class SetFilter(value: String) extends FilterAction + + def apply(actions: Observer[Action]): HtmlElement = + div( + cls := "px-6 pt-4 pb-4", + form( + cls := "flex space-x-4", + onSubmit.mapTo(SubmitFilter) --> actions, + div( + cls := "flex-1 min-w-0", + label( + forId := "search", + cls := "sr-only", + "Hledat" + ), + div( + cls := "relative rounded-md shadow-sm", + div( + cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", + Icons.solid.search().amend(svg.cls := "text-gray-400") + ), + input( + tpe := "search", + name := "search", + idAttr := "search", + cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", + placeholder := "Hledat", + composeEvents(onInput.mapToValue.setAsValue.map(SetFilter(_)))( + _.throttle(500) + ) --> actions + ) + ) + ), + button( + tpe := "submit", + cls := "inline-flex justify-center px-3.5 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500", + Icons.solid.filter().amend(svg.cls := "text-gray-400"), + span( + cls := "sr-only", + "Hledat" + ) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/directory/components/UserRow.scala b/app/src/main/scala/mdr/pdb/app/pages/directory/components/UserRow.scala new file mode 100644 index 0000000..4aa1672 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/directory/components/UserRow.scala @@ -0,0 +1,52 @@ +package mdr.pdb.app.pages.directory.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.airstream.core.Signal +import fiftyforms.ui.components.tailwind.Avatar + +object UserRow: + case class ViewModel( + osobniCislo: String, + celeJmeno: String, + prijmeni: String, + hlavniFunkce: Option[String], + img: Option[String], + container: () => HtmlElement = () => div() + ) { + val search = prijmeni.toLowerCase + } + + def apply($m: Signal[ViewModel]): HtmlElement = + inline def avatarImage = + Avatar($m.map(_.img)).avatarImage(10) + + li( + div( + cls := "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500", + div( + cls := "flex-shrink-0", + child <-- avatarImage + ), + div( + cls := "flex-1 min-w-0", + child <-- $m.map { o => + o.container() + .amend( + cls := "focus:outline-none", + span( + cls := "absolute inset-0", + aria.hidden := true + ), + p( + cls := "text-sm font-medium text-gray-900", + o.celeJmeno + ), + p( + cls := "text-sm text-gray-500 truncate", + o.hlavniFunkce + ) + ) + } + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/errors/ErrorPage.scala b/app/src/main/scala/mdr/pdb/app/pages/errors/ErrorPage.scala new file mode 100644 index 0000000..6b195ef --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/errors/ErrorPage.scala @@ -0,0 +1,99 @@ +package mdr.pdb.app.pages.errors + +import com.raquo.domtypes.generic.codecs.StringAsIsCodec +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.Page +import mdr.pdb.app.Action +import com.raquo.waypoint.Router + +object ErrorPage: + case class ViewModel( + homePage: Page, + errorName: String, + title: String, + subTitle: String + ) + + def apply(m: ViewModel, actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + val ViewModel(homePage, errorName, title, subTitle) = m + div( + cls := "min-h-full pt-16 pb-12 flex flex-col bg-white", + main( + cls := "flex-grow flex flex-col justify-center max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", + div( + cls := "flex-shrink-0 flex justify-center", + a( + href := "/", + cls := "inline-flex", + span( + cls := "sr-only", + """Workflow""" + ), + img( + cls := "h-12 w-auto", + src := "https://tailwindui.com/img/logos/workflow-mark.svg?color=indigo&shade=600", + alt := "" + ) + ) + ), + div( + cls := "py-16", + div( + cls := "text-center", + p( + cls := "text-sm font-semibold text-indigo-600 uppercase tracking-wide", + errorName + ), + h1( + cls := "mt-2 text-4xl font-extrabold text-gray-900 tracking-tight sm:text-5xl", + title + ), + p( + cls := "mt-2 text-base text-gray-500", + subTitle + ), + div( + cls := "mt-6", + PageLink + .container(homePage, actionBus) + .amend( + cls := "text-base font-medium text-indigo-600 hover:text-indigo-500", + """Go back home""" + ) + ) + ) + ) + ), + footer( + cls := "flex-shrink-0 max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", + nav( + cls := "flex justify-center space-x-4", + a( + href := "#", + cls := "text-sm font-medium text-gray-500 hover:text-gray-600", + """Contact Support""" + ), + span( + cls := "inline-block border-l border-gray-300", + aria.hidden := true + ), + a( + href := "#", + cls := "text-sm font-medium text-gray-500 hover:text-gray-600", + """Status""" + ), + span( + cls := "inline-block border-l border-gray-300", + aria.hidden := true + ), + a( + href := "#", + cls := "text-sm font-medium text-gray-500 hover:text-gray-600", + """Twitter""" + ) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/errors/NotFoundPage.scala b/app/src/main/scala/mdr/pdb/app/pages/errors/NotFoundPage.scala new file mode 100644 index 0000000..97760e1 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/errors/NotFoundPage.scala @@ -0,0 +1,21 @@ +package mdr.pdb.app.pages.errors + +import com.raquo.domtypes.generic.codecs.StringAsIsCodec +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.Page +import mdr.pdb.app.Action + +object NotFoundPage: + def apply(homePage: Page, url: String, actionBus: Observer[Action])(using + router: Router[Page] + ): HtmlElement = + ErrorPage( + ErrorPage.ViewModel( + homePage, + "404 error", + "Page not found.", + s"Sorry, but page $url doesn't exist." + ), + actionBus + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala b/app/src/main/scala/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala new file mode 100644 index 0000000..77b0af8 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala @@ -0,0 +1,32 @@ +package mdr.pdb.app.pages.errors + +import com.raquo.domtypes.generic.codecs.StringAsIsCodec +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.Page +import mdr.pdb.app.Action + +object UnhandledErrorPage: + + case class ViewModel( + homePage: Page, + errorName: Option[String], + errorMessage: Option[String] + ) + + def apply(m: ViewModel, actionBus: Observer[Action])(using + router: Router[Page] + ): HtmlElement = + ErrorPage( + ErrorPage.ViewModel( + m.homePage, + "Unexpected error occurred", + m.errorName.getOrElse( + "Uh oh!" + ), // TODO: translations, better text than uh oh + m.errorMessage.getOrElse( + "This wasn't supposed to happen! Please try again." + ) + ), + actionBus + ) diff --git a/app/src/main/scala/mdr/pdb/app/services/DataFetcher.scala b/app/src/main/scala/mdr/pdb/app/services/DataFetcher.scala new file mode 100644 index 0000000..134c221 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/services/DataFetcher.scala @@ -0,0 +1,6 @@ +package mdr.pdb.app.services + +import com.raquo.airstream.core.Observer + +trait DataFetcher[K, A]: + def fetch(id: K, sink: Observer[A]): Unit diff --git a/app/src/main/scala/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/mdr/pdb/app/state/AppState.scala new file mode 100644 index 0000000..e44b2df --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/state/AppState.scala @@ -0,0 +1,120 @@ +package mdr.pdb.app +package state + +import com.raquo.airstream.core.EventStream +import mdr.pdb.{UserInfo, OsobniCislo} +import com.raquo.airstream.core.Observer +import scala.scalajs.js +import scala.scalajs.js.JSON +import zio.json._ +import com.raquo.airstream.eventbus.EventBus +import com.raquo.airstream.ownership.Owner +import com.raquo.waypoint.Router +import mdr.pdb.Parameter +import mdr.pdb.ParameterCriteria +import mdr.pdb.UserFunction +import mdr.pdb.UserContract +import fiftyforms.services.files.File + +trait AppState + extends connectors.DetailPageConnector.AppState + with connectors.DetailParametruPageConnector.AppState + with connectors.DetailKriteriaPageConnector.AppState + with pages.detail.UpravDukaz.State: + def users: EventStream[List[UserInfo]] + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + +class MockAppState(implicit owner: Owner, router: Router[Page]) + extends AppState: + + given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) + given JsonDecoder[UserFunction] = DeriveJsonDecoder.gen + given JsonDecoder[UserContract] = DeriveJsonDecoder.gen + given JsonDecoder[UserInfo] = DeriveJsonDecoder.gen + + given JsonDecoder[ParameterCriteria] = DeriveJsonDecoder.gen + given JsonDecoder[Parameter] = DeriveJsonDecoder.gen + + private val actions = EventBus[Action]() + private val (parametersStream, pushParameters) = + EventStream.withCallback[List[Parameter]] + private val (usersStream, pushUsers) = + EventStream.withCallback[List[UserInfo]] + private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] + private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] + + private val mockData: List[UserInfo] = + mockUsers + .asInstanceOf[js.Dictionary[js.Object]] + .values + // TODO: is there a more efficient way to parse from JS object directly? + .map { o => + val parsed = JSON.stringify(o).fromJson[UserInfo] + parsed.left.foreach(org.scalajs.dom.console.log(_)) + parsed + } + .collect { case Right(u) => + u + } + .toList + + private val mockParameters: List[Parameter] = + pdbParams + .asInstanceOf[js.Dictionary[js.Object]] + .values + .map(o => JSON.stringify(o).fromJson[Parameter]) + .collect { case Right(p) => p } + .toList + + // TODO: Extract to separate event handler + actions.events.foreach { + case FetchDirectory => pushUsers(mockData) + case FetchUserDetails(osc) => + mockData.find(_.personalNumber == osc).foreach { o => + pushDetails(o) + router.replaceState(Page.Detail(o)) + } + case FetchParameters(osc) => + pushParameters(mockParameters) + case FetchParameter(osc, paramId) => + for + o <- mockData.find(_.personalNumber == osc) + p <- mockParameters.find(_.id == paramId) + do + pushDetails(o) + pushParameters(mockParameters) + router.replaceState(Page.DetailParametru(o, p)) + case FetchParameterCriteria(osc, paramId, critId, page) => + for + o <- mockData.find(_.personalNumber == osc) + p <- mockParameters.find(_.id == paramId) + c <- p.criteria.find(_.id == critId) + do + pushDetails(o) + pushParameters(mockParameters) + router.replaceState(page(o, p, c)) + case NavigateTo(page) => router.pushState(page) + case FetchAvailableFiles(osc) => + pushFiles( + List( + File("https://tc163.cmi.cz/here", "Example file") + ) + ) + } + + override def users: EventStream[List[UserInfo]] = + usersStream.debugWithName("users") + + override def details: EventStream[UserInfo] = + detailsStream.debugWithName("details") + + override def parameters: EventStream[List[Parameter]] = + parametersStream.debugWithName("parameters") + + override def availableFiles: EventStream[List[File]] = + filesStream.debugWithName("available files") + + override def actionBus: Observer[Action] = + actions.writer.debugWithName("actions writer") diff --git a/build.sbt b/build.sbt index 2afc879..0daf796 100644 --- a/build.sbt +++ b/build.sbt @@ -11,6 +11,12 @@ lazy val core = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Pure) .in(file("core")) + .settings( + IWDeps.useZIO(Test), + IWDeps.tapirCore, + IWDeps.tapirZIO, + IWDeps.tapirZIOJson + ) lazy val ui = (project in file("ui")) .enablePlugins(ScalaJSPlugin) diff --git a/core/src/main/scala/cz/e_bs/cmi/mdr/pdb/Parameter.scala b/core/src/main/scala/cz/e_bs/cmi/mdr/pdb/Parameter.scala deleted file mode 100644 index 9d68240..0000000 --- a/core/src/main/scala/cz/e_bs/cmi/mdr/pdb/Parameter.scala +++ /dev/null @@ -1,27 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb - -import java.math.BigInteger -import java.security.MessageDigest - -object ParameterCriteria { - type Id = String -} - -case class ParameterCriteria( - chapterId: String, - itemId: String, - criteriumText: String -) { - val id: ParameterCriteria.Id = s"${chapterId}${itemId}" -} - -object Parameter { - type Id = String -} - -case class Parameter( - id: Parameter.Id, - name: String, - description: String, - criteria: List[ParameterCriteria] -) diff --git a/core/src/main/scala/cz/e_bs/cmi/mdr/pdb/UserProfile.scala b/core/src/main/scala/cz/e_bs/cmi/mdr/pdb/UserProfile.scala deleted file mode 100644 index 455a445..0000000 --- a/core/src/main/scala/cz/e_bs/cmi/mdr/pdb/UserProfile.scala +++ /dev/null @@ -1,44 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb - -import java.time.LocalDate - -opaque type OsobniCislo = String - -object OsobniCislo: - // TODO: validation - def apply(osc: String): OsobniCislo = osc - -extension (osc: OsobniCislo) def toString: String = osc - -case class UserContract( - rel: String, - startDate: LocalDate, - endDate: Option[LocalDate] -) -case class UserFunction(name: String, dept: String, ou: String) - -case class UserInfo( - personalNumber: OsobniCislo, - username: String, - givenName: String, - surname: String, - titlesBeforeName: Option[String] = None, - titlesAfterName: Option[String] = None, - email: Option[String] = None, - phone: Option[String] = None, - mainFunction: Option[UserFunction] = None, - userContracts: List[UserContract] = Nil, - img: Option[String] = None -) { - val name = - List( - Some( - List(titlesBeforeName, Some(givenName), Some(surname)).flatten.mkString( - " " - ) - ), - titlesAfterName - ).flatten.mkString(", ") -} - -case class UserProfile(username: String, userInfo: UserInfo) diff --git a/core/src/main/scala/cz/e_bs/cmi/mdr/pdb/frontend/domain.scala b/core/src/main/scala/cz/e_bs/cmi/mdr/pdb/frontend/domain.scala deleted file mode 100644 index 10980c5..0000000 --- a/core/src/main/scala/cz/e_bs/cmi/mdr/pdb/frontend/domain.scala +++ /dev/null @@ -1,17 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb -package frontend - -import java.time.LocalDate -import java.time.Instant - -sealed trait Command - -type DocumentRef = String - -case class AutorizujDukaz( - osoba: OsobniCislo, - parametr: Parameter.Id, - kriterium: ParameterCriteria.Id, - dukaz: List[DocumentRef], - platiDo: Option[LocalDate] -) extends Command diff --git a/core/src/main/scala/mdr/pdb/Parameter.scala b/core/src/main/scala/mdr/pdb/Parameter.scala new file mode 100644 index 0000000..0b89f6c --- /dev/null +++ b/core/src/main/scala/mdr/pdb/Parameter.scala @@ -0,0 +1,27 @@ +package mdr.pdb + +import java.math.BigInteger +import java.security.MessageDigest + +object ParameterCriteria { + type Id = String +} + +case class ParameterCriteria( + chapterId: String, + itemId: String, + criteriumText: String +) { + val id: ParameterCriteria.Id = s"${chapterId}${itemId}" +} + +object Parameter { + type Id = String +} + +case class Parameter( + id: Parameter.Id, + name: String, + description: String, + criteria: List[ParameterCriteria] +) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala deleted file mode 100644 index a3b00af..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala +++ /dev/null @@ -1,103 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import scala.scalajs.js.annotation.JSExportTopLevel -import scala.scalajs.js.annotation.JSExport -import scala.scalajs.js.annotation.JSImport -import scala.scalajs.js -import org.scalajs.dom -import com.raquo.laminar.api.L.{*, given} -import scala.scalajs.js.Date -import com.raquo.waypoint.Router -import com.raquo.waypoint.SplitRender -import cz.e_bs.cmi.mdr.pdb.app.services.DataFetcher -import scala.scalajs.js.JSON -import zio.json._ -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.state.AppState - -@js.native -@JSImport("stylesheets/main.css", JSImport.Namespace) -object Css extends js.Any - -@js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native -@JSImport("params/pdb-params.json", JSImport.Default) -object pdbParams extends js.Object - -@JSExportTopLevel("app") -object Main: - - @JSExport - def main(args: Array[String]): Unit = - import Routes.given - onLoad { - setupAirstream() - val appContainer = dom.document.querySelector("#app") - val _ = - render( - appContainer, - renderPage(state.MockAppState(using unsafeWindowOwner, router)) - ) - } - - private def onLoad(f: => Unit): Unit = - documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) - - private def setupAirstream()(using router: Router[Page]): Unit = - AirstreamError.registerUnhandledErrorCallback(err => - router.forcePage( - Page.UnhandledError( - Some(err.getClass.getName), // TODO: Fill only in dev mode - Some(err.getMessage) - ) - ) - ) - - def renderPage(state: AppState)(using - router: Router[Page] - ): HtmlElement = - val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) - .collectSignal[Page.Detail]( - connectors - .DetailPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailParametru]( - connectors - .DetailParametruPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailKriteria]( - connectors - .DetailKriteriaPageConnector(state)(_) - .apply - ) - .collectSignal[Page.UpravDukazKriteria]( - pages.detail.UpravDukaz.Connector(state)(_).apply - ) - .collectStatic(Page.Dashboard)( - connectors.DashboardPageConnector(state.actionBus).apply - ) - .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) - ) - .collect[Page.UnhandledError](pg => - pages.errors - .UnhandledErrorPage( - pages.errors.UnhandledErrorPage - .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), - state.actionBus - ) - ) - .collectStatic(Page.Directory)( - connectors - .DirectoryPageConnector(state.users, state.actionBus) - .apply - ) - div(cls := "h-full", child <-- pageSplitter.$view) - - // Pull in the stylesheet - val css: Css.type = Css diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala deleted file mode 100644 index 3bbc8c0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ /dev/null @@ -1,198 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.* -import org.scalajs.dom -import zio.json.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import scala.scalajs.js -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.Page.Titled - -// enum is not working with Waypoints' SplitRender collectStatic -sealed abstract class Page( - val id: String, - val title: String, - val parent: Option[Page] -) { - val path: Vector[Page] = - parent match - case None => Vector(this) - case Some(p) => p.path :+ this - - val isRoot: Boolean = parent.isEmpty -} - -object Page: - - case class Titled[V](value: V, title: Option[String] = None): - val show: String = title.getOrElse(value.toString) - - case object Directory extends Page("directory", "Adresář", None) - - case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) - - case class Detail(osobniCislo: Titled[OsobniCislo]) - extends Page("user", osobniCislo.show, Some(Directory)) - - object Detail { - def apply(o: UserInfo): Detail = Detail( - Titled(o.personalNumber, Some(o.name)) - ) - } - - case class DetailParametru( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String] - ) extends Page( - "parameter", - parametr.show, - Some(Detail(osobniCislo)) - ) - - object DetailParametru { - def apply(o: UserInfo, p: Parameter): DetailParametru = - DetailParametru( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)) - ) - } - - case class DetailKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "criteria", - kriterium.show, - Some(DetailParametru(osobniCislo, parametr)) - ) - - object DetailKriteria { - def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = - DetailKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class UpravDukazKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "addProof", - "Důkaz", - Some(DetailKriteria(osobniCislo, parametr, kriterium)) - ) - - object UpravDukazKriteria { - def apply( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): UpravDukazKriteria = - UpravDukazKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class NotFound(url: String) extends Page("404", "404", Some(Directory)) - - case class UnhandledError( - errorName: Option[String], - errorMessage: Option[String] - ) extends Page("500", "Unexpected error", Some(Directory)) - -object Routes: - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) - given [V: JsonEncoder]: JsonEncoder[Titled[V]] = - DeriveJsonEncoder.gen[Titled[V]] - given [V: JsonDecoder]: JsonDecoder[Titled[V]] = - DeriveJsonDecoder.gen[Titled[V]] - given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] - given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] - - val base = - js.`import`.meta.env.BASE_URL - .asInstanceOf[String] + "app" - - val homePage: Page = Page.Directory - - given router: Router[Page] = Router[Page]( - routes = List( - Route.static(homePage, root / endOfSegments, basePath = base), - Route.static( - Page.Dashboard, - root / "dashboard" / endOfSegments, - basePath = base - ), - Route[Page.Detail, String]( - encode = _.osobniCislo.value.toString, - decode = osc => Page.Detail(Titled(OsobniCislo(osc))), - root / "osoba" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.DetailParametru, (String, String)]( - encode = p => (p.osobniCislo.value.toString, p.parametr.value), - decode = - p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / endOfSegments, - basePath = base - ), - Route[Page.DetailKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.DetailKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.UpravDukazKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.UpravDukazKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / "edit" / endOfSegments, - basePath = base - ) - ), - serializePage = _.toJson, - deserializePage = _.fromJson[Page] - .fold(s => throw IllegalStateException(s), identity), - getPageTitle = _.title, - routeFallback = url => Page.NotFound(url), - deserializeFallback = _ => Page.Dashboard - )( - $popStateEvent = windowEvents.onPopState, - owner = unsafeWindowOwner - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala deleted file mode 100644 index b24b82b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -sealed trait Action - -case object FetchDirectory extends Action -case class FetchUserDetails(osc: OsobniCislo) extends Action -case class FetchParameters(osc: OsobniCislo) extends Action -case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action -case class FetchParameterCriteria( - osc: OsobniCislo, - paramId: String, - critId: String, - page: (UserInfo, Parameter, ParameterCriteria) => Page -) extends Action -case class FetchAvailableFiles(osc: OsobniCislo) extends Action -case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala deleted file mode 100644 index c637d35..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala +++ /dev/null @@ -1,82 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.{UserProfile, UserInfo, OsobniCislo} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo -import cz.e_bs.cmi.mdr.pdb.UserFunction - -object AppPage: - // TODO: pages by logged in user - val pages: List[Page] = List(Page.Directory, Page.Dashboard) - - import NavigationBar.{Logo, MenuItem} - - val logo = Logo( - "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", - "Workflow" - ) - - // TODO: menu items by user profile - val userMenu = - List( - MenuItem("Your Profile"), - MenuItem("Settings"), - MenuItem("Sign out") - ) - - // TODO: load user profile - val $userProfile = Var( - UserProfile( - "tom", - UserInfo( - OsobniCislo("1031"), - "tom", - "Tom", - "Cook", - None, - None, - Some("tom@example.com"), - Some("+420 222 866 180"), - Some( - UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") - ), - Nil, - None - ) - ) - ) - - val $userInfo = $userProfile.signal.map(_.userInfo) - - type ViewModel = Option[HtmlElement] - def apply( - actionBus: Observer[Action] - )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using - router: Router[Page] - ): HtmlElement = - PageLayout(actionBus)( - $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => - PageLayout.ViewModel( - NavigationBar.ViewModel( - u, - pages.map(p => - NavigationBar.Link( - () => - PageLink( - p, - actionBus - ), - p == cp - ) - ), - userMenu, - logo - ), - c - ) - ), - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala deleted file mode 100644 index bf473c6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ /dev/null @@ -1,143 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden -import fiftyforms.ui.components.tailwind.list.IconText.ViewModel -import fiftyforms.ui.components.tailwind.Icons -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Action - -object Breadcrumbs: - - private def slash = - import svg.{*, given} - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-300", - xmlns := "http://www.w3.org/2000/svg", - fill := "currentColor", - viewBox := "0 0 20 20", - ariaHidden := true, - path( - d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" - ) - ) - - def backIcon = - Icons.solid - .`arrow-narrow-left`() - .amend( - svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" - ) - - object Link: - case class ViewModel( - page: Page, - icon: Option[SvgElement], - extraClasses: String, - text: String, - textClasses: Option[String] = None - ) - - val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" - - def shortHome(p: Page) = ViewModel( - p, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - "Zpět na úvodní stránku", - None - ) - - def fullHome(p: Page) = ViewModel( - p, - Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), - "text-gray-400 hover:text-gray-500", - "Domů", - Some("sr-only") - ) - - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - PageLink - .container($m.map(_.page), actionBus) - .amend( - cls <-- $m.map(_.extraClasses), - child.maybe <-- $m.map(_.icon), - span( - cls <-- $m.map(_.textClasses.getOrElse("")), - child.text <-- $m.map(_.text) - ) - ) - - object FullBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - ol( - role := "list", - cls := "flex items-center space-x-4", - children <-- $m.map(_.path).split(_.id)((_, _, $p) => - li( - div( - cls := "flex items-center", - child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), - Link( - $p.map(p => - if (p.isRoot) then Link.fullHome(p) - else - Link.ViewModel( - p, - None, - s"ml-4 max-w-xs truncate ${Link.baseClasses}", - p.title - ) - ), - actionBus - ) - ) - ) - ) - ) - - object ShortBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - Link( - $m.map { p => - val target = p.parent.getOrElse(p) - if target.isRoot then Link.shortHome(target) - else - Link.ViewModel( - target, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - s"Zpět na ${target.title}" - ) - }, - actionBus - ) - - def apply(actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - nav( - cls := "flex", - aria.label := "Breadcrumb", - div( - cls := "flex sm:hidden", - ShortBreadcrumbs(router.$currentPage, actionBus) - ), - div( - cls := "hidden sm:block", - FullBreadcrumbs(router.$currentPage, actionBus) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala deleted file mode 100644 index 0db2047..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ /dev/null @@ -1,242 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent -import fiftyforms.ui.components.tailwind.Icons -import fiftyforms.ui.components.tailwind.Avatar -import cz.e_bs.cmi.mdr.pdb.UserInfo -import io.laminext.syntax.core.* - -object NavigationBar: - - case class Logo(img: String, name: String) - case class Link(a: () => Anchor, active: Boolean) - case class MenuItem(title: String) - - case class ViewModel( - userInfo: UserInfo, - pages: List[Link], - userMenu: List[MenuItem], - logo: Logo - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - val $userInfo = $m.map(_.userInfo) - val mobileMenuOpen = Var(false) - - val desktopOnly = cls("hidden md:block") - val mobileOnly = cls("md:hidden") - - inline def avatarImage(size: Int = 8) = - Avatar($userInfo.map(_.img)).avatarImage(size) - - def notificationButton = button( - tpe := "button", - cls := List( - "bg-indigo-600", - "focus:outline-none", - "focus:ring-2", - "focus:ring-offset-2", - "focus:ring-offset-indigo-600", - "focus:ring-white", - "hover:text-white", - "p-1", - "rounded-full", - "text-indigo-200" - ), - span(cls := "sr-only", "View notifications"), - Icons.outline.bell() - ) - - def userProfile: HtmlElement = - val menuOpen = Var(false) - - def menuItem(item: MenuItem, idx: Int): HtmlElement = - a( - href := "#", - cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", - role := "menuitem", - tabIndex := -1, - idAttr := s"user-menu-item-$idx", - item.title - ) - - div( - cls := "ml-3 relative", - div( - button( - tpe := "button", - cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - idAttr := "user-menu-button", - aria.expanded <-- menuOpen.signal, - aria.hasPopup := true, - span(cls := "sr-only", "Open user menu"), - child <-- avatarImage(), - onClick.preventDefault.mapTo( - !menuOpen.now() - ) --> menuOpen.writer - ) - ), - /* - * */ - div( - cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", - cls <-- menuOpen.signal.map { o => - if (o) "md:block" else "md:hidden" - }, - role := "menu", - aria.orientation := "vertical", - aria.labelledBy := "user-menu-button", - tabIndex := -1, - // : keyboard navigation - children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) - ) - ) - - def mobileProfile = - def menuItem(item: MenuItem): HtmlElement = - a( - href := "#", - cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", - item.title - ) - - div( - cls := "pt-4 pb-3 border-t border-indigo-700", - div( - cls := "flex items-center px-5", - div( - cls := "flex-shrink-0", - child <-- avatarImage(10) - ), - div( - cls := "ml-3", - div( - cls := "text-base font-medium text-white", - child.text <-- $userInfo.map(_.name) - ), - child.maybe <-- $userInfo.map( - _.email.map(e => - div( - cls := "text-sm font-medium text-indigo-300", - e - ) - ) - ) - ), - notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) - ), - div( - cls := "mt-3 px-2 space-y-1", - children <-- $m.map(_.userMenu.map(menuItem)) - ) - ) - - def pageLink(page: Link): Anchor = - page - .a() - .amend( - cls := "text-white px-3 py-2 rounded-md text-sm font-medium", - cls := Seq( - "bg-indigo-700" -> page.active, - "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active - ), - ariaCurrent := (if page.active then "page" else "false") - ) - - def logoImg: Image = - img( - cls := "h-8 w-8", - src <-- $m.map(_.logo.img), - alt <-- $m.map(_.logo.name) - ) - - def mobileMenuButton = button( - tpe := "button", - cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - aria.controls := "mobile-menu", - aria.expanded <-- mobileMenuOpen.signal, - span(cls := "sr-only", "Open main menu"), - Icons.outline - .menu() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") - ), - Icons.outline - .x() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") - ), - onClick.preventDefault.mapTo( - !mobileMenuOpen.now() - ) --> mobileMenuOpen.writer - ) - - def navBarLeft = - div( - cls := "flex items-center", - div(cls := "flex-shrink-0", logoImg), - div( - desktopOnly, - div( - cls := "ml-10 flex items-baseline space-x-4", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ) - ) - ) - - def navBarRight = - div( - desktopOnly, - div( - cls := "ml-4 flex items-center md:ml-6", - notificationButton, - userProfile - ) - ) - - def navBarMobile = - div( - cls := "-mr-2 flex", - mobileOnly, - mobileMenuButton - ) - - def navBar = - div( - cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex items-center justify-between h-16", - navBarLeft, - navBarRight, - navBarMobile - ) - ) - - def mobileMenu = - div( - mobileOnly, - cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }, - idAttr := "mobile-menu", - div( - cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ), - mobileProfile - ) - - nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala deleted file mode 100644 index 75339df..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object PageHeader: - def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = - header( - cls := "bg-white shadow-sm", - div( - cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", - h1( - cls := "text-lg leading-6 font-semibold text-gray-900", - Breadcrumbs(actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala deleted file mode 100644 index 5096463..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ /dev/null @@ -1,30 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import fiftyforms.ui.components.tailwind.Loading -import io.laminext.syntax.core.* - -object PageLayout: - case class ViewModel( - navigation: NavigationBar.ViewModel, - content: Option[HtmlElement] - ) - def apply(actionBus: Observer[Action])( - $m: Signal[ViewModel], - mods: Modifier[HtmlElement]* - )(using router: Router[Page]): HtmlElement = - val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) - div( - cls := "h-full flex flex-col", - NavigationBar($m.map(_.navigation)), - child.maybe <-- router.$currentPage.map(_.isRoot) - .switch(None, Some(PageHeader(actionBus))), - main( - cls := "flex-grow-1 overflow-y-auto", - mods, - child <-- $maybeContent.map(_.getOrElse(Loading)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala deleted file mode 100644 index 62d820f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.LinkSupport.* -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo - -object PageLink: - type ViewModel = Page - def apply($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = - a(mods($m, actions), child.text <-- $m.map(_.title)) - - def apply(m: ViewModel, actions: Observer[Action])(using - router: Router[Page] - ): Anchor = apply(Val(m), actions) - - def container($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = a(mods($m, actions)) - - def container(m: ViewModel, actions: Observer[Action])(using - Router[Page] - ): Anchor = container(Val(m), actions) - - private def mods($m: Signal[Page], actions: Observer[Action])(using - router: Router[Page] - ): Modifier[Anchor] = - Seq( - href <-- $m.map(router.absoluteUrlForPage).recover { case _ => - Some("invalid url") - }, - composeEvents(onClick.noKeyMod.preventDefault)( - _.sample($m) - .map(NavigateTo.apply) - ) --> actions - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala deleted file mode 100644 index 87f825a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.pages.dashboard.DashboardPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -class DashboardPageConnector(actionBus: Observer[Action])(using - router: Router[Page] -): - def apply: HtmlElement = - AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala deleted file mode 100644 index e9b0f71..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala +++ /dev/null @@ -1,76 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailKriteriaPage -import pages.detail.components.DukazyKriteria -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -object DetailKriteriaPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailKriteriaPageConnector( - state: DetailKriteriaPageConnector.AppState -)( - $page: Signal[Page.DetailKriteria] -)(using Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => - (p.osobniCislo.value, p.parametr.value, p.kriterium.value) - )((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map( - FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) - ) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, s, $s) => - DetailKriteriaPage($s)(state.actionBus.contramap { - case DukazyKriteria.Add => - NavigateTo( - Page.UpravDukazKriteria( - Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), - Page.Titled(s.parametr.id, Some(s.parametr.nazev)), - Page.Titled(s.kriterium.id, Some(s.kriterium.id)) - ) - ) - }) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): DetailKriteriaPage.ViewModel = - DetailKriteriaPage.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala deleted file mode 100644 index 4d8024f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ /dev/null @@ -1,57 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import pages.detail.DetailPage -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import fiftyforms.ui.components.tailwind.Color - -object DetailPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailPageConnector(state: DetailPageConnector.AppState)( - $page: Signal[Page.Detail] -)(using router: Router[Page]): - val $oscChangeSignal = $page.splitOne(_.osobniCislo.value)((osc, _, _) => osc) - val $pageChangeSignal = - $oscChangeSignal.flatMap(osc => - EventStream.fromSeq(Seq(FetchUserDetails(osc), FetchParameters(osc))) - ) - // TODO: filter the value based on the current osc - // OSC change will fetch new data, but still - // - we need to be sure that what we got is really what we ought to display - // - we want to display stale data accordingly (at least with loading indicator) - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - def apply: HtmlElement = - AppPage(state.actionBus)( - $data.combineWithFn($params)(_ zip _) - .map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: List[Parameter] - ): DetailPage.ViewModel = - DetailPage.ViewModel( - o.toDetailOsoby, - p.map( - _.toParametr(param => - PageLink.container(Page.DetailParametru(o, param), state.actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala deleted file mode 100644 index fdaa7f8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailParametruPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink - -object DetailParametruPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailParametruPageConnector( - state: DetailParametruPageConnector.AppState -)( - $page: Signal[Page.DetailParametru] -)(using router: Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => (p.osobniCislo.value, p.parametr.value))((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map(FetchParameter(_, _)) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - } yield (da, pb) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailParametruPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter - ): DetailParametruPage.ViewModel = - DetailParametruPage.ViewModel( - o.toDetailOsoby, - p.toParametr(p => - PageLink.container(Page.DetailParametru(o, p), state.actionBus) - ), - p.criteria.map( - _.toKriterium { c => - PageLink.container( - Page.DetailKriteria(o, p, c), - state.actionBus - ) - } - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala deleted file mode 100644 index 049aa22..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -case class DirectoryPageConnector( - $input: EventStream[List[UserInfo]], - actionBus: Observer[Action] -)(using router: Router[Page]): - val $data = $input.startWithNone - val $actionSignal = EventStream.fromValue(FetchDirectory) - - def apply: HtmlElement = - AppPage(actionBus)( - $data.split(_ => ())((_, _, s) => - pages.directory.DirectoryPage( - s.map( - _.map( - _.toUserRow(u => - PageLink.container( - Page.Detail(Page.Titled(u.personalNumber)), - actionBus - ) - ) - ) - ) - ) - ), - $actionSignal --> actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala deleted file mode 100644 index 88e7450..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ /dev/null @@ -1,68 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii -import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria -import fiftyforms.ui.components.tailwind.Color - -extension (o: UserInfo) - def toDetailOsoby: DetailOsoby.ViewModel = - DetailOsoby.ViewModel( - o.personalNumber, - o.name, - o.email, - o.phone, - o.img, - o.mainFunction.map(f => - DetailOsoby.Funkce.ViewModel(f.name, f.dept, f.ou) - ), - o.userContracts.headOption.map(c => - DetailOsoby.PracovniPomer.ViewModel(c.rel, c.startDate, c.endDate) - ) - ) - -extension (param: Parameter) - def toParametr(container: Parameter => Anchor): DetailParametru.ViewModel = - DetailParametru.ViewModel( - id = param.id, - nazev = param.name, - popis = param.description, - status = "Nesplněno", - statusColor = Color.red, - a = container(param) - ) - -extension (crit: ParameterCriteria) - def toKriterium( - container: ParameterCriteria => Anchor - ): DetailKriteria.ViewModel = - DetailKriteria.ViewModel( - nazev = crit.criteriumText, - kapitola = crit.chapterId, - bod = crit.itemId, - status = "Nesplněno", - statusColor = Color.red, - splneno = false, - dukazy = Nil, - container = container(crit) - ) - -extension (user: UserInfo) - def toUserRow( - container: UserInfo => HtmlElement = (_: UserInfo) => div() - ): UserRow.ViewModel = - UserRow.ViewModel( - osobniCislo = user.personalNumber.toString, - celeJmeno = user.name, - prijmeni = user.surname, - hlavniFunkce = user.mainFunction.map(_.name), - img = user.img, - container = () => container(user) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala deleted file mode 100644 index 7d5b2c1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.dashboard - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.Page - -object DashboardPage: - - def render: HtmlElement = - div("Dashboard page") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala deleted file mode 100644 index 9ef0698..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala +++ /dev/null @@ -1,31 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailKriteriaPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply($m: Signal[ViewModel])( - action: Observer[DukazyKriteria.Action] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - DukazyKriteria($m.map(_.kriterium.dukazy))(action) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala deleted file mode 100644 index 3b316e1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object DetailPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametry: SeznamParametru.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby($m.map(_.osoba)), - SeznamParametru($m.map(_.parametry)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala deleted file mode 100644 index 17224cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala +++ /dev/null @@ -1,24 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailParametruPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriteria: SeznamKriterii.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby.header($m.map(_.osoba)), - DetailParametru($m.map(_.parametr)), - SeznamKriterii($m.map(_.kriteria)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala deleted file mode 100644 index dc912fa..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ /dev/null @@ -1,107 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb -package app -package pages.detail - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.UpravDukazForm -import fiftyforms.services.files.File - -object UpravDukaz: - - type ThisPage = Page.UpravDukazKriteria - type PageKey = (OsobniCislo, String, String) - - trait State { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def availableFiles: EventStream[List[File]] - def actionBus: Observer[Action] - } - - def keyOfPage(page: ThisPage): PageKey = - (page.osobniCislo.value, page.parametr.value, page.kriterium.value) - - def onChangeAction(key: PageKey): Action = - FetchParameterCriteria( - key._1, - key._2, - key._3, - Page.UpravDukazKriteria(_, _, _) - ) - - class Connector(state: State)($page: Signal[ThisPage])(using Router[Page]): - val $paramChangeSignal = $page.splitOne(keyOfPage)((x, _, _) => x) - val $pageChangeSignal = $paramChangeSignal.map(onChangeAction) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.split(_ => ())((_, s, $s) => - PageComponent( - $s.map(buildModel), - state.availableFiles, - state.actionBus.contramap { - case UpravDukazForm.Cancelled => - NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) - case UpravDukazForm.AvailableFilesRequested => - FetchAvailableFiles(s._1.personalNumber) - } - ) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): PageComponent.ViewModel = - import connectors.* - PageComponent.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) - - object PageComponent: - import components.* - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply( - $m: Signal[ViewModel], - availableFilesStream: EventStream[List[File]], - events: Observer[UpravDukazForm.Event] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - UpravDukazForm(availableFilesStream)(events) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala deleted file mode 100644 index ca311b0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.Icons -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Color - -object DetailKriteria: - case class ViewModel( - nazev: String, - kapitola: String, - bod: String, - status: String, - statusColor: Color, - splneno: Boolean, - dukazy: List[DukazKriteria.ViewModel], - container: HtmlElement = div() - ) { - val id = s"${kapitola}${bod}" - } - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h3( - cls := "text-xl leading-6 font-bold text-gray-900", - child.text <-- $m.map(_.nazev) - ), - h3( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - "Kapitola ", - child.text <-- $m.map(_.kapitola), - " bod ", - child <-- $m.map(_.bod) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala deleted file mode 100644 index ae90290..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala +++ /dev/null @@ -1,100 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Avatar - -object DetailOsoby: - - object Funkce: - case class ViewModel( - nazev: String, - stredisko: String, - voj: String - ) - - def render($m: Signal[ViewModel]): HtmlElement = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.nazev), - span( - cls := "hidden md:inline", - " @ ", - child.text <-- $m.map(_.stredisko), - ", ", - child.text <-- $m.map(_.voj) - ) - ) - - object PracovniPomer: - case class ViewModel( - druh: String, - pocatek: LocalDate, - konec: Option[LocalDate] - ) - - def render($m: Signal[ViewModel]): HtmlElement = - import fiftyforms.ui.components.tailwind.CustomAttrs.datetime - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.druh), - " od ", - time( - datetime <-- $m.map(_.pocatek.toString), - child.text <-- $m.map(_.pocatek.toString) - ) - ) - - case class ViewModel( - osobniCislo: OsobniCislo, - jmeno: String, - email: Option[String], - telefon: Option[String], - img: Option[String], - hlavniFunkce: Option[Funkce.ViewModel], - pracovniPomer: Option[PracovniPomer.ViewModel] - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-4", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(16) - ), - div( - h1( - cls := "text-2xl font-bold text-gray-900", - child.text <-- $m.map(_.jmeno) - ), - child.maybe <-- $m.map(_.hlavniFunkce).split(_ => ())((_, _, d) => - Funkce.render(d) - ), - child.maybe <-- $m.map(_.pracovniPomer).split(_ => ())((_, _, d) => - PracovniPomer.render(d) - ) - ) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-5", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(8) - ), - div( - h1( - cls := "text-lg font-medium text-gray-800", - child.text <-- $m.map(_.jmeno) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala deleted file mode 100644 index e11cb62..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color - -object DetailParametru: - case class ViewModel( - id: String, - nazev: String, - popis: String, - status: String, - statusColor: Color, - a: Anchor - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h2( - cls := "text-2xl leading-6 font-medium text-gray-900", - child.text <-- $m.map(_.nazev) - ), - p( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - child.text <-- $m.map(_.popis) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - h2( - cls := "text-lg leading-6 font-medium text-gray-600", - child.text <-- $m.map(_.nazev) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala deleted file mode 100644 index a00ede8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala +++ /dev/null @@ -1,177 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.CustomAttrs - -object DukazKriteria: - case class Osoba(osobniCislo: String, jmeno: String) - case class Udalost(osoba: Osoba, datum: LocalDate) - case class Dokument( - url: String, - nazev: String, - pridal: Udalost, - odebral: Option[Udalost] - ) - case class ViewModel( - dokumenty: List[Dokument], - autorizoval: Option[Udalost], - platiDo: Option[LocalDate], - poznámka: Option[String] - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-lg", - div( - cls := "px-4 py-5 sm:px-6", - h3( - cls := "text-lg leading-6 font-medium text-gray-900", - """Applicant Information""" - ), - p( - cls := "mt-1 max-w-2xl text-sm text-gray-500", - """Personal details and application.""" - ) - ), - div( - cls := "border-t border-gray-200 px-4 py-5 sm:px-6", - dl( - cls := "grid grid-cols-1 gap-x-4 gap-y-8 sm:grid-cols-2", - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Full name""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Margot Foster""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Application for""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Backend Developer""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Email address""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """margotfoster@example.com""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Salary expectation""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """$120,000""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """About""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Fugiat ipsum ipsum deserunt culpa aute sint do nostrud anim incididunt cillum culpa consequat. Excepteur qui ipsum aliquip consequat sint. Sit id mollit nulla mollit nostrud in ea officia proident. Irure nostrud pariatur mollit ad adipisicing reprehenderit deserunt qui eu.""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """Attachments""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - ul( - role := "list", - cls := "border border-gray-200 rounded-md divide-y divide-gray-200", - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """resume_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ), - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """coverletter_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala deleted file mode 100644 index 22f134f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala +++ /dev/null @@ -1,35 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object DukazyKriteria: - sealed trait Action - case object Add extends Action - - type ViewModel = List[DukazKriteria.ViewModel] - - def apply($m: Signal[ViewModel])(actions: Observer[Action]): HtmlElement = - div( - child.maybe <-- $m.map(m => - if (m.isEmpty) then Some(prazdnyDukaz(actions)) - else None - ), - children <-- $m.map(_.zipWithIndex).split(_._2)((_, _, $s) => - DukazKriteria($s.map(_._1)) - ) - ) - - private def prazdnyDukaz(actions: Observer[Action]): HtmlElement = - button( - tpe := "button", - onClick.preventDefault.mapTo(Add) --> actions, - cls := "relative block w-full border-2 border-gray-300 border-dashed rounded-lg p-12 text-center hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - Icons.outline - .`document-add`(12) - .amend(svg.cls := "mx-auto text-gray-400"), - span( - cls := "mt-2 block text-sm font-medium text-gray-900", - "Přidat důkaz" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala deleted file mode 100644 index f628697..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.list.StackedList -import fiftyforms.ui.components.tailwind.list.ListRow -import fiftyforms.ui.components.tailwind.list.RowTag -import fiftyforms.ui.components.tailwind.list.RowNext -import java.time.LocalDate - -object SeznamKriterii: - type ViewModel = List[DetailKriteria.ViewModel] - - private val kritList = new StackedList[DetailKriteria.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - kritList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = - p(cls := "text-sm text-gray-500", s"${i.kapitola}${i.bod}"), - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.container - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala deleted file mode 100644 index 2bfe4bc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.list.{ - StackedList, - ListRow, - RowTag, - PropList, - IconText, - RowNext -} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.LinkSupport.* - -object SeznamParametru: - type ViewModel = List[DetailParametru.ViewModel] - - private val parametrList = new StackedList[DetailParametru.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - parametrList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = emptyNode, - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.a - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala deleted file mode 100644 index f048e9d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ /dev/null @@ -1,101 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.form.* -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement -import fiftyforms.services.files.components.tailwind.FilePicker -import cz.e_bs.cmi.mdr.pdb.frontend.AutorizujDukaz -import cz.e_bs.cmi.mdr.pdb.frontend.DocumentRef -import fiftyforms.services.files.File - -object UpravDukazForm: - sealed trait Event - case object Cancelled extends Event - case object AvailableFilesRequested extends Event - def apply(availableFilesStream: EventStream[List[File]])( - updates: Observer[Event] - ): HtmlElement = - val (filesStream, filesObserver) = - EventStream.withObserver[FilePicker.Event] - val files = Var[List[File]](Nil) - def submitButtons: HtmlElement = - div( - cls := "pt-5", - div( - cls := "flex justify-end", - button( - tpe := "button", - cls := "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zrušit", - onClick.mapTo(Cancelled) --> updates - ), - button( - tpe := "submit", - cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - disabled <-- files.signal.map(_.isEmpty), - "Autorizovat" - ) - ) - ) - - div( - filesStream.collect { case FilePicker.SelectionUpdated(files) => - files.to(List) - } --> files.writer, - filesStream.collect { case FilePicker.AvailableFilesRequested => - AvailableFilesRequested - } --> updates, - cls := "bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6", - Form( - Form.Body( - Form.Section( - FormHeader( - FormHeader.ViewModel( - "Doložení kritéria", - "Sestavte doklady poskytující důkaz kritéria a potvrďte odesláním formuláře. Případné limitace či jiné relevantní údaje vepište do pole Komentář." - ) - ), - FormFields( - FormRow( - "dokumenty", - "Dokumenty", - FilePicker(files.signal, availableFilesStream)(filesObserver) - .amend(idAttr := "dokumenty", cls("max-w-lg")) - ).toHtml, - FormRow( - "platnost", - "Platnost", - input( - idAttr := "platnost", - name := "platnost", - tpe := "date", - autoComplete := "date", - cls := "block max-w-lg w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" - ) - ).toHtml, - FormRow( - "komentar", - "Komentář", - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - textArea( - idAttr := "komentar", - name := "about", - rows := 3, - cls := "max-w-lg shadow-sm block w-full focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border border-gray-300 rounded-md" - ), - p( - cls := "mt-2 text-sm text-gray-500", - "Doplňte prosím potřebné informace související s doložením kritéria, včetně případných limitací." - ) - ) - ).toHtml - ) - ) - ), - submitButtons - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala deleted file mode 100644 index 41033f6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -import components._ - -object DirectoryPage: - - type ViewModel = List[UserRow.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - val (actionsStream, actionObserver) = - EventStream.withObserver[SearchForm.Action] - val $filter = actionsStream - .collect { - case SearchForm.ClearFilter => None - case SearchForm.SetFilter(t) => Some(t) - } - .startWith(None) - val byLetter = for { - d <- $m - f <- $filter - } yield for { - (letter, users) <- d - .filter { user => - f match - case None => true - case Some(t) => user.search.contains(t) - } - .groupBy(_.prijmeni.head) - .to(List) - .sortBy(_._1) - } yield (letter.toString, users.sortBy(_.prijmeni)) - - div( - cls := "h-full max-w-7xl mx-auto order-first flex flex-col flex-shrink-0", - SearchForm(actionObserver), - Directory(byLetter) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala deleted file mode 100644 index 73f915d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -object Directory: - - object Header: - type ViewModel = String - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500", - h3(child.text <-- $m) - ) - - object UserList: - type ViewModel = List[UserRow.ViewModel] - def apply($m: Signal[ViewModel]): HtmlElement = - ul( - role := "list", - cls := "relative z-0 divide-y divide-gray-200", - children <-- $m.split(_.osobniCislo)((_, _, s) => UserRow(s)) - ) - - type ViewModel = List[(String, List[UserRow.ViewModel])] - def apply($m: Signal[ViewModel]): HtmlElement = - nav( - cls := "flex-1 min-h-0 overflow-y-auto", - aria.label := "Directory", - children <-- $m.split(_._1)((_, _, s) => - div( - cls := "relative", - Header(s.map(_._1)), - UserList(s.map(_._2)) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala deleted file mode 100644 index e182910..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala +++ /dev/null @@ -1,54 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object SearchForm: - sealed trait Action - case object SubmitFilter extends Action - sealed trait FilterAction extends Action - case object ClearFilter extends FilterAction - case class SetFilter(value: String) extends FilterAction - - def apply(actions: Observer[Action]): HtmlElement = - div( - cls := "px-6 pt-4 pb-4", - form( - cls := "flex space-x-4", - onSubmit.mapTo(SubmitFilter) --> actions, - div( - cls := "flex-1 min-w-0", - label( - forId := "search", - cls := "sr-only", - "Hledat" - ), - div( - cls := "relative rounded-md shadow-sm", - div( - cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", - Icons.solid.search().amend(svg.cls := "text-gray-400") - ), - input( - tpe := "search", - name := "search", - idAttr := "search", - cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", - placeholder := "Hledat", - composeEvents(onInput.mapToValue.setAsValue.map(SetFilter(_)))( - _.throttle(500) - ) --> actions - ) - ) - ), - button( - tpe := "submit", - cls := "inline-flex justify-center px-3.5 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500", - Icons.solid.filter().amend(svg.cls := "text-gray-400"), - span( - cls := "sr-only", - "Hledat" - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala deleted file mode 100644 index 82f7723..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala +++ /dev/null @@ -1,52 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.airstream.core.Signal -import fiftyforms.ui.components.tailwind.Avatar - -object UserRow: - case class ViewModel( - osobniCislo: String, - celeJmeno: String, - prijmeni: String, - hlavniFunkce: Option[String], - img: Option[String], - container: () => HtmlElement = () => div() - ) { - val search = prijmeni.toLowerCase - } - - def apply($m: Signal[ViewModel]): HtmlElement = - inline def avatarImage = - Avatar($m.map(_.img)).avatarImage(10) - - li( - div( - cls := "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500", - div( - cls := "flex-shrink-0", - child <-- avatarImage - ), - div( - cls := "flex-1 min-w-0", - child <-- $m.map { o => - o.container() - .amend( - cls := "focus:outline-none", - span( - cls := "absolute inset-0", - aria.hidden := true - ), - p( - cls := "text-sm font-medium text-gray-900", - o.celeJmeno - ), - p( - cls := "text-sm text-gray-500 truncate", - o.hlavniFunkce - ) - ) - } - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala deleted file mode 100644 index 467c5f5..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala +++ /dev/null @@ -1,99 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router - -object ErrorPage: - case class ViewModel( - homePage: Page, - errorName: String, - title: String, - subTitle: String - ) - - def apply(m: ViewModel, actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - val ViewModel(homePage, errorName, title, subTitle) = m - div( - cls := "min-h-full pt-16 pb-12 flex flex-col bg-white", - main( - cls := "flex-grow flex flex-col justify-center max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex-shrink-0 flex justify-center", - a( - href := "/", - cls := "inline-flex", - span( - cls := "sr-only", - """Workflow""" - ), - img( - cls := "h-12 w-auto", - src := "https://tailwindui.com/img/logos/workflow-mark.svg?color=indigo&shade=600", - alt := "" - ) - ) - ), - div( - cls := "py-16", - div( - cls := "text-center", - p( - cls := "text-sm font-semibold text-indigo-600 uppercase tracking-wide", - errorName - ), - h1( - cls := "mt-2 text-4xl font-extrabold text-gray-900 tracking-tight sm:text-5xl", - title - ), - p( - cls := "mt-2 text-base text-gray-500", - subTitle - ), - div( - cls := "mt-6", - PageLink - .container(homePage, actionBus) - .amend( - cls := "text-base font-medium text-indigo-600 hover:text-indigo-500", - """Go back home""" - ) - ) - ) - ) - ), - footer( - cls := "flex-shrink-0 max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", - nav( - cls := "flex justify-center space-x-4", - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Contact Support""" - ), - span( - cls := "inline-block border-l border-gray-300", - aria.hidden := true - ), - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Status""" - ), - span( - cls := "inline-block border-l border-gray-300", - aria.hidden := true - ), - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Twitter""" - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala deleted file mode 100644 index bd9baec..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action - -object NotFoundPage: - def apply(homePage: Page, url: String, actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - ErrorPage( - ErrorPage.ViewModel( - homePage, - "404 error", - "Page not found.", - s"Sorry, but page $url doesn't exist." - ), - actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala deleted file mode 100644 index 476fe3c..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action - -object UnhandledErrorPage: - - case class ViewModel( - homePage: Page, - errorName: Option[String], - errorMessage: Option[String] - ) - - def apply(m: ViewModel, actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - ErrorPage( - ErrorPage.ViewModel( - m.homePage, - "Unexpected error occurred", - m.errorName.getOrElse( - "Uh oh!" - ), // TODO: translations, better text than uh oh - m.errorMessage.getOrElse( - "This wasn't supposed to happen! Please try again." - ) - ), - actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala deleted file mode 100644 index 25f059f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala +++ /dev/null @@ -1,6 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.services - -import com.raquo.airstream.core.Observer - -trait DataFetcher[K, A]: - def fetch(id: K, sink: Observer[A]): Unit diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala deleted file mode 100644 index a469830..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala +++ /dev/null @@ -1,120 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package state - -import com.raquo.airstream.core.EventStream -import cz.e_bs.cmi.mdr.pdb.{UserInfo, OsobniCislo} -import com.raquo.airstream.core.Observer -import scala.scalajs.js -import scala.scalajs.js.JSON -import zio.json._ -import com.raquo.airstream.eventbus.EventBus -import com.raquo.airstream.ownership.Owner -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.UserFunction -import cz.e_bs.cmi.mdr.pdb.UserContract -import fiftyforms.services.files.File - -trait AppState - extends connectors.DetailPageConnector.AppState - with connectors.DetailParametruPageConnector.AppState - with connectors.DetailKriteriaPageConnector.AppState - with pages.detail.UpravDukaz.State: - def users: EventStream[List[UserInfo]] - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - -class MockAppState(implicit owner: Owner, router: Router[Page]) - extends AppState: - - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonDecoder[UserFunction] = DeriveJsonDecoder.gen - given JsonDecoder[UserContract] = DeriveJsonDecoder.gen - given JsonDecoder[UserInfo] = DeriveJsonDecoder.gen - - given JsonDecoder[ParameterCriteria] = DeriveJsonDecoder.gen - given JsonDecoder[Parameter] = DeriveJsonDecoder.gen - - private val actions = EventBus[Action]() - private val (parametersStream, pushParameters) = - EventStream.withCallback[List[Parameter]] - private val (usersStream, pushUsers) = - EventStream.withCallback[List[UserInfo]] - private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] - private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] - - private val mockData: List[UserInfo] = - mockUsers - .asInstanceOf[js.Dictionary[js.Object]] - .values - // TODO: is there a more efficient way to parse from JS object directly? - .map { o => - val parsed = JSON.stringify(o).fromJson[UserInfo] - parsed.left.foreach(org.scalajs.dom.console.log(_)) - parsed - } - .collect { case Right(u) => - u - } - .toList - - private val mockParameters: List[Parameter] = - pdbParams - .asInstanceOf[js.Dictionary[js.Object]] - .values - .map(o => JSON.stringify(o).fromJson[Parameter]) - .collect { case Right(p) => p } - .toList - - // TODO: Extract to separate event handler - actions.events.foreach { - case FetchDirectory => pushUsers(mockData) - case FetchUserDetails(osc) => - mockData.find(_.personalNumber == osc).foreach { o => - pushDetails(o) - router.replaceState(Page.Detail(o)) - } - case FetchParameters(osc) => - pushParameters(mockParameters) - case FetchParameter(osc, paramId) => - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(Page.DetailParametru(o, p)) - case FetchParameterCriteria(osc, paramId, critId, page) => - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - c <- p.criteria.find(_.id == critId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(page(o, p, c)) - case NavigateTo(page) => router.pushState(page) - case FetchAvailableFiles(osc) => - pushFiles( - List( - File("https://tc163.cmi.cz/here", "Example file") - ) - ) - } - - override def users: EventStream[List[UserInfo]] = - usersStream.debugWithName("users") - - override def details: EventStream[UserInfo] = - detailsStream.debugWithName("details") - - override def parameters: EventStream[List[Parameter]] = - parametersStream.debugWithName("parameters") - - override def availableFiles: EventStream[List[File]] = - filesStream.debugWithName("available files") - - override def actionBus: Observer[Action] = - actions.writer.debugWithName("actions writer") diff --git a/app/src/main/scala/mdr/pdb/app/Main.scala b/app/src/main/scala/mdr/pdb/app/Main.scala new file mode 100644 index 0000000..081b4ee --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/Main.scala @@ -0,0 +1,103 @@ +package mdr.pdb.app + +import scala.scalajs.js.annotation.JSExportTopLevel +import scala.scalajs.js.annotation.JSExport +import scala.scalajs.js.annotation.JSImport +import scala.scalajs.js +import org.scalajs.dom +import com.raquo.laminar.api.L.{*, given} +import scala.scalajs.js.Date +import com.raquo.waypoint.Router +import com.raquo.waypoint.SplitRender +import mdr.pdb.app.services.DataFetcher +import scala.scalajs.js.JSON +import zio.json._ +import mdr.pdb.UserInfo +import mdr.pdb.app.state.AppState + +@js.native +@JSImport("stylesheets/main.css", JSImport.Namespace) +object Css extends js.Any + +@js.native +@JSImport("data/users.json", JSImport.Default) +object mockUsers extends js.Object + +@js.native +@JSImport("params/pdb-params.json", JSImport.Default) +object pdbParams extends js.Object + +@JSExportTopLevel("app") +object Main: + + @JSExport + def main(args: Array[String]): Unit = + import Routes.given + onLoad { + setupAirstream() + val appContainer = dom.document.querySelector("#app") + val _ = + render( + appContainer, + renderPage(state.MockAppState(using unsafeWindowOwner, router)) + ) + } + + private def onLoad(f: => Unit): Unit = + documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) + + private def setupAirstream()(using router: Router[Page]): Unit = + AirstreamError.registerUnhandledErrorCallback(err => + router.forcePage( + Page.UnhandledError( + Some(err.getClass.getName), // TODO: Fill only in dev mode + Some(err.getMessage) + ) + ) + ) + + def renderPage(state: AppState)(using + router: Router[Page] + ): HtmlElement = + val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) + .collectSignal[Page.Detail]( + connectors + .DetailPageConnector(state)(_) + .apply + ) + .collectSignal[Page.DetailParametru]( + connectors + .DetailParametruPageConnector(state)(_) + .apply + ) + .collectSignal[Page.DetailKriteria]( + connectors + .DetailKriteriaPageConnector(state)(_) + .apply + ) + .collectSignal[Page.UpravDukazKriteria]( + pages.detail.UpravDukaz.Connector(state)(_).apply + ) + .collectStatic(Page.Dashboard)( + connectors.DashboardPageConnector(state.actionBus).apply + ) + .collect[Page.NotFound](pg => + pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) + ) + .collect[Page.UnhandledError](pg => + pages.errors + .UnhandledErrorPage( + pages.errors.UnhandledErrorPage + .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), + state.actionBus + ) + ) + .collectStatic(Page.Directory)( + connectors + .DirectoryPageConnector(state.users, state.actionBus) + .apply + ) + div(cls := "h-full", child <-- pageSplitter.$view) + + // Pull in the stylesheet + val css: Css.type = Css diff --git a/app/src/main/scala/mdr/pdb/app/Routes.scala b/app/src/main/scala/mdr/pdb/app/Routes.scala new file mode 100644 index 0000000..2cc860c --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/Routes.scala @@ -0,0 +1,198 @@ +package mdr.pdb.app + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.* +import org.scalajs.dom +import zio.json.{*, given} +import mdr.pdb.OsobniCislo + +import scala.scalajs.js +import mdr.pdb.UserInfo +import mdr.pdb.Parameter +import mdr.pdb.ParameterCriteria +import mdr.pdb.app.Page.Titled + +// enum is not working with Waypoints' SplitRender collectStatic +sealed abstract class Page( + val id: String, + val title: String, + val parent: Option[Page] +) { + val path: Vector[Page] = + parent match + case None => Vector(this) + case Some(p) => p.path :+ this + + val isRoot: Boolean = parent.isEmpty +} + +object Page: + + case class Titled[V](value: V, title: Option[String] = None): + val show: String = title.getOrElse(value.toString) + + case object Directory extends Page("directory", "Adresář", None) + + case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) + + case class Detail(osobniCislo: Titled[OsobniCislo]) + extends Page("user", osobniCislo.show, Some(Directory)) + + object Detail { + def apply(o: UserInfo): Detail = Detail( + Titled(o.personalNumber, Some(o.name)) + ) + } + + case class DetailParametru( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String] + ) extends Page( + "parameter", + parametr.show, + Some(Detail(osobniCislo)) + ) + + object DetailParametru { + def apply(o: UserInfo, p: Parameter): DetailParametru = + DetailParametru( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)) + ) + } + + case class DetailKriteria( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String], + kriterium: Titled[String] + ) extends Page( + "criteria", + kriterium.show, + Some(DetailParametru(osobniCislo, parametr)) + ) + + object DetailKriteria { + def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = + DetailKriteria( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)), + Titled(k.id, Some(k.id)) + ) + } + + case class UpravDukazKriteria( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String], + kriterium: Titled[String] + ) extends Page( + "addProof", + "Důkaz", + Some(DetailKriteria(osobniCislo, parametr, kriterium)) + ) + + object UpravDukazKriteria { + def apply( + o: UserInfo, + p: Parameter, + k: ParameterCriteria + ): UpravDukazKriteria = + UpravDukazKriteria( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)), + Titled(k.id, Some(k.id)) + ) + } + + case class NotFound(url: String) extends Page("404", "404", Some(Directory)) + + case class UnhandledError( + errorName: Option[String], + errorMessage: Option[String] + ) extends Page("500", "Unexpected error", Some(Directory)) + +object Routes: + given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) + given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) + given [V: JsonEncoder]: JsonEncoder[Titled[V]] = + DeriveJsonEncoder.gen[Titled[V]] + given [V: JsonDecoder]: JsonDecoder[Titled[V]] = + DeriveJsonDecoder.gen[Titled[V]] + given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] + given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] + + val base = + js.`import`.meta.env.BASE_URL + .asInstanceOf[String] + "app" + + val homePage: Page = Page.Directory + + given router: Router[Page] = Router[Page]( + routes = List( + Route.static(homePage, root / endOfSegments, basePath = base), + Route.static( + Page.Dashboard, + root / "dashboard" / endOfSegments, + basePath = base + ), + Route[Page.Detail, String]( + encode = _.osobniCislo.value.toString, + decode = osc => Page.Detail(Titled(OsobniCislo(osc))), + root / "osoba" / segment[String] / endOfSegments, + basePath = base + ), + Route[Page.DetailParametru, (String, String)]( + encode = p => (p.osobniCislo.value.toString, p.parametr.value), + decode = + p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / endOfSegments, + basePath = base + ), + Route[Page.DetailKriteria, (String, String, String)]( + encode = p => + ( + p.osobniCislo.value.toString, + p.parametr.value, + p.kriterium.value.replaceAll("\\.", "--") + ), + decode = p => + Page.DetailKriteria( + Titled(OsobniCislo(p._1)), + Titled(p._2), + Titled(p._3.replaceAll("--", ".")) + ), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / "kriterium" / segment[String] / endOfSegments, + basePath = base + ), + Route[Page.UpravDukazKriteria, (String, String, String)]( + encode = p => + ( + p.osobniCislo.value.toString, + p.parametr.value, + p.kriterium.value.replaceAll("\\.", "--") + ), + decode = p => + Page.UpravDukazKriteria( + Titled(OsobniCislo(p._1)), + Titled(p._2), + Titled(p._3.replaceAll("--", ".")) + ), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / "kriterium" / segment[String] / "edit" / endOfSegments, + basePath = base + ) + ), + serializePage = _.toJson, + deserializePage = _.fromJson[Page] + .fold(s => throw IllegalStateException(s), identity), + getPageTitle = _.title, + routeFallback = url => Page.NotFound(url), + deserializeFallback = _ => Page.Dashboard + )( + $popStateEvent = windowEvents.onPopState, + owner = unsafeWindowOwner + ) diff --git a/app/src/main/scala/mdr/pdb/app/actions.scala b/app/src/main/scala/mdr/pdb/app/actions.scala new file mode 100644 index 0000000..d078b3a --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/actions.scala @@ -0,0 +1,21 @@ +package mdr.pdb.app + +import mdr.pdb.OsobniCislo +import mdr.pdb.UserInfo +import mdr.pdb.Parameter +import mdr.pdb.ParameterCriteria + +sealed trait Action + +case object FetchDirectory extends Action +case class FetchUserDetails(osc: OsobniCislo) extends Action +case class FetchParameters(osc: OsobniCislo) extends Action +case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action +case class FetchParameterCriteria( + osc: OsobniCislo, + paramId: String, + critId: String, + page: (UserInfo, Parameter, ParameterCriteria) => Page +) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action +case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/mdr/pdb/app/components/AppPage.scala new file mode 100644 index 0000000..130c817 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/AppPage.scala @@ -0,0 +1,82 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.Page +import mdr.pdb.{UserProfile, UserInfo, OsobniCislo} +import com.raquo.waypoint.Router +import mdr.pdb.app.Action +import mdr.pdb.app.NavigateTo +import mdr.pdb.UserFunction + +object AppPage: + // TODO: pages by logged in user + val pages: List[Page] = List(Page.Directory, Page.Dashboard) + + import NavigationBar.{Logo, MenuItem} + + val logo = Logo( + "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", + "Workflow" + ) + + // TODO: menu items by user profile + val userMenu = + List( + MenuItem("Your Profile"), + MenuItem("Settings"), + MenuItem("Sign out") + ) + + // TODO: load user profile + val $userProfile = Var( + UserProfile( + "tom", + UserInfo( + OsobniCislo("1031"), + "tom", + "Tom", + "Cook", + None, + None, + Some("tom@example.com"), + Some("+420 222 866 180"), + Some( + UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") + ), + Nil, + None + ) + ) + ) + + val $userInfo = $userProfile.signal.map(_.userInfo) + + type ViewModel = Option[HtmlElement] + def apply( + actionBus: Observer[Action] + )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using + router: Router[Page] + ): HtmlElement = + PageLayout(actionBus)( + $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => + PageLayout.ViewModel( + NavigationBar.ViewModel( + u, + pages.map(p => + NavigationBar.Link( + () => + PageLink( + p, + actionBus + ), + p == cp + ) + ), + userMenu, + logo + ), + c + ) + ), + mods + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala new file mode 100644 index 0000000..9375398 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala @@ -0,0 +1,143 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.tailwind.list.IconText.ViewModel +import fiftyforms.ui.components.tailwind.Icons +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.Page + +import com.raquo.laminar.api.L.{*, given} +import io.laminext.syntax.core.{*, given} +import mdr.pdb.app.Action + +object Breadcrumbs: + + private def slash = + import svg.{*, given} + svg( + cls := "flex-shrink-0 h-5 w-5 text-gray-300", + xmlns := "http://www.w3.org/2000/svg", + fill := "currentColor", + viewBox := "0 0 20 20", + ariaHidden := true, + path( + d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" + ) + ) + + def backIcon = + Icons.solid + .`arrow-narrow-left`() + .amend( + svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" + ) + + object Link: + case class ViewModel( + page: Page, + icon: Option[SvgElement], + extraClasses: String, + text: String, + textClasses: Option[String] = None + ) + + val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" + + def shortHome(p: Page) = ViewModel( + p, + Some(backIcon), + "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", + "Zpět na úvodní stránku", + None + ) + + def fullHome(p: Page) = ViewModel( + p, + Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), + "text-gray-400 hover:text-gray-500", + "Domů", + Some("sr-only") + ) + + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + PageLink + .container($m.map(_.page), actionBus) + .amend( + cls <-- $m.map(_.extraClasses), + child.maybe <-- $m.map(_.icon), + span( + cls <-- $m.map(_.textClasses.getOrElse("")), + child.text <-- $m.map(_.text) + ) + ) + + object FullBreadcrumbs: + type ViewModel = Page + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + ol( + role := "list", + cls := "flex items-center space-x-4", + children <-- $m.map(_.path).split(_.id)((_, _, $p) => + li( + div( + cls := "flex items-center", + child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), + Link( + $p.map(p => + if (p.isRoot) then Link.fullHome(p) + else + Link.ViewModel( + p, + None, + s"ml-4 max-w-xs truncate ${Link.baseClasses}", + p.title + ) + ), + actionBus + ) + ) + ) + ) + ) + + object ShortBreadcrumbs: + type ViewModel = Page + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + Link( + $m.map { p => + val target = p.parent.getOrElse(p) + if target.isRoot then Link.shortHome(target) + else + Link.ViewModel( + target, + Some(backIcon), + "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", + s"Zpět na ${target.title}" + ) + }, + actionBus + ) + + def apply(actionBus: Observer[Action])(using + router: Router[Page] + ): HtmlElement = + nav( + cls := "flex", + aria.label := "Breadcrumb", + div( + cls := "flex sm:hidden", + ShortBreadcrumbs(router.$currentPage, actionBus) + ), + div( + cls := "hidden sm:block", + FullBreadcrumbs(router.$currentPage, actionBus) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala new file mode 100644 index 0000000..4bb8b8d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala @@ -0,0 +1,242 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent +import fiftyforms.ui.components.tailwind.Icons +import fiftyforms.ui.components.tailwind.Avatar +import mdr.pdb.UserInfo +import io.laminext.syntax.core.* + +object NavigationBar: + + case class Logo(img: String, name: String) + case class Link(a: () => Anchor, active: Boolean) + case class MenuItem(title: String) + + case class ViewModel( + userInfo: UserInfo, + pages: List[Link], + userMenu: List[MenuItem], + logo: Logo + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + val $userInfo = $m.map(_.userInfo) + val mobileMenuOpen = Var(false) + + val desktopOnly = cls("hidden md:block") + val mobileOnly = cls("md:hidden") + + inline def avatarImage(size: Int = 8) = + Avatar($userInfo.map(_.img)).avatarImage(size) + + def notificationButton = button( + tpe := "button", + cls := List( + "bg-indigo-600", + "focus:outline-none", + "focus:ring-2", + "focus:ring-offset-2", + "focus:ring-offset-indigo-600", + "focus:ring-white", + "hover:text-white", + "p-1", + "rounded-full", + "text-indigo-200" + ), + span(cls := "sr-only", "View notifications"), + Icons.outline.bell() + ) + + def userProfile: HtmlElement = + val menuOpen = Var(false) + + def menuItem(item: MenuItem, idx: Int): HtmlElement = + a( + href := "#", + cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", + role := "menuitem", + tabIndex := -1, + idAttr := s"user-menu-item-$idx", + item.title + ) + + div( + cls := "ml-3 relative", + div( + button( + tpe := "button", + cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", + idAttr := "user-menu-button", + aria.expanded <-- menuOpen.signal, + aria.hasPopup := true, + span(cls := "sr-only", "Open user menu"), + child <-- avatarImage(), + onClick.preventDefault.mapTo( + !menuOpen.now() + ) --> menuOpen.writer + ) + ), + /* + * */ + div( + cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", + cls <-- menuOpen.signal.map { o => + if (o) "md:block" else "md:hidden" + }, + role := "menu", + aria.orientation := "vertical", + aria.labelledBy := "user-menu-button", + tabIndex := -1, + // : keyboard navigation + children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) + ) + ) + + def mobileProfile = + def menuItem(item: MenuItem): HtmlElement = + a( + href := "#", + cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", + item.title + ) + + div( + cls := "pt-4 pb-3 border-t border-indigo-700", + div( + cls := "flex items-center px-5", + div( + cls := "flex-shrink-0", + child <-- avatarImage(10) + ), + div( + cls := "ml-3", + div( + cls := "text-base font-medium text-white", + child.text <-- $userInfo.map(_.name) + ), + child.maybe <-- $userInfo.map( + _.email.map(e => + div( + cls := "text-sm font-medium text-indigo-300", + e + ) + ) + ) + ), + notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) + ), + div( + cls := "mt-3 px-2 space-y-1", + children <-- $m.map(_.userMenu.map(menuItem)) + ) + ) + + def pageLink(page: Link): Anchor = + page + .a() + .amend( + cls := "text-white px-3 py-2 rounded-md text-sm font-medium", + cls := Seq( + "bg-indigo-700" -> page.active, + "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active + ), + ariaCurrent := (if page.active then "page" else "false") + ) + + def logoImg: Image = + img( + cls := "h-8 w-8", + src <-- $m.map(_.logo.img), + alt <-- $m.map(_.logo.name) + ) + + def mobileMenuButton = button( + tpe := "button", + cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", + aria.controls := "mobile-menu", + aria.expanded <-- mobileMenuOpen.signal, + span(cls := "sr-only", "Open main menu"), + Icons.outline + .menu() + .amend( + svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") + ), + Icons.outline + .x() + .amend( + svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") + ), + onClick.preventDefault.mapTo( + !mobileMenuOpen.now() + ) --> mobileMenuOpen.writer + ) + + def navBarLeft = + div( + cls := "flex items-center", + div(cls := "flex-shrink-0", logoImg), + div( + desktopOnly, + div( + cls := "ml-10 flex items-baseline space-x-4", + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) + ) + ) + ) + + def navBarRight = + div( + desktopOnly, + div( + cls := "ml-4 flex items-center md:ml-6", + notificationButton, + userProfile + ) + ) + + def navBarMobile = + div( + cls := "-mr-2 flex", + mobileOnly, + mobileMenuButton + ) + + def navBar = + div( + cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", + div( + cls := "flex items-center justify-between h-16", + navBarLeft, + navBarRight, + navBarMobile + ) + ) + + def mobileMenu = + div( + mobileOnly, + cls <-- mobileMenuOpen.signal.map { o => + if (o) "block" else "hidden" + }, + idAttr := "mobile-menu", + div( + cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) + ), + mobileProfile + ) + + nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala new file mode 100644 index 0000000..c4cb10d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala @@ -0,0 +1,19 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.Page +import com.raquo.waypoint.Router +import mdr.pdb.app.Action + +object PageHeader: + def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = + header( + cls := "bg-white shadow-sm", + div( + cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", + h1( + cls := "text-lg leading-6 font-semibold text-gray-900", + Breadcrumbs(actionBus) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala new file mode 100644 index 0000000..aae4539 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala @@ -0,0 +1,30 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.Page +import mdr.pdb.app.Action +import fiftyforms.ui.components.tailwind.Loading +import io.laminext.syntax.core.* + +object PageLayout: + case class ViewModel( + navigation: NavigationBar.ViewModel, + content: Option[HtmlElement] + ) + def apply(actionBus: Observer[Action])( + $m: Signal[ViewModel], + mods: Modifier[HtmlElement]* + )(using router: Router[Page]): HtmlElement = + val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) + div( + cls := "h-full flex flex-col", + NavigationBar($m.map(_.navigation)), + child.maybe <-- router.$currentPage.map(_.isRoot) + .switch(None, Some(PageHeader(actionBus))), + main( + cls := "flex-grow-1 overflow-y-auto", + mods, + child <-- $maybeContent.map(_.getOrElse(Loading)) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/mdr/pdb/app/components/PageLink.scala new file mode 100644 index 0000000..e6e7052 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageLink.scala @@ -0,0 +1,40 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.LinkSupport.* +import mdr.pdb.app.Page +import mdr.pdb.app.Action +import com.raquo.waypoint.Router +import mdr.pdb.app.NavigateTo + +object PageLink: + type ViewModel = Page + def apply($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = + a(mods($m, actions), child.text <-- $m.map(_.title)) + + def apply(m: ViewModel, actions: Observer[Action])(using + router: Router[Page] + ): Anchor = apply(Val(m), actions) + + def container($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = a(mods($m, actions)) + + def container(m: ViewModel, actions: Observer[Action])(using + Router[Page] + ): Anchor = container(Val(m), actions) + + private def mods($m: Signal[Page], actions: Observer[Action])(using + router: Router[Page] + ): Modifier[Anchor] = + Seq( + href <-- $m.map(router.absoluteUrlForPage).recover { case _ => + Some("invalid url") + }, + composeEvents(onClick.noKeyMod.preventDefault)( + _.sample($m) + .map(NavigateTo.apply) + ) --> actions + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala new file mode 100644 index 0000000..7682772 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala @@ -0,0 +1,13 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.pages.dashboard.DashboardPage +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage + +class DashboardPageConnector(actionBus: Observer[Action])(using + router: Router[Page] +): + def apply: HtmlElement = + AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala new file mode 100644 index 0000000..76b9133 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala @@ -0,0 +1,76 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.Parameter +import mdr.pdb.UserInfo +import mdr.pdb.app.pages.detail.DetailParametruPage +import pages.detail.DetailKriteriaPage +import pages.detail.components.DukazyKriteria +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.ParameterCriteria + +object DetailKriteriaPageConnector { + trait AppState { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + } +} + +case class DetailKriteriaPageConnector( + state: DetailKriteriaPageConnector.AppState +)( + $page: Signal[Page.DetailKriteria] +)(using Router[Page]): + val $paramChangeSignal = + $page.splitOne(p => + (p.osobniCislo.value, p.parametr.value, p.kriterium.value) + )((x, _, _) => x) + val $pageChangeSignal = + $paramChangeSignal.map( + FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) + ) + + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + val $merged = + $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => + for { + da <- d + pa <- p + pb <- pa.find(_.id == pc._2) + ka <- pb.criteria.find(_.id == pc._3) + } yield (da, pb, ka) + ) + + def apply: HtmlElement = + AppPage(state.actionBus)( + $merged.map(_.map(buildModel)) + .split(_ => ())((_, s, $s) => + DetailKriteriaPage($s)(state.actionBus.contramap { + case DukazyKriteria.Add => + NavigateTo( + Page.UpravDukazKriteria( + Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), + Page.Titled(s.parametr.id, Some(s.parametr.nazev)), + Page.Titled(s.kriterium.id, Some(s.kriterium.id)) + ) + ) + }) + ), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: Parameter, + k: ParameterCriteria + ): DetailKriteriaPage.ViewModel = + DetailKriteriaPage.ViewModel( + o.toDetailOsoby, + p.toParametr(_ => a()), + k.toKriterium(_ => a()) + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DetailPageConnector.scala new file mode 100644 index 0000000..df3a6b7 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DetailPageConnector.scala @@ -0,0 +1,57 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.UserInfo +import pages.detail.DetailPage +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.pages.detail.components.DetailOsoby +import mdr.pdb.Parameter +import mdr.pdb.app.pages.detail.components.SeznamParametru +import fiftyforms.ui.components.tailwind.Color + +object DetailPageConnector { + trait AppState { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + } +} + +case class DetailPageConnector(state: DetailPageConnector.AppState)( + $page: Signal[Page.Detail] +)(using router: Router[Page]): + val $oscChangeSignal = $page.splitOne(_.osobniCislo.value)((osc, _, _) => osc) + val $pageChangeSignal = + $oscChangeSignal.flatMap(osc => + EventStream.fromSeq(Seq(FetchUserDetails(osc), FetchParameters(osc))) + ) + // TODO: filter the value based on the current osc + // OSC change will fetch new data, but still + // - we need to be sure that what we got is really what we ought to display + // - we want to display stale data accordingly (at least with loading indicator) + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + def apply: HtmlElement = + AppPage(state.actionBus)( + $data.combineWithFn($params)(_ zip _) + .map(_.map(buildModel)) + .split(_ => ())((_, _, s) => DetailPage(s)), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: List[Parameter] + ): DetailPage.ViewModel = + DetailPage.ViewModel( + o.toDetailOsoby, + p.map( + _.toParametr(param => + PageLink.container(Page.DetailParametru(o, param), state.actionBus) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DetailParametruPageConnector.scala new file mode 100644 index 0000000..fa7193d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DetailParametruPageConnector.scala @@ -0,0 +1,67 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.Parameter +import mdr.pdb.UserInfo +import mdr.pdb.app.pages.detail.DetailParametruPage +import pages.detail.DetailParametruPage +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.components.PageLink + +object DetailParametruPageConnector { + trait AppState { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + } +} + +case class DetailParametruPageConnector( + state: DetailParametruPageConnector.AppState +)( + $page: Signal[Page.DetailParametru] +)(using router: Router[Page]): + val $paramChangeSignal = + $page.splitOne(p => (p.osobniCislo.value, p.parametr.value))((x, _, _) => x) + val $pageChangeSignal = + $paramChangeSignal.map(FetchParameter(_, _)) + + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + val $merged = + $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => + for { + da <- d + pa <- p + pb <- pa.find(_.id == pc._2) + } yield (da, pb) + ) + + def apply: HtmlElement = + AppPage(state.actionBus)( + $merged.map(_.map(buildModel)) + .split(_ => ())((_, _, s) => DetailParametruPage(s)), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: Parameter + ): DetailParametruPage.ViewModel = + DetailParametruPage.ViewModel( + o.toDetailOsoby, + p.toParametr(p => + PageLink.container(Page.DetailParametru(o, p), state.actionBus) + ), + p.criteria.map( + _.toKriterium { c => + PageLink.container( + Page.DetailKriteria(o, p, c), + state.actionBus + ) + } + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DirectoryPageConnector.scala new file mode 100644 index 0000000..ab2c045 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DirectoryPageConnector.scala @@ -0,0 +1,34 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.UserInfo +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.components.AppPage + +case class DirectoryPageConnector( + $input: EventStream[List[UserInfo]], + actionBus: Observer[Action] +)(using router: Router[Page]): + val $data = $input.startWithNone + val $actionSignal = EventStream.fromValue(FetchDirectory) + + def apply: HtmlElement = + AppPage(actionBus)( + $data.split(_ => ())((_, _, s) => + pages.directory.DirectoryPage( + s.map( + _.map( + _.toUserRow(u => + PageLink.container( + Page.Detail(Page.Titled(u.personalNumber)), + actionBus + ) + ) + ) + ) + ) + ), + $actionSignal --> actionBus + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/mdr/pdb/app/connectors/conversions.scala new file mode 100644 index 0000000..f581796 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/conversions.scala @@ -0,0 +1,68 @@ +package mdr.pdb.app.connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.UserInfo +import mdr.pdb.app.pages.detail.components.DetailOsoby +import mdr.pdb.Parameter +import mdr.pdb.app.pages.detail.components.SeznamParametru +import mdr.pdb.ParameterCriteria +import mdr.pdb.app.pages.detail.components.SeznamKriterii +import mdr.pdb.app.pages.directory.components.UserRow +import mdr.pdb.app.pages.detail.components.DetailParametru +import mdr.pdb.app.pages.detail.components.DetailKriteria +import fiftyforms.ui.components.tailwind.Color + +extension (o: UserInfo) + def toDetailOsoby: DetailOsoby.ViewModel = + DetailOsoby.ViewModel( + o.personalNumber, + o.name, + o.email, + o.phone, + o.img, + o.mainFunction.map(f => + DetailOsoby.Funkce.ViewModel(f.name, f.dept, f.ou) + ), + o.userContracts.headOption.map(c => + DetailOsoby.PracovniPomer.ViewModel(c.rel, c.startDate, c.endDate) + ) + ) + +extension (param: Parameter) + def toParametr(container: Parameter => Anchor): DetailParametru.ViewModel = + DetailParametru.ViewModel( + id = param.id, + nazev = param.name, + popis = param.description, + status = "Nesplněno", + statusColor = Color.red, + a = container(param) + ) + +extension (crit: ParameterCriteria) + def toKriterium( + container: ParameterCriteria => Anchor + ): DetailKriteria.ViewModel = + DetailKriteria.ViewModel( + nazev = crit.criteriumText, + kapitola = crit.chapterId, + bod = crit.itemId, + status = "Nesplněno", + statusColor = Color.red, + splneno = false, + dukazy = Nil, + container = container(crit) + ) + +extension (user: UserInfo) + def toUserRow( + container: UserInfo => HtmlElement = (_: UserInfo) => div() + ): UserRow.ViewModel = + UserRow.ViewModel( + osobniCislo = user.personalNumber.toString, + celeJmeno = user.name, + prijmeni = user.surname, + hlavniFunkce = user.mainFunction.map(_.name), + img = user.img, + container = () => container(user) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/dashboard/DashboardPage.scala b/app/src/main/scala/mdr/pdb/app/pages/dashboard/DashboardPage.scala new file mode 100644 index 0000000..0eb8f7a --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/dashboard/DashboardPage.scala @@ -0,0 +1,11 @@ +package mdr.pdb.app.pages.dashboard + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.Page + +object DashboardPage: + + def render: HtmlElement = + div("Dashboard page") diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala new file mode 100644 index 0000000..ff3a25f --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala @@ -0,0 +1,31 @@ +package mdr.pdb.app.pages.detail + +import com.raquo.laminar.api.L.{*, given} + +import components._ + +object DetailKriteriaPage: + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametr: DetailParametru.ViewModel, + kriterium: DetailKriteria.ViewModel + ) + + def apply($m: Signal[ViewModel])( + action: Observer[DukazyKriteria.Action] + ): HtmlElement = + div( + cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + div( + DetailOsoby.header($m.map(_.osoba)), + DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") + ), + div( + DetailKriteria($m.map(_.kriterium)), + DukazyKriteria($m.map(_.kriterium.dukazy))(action) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/DetailPage.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailPage.scala new file mode 100644 index 0000000..b591cc9 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailPage.scala @@ -0,0 +1,25 @@ +package mdr.pdb.app.pages.detail + +import com.raquo.laminar.api.L.{*, given} + +import components._ +import mdr.pdb.app.Page +import com.raquo.waypoint.Router +import mdr.pdb.app.Action + +object DetailPage: + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametry: SeznamParametru.ViewModel + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + DetailOsoby($m.map(_.osoba)), + SeznamParametru($m.map(_.parametry)) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/DetailParametruPage.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailParametruPage.scala new file mode 100644 index 0000000..3322279 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailParametruPage.scala @@ -0,0 +1,24 @@ +package mdr.pdb.app.pages.detail + +import com.raquo.laminar.api.L.{*, given} + +import components._ + +object DetailParametruPage: + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametr: DetailParametru.ViewModel, + kriteria: SeznamKriterii.ViewModel + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + DetailOsoby.header($m.map(_.osoba)), + DetailParametru($m.map(_.parametr)), + SeznamKriterii($m.map(_.kriteria)) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala new file mode 100644 index 0000000..b58618a --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala @@ -0,0 +1,107 @@ +package mdr.pdb +package app +package pages.detail + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.pages.detail.components.UpravDukazForm +import fiftyforms.services.files.File + +object UpravDukaz: + + type ThisPage = Page.UpravDukazKriteria + type PageKey = (OsobniCislo, String, String) + + trait State { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def availableFiles: EventStream[List[File]] + def actionBus: Observer[Action] + } + + def keyOfPage(page: ThisPage): PageKey = + (page.osobniCislo.value, page.parametr.value, page.kriterium.value) + + def onChangeAction(key: PageKey): Action = + FetchParameterCriteria( + key._1, + key._2, + key._3, + Page.UpravDukazKriteria(_, _, _) + ) + + class Connector(state: State)($page: Signal[ThisPage])(using Router[Page]): + val $paramChangeSignal = $page.splitOne(keyOfPage)((x, _, _) => x) + val $pageChangeSignal = $paramChangeSignal.map(onChangeAction) + + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + val $merged = + $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => + for { + da <- d + pa <- p + pb <- pa.find(_.id == pc._2) + ka <- pb.criteria.find(_.id == pc._3) + } yield (da, pb, ka) + ) + + def apply: HtmlElement = + AppPage(state.actionBus)( + $merged.split(_ => ())((_, s, $s) => + PageComponent( + $s.map(buildModel), + state.availableFiles, + state.actionBus.contramap { + case UpravDukazForm.Cancelled => + NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) + case UpravDukazForm.AvailableFilesRequested => + FetchAvailableFiles(s._1.personalNumber) + } + ) + ), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: Parameter, + k: ParameterCriteria + ): PageComponent.ViewModel = + import connectors.* + PageComponent.ViewModel( + o.toDetailOsoby, + p.toParametr(_ => a()), + k.toKriterium(_ => a()) + ) + + object PageComponent: + import components.* + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametr: DetailParametru.ViewModel, + kriterium: DetailKriteria.ViewModel + ) + + def apply( + $m: Signal[ViewModel], + availableFilesStream: EventStream[List[File]], + events: Observer[UpravDukazForm.Event] + ): HtmlElement = + div( + cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + div( + DetailOsoby.header($m.map(_.osoba)), + DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") + ), + div( + DetailKriteria($m.map(_.kriterium)), + UpravDukazForm(availableFilesStream)(events) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailKriteria.scala new file mode 100644 index 0000000..a3ab634 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailKriteria.scala @@ -0,0 +1,37 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.CustomAttrs +import fiftyforms.ui.components.tailwind.Icons +import java.time.LocalDate +import fiftyforms.ui.components.tailwind.Color + +object DetailKriteria: + case class ViewModel( + nazev: String, + kapitola: String, + bod: String, + status: String, + statusColor: Color, + splneno: Boolean, + dukazy: List[DukazKriteria.ViewModel], + container: HtmlElement = div() + ) { + val id = s"${kapitola}${bod}" + } + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "pb-5 border-b border-gray-200", + h3( + cls := "text-xl leading-6 font-bold text-gray-900", + child.text <-- $m.map(_.nazev) + ), + h3( + cls := "mt-2 max-w-4xl text-sm text-gray-500", + "Kapitola ", + child.text <-- $m.map(_.kapitola), + " bod ", + child <-- $m.map(_.bod) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailOsoby.scala new file mode 100644 index 0000000..0fe2ba5 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailOsoby.scala @@ -0,0 +1,100 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.OsobniCislo + +import java.time.LocalDate +import fiftyforms.ui.components.tailwind.Avatar + +object DetailOsoby: + + object Funkce: + case class ViewModel( + nazev: String, + stredisko: String, + voj: String + ) + + def render($m: Signal[ViewModel]): HtmlElement = + p( + cls := "text-sm font-medium text-gray-500", + child.text <-- $m.map(_.nazev), + span( + cls := "hidden md:inline", + " @ ", + child.text <-- $m.map(_.stredisko), + ", ", + child.text <-- $m.map(_.voj) + ) + ) + + object PracovniPomer: + case class ViewModel( + druh: String, + pocatek: LocalDate, + konec: Option[LocalDate] + ) + + def render($m: Signal[ViewModel]): HtmlElement = + import fiftyforms.ui.components.tailwind.CustomAttrs.datetime + p( + cls := "text-sm font-medium text-gray-500", + child.text <-- $m.map(_.druh), + " od ", + time( + datetime <-- $m.map(_.pocatek.toString), + child.text <-- $m.map(_.pocatek.toString) + ) + ) + + case class ViewModel( + osobniCislo: OsobniCislo, + jmeno: String, + email: Option[String], + telefon: Option[String], + img: Option[String], + hlavniFunkce: Option[Funkce.ViewModel], + pracovniPomer: Option[PracovniPomer.ViewModel] + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "md:flex md:items-center md:justify-between md:space-x-4", + div( + cls := "flex items-start space-x-4", + div( + cls := "flex-shrink-0", + Avatar($m.map(_.img)).avatar(16) + ), + div( + h1( + cls := "text-2xl font-bold text-gray-900", + child.text <-- $m.map(_.jmeno) + ), + child.maybe <-- $m.map(_.hlavniFunkce).split(_ => ())((_, _, d) => + Funkce.render(d) + ), + child.maybe <-- $m.map(_.pracovniPomer).split(_ => ())((_, _, d) => + PracovniPomer.render(d) + ) + ) + ) + ) + + def header($m: Signal[ViewModel]): HtmlElement = + div( + cls := "md:flex md:items-center md:justify-between md:space-x-5", + div( + cls := "flex items-start space-x-4", + div( + cls := "flex-shrink-0", + Avatar($m.map(_.img)).avatar(8) + ), + div( + h1( + cls := "text-lg font-medium text-gray-800", + child.text <-- $m.map(_.jmeno) + ) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailParametru.scala new file mode 100644 index 0000000..6310dde --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailParametru.scala @@ -0,0 +1,34 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.Color + +object DetailParametru: + case class ViewModel( + id: String, + nazev: String, + popis: String, + status: String, + statusColor: Color, + a: Anchor + ) + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "pb-5 border-b border-gray-200", + h2( + cls := "text-2xl leading-6 font-medium text-gray-900", + child.text <-- $m.map(_.nazev) + ), + p( + cls := "mt-2 max-w-4xl text-sm text-gray-500", + child.text <-- $m.map(_.popis) + ) + ) + + def header($m: Signal[ViewModel]): HtmlElement = + div( + h2( + cls := "text-lg leading-6 font-medium text-gray-600", + child.text <-- $m.map(_.nazev) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazKriteria.scala new file mode 100644 index 0000000..c292919 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazKriteria.scala @@ -0,0 +1,177 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import java.time.LocalDate +import fiftyforms.ui.components.tailwind.CustomAttrs + +object DukazKriteria: + case class Osoba(osobniCislo: String, jmeno: String) + case class Udalost(osoba: Osoba, datum: LocalDate) + case class Dokument( + url: String, + nazev: String, + pridal: Udalost, + odebral: Option[Udalost] + ) + case class ViewModel( + dokumenty: List[Dokument], + autorizoval: Option[Udalost], + platiDo: Option[LocalDate], + poznámka: Option[String] + ) + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "bg-white shadow overflow-hidden sm:rounded-lg", + div( + cls := "px-4 py-5 sm:px-6", + h3( + cls := "text-lg leading-6 font-medium text-gray-900", + """Applicant Information""" + ), + p( + cls := "mt-1 max-w-2xl text-sm text-gray-500", + """Personal details and application.""" + ) + ), + div( + cls := "border-t border-gray-200 px-4 py-5 sm:px-6", + dl( + cls := "grid grid-cols-1 gap-x-4 gap-y-8 sm:grid-cols-2", + div( + cls := "sm:col-span-1", + dt( + cls := "text-sm font-medium text-gray-500", + """Full name""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """Margot Foster""" + ) + ), + div( + cls := "sm:col-span-1", + dt( + cls := "text-sm font-medium text-gray-500", + """Application for""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """Backend Developer""" + ) + ), + div( + cls := "sm:col-span-1", + dt( + cls := "text-sm font-medium text-gray-500", + """Email address""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """margotfoster@example.com""" + ) + ), + div( + cls := "sm:col-span-1", + dt( + cls := "text-sm font-medium text-gray-500", + """Salary expectation""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """$120,000""" + ) + ), + div( + cls := "sm:col-span-2", + dt( + cls := "text-sm font-medium text-gray-500", + """About""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """Fugiat ipsum ipsum deserunt culpa aute sint do nostrud anim incididunt cillum culpa consequat. Excepteur qui ipsum aliquip consequat sint. Sit id mollit nulla mollit nostrud in ea officia proident. Irure nostrud pariatur mollit ad adipisicing reprehenderit deserunt qui eu.""" + ) + ), + div( + cls := "sm:col-span-2", + dt( + cls := "text-sm font-medium text-gray-500", + """Attachments""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + ul( + role := "list", + cls := "border border-gray-200 rounded-md divide-y divide-gray-200", + li( + cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", + div( + cls := "w-0 flex-1 flex items-center", { + import svg.* + import CustomAttrs.svg.ariaHidden + svg( + cls := "flex-shrink-0 h-5 w-5 text-gray-400", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + ariaHidden := true, + path( + fillRule := "evenodd", + d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", + clipRule := "evenodd" + ) + ) + }, + span( + cls := "ml-2 flex-1 w-0 truncate", + """resume_back_end_developer.pdf""" + ) + ), + div( + cls := "ml-4 flex-shrink-0", + a( + href := "#", + cls := "font-medium text-indigo-600 hover:text-indigo-500", + """Download""" + ) + ) + ), + li( + cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", + div( + cls := "w-0 flex-1 flex items-center", { + import svg.* + import CustomAttrs.svg.ariaHidden + svg( + cls := "flex-shrink-0 h-5 w-5 text-gray-400", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + ariaHidden := true, + path( + fillRule := "evenodd", + d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", + clipRule := "evenodd" + ) + ) + }, + span( + cls := "ml-2 flex-1 w-0 truncate", + """coverletter_back_end_developer.pdf""" + ) + ), + div( + cls := "ml-4 flex-shrink-0", + a( + href := "#", + cls := "font-medium text-indigo-600 hover:text-indigo-500", + """Download""" + ) + ) + ) + ) + ) + ) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala new file mode 100644 index 0000000..d604860 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala @@ -0,0 +1,35 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.Icons + +object DukazyKriteria: + sealed trait Action + case object Add extends Action + + type ViewModel = List[DukazKriteria.ViewModel] + + def apply($m: Signal[ViewModel])(actions: Observer[Action]): HtmlElement = + div( + child.maybe <-- $m.map(m => + if (m.isEmpty) then Some(prazdnyDukaz(actions)) + else None + ), + children <-- $m.map(_.zipWithIndex).split(_._2)((_, _, $s) => + DukazKriteria($s.map(_._1)) + ) + ) + + private def prazdnyDukaz(actions: Observer[Action]): HtmlElement = + button( + tpe := "button", + onClick.preventDefault.mapTo(Add) --> actions, + cls := "relative block w-full border-2 border-gray-300 border-dashed rounded-lg p-12 text-center hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + Icons.outline + .`document-add`(12) + .amend(svg.cls := "mx-auto text-gray-400"), + span( + cls := "mt-2 block text-sm font-medium text-gray-900", + "Přidat důkaz" + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala new file mode 100644 index 0000000..7e77a7b --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala @@ -0,0 +1,34 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.Color +import fiftyforms.ui.components.tailwind.list.StackedList +import fiftyforms.ui.components.tailwind.list.ListRow +import fiftyforms.ui.components.tailwind.list.RowTag +import fiftyforms.ui.components.tailwind.list.RowNext +import java.time.LocalDate + +object SeznamKriterii: + type ViewModel = List[DetailKriteria.ViewModel] + + private val kritList = new StackedList[DetailKriteria.ViewModel] + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "bg-white shadow overflow-hidden sm:rounded-md", + kritList($m, _.id) { $i => + $i.map { i => + ListRow.ViewModel( + title = i.nazev, + topRight = RowTag.render( + $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) + ), + bottomLeft = + p(cls := "text-sm text-gray-500", s"${i.kapitola}${i.bod}"), + bottomRight = emptyNode, + farRight = RowNext.render, + containerElement = i.container + ) + } + } + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamParametru.scala new file mode 100644 index 0000000..0760b32 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamParametru.scala @@ -0,0 +1,37 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.list.{ + StackedList, + ListRow, + RowTag, + PropList, + IconText, + RowNext +} +import fiftyforms.ui.components.tailwind.Color +import fiftyforms.ui.components.tailwind.LinkSupport.* + +object SeznamParametru: + type ViewModel = List[DetailParametru.ViewModel] + + private val parametrList = new StackedList[DetailParametru.ViewModel] + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "bg-white shadow overflow-hidden sm:rounded-md", + parametrList($m, _.id) { $i => + $i.map { i => + ListRow.ViewModel( + title = i.nazev, + topRight = RowTag.render( + $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) + ), + bottomLeft = emptyNode, + bottomRight = emptyNode, + farRight = RowNext.render, + containerElement = i.a + ) + } + } + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala new file mode 100644 index 0000000..28753c9 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala @@ -0,0 +1,101 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import io.laminext.syntax.core.* +import fiftyforms.ui.components.tailwind.CustomAttrs +import fiftyforms.ui.components.tailwind.form.* +import org.scalajs.dom +import com.raquo.laminar.nodes.ReactiveHtmlElement +import fiftyforms.services.files.components.tailwind.FilePicker +import mdr.pdb.frontend.AutorizujDukaz +import mdr.pdb.frontend.DocumentRef +import fiftyforms.services.files.File + +object UpravDukazForm: + sealed trait Event + case object Cancelled extends Event + case object AvailableFilesRequested extends Event + def apply(availableFilesStream: EventStream[List[File]])( + updates: Observer[Event] + ): HtmlElement = + val (filesStream, filesObserver) = + EventStream.withObserver[FilePicker.Event] + val files = Var[List[File]](Nil) + def submitButtons: HtmlElement = + div( + cls := "pt-5", + div( + cls := "flex justify-end", + button( + tpe := "button", + cls := "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + "Zrušit", + onClick.mapTo(Cancelled) --> updates + ), + button( + tpe := "submit", + cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + disabled <-- files.signal.map(_.isEmpty), + "Autorizovat" + ) + ) + ) + + div( + filesStream.collect { case FilePicker.SelectionUpdated(files) => + files.to(List) + } --> files.writer, + filesStream.collect { case FilePicker.AvailableFilesRequested => + AvailableFilesRequested + } --> updates, + cls := "bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6", + Form( + Form.Body( + Form.Section( + FormHeader( + FormHeader.ViewModel( + "Doložení kritéria", + "Sestavte doklady poskytující důkaz kritéria a potvrďte odesláním formuláře. Případné limitace či jiné relevantní údaje vepište do pole Komentář." + ) + ), + FormFields( + FormRow( + "dokumenty", + "Dokumenty", + FilePicker(files.signal, availableFilesStream)(filesObserver) + .amend(idAttr := "dokumenty", cls("max-w-lg")) + ).toHtml, + FormRow( + "platnost", + "Platnost", + input( + idAttr := "platnost", + name := "platnost", + tpe := "date", + autoComplete := "date", + cls := "block max-w-lg w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" + ) + ).toHtml, + FormRow( + "komentar", + "Komentář", + div( + cls := "mt-1 sm:mt-0 sm:col-span-2", + textArea( + idAttr := "komentar", + name := "about", + rows := 3, + cls := "max-w-lg shadow-sm block w-full focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border border-gray-300 rounded-md" + ), + p( + cls := "mt-2 text-sm text-gray-500", + "Doplňte prosím potřebné informace související s doložením kritéria, včetně případných limitací." + ) + ) + ).toHtml + ) + ) + ), + submitButtons + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/directory/DirectoryPage.scala b/app/src/main/scala/mdr/pdb/app/pages/directory/DirectoryPage.scala new file mode 100644 index 0000000..7a72ca3 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/directory/DirectoryPage.scala @@ -0,0 +1,40 @@ +package mdr.pdb.app.pages.directory + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.UserInfo + +import components._ + +object DirectoryPage: + + type ViewModel = List[UserRow.ViewModel] + + def apply($m: Signal[ViewModel]): HtmlElement = + val (actionsStream, actionObserver) = + EventStream.withObserver[SearchForm.Action] + val $filter = actionsStream + .collect { + case SearchForm.ClearFilter => None + case SearchForm.SetFilter(t) => Some(t) + } + .startWith(None) + val byLetter = for { + d <- $m + f <- $filter + } yield for { + (letter, users) <- d + .filter { user => + f match + case None => true + case Some(t) => user.search.contains(t) + } + .groupBy(_.prijmeni.head) + .to(List) + .sortBy(_._1) + } yield (letter.toString, users.sortBy(_.prijmeni)) + + div( + cls := "h-full max-w-7xl mx-auto order-first flex flex-col flex-shrink-0", + SearchForm(actionObserver), + Directory(byLetter) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/directory/components/Directory.scala b/app/src/main/scala/mdr/pdb/app/pages/directory/components/Directory.scala new file mode 100644 index 0000000..f7d6f99 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/directory/components/Directory.scala @@ -0,0 +1,37 @@ +package mdr.pdb.app.pages.directory.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.UserInfo + +object Directory: + + object Header: + type ViewModel = String + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500", + h3(child.text <-- $m) + ) + + object UserList: + type ViewModel = List[UserRow.ViewModel] + def apply($m: Signal[ViewModel]): HtmlElement = + ul( + role := "list", + cls := "relative z-0 divide-y divide-gray-200", + children <-- $m.split(_.osobniCislo)((_, _, s) => UserRow(s)) + ) + + type ViewModel = List[(String, List[UserRow.ViewModel])] + def apply($m: Signal[ViewModel]): HtmlElement = + nav( + cls := "flex-1 min-h-0 overflow-y-auto", + aria.label := "Directory", + children <-- $m.split(_._1)((_, _, s) => + div( + cls := "relative", + Header(s.map(_._1)), + UserList(s.map(_._2)) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/directory/components/SearchForm.scala b/app/src/main/scala/mdr/pdb/app/pages/directory/components/SearchForm.scala new file mode 100644 index 0000000..fb9ecc4 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/directory/components/SearchForm.scala @@ -0,0 +1,54 @@ +package mdr.pdb.app.pages.directory.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.Icons + +object SearchForm: + sealed trait Action + case object SubmitFilter extends Action + sealed trait FilterAction extends Action + case object ClearFilter extends FilterAction + case class SetFilter(value: String) extends FilterAction + + def apply(actions: Observer[Action]): HtmlElement = + div( + cls := "px-6 pt-4 pb-4", + form( + cls := "flex space-x-4", + onSubmit.mapTo(SubmitFilter) --> actions, + div( + cls := "flex-1 min-w-0", + label( + forId := "search", + cls := "sr-only", + "Hledat" + ), + div( + cls := "relative rounded-md shadow-sm", + div( + cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", + Icons.solid.search().amend(svg.cls := "text-gray-400") + ), + input( + tpe := "search", + name := "search", + idAttr := "search", + cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", + placeholder := "Hledat", + composeEvents(onInput.mapToValue.setAsValue.map(SetFilter(_)))( + _.throttle(500) + ) --> actions + ) + ) + ), + button( + tpe := "submit", + cls := "inline-flex justify-center px-3.5 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500", + Icons.solid.filter().amend(svg.cls := "text-gray-400"), + span( + cls := "sr-only", + "Hledat" + ) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/directory/components/UserRow.scala b/app/src/main/scala/mdr/pdb/app/pages/directory/components/UserRow.scala new file mode 100644 index 0000000..4aa1672 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/directory/components/UserRow.scala @@ -0,0 +1,52 @@ +package mdr.pdb.app.pages.directory.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.airstream.core.Signal +import fiftyforms.ui.components.tailwind.Avatar + +object UserRow: + case class ViewModel( + osobniCislo: String, + celeJmeno: String, + prijmeni: String, + hlavniFunkce: Option[String], + img: Option[String], + container: () => HtmlElement = () => div() + ) { + val search = prijmeni.toLowerCase + } + + def apply($m: Signal[ViewModel]): HtmlElement = + inline def avatarImage = + Avatar($m.map(_.img)).avatarImage(10) + + li( + div( + cls := "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500", + div( + cls := "flex-shrink-0", + child <-- avatarImage + ), + div( + cls := "flex-1 min-w-0", + child <-- $m.map { o => + o.container() + .amend( + cls := "focus:outline-none", + span( + cls := "absolute inset-0", + aria.hidden := true + ), + p( + cls := "text-sm font-medium text-gray-900", + o.celeJmeno + ), + p( + cls := "text-sm text-gray-500 truncate", + o.hlavniFunkce + ) + ) + } + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/errors/ErrorPage.scala b/app/src/main/scala/mdr/pdb/app/pages/errors/ErrorPage.scala new file mode 100644 index 0000000..6b195ef --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/errors/ErrorPage.scala @@ -0,0 +1,99 @@ +package mdr.pdb.app.pages.errors + +import com.raquo.domtypes.generic.codecs.StringAsIsCodec +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.Page +import mdr.pdb.app.Action +import com.raquo.waypoint.Router + +object ErrorPage: + case class ViewModel( + homePage: Page, + errorName: String, + title: String, + subTitle: String + ) + + def apply(m: ViewModel, actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + val ViewModel(homePage, errorName, title, subTitle) = m + div( + cls := "min-h-full pt-16 pb-12 flex flex-col bg-white", + main( + cls := "flex-grow flex flex-col justify-center max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", + div( + cls := "flex-shrink-0 flex justify-center", + a( + href := "/", + cls := "inline-flex", + span( + cls := "sr-only", + """Workflow""" + ), + img( + cls := "h-12 w-auto", + src := "https://tailwindui.com/img/logos/workflow-mark.svg?color=indigo&shade=600", + alt := "" + ) + ) + ), + div( + cls := "py-16", + div( + cls := "text-center", + p( + cls := "text-sm font-semibold text-indigo-600 uppercase tracking-wide", + errorName + ), + h1( + cls := "mt-2 text-4xl font-extrabold text-gray-900 tracking-tight sm:text-5xl", + title + ), + p( + cls := "mt-2 text-base text-gray-500", + subTitle + ), + div( + cls := "mt-6", + PageLink + .container(homePage, actionBus) + .amend( + cls := "text-base font-medium text-indigo-600 hover:text-indigo-500", + """Go back home""" + ) + ) + ) + ) + ), + footer( + cls := "flex-shrink-0 max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", + nav( + cls := "flex justify-center space-x-4", + a( + href := "#", + cls := "text-sm font-medium text-gray-500 hover:text-gray-600", + """Contact Support""" + ), + span( + cls := "inline-block border-l border-gray-300", + aria.hidden := true + ), + a( + href := "#", + cls := "text-sm font-medium text-gray-500 hover:text-gray-600", + """Status""" + ), + span( + cls := "inline-block border-l border-gray-300", + aria.hidden := true + ), + a( + href := "#", + cls := "text-sm font-medium text-gray-500 hover:text-gray-600", + """Twitter""" + ) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/errors/NotFoundPage.scala b/app/src/main/scala/mdr/pdb/app/pages/errors/NotFoundPage.scala new file mode 100644 index 0000000..97760e1 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/errors/NotFoundPage.scala @@ -0,0 +1,21 @@ +package mdr.pdb.app.pages.errors + +import com.raquo.domtypes.generic.codecs.StringAsIsCodec +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.Page +import mdr.pdb.app.Action + +object NotFoundPage: + def apply(homePage: Page, url: String, actionBus: Observer[Action])(using + router: Router[Page] + ): HtmlElement = + ErrorPage( + ErrorPage.ViewModel( + homePage, + "404 error", + "Page not found.", + s"Sorry, but page $url doesn't exist." + ), + actionBus + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala b/app/src/main/scala/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala new file mode 100644 index 0000000..77b0af8 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala @@ -0,0 +1,32 @@ +package mdr.pdb.app.pages.errors + +import com.raquo.domtypes.generic.codecs.StringAsIsCodec +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.Page +import mdr.pdb.app.Action + +object UnhandledErrorPage: + + case class ViewModel( + homePage: Page, + errorName: Option[String], + errorMessage: Option[String] + ) + + def apply(m: ViewModel, actionBus: Observer[Action])(using + router: Router[Page] + ): HtmlElement = + ErrorPage( + ErrorPage.ViewModel( + m.homePage, + "Unexpected error occurred", + m.errorName.getOrElse( + "Uh oh!" + ), // TODO: translations, better text than uh oh + m.errorMessage.getOrElse( + "This wasn't supposed to happen! Please try again." + ) + ), + actionBus + ) diff --git a/app/src/main/scala/mdr/pdb/app/services/DataFetcher.scala b/app/src/main/scala/mdr/pdb/app/services/DataFetcher.scala new file mode 100644 index 0000000..134c221 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/services/DataFetcher.scala @@ -0,0 +1,6 @@ +package mdr.pdb.app.services + +import com.raquo.airstream.core.Observer + +trait DataFetcher[K, A]: + def fetch(id: K, sink: Observer[A]): Unit diff --git a/app/src/main/scala/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/mdr/pdb/app/state/AppState.scala new file mode 100644 index 0000000..e44b2df --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/state/AppState.scala @@ -0,0 +1,120 @@ +package mdr.pdb.app +package state + +import com.raquo.airstream.core.EventStream +import mdr.pdb.{UserInfo, OsobniCislo} +import com.raquo.airstream.core.Observer +import scala.scalajs.js +import scala.scalajs.js.JSON +import zio.json._ +import com.raquo.airstream.eventbus.EventBus +import com.raquo.airstream.ownership.Owner +import com.raquo.waypoint.Router +import mdr.pdb.Parameter +import mdr.pdb.ParameterCriteria +import mdr.pdb.UserFunction +import mdr.pdb.UserContract +import fiftyforms.services.files.File + +trait AppState + extends connectors.DetailPageConnector.AppState + with connectors.DetailParametruPageConnector.AppState + with connectors.DetailKriteriaPageConnector.AppState + with pages.detail.UpravDukaz.State: + def users: EventStream[List[UserInfo]] + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + +class MockAppState(implicit owner: Owner, router: Router[Page]) + extends AppState: + + given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) + given JsonDecoder[UserFunction] = DeriveJsonDecoder.gen + given JsonDecoder[UserContract] = DeriveJsonDecoder.gen + given JsonDecoder[UserInfo] = DeriveJsonDecoder.gen + + given JsonDecoder[ParameterCriteria] = DeriveJsonDecoder.gen + given JsonDecoder[Parameter] = DeriveJsonDecoder.gen + + private val actions = EventBus[Action]() + private val (parametersStream, pushParameters) = + EventStream.withCallback[List[Parameter]] + private val (usersStream, pushUsers) = + EventStream.withCallback[List[UserInfo]] + private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] + private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] + + private val mockData: List[UserInfo] = + mockUsers + .asInstanceOf[js.Dictionary[js.Object]] + .values + // TODO: is there a more efficient way to parse from JS object directly? + .map { o => + val parsed = JSON.stringify(o).fromJson[UserInfo] + parsed.left.foreach(org.scalajs.dom.console.log(_)) + parsed + } + .collect { case Right(u) => + u + } + .toList + + private val mockParameters: List[Parameter] = + pdbParams + .asInstanceOf[js.Dictionary[js.Object]] + .values + .map(o => JSON.stringify(o).fromJson[Parameter]) + .collect { case Right(p) => p } + .toList + + // TODO: Extract to separate event handler + actions.events.foreach { + case FetchDirectory => pushUsers(mockData) + case FetchUserDetails(osc) => + mockData.find(_.personalNumber == osc).foreach { o => + pushDetails(o) + router.replaceState(Page.Detail(o)) + } + case FetchParameters(osc) => + pushParameters(mockParameters) + case FetchParameter(osc, paramId) => + for + o <- mockData.find(_.personalNumber == osc) + p <- mockParameters.find(_.id == paramId) + do + pushDetails(o) + pushParameters(mockParameters) + router.replaceState(Page.DetailParametru(o, p)) + case FetchParameterCriteria(osc, paramId, critId, page) => + for + o <- mockData.find(_.personalNumber == osc) + p <- mockParameters.find(_.id == paramId) + c <- p.criteria.find(_.id == critId) + do + pushDetails(o) + pushParameters(mockParameters) + router.replaceState(page(o, p, c)) + case NavigateTo(page) => router.pushState(page) + case FetchAvailableFiles(osc) => + pushFiles( + List( + File("https://tc163.cmi.cz/here", "Example file") + ) + ) + } + + override def users: EventStream[List[UserInfo]] = + usersStream.debugWithName("users") + + override def details: EventStream[UserInfo] = + detailsStream.debugWithName("details") + + override def parameters: EventStream[List[Parameter]] = + parametersStream.debugWithName("parameters") + + override def availableFiles: EventStream[List[File]] = + filesStream.debugWithName("available files") + + override def actionBus: Observer[Action] = + actions.writer.debugWithName("actions writer") diff --git a/build.sbt b/build.sbt index 2afc879..0daf796 100644 --- a/build.sbt +++ b/build.sbt @@ -11,6 +11,12 @@ lazy val core = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Pure) .in(file("core")) + .settings( + IWDeps.useZIO(Test), + IWDeps.tapirCore, + IWDeps.tapirZIO, + IWDeps.tapirZIOJson + ) lazy val ui = (project in file("ui")) .enablePlugins(ScalaJSPlugin) diff --git a/core/src/main/scala/cz/e_bs/cmi/mdr/pdb/Parameter.scala b/core/src/main/scala/cz/e_bs/cmi/mdr/pdb/Parameter.scala deleted file mode 100644 index 9d68240..0000000 --- a/core/src/main/scala/cz/e_bs/cmi/mdr/pdb/Parameter.scala +++ /dev/null @@ -1,27 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb - -import java.math.BigInteger -import java.security.MessageDigest - -object ParameterCriteria { - type Id = String -} - -case class ParameterCriteria( - chapterId: String, - itemId: String, - criteriumText: String -) { - val id: ParameterCriteria.Id = s"${chapterId}${itemId}" -} - -object Parameter { - type Id = String -} - -case class Parameter( - id: Parameter.Id, - name: String, - description: String, - criteria: List[ParameterCriteria] -) diff --git a/core/src/main/scala/cz/e_bs/cmi/mdr/pdb/UserProfile.scala b/core/src/main/scala/cz/e_bs/cmi/mdr/pdb/UserProfile.scala deleted file mode 100644 index 455a445..0000000 --- a/core/src/main/scala/cz/e_bs/cmi/mdr/pdb/UserProfile.scala +++ /dev/null @@ -1,44 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb - -import java.time.LocalDate - -opaque type OsobniCislo = String - -object OsobniCislo: - // TODO: validation - def apply(osc: String): OsobniCislo = osc - -extension (osc: OsobniCislo) def toString: String = osc - -case class UserContract( - rel: String, - startDate: LocalDate, - endDate: Option[LocalDate] -) -case class UserFunction(name: String, dept: String, ou: String) - -case class UserInfo( - personalNumber: OsobniCislo, - username: String, - givenName: String, - surname: String, - titlesBeforeName: Option[String] = None, - titlesAfterName: Option[String] = None, - email: Option[String] = None, - phone: Option[String] = None, - mainFunction: Option[UserFunction] = None, - userContracts: List[UserContract] = Nil, - img: Option[String] = None -) { - val name = - List( - Some( - List(titlesBeforeName, Some(givenName), Some(surname)).flatten.mkString( - " " - ) - ), - titlesAfterName - ).flatten.mkString(", ") -} - -case class UserProfile(username: String, userInfo: UserInfo) diff --git a/core/src/main/scala/cz/e_bs/cmi/mdr/pdb/frontend/domain.scala b/core/src/main/scala/cz/e_bs/cmi/mdr/pdb/frontend/domain.scala deleted file mode 100644 index 10980c5..0000000 --- a/core/src/main/scala/cz/e_bs/cmi/mdr/pdb/frontend/domain.scala +++ /dev/null @@ -1,17 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb -package frontend - -import java.time.LocalDate -import java.time.Instant - -sealed trait Command - -type DocumentRef = String - -case class AutorizujDukaz( - osoba: OsobniCislo, - parametr: Parameter.Id, - kriterium: ParameterCriteria.Id, - dukaz: List[DocumentRef], - platiDo: Option[LocalDate] -) extends Command diff --git a/core/src/main/scala/mdr/pdb/Parameter.scala b/core/src/main/scala/mdr/pdb/Parameter.scala new file mode 100644 index 0000000..0b89f6c --- /dev/null +++ b/core/src/main/scala/mdr/pdb/Parameter.scala @@ -0,0 +1,27 @@ +package mdr.pdb + +import java.math.BigInteger +import java.security.MessageDigest + +object ParameterCriteria { + type Id = String +} + +case class ParameterCriteria( + chapterId: String, + itemId: String, + criteriumText: String +) { + val id: ParameterCriteria.Id = s"${chapterId}${itemId}" +} + +object Parameter { + type Id = String +} + +case class Parameter( + id: Parameter.Id, + name: String, + description: String, + criteria: List[ParameterCriteria] +) diff --git a/core/src/main/scala/mdr/pdb/UserProfile.scala b/core/src/main/scala/mdr/pdb/UserProfile.scala new file mode 100644 index 0000000..8373b82 --- /dev/null +++ b/core/src/main/scala/mdr/pdb/UserProfile.scala @@ -0,0 +1,44 @@ +package mdr.pdb + +import java.time.LocalDate + +opaque type OsobniCislo = String + +object OsobniCislo: + // TODO: validation + def apply(osc: String): OsobniCislo = osc + +extension (osc: OsobniCislo) def toString: String = osc + +case class UserContract( + rel: String, + startDate: LocalDate, + endDate: Option[LocalDate] +) +case class UserFunction(name: String, dept: String, ou: String) + +case class UserInfo( + personalNumber: OsobniCislo, + username: String, + givenName: String, + surname: String, + titlesBeforeName: Option[String] = None, + titlesAfterName: Option[String] = None, + email: Option[String] = None, + phone: Option[String] = None, + mainFunction: Option[UserFunction] = None, + userContracts: List[UserContract] = Nil, + img: Option[String] = None +) { + val name = + List( + Some( + List(titlesBeforeName, Some(givenName), Some(surname)).flatten.mkString( + " " + ) + ), + titlesAfterName + ).flatten.mkString(", ") +} + +case class UserProfile(username: String, userInfo: UserInfo) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala deleted file mode 100644 index a3b00af..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Main.scala +++ /dev/null @@ -1,103 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import scala.scalajs.js.annotation.JSExportTopLevel -import scala.scalajs.js.annotation.JSExport -import scala.scalajs.js.annotation.JSImport -import scala.scalajs.js -import org.scalajs.dom -import com.raquo.laminar.api.L.{*, given} -import scala.scalajs.js.Date -import com.raquo.waypoint.Router -import com.raquo.waypoint.SplitRender -import cz.e_bs.cmi.mdr.pdb.app.services.DataFetcher -import scala.scalajs.js.JSON -import zio.json._ -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.state.AppState - -@js.native -@JSImport("stylesheets/main.css", JSImport.Namespace) -object Css extends js.Any - -@js.native -@JSImport("data/users.json", JSImport.Default) -object mockUsers extends js.Object - -@js.native -@JSImport("params/pdb-params.json", JSImport.Default) -object pdbParams extends js.Object - -@JSExportTopLevel("app") -object Main: - - @JSExport - def main(args: Array[String]): Unit = - import Routes.given - onLoad { - setupAirstream() - val appContainer = dom.document.querySelector("#app") - val _ = - render( - appContainer, - renderPage(state.MockAppState(using unsafeWindowOwner, router)) - ) - } - - private def onLoad(f: => Unit): Unit = - documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) - - private def setupAirstream()(using router: Router[Page]): Unit = - AirstreamError.registerUnhandledErrorCallback(err => - router.forcePage( - Page.UnhandledError( - Some(err.getClass.getName), // TODO: Fill only in dev mode - Some(err.getMessage) - ) - ) - ) - - def renderPage(state: AppState)(using - router: Router[Page] - ): HtmlElement = - val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) - .collectSignal[Page.Detail]( - connectors - .DetailPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailParametru]( - connectors - .DetailParametruPageConnector(state)(_) - .apply - ) - .collectSignal[Page.DetailKriteria]( - connectors - .DetailKriteriaPageConnector(state)(_) - .apply - ) - .collectSignal[Page.UpravDukazKriteria]( - pages.detail.UpravDukaz.Connector(state)(_).apply - ) - .collectStatic(Page.Dashboard)( - connectors.DashboardPageConnector(state.actionBus).apply - ) - .collect[Page.NotFound](pg => - pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) - ) - .collect[Page.UnhandledError](pg => - pages.errors - .UnhandledErrorPage( - pages.errors.UnhandledErrorPage - .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), - state.actionBus - ) - ) - .collectStatic(Page.Directory)( - connectors - .DirectoryPageConnector(state.users, state.actionBus) - .apply - ) - div(cls := "h-full", child <-- pageSplitter.$view) - - // Pull in the stylesheet - val css: Css.type = Css diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala deleted file mode 100644 index 3bbc8c0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/Routes.scala +++ /dev/null @@ -1,198 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.* -import org.scalajs.dom -import zio.json.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import scala.scalajs.js -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.Page.Titled - -// enum is not working with Waypoints' SplitRender collectStatic -sealed abstract class Page( - val id: String, - val title: String, - val parent: Option[Page] -) { - val path: Vector[Page] = - parent match - case None => Vector(this) - case Some(p) => p.path :+ this - - val isRoot: Boolean = parent.isEmpty -} - -object Page: - - case class Titled[V](value: V, title: Option[String] = None): - val show: String = title.getOrElse(value.toString) - - case object Directory extends Page("directory", "Adresář", None) - - case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) - - case class Detail(osobniCislo: Titled[OsobniCislo]) - extends Page("user", osobniCislo.show, Some(Directory)) - - object Detail { - def apply(o: UserInfo): Detail = Detail( - Titled(o.personalNumber, Some(o.name)) - ) - } - - case class DetailParametru( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String] - ) extends Page( - "parameter", - parametr.show, - Some(Detail(osobniCislo)) - ) - - object DetailParametru { - def apply(o: UserInfo, p: Parameter): DetailParametru = - DetailParametru( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)) - ) - } - - case class DetailKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "criteria", - kriterium.show, - Some(DetailParametru(osobniCislo, parametr)) - ) - - object DetailKriteria { - def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = - DetailKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class UpravDukazKriteria( - osobniCislo: Titled[OsobniCislo], - parametr: Titled[String], - kriterium: Titled[String] - ) extends Page( - "addProof", - "Důkaz", - Some(DetailKriteria(osobniCislo, parametr, kriterium)) - ) - - object UpravDukazKriteria { - def apply( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): UpravDukazKriteria = - UpravDukazKriteria( - Titled(o.personalNumber, Some(o.name)), - Titled(p.id, Some(p.name)), - Titled(k.id, Some(k.id)) - ) - } - - case class NotFound(url: String) extends Page("404", "404", Some(Directory)) - - case class UnhandledError( - errorName: Option[String], - errorMessage: Option[String] - ) extends Page("500", "Unexpected error", Some(Directory)) - -object Routes: - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) - given [V: JsonEncoder]: JsonEncoder[Titled[V]] = - DeriveJsonEncoder.gen[Titled[V]] - given [V: JsonDecoder]: JsonDecoder[Titled[V]] = - DeriveJsonDecoder.gen[Titled[V]] - given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] - given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] - - val base = - js.`import`.meta.env.BASE_URL - .asInstanceOf[String] + "app" - - val homePage: Page = Page.Directory - - given router: Router[Page] = Router[Page]( - routes = List( - Route.static(homePage, root / endOfSegments, basePath = base), - Route.static( - Page.Dashboard, - root / "dashboard" / endOfSegments, - basePath = base - ), - Route[Page.Detail, String]( - encode = _.osobniCislo.value.toString, - decode = osc => Page.Detail(Titled(OsobniCislo(osc))), - root / "osoba" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.DetailParametru, (String, String)]( - encode = p => (p.osobniCislo.value.toString, p.parametr.value), - decode = - p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / endOfSegments, - basePath = base - ), - Route[Page.DetailKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.DetailKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / endOfSegments, - basePath = base - ), - Route[Page.UpravDukazKriteria, (String, String, String)]( - encode = p => - ( - p.osobniCislo.value.toString, - p.parametr.value, - p.kriterium.value.replaceAll("\\.", "--") - ), - decode = p => - Page.UpravDukazKriteria( - Titled(OsobniCislo(p._1)), - Titled(p._2), - Titled(p._3.replaceAll("--", ".")) - ), - root / "osoba" / segment[String] / "parametr" / segment[ - String - ] / "kriterium" / segment[String] / "edit" / endOfSegments, - basePath = base - ) - ), - serializePage = _.toJson, - deserializePage = _.fromJson[Page] - .fold(s => throw IllegalStateException(s), identity), - getPageTitle = _.title, - routeFallback = url => Page.NotFound(url), - deserializeFallback = _ => Page.Dashboard - )( - $popStateEvent = windowEvents.onPopState, - owner = unsafeWindowOwner - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala deleted file mode 100644 index b24b82b..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/actions.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app - -import cz.e_bs.cmi.mdr.pdb.OsobniCislo -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -sealed trait Action - -case object FetchDirectory extends Action -case class FetchUserDetails(osc: OsobniCislo) extends Action -case class FetchParameters(osc: OsobniCislo) extends Action -case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action -case class FetchParameterCriteria( - osc: OsobniCislo, - paramId: String, - critId: String, - page: (UserInfo, Parameter, ParameterCriteria) => Page -) extends Action -case class FetchAvailableFiles(osc: OsobniCislo) extends Action -case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala deleted file mode 100644 index c637d35..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/AppPage.scala +++ /dev/null @@ -1,82 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.{UserProfile, UserInfo, OsobniCislo} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo -import cz.e_bs.cmi.mdr.pdb.UserFunction - -object AppPage: - // TODO: pages by logged in user - val pages: List[Page] = List(Page.Directory, Page.Dashboard) - - import NavigationBar.{Logo, MenuItem} - - val logo = Logo( - "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", - "Workflow" - ) - - // TODO: menu items by user profile - val userMenu = - List( - MenuItem("Your Profile"), - MenuItem("Settings"), - MenuItem("Sign out") - ) - - // TODO: load user profile - val $userProfile = Var( - UserProfile( - "tom", - UserInfo( - OsobniCislo("1031"), - "tom", - "Tom", - "Cook", - None, - None, - Some("tom@example.com"), - Some("+420 222 866 180"), - Some( - UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") - ), - Nil, - None - ) - ) - ) - - val $userInfo = $userProfile.signal.map(_.userInfo) - - type ViewModel = Option[HtmlElement] - def apply( - actionBus: Observer[Action] - )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using - router: Router[Page] - ): HtmlElement = - PageLayout(actionBus)( - $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => - PageLayout.ViewModel( - NavigationBar.ViewModel( - u, - pages.map(p => - NavigationBar.Link( - () => - PageLink( - p, - actionBus - ), - p == cp - ) - ), - userMenu, - logo - ), - c - ) - ), - mods - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala deleted file mode 100644 index bf473c6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/Breadcrumbs.scala +++ /dev/null @@ -1,143 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden -import fiftyforms.ui.components.tailwind.list.IconText.ViewModel -import fiftyforms.ui.components.tailwind.Icons -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Action - -object Breadcrumbs: - - private def slash = - import svg.{*, given} - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-300", - xmlns := "http://www.w3.org/2000/svg", - fill := "currentColor", - viewBox := "0 0 20 20", - ariaHidden := true, - path( - d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" - ) - ) - - def backIcon = - Icons.solid - .`arrow-narrow-left`() - .amend( - svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" - ) - - object Link: - case class ViewModel( - page: Page, - icon: Option[SvgElement], - extraClasses: String, - text: String, - textClasses: Option[String] = None - ) - - val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" - - def shortHome(p: Page) = ViewModel( - p, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - "Zpět na úvodní stránku", - None - ) - - def fullHome(p: Page) = ViewModel( - p, - Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), - "text-gray-400 hover:text-gray-500", - "Domů", - Some("sr-only") - ) - - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - PageLink - .container($m.map(_.page), actionBus) - .amend( - cls <-- $m.map(_.extraClasses), - child.maybe <-- $m.map(_.icon), - span( - cls <-- $m.map(_.textClasses.getOrElse("")), - child.text <-- $m.map(_.text) - ) - ) - - object FullBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - ol( - role := "list", - cls := "flex items-center space-x-4", - children <-- $m.map(_.path).split(_.id)((_, _, $p) => - li( - div( - cls := "flex items-center", - child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), - Link( - $p.map(p => - if (p.isRoot) then Link.fullHome(p) - else - Link.ViewModel( - p, - None, - s"ml-4 max-w-xs truncate ${Link.baseClasses}", - p.title - ) - ), - actionBus - ) - ) - ) - ) - ) - - object ShortBreadcrumbs: - type ViewModel = Page - def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - Link( - $m.map { p => - val target = p.parent.getOrElse(p) - if target.isRoot then Link.shortHome(target) - else - Link.ViewModel( - target, - Some(backIcon), - "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", - s"Zpět na ${target.title}" - ) - }, - actionBus - ) - - def apply(actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - nav( - cls := "flex", - aria.label := "Breadcrumb", - div( - cls := "flex sm:hidden", - ShortBreadcrumbs(router.$currentPage, actionBus) - ), - div( - cls := "hidden sm:block", - FullBreadcrumbs(router.$currentPage, actionBus) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala deleted file mode 100644 index 0db2047..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/NavigationBar.scala +++ /dev/null @@ -1,242 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent -import fiftyforms.ui.components.tailwind.Icons -import fiftyforms.ui.components.tailwind.Avatar -import cz.e_bs.cmi.mdr.pdb.UserInfo -import io.laminext.syntax.core.* - -object NavigationBar: - - case class Logo(img: String, name: String) - case class Link(a: () => Anchor, active: Boolean) - case class MenuItem(title: String) - - case class ViewModel( - userInfo: UserInfo, - pages: List[Link], - userMenu: List[MenuItem], - logo: Logo - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - val $userInfo = $m.map(_.userInfo) - val mobileMenuOpen = Var(false) - - val desktopOnly = cls("hidden md:block") - val mobileOnly = cls("md:hidden") - - inline def avatarImage(size: Int = 8) = - Avatar($userInfo.map(_.img)).avatarImage(size) - - def notificationButton = button( - tpe := "button", - cls := List( - "bg-indigo-600", - "focus:outline-none", - "focus:ring-2", - "focus:ring-offset-2", - "focus:ring-offset-indigo-600", - "focus:ring-white", - "hover:text-white", - "p-1", - "rounded-full", - "text-indigo-200" - ), - span(cls := "sr-only", "View notifications"), - Icons.outline.bell() - ) - - def userProfile: HtmlElement = - val menuOpen = Var(false) - - def menuItem(item: MenuItem, idx: Int): HtmlElement = - a( - href := "#", - cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", - role := "menuitem", - tabIndex := -1, - idAttr := s"user-menu-item-$idx", - item.title - ) - - div( - cls := "ml-3 relative", - div( - button( - tpe := "button", - cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - idAttr := "user-menu-button", - aria.expanded <-- menuOpen.signal, - aria.hasPopup := true, - span(cls := "sr-only", "Open user menu"), - child <-- avatarImage(), - onClick.preventDefault.mapTo( - !menuOpen.now() - ) --> menuOpen.writer - ) - ), - /* - * */ - div( - cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", - cls <-- menuOpen.signal.map { o => - if (o) "md:block" else "md:hidden" - }, - role := "menu", - aria.orientation := "vertical", - aria.labelledBy := "user-menu-button", - tabIndex := -1, - // : keyboard navigation - children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) - ) - ) - - def mobileProfile = - def menuItem(item: MenuItem): HtmlElement = - a( - href := "#", - cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", - item.title - ) - - div( - cls := "pt-4 pb-3 border-t border-indigo-700", - div( - cls := "flex items-center px-5", - div( - cls := "flex-shrink-0", - child <-- avatarImage(10) - ), - div( - cls := "ml-3", - div( - cls := "text-base font-medium text-white", - child.text <-- $userInfo.map(_.name) - ), - child.maybe <-- $userInfo.map( - _.email.map(e => - div( - cls := "text-sm font-medium text-indigo-300", - e - ) - ) - ) - ), - notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) - ), - div( - cls := "mt-3 px-2 space-y-1", - children <-- $m.map(_.userMenu.map(menuItem)) - ) - ) - - def pageLink(page: Link): Anchor = - page - .a() - .amend( - cls := "text-white px-3 py-2 rounded-md text-sm font-medium", - cls := Seq( - "bg-indigo-700" -> page.active, - "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active - ), - ariaCurrent := (if page.active then "page" else "false") - ) - - def logoImg: Image = - img( - cls := "h-8 w-8", - src <-- $m.map(_.logo.img), - alt <-- $m.map(_.logo.name) - ) - - def mobileMenuButton = button( - tpe := "button", - cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", - aria.controls := "mobile-menu", - aria.expanded <-- mobileMenuOpen.signal, - span(cls := "sr-only", "Open main menu"), - Icons.outline - .menu() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") - ), - Icons.outline - .x() - .amend( - svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") - ), - onClick.preventDefault.mapTo( - !mobileMenuOpen.now() - ) --> mobileMenuOpen.writer - ) - - def navBarLeft = - div( - cls := "flex items-center", - div(cls := "flex-shrink-0", logoImg), - div( - desktopOnly, - div( - cls := "ml-10 flex items-baseline space-x-4", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ) - ) - ) - - def navBarRight = - div( - desktopOnly, - div( - cls := "ml-4 flex items-center md:ml-6", - notificationButton, - userProfile - ) - ) - - def navBarMobile = - div( - cls := "-mr-2 flex", - mobileOnly, - mobileMenuButton - ) - - def navBar = - div( - cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex items-center justify-between h-16", - navBarLeft, - navBarRight, - navBarMobile - ) - ) - - def mobileMenu = - div( - mobileOnly, - cls <-- mobileMenuOpen.signal.map { o => - if (o) "block" else "hidden" - }, - idAttr := "mobile-menu", - div( - cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", - children <-- $m.map( - _.pages.map(p => pageLink(p).amend(cls := "block")) - ) - ), - mobileProfile - ) - - nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala deleted file mode 100644 index 75339df..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageHeader.scala +++ /dev/null @@ -1,19 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object PageHeader: - def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = - header( - cls := "bg-white shadow-sm", - div( - cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", - h1( - cls := "text-lg leading-6 font-semibold text-gray-900", - Breadcrumbs(actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala deleted file mode 100644 index 5096463..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLayout.scala +++ /dev/null @@ -1,30 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import fiftyforms.ui.components.tailwind.Loading -import io.laminext.syntax.core.* - -object PageLayout: - case class ViewModel( - navigation: NavigationBar.ViewModel, - content: Option[HtmlElement] - ) - def apply(actionBus: Observer[Action])( - $m: Signal[ViewModel], - mods: Modifier[HtmlElement]* - )(using router: Router[Page]): HtmlElement = - val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) - div( - cls := "h-full flex flex-col", - NavigationBar($m.map(_.navigation)), - child.maybe <-- router.$currentPage.map(_.isRoot) - .switch(None, Some(PageHeader(actionBus))), - main( - cls := "flex-grow-1 overflow-y-auto", - mods, - child <-- $maybeContent.map(_.getOrElse(Loading)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala deleted file mode 100644 index 62d820f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/components/PageLink.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.LinkSupport.* -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.NavigateTo - -object PageLink: - type ViewModel = Page - def apply($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = - a(mods($m, actions), child.text <-- $m.map(_.title)) - - def apply(m: ViewModel, actions: Observer[Action])(using - router: Router[Page] - ): Anchor = apply(Val(m), actions) - - def container($m: Signal[ViewModel], actions: Observer[Action])(using - router: Router[Page] - ): Anchor = a(mods($m, actions)) - - def container(m: ViewModel, actions: Observer[Action])(using - Router[Page] - ): Anchor = container(Val(m), actions) - - private def mods($m: Signal[Page], actions: Observer[Action])(using - router: Router[Page] - ): Modifier[Anchor] = - Seq( - href <-- $m.map(router.absoluteUrlForPage).recover { case _ => - Some("invalid url") - }, - composeEvents(onClick.noKeyMod.preventDefault)( - _.sample($m) - .map(NavigateTo.apply) - ) --> actions - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala deleted file mode 100644 index 87f825a..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DashboardPageConnector.scala +++ /dev/null @@ -1,13 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.pages.dashboard.DashboardPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -class DashboardPageConnector(actionBus: Observer[Action])(using - router: Router[Page] -): - def apply: HtmlElement = - AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala deleted file mode 100644 index e9b0f71..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala +++ /dev/null @@ -1,76 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailKriteriaPage -import pages.detail.components.DukazyKriteria -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria - -object DetailKriteriaPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailKriteriaPageConnector( - state: DetailKriteriaPageConnector.AppState -)( - $page: Signal[Page.DetailKriteria] -)(using Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => - (p.osobniCislo.value, p.parametr.value, p.kriterium.value) - )((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map( - FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) - ) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, s, $s) => - DetailKriteriaPage($s)(state.actionBus.contramap { - case DukazyKriteria.Add => - NavigateTo( - Page.UpravDukazKriteria( - Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), - Page.Titled(s.parametr.id, Some(s.parametr.nazev)), - Page.Titled(s.kriterium.id, Some(s.kriterium.id)) - ) - ) - }) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): DetailKriteriaPage.ViewModel = - DetailKriteriaPage.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala deleted file mode 100644 index 4d8024f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailPageConnector.scala +++ /dev/null @@ -1,57 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import pages.detail.DetailPage -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import fiftyforms.ui.components.tailwind.Color - -object DetailPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailPageConnector(state: DetailPageConnector.AppState)( - $page: Signal[Page.Detail] -)(using router: Router[Page]): - val $oscChangeSignal = $page.splitOne(_.osobniCislo.value)((osc, _, _) => osc) - val $pageChangeSignal = - $oscChangeSignal.flatMap(osc => - EventStream.fromSeq(Seq(FetchUserDetails(osc), FetchParameters(osc))) - ) - // TODO: filter the value based on the current osc - // OSC change will fetch new data, but still - // - we need to be sure that what we got is really what we ought to display - // - we want to display stale data accordingly (at least with loading indicator) - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - def apply: HtmlElement = - AppPage(state.actionBus)( - $data.combineWithFn($params)(_ zip _) - .map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: List[Parameter] - ): DetailPage.ViewModel = - DetailPage.ViewModel( - o.toDetailOsoby, - p.map( - _.toParametr(param => - PageLink.container(Page.DetailParametru(o, param), state.actionBus) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala deleted file mode 100644 index fdaa7f8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DetailParametruPageConnector.scala +++ /dev/null @@ -1,67 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.DetailParametruPage -import pages.detail.DetailParametruPage -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink - -object DetailParametruPageConnector { - trait AppState { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - } -} - -case class DetailParametruPageConnector( - state: DetailParametruPageConnector.AppState -)( - $page: Signal[Page.DetailParametru] -)(using router: Router[Page]): - val $paramChangeSignal = - $page.splitOne(p => (p.osobniCislo.value, p.parametr.value))((x, _, _) => x) - val $pageChangeSignal = - $paramChangeSignal.map(FetchParameter(_, _)) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - } yield (da, pb) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.map(_.map(buildModel)) - .split(_ => ())((_, _, s) => DetailParametruPage(s)), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter - ): DetailParametruPage.ViewModel = - DetailParametruPage.ViewModel( - o.toDetailOsoby, - p.toParametr(p => - PageLink.container(Page.DetailParametru(o, p), state.actionBus) - ), - p.criteria.map( - _.toKriterium { c => - PageLink.container( - Page.DetailKriteria(o, p, c), - state.actionBus - ) - } - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala deleted file mode 100644 index 049aa22..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/DirectoryPageConnector.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package connectors - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage - -case class DirectoryPageConnector( - $input: EventStream[List[UserInfo]], - actionBus: Observer[Action] -)(using router: Router[Page]): - val $data = $input.startWithNone - val $actionSignal = EventStream.fromValue(FetchDirectory) - - def apply: HtmlElement = - AppPage(actionBus)( - $data.split(_ => ())((_, _, s) => - pages.directory.DirectoryPage( - s.map( - _.map( - _.toUserRow(u => - PageLink.container( - Page.Detail(Page.Titled(u.personalNumber)), - actionBus - ) - ) - ) - ) - ) - ), - $actionSignal --> actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala deleted file mode 100644 index 88e7450..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/connectors/conversions.scala +++ /dev/null @@ -1,68 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.connectors - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailOsoby -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamParametru -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.SeznamKriterii -import cz.e_bs.cmi.mdr.pdb.app.pages.directory.components.UserRow -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailParametru -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.DetailKriteria -import fiftyforms.ui.components.tailwind.Color - -extension (o: UserInfo) - def toDetailOsoby: DetailOsoby.ViewModel = - DetailOsoby.ViewModel( - o.personalNumber, - o.name, - o.email, - o.phone, - o.img, - o.mainFunction.map(f => - DetailOsoby.Funkce.ViewModel(f.name, f.dept, f.ou) - ), - o.userContracts.headOption.map(c => - DetailOsoby.PracovniPomer.ViewModel(c.rel, c.startDate, c.endDate) - ) - ) - -extension (param: Parameter) - def toParametr(container: Parameter => Anchor): DetailParametru.ViewModel = - DetailParametru.ViewModel( - id = param.id, - nazev = param.name, - popis = param.description, - status = "Nesplněno", - statusColor = Color.red, - a = container(param) - ) - -extension (crit: ParameterCriteria) - def toKriterium( - container: ParameterCriteria => Anchor - ): DetailKriteria.ViewModel = - DetailKriteria.ViewModel( - nazev = crit.criteriumText, - kapitola = crit.chapterId, - bod = crit.itemId, - status = "Nesplněno", - statusColor = Color.red, - splneno = false, - dukazy = Nil, - container = container(crit) - ) - -extension (user: UserInfo) - def toUserRow( - container: UserInfo => HtmlElement = (_: UserInfo) => div() - ): UserRow.ViewModel = - UserRow.ViewModel( - osobniCislo = user.personalNumber.toString, - celeJmeno = user.name, - prijmeni = user.surname, - hlavniFunkce = user.mainFunction.map(_.name), - img = user.img, - container = () => container(user) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala deleted file mode 100644 index 7d5b2c1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/dashboard/DashboardPage.scala +++ /dev/null @@ -1,11 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.dashboard - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.Page - -object DashboardPage: - - def render: HtmlElement = - div("Dashboard page") diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala deleted file mode 100644 index 9ef0698..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala +++ /dev/null @@ -1,31 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailKriteriaPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply($m: Signal[ViewModel])( - action: Observer[DukazyKriteria.Action] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - DukazyKriteria($m.map(_.kriterium.dukazy))(action) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala deleted file mode 100644 index 3b316e1..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailPage.scala +++ /dev/null @@ -1,25 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ -import cz.e_bs.cmi.mdr.pdb.app.Page -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Action - -object DetailPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametry: SeznamParametru.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby($m.map(_.osoba)), - SeznamParametru($m.map(_.parametry)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala deleted file mode 100644 index 17224cc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/DetailParametruPage.scala +++ /dev/null @@ -1,24 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail - -import com.raquo.laminar.api.L.{*, given} - -import components._ - -object DetailParametruPage: - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriteria: SeznamKriterii.ViewModel - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - DetailOsoby.header($m.map(_.osoba)), - DetailParametru($m.map(_.parametr)), - SeznamKriterii($m.map(_.kriteria)) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala deleted file mode 100644 index dc912fa..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/UpravDukaz.scala +++ /dev/null @@ -1,107 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb -package app -package pages.detail - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.components.AppPage -import cz.e_bs.cmi.mdr.pdb.app.pages.detail.components.UpravDukazForm -import fiftyforms.services.files.File - -object UpravDukaz: - - type ThisPage = Page.UpravDukazKriteria - type PageKey = (OsobniCislo, String, String) - - trait State { - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def availableFiles: EventStream[List[File]] - def actionBus: Observer[Action] - } - - def keyOfPage(page: ThisPage): PageKey = - (page.osobniCislo.value, page.parametr.value, page.kriterium.value) - - def onChangeAction(key: PageKey): Action = - FetchParameterCriteria( - key._1, - key._2, - key._3, - Page.UpravDukazKriteria(_, _, _) - ) - - class Connector(state: State)($page: Signal[ThisPage])(using Router[Page]): - val $paramChangeSignal = $page.splitOne(keyOfPage)((x, _, _) => x) - val $pageChangeSignal = $paramChangeSignal.map(onChangeAction) - - val $data = state.details.startWithNone - val $params = state.parameters.startWithNone - - val $merged = - $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => - for { - da <- d - pa <- p - pb <- pa.find(_.id == pc._2) - ka <- pb.criteria.find(_.id == pc._3) - } yield (da, pb, ka) - ) - - def apply: HtmlElement = - AppPage(state.actionBus)( - $merged.split(_ => ())((_, s, $s) => - PageComponent( - $s.map(buildModel), - state.availableFiles, - state.actionBus.contramap { - case UpravDukazForm.Cancelled => - NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) - case UpravDukazForm.AvailableFilesRequested => - FetchAvailableFiles(s._1.personalNumber) - } - ) - ), - $pageChangeSignal --> state.actionBus - ) - - private def buildModel( - o: UserInfo, - p: Parameter, - k: ParameterCriteria - ): PageComponent.ViewModel = - import connectors.* - PageComponent.ViewModel( - o.toDetailOsoby, - p.toParametr(_ => a()), - k.toKriterium(_ => a()) - ) - - object PageComponent: - import components.* - - case class ViewModel( - osoba: DetailOsoby.ViewModel, - parametr: DetailParametru.ViewModel, - kriterium: DetailKriteria.ViewModel - ) - - def apply( - $m: Signal[ViewModel], - availableFilesStream: EventStream[List[File]], - events: Observer[UpravDukazForm.Event] - ): HtmlElement = - div( - cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", - div( - cls := "flex flex-col space-y-4", - div( - DetailOsoby.header($m.map(_.osoba)), - DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") - ), - div( - DetailKriteria($m.map(_.kriterium)), - UpravDukazForm(availableFilesStream)(events) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala deleted file mode 100644 index ca311b0..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailKriteria.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.Icons -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Color - -object DetailKriteria: - case class ViewModel( - nazev: String, - kapitola: String, - bod: String, - status: String, - statusColor: Color, - splneno: Boolean, - dukazy: List[DukazKriteria.ViewModel], - container: HtmlElement = div() - ) { - val id = s"${kapitola}${bod}" - } - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h3( - cls := "text-xl leading-6 font-bold text-gray-900", - child.text <-- $m.map(_.nazev) - ), - h3( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - "Kapitola ", - child.text <-- $m.map(_.kapitola), - " bod ", - child <-- $m.map(_.bod) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala deleted file mode 100644 index ae90290..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailOsoby.scala +++ /dev/null @@ -1,100 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.OsobniCislo - -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.Avatar - -object DetailOsoby: - - object Funkce: - case class ViewModel( - nazev: String, - stredisko: String, - voj: String - ) - - def render($m: Signal[ViewModel]): HtmlElement = - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.nazev), - span( - cls := "hidden md:inline", - " @ ", - child.text <-- $m.map(_.stredisko), - ", ", - child.text <-- $m.map(_.voj) - ) - ) - - object PracovniPomer: - case class ViewModel( - druh: String, - pocatek: LocalDate, - konec: Option[LocalDate] - ) - - def render($m: Signal[ViewModel]): HtmlElement = - import fiftyforms.ui.components.tailwind.CustomAttrs.datetime - p( - cls := "text-sm font-medium text-gray-500", - child.text <-- $m.map(_.druh), - " od ", - time( - datetime <-- $m.map(_.pocatek.toString), - child.text <-- $m.map(_.pocatek.toString) - ) - ) - - case class ViewModel( - osobniCislo: OsobniCislo, - jmeno: String, - email: Option[String], - telefon: Option[String], - img: Option[String], - hlavniFunkce: Option[Funkce.ViewModel], - pracovniPomer: Option[PracovniPomer.ViewModel] - ) - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-4", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(16) - ), - div( - h1( - cls := "text-2xl font-bold text-gray-900", - child.text <-- $m.map(_.jmeno) - ), - child.maybe <-- $m.map(_.hlavniFunkce).split(_ => ())((_, _, d) => - Funkce.render(d) - ), - child.maybe <-- $m.map(_.pracovniPomer).split(_ => ())((_, _, d) => - PracovniPomer.render(d) - ) - ) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - cls := "md:flex md:items-center md:justify-between md:space-x-5", - div( - cls := "flex items-start space-x-4", - div( - cls := "flex-shrink-0", - Avatar($m.map(_.img)).avatar(8) - ), - div( - h1( - cls := "text-lg font-medium text-gray-800", - child.text <-- $m.map(_.jmeno) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala deleted file mode 100644 index e11cb62..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DetailParametru.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color - -object DetailParametru: - case class ViewModel( - id: String, - nazev: String, - popis: String, - status: String, - statusColor: Color, - a: Anchor - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "pb-5 border-b border-gray-200", - h2( - cls := "text-2xl leading-6 font-medium text-gray-900", - child.text <-- $m.map(_.nazev) - ), - p( - cls := "mt-2 max-w-4xl text-sm text-gray-500", - child.text <-- $m.map(_.popis) - ) - ) - - def header($m: Signal[ViewModel]): HtmlElement = - div( - h2( - cls := "text-lg leading-6 font-medium text-gray-600", - child.text <-- $m.map(_.nazev) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala deleted file mode 100644 index a00ede8..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazKriteria.scala +++ /dev/null @@ -1,177 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import java.time.LocalDate -import fiftyforms.ui.components.tailwind.CustomAttrs - -object DukazKriteria: - case class Osoba(osobniCislo: String, jmeno: String) - case class Udalost(osoba: Osoba, datum: LocalDate) - case class Dokument( - url: String, - nazev: String, - pridal: Udalost, - odebral: Option[Udalost] - ) - case class ViewModel( - dokumenty: List[Dokument], - autorizoval: Option[Udalost], - platiDo: Option[LocalDate], - poznámka: Option[String] - ) - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-lg", - div( - cls := "px-4 py-5 sm:px-6", - h3( - cls := "text-lg leading-6 font-medium text-gray-900", - """Applicant Information""" - ), - p( - cls := "mt-1 max-w-2xl text-sm text-gray-500", - """Personal details and application.""" - ) - ), - div( - cls := "border-t border-gray-200 px-4 py-5 sm:px-6", - dl( - cls := "grid grid-cols-1 gap-x-4 gap-y-8 sm:grid-cols-2", - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Full name""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Margot Foster""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Application for""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Backend Developer""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Email address""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """margotfoster@example.com""" - ) - ), - div( - cls := "sm:col-span-1", - dt( - cls := "text-sm font-medium text-gray-500", - """Salary expectation""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """$120,000""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """About""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - """Fugiat ipsum ipsum deserunt culpa aute sint do nostrud anim incididunt cillum culpa consequat. Excepteur qui ipsum aliquip consequat sint. Sit id mollit nulla mollit nostrud in ea officia proident. Irure nostrud pariatur mollit ad adipisicing reprehenderit deserunt qui eu.""" - ) - ), - div( - cls := "sm:col-span-2", - dt( - cls := "text-sm font-medium text-gray-500", - """Attachments""" - ), - dd( - cls := "mt-1 text-sm text-gray-900", - ul( - role := "list", - cls := "border border-gray-200 rounded-md divide-y divide-gray-200", - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """resume_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ), - li( - cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", - div( - cls := "w-0 flex-1 flex items-center", { - import svg.* - import CustomAttrs.svg.ariaHidden - svg( - cls := "flex-shrink-0 h-5 w-5 text-gray-400", - xmlns := "http://www.w3.org/2000/svg", - viewBox := "0 0 20 20", - fill := "currentColor", - ariaHidden := true, - path( - fillRule := "evenodd", - d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", - clipRule := "evenodd" - ) - ) - }, - span( - cls := "ml-2 flex-1 w-0 truncate", - """coverletter_back_end_developer.pdf""" - ) - ), - div( - cls := "ml-4 flex-shrink-0", - a( - href := "#", - cls := "font-medium text-indigo-600 hover:text-indigo-500", - """Download""" - ) - ) - ) - ) - ) - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala deleted file mode 100644 index 22f134f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala +++ /dev/null @@ -1,35 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object DukazyKriteria: - sealed trait Action - case object Add extends Action - - type ViewModel = List[DukazKriteria.ViewModel] - - def apply($m: Signal[ViewModel])(actions: Observer[Action]): HtmlElement = - div( - child.maybe <-- $m.map(m => - if (m.isEmpty) then Some(prazdnyDukaz(actions)) - else None - ), - children <-- $m.map(_.zipWithIndex).split(_._2)((_, _, $s) => - DukazKriteria($s.map(_._1)) - ) - ) - - private def prazdnyDukaz(actions: Observer[Action]): HtmlElement = - button( - tpe := "button", - onClick.preventDefault.mapTo(Add) --> actions, - cls := "relative block w-full border-2 border-gray-300 border-dashed rounded-lg p-12 text-center hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - Icons.outline - .`document-add`(12) - .amend(svg.cls := "mx-auto text-gray-400"), - span( - cls := "mt-2 block text-sm font-medium text-gray-900", - "Přidat důkaz" - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala deleted file mode 100644 index f628697..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala +++ /dev/null @@ -1,34 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.list.StackedList -import fiftyforms.ui.components.tailwind.list.ListRow -import fiftyforms.ui.components.tailwind.list.RowTag -import fiftyforms.ui.components.tailwind.list.RowNext -import java.time.LocalDate - -object SeznamKriterii: - type ViewModel = List[DetailKriteria.ViewModel] - - private val kritList = new StackedList[DetailKriteria.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - kritList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = - p(cls := "text-sm text-gray-500", s"${i.kapitola}${i.bod}"), - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.container - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala deleted file mode 100644 index 2bfe4bc..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/SeznamParametru.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.list.{ - StackedList, - ListRow, - RowTag, - PropList, - IconText, - RowNext -} -import fiftyforms.ui.components.tailwind.Color -import fiftyforms.ui.components.tailwind.LinkSupport.* - -object SeznamParametru: - type ViewModel = List[DetailParametru.ViewModel] - - private val parametrList = new StackedList[DetailParametru.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "bg-white shadow overflow-hidden sm:rounded-md", - parametrList($m, _.id) { $i => - $i.map { i => - ListRow.ViewModel( - title = i.nazev, - topRight = RowTag.render( - $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) - ), - bottomLeft = emptyNode, - bottomRight = emptyNode, - farRight = RowNext.render, - containerElement = i.a - ) - } - } - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala deleted file mode 100644 index f048e9d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala +++ /dev/null @@ -1,101 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.detail.components - -import com.raquo.laminar.api.L.{*, given} -import io.laminext.syntax.core.* -import fiftyforms.ui.components.tailwind.CustomAttrs -import fiftyforms.ui.components.tailwind.form.* -import org.scalajs.dom -import com.raquo.laminar.nodes.ReactiveHtmlElement -import fiftyforms.services.files.components.tailwind.FilePicker -import cz.e_bs.cmi.mdr.pdb.frontend.AutorizujDukaz -import cz.e_bs.cmi.mdr.pdb.frontend.DocumentRef -import fiftyforms.services.files.File - -object UpravDukazForm: - sealed trait Event - case object Cancelled extends Event - case object AvailableFilesRequested extends Event - def apply(availableFilesStream: EventStream[List[File]])( - updates: Observer[Event] - ): HtmlElement = - val (filesStream, filesObserver) = - EventStream.withObserver[FilePicker.Event] - val files = Var[List[File]](Nil) - def submitButtons: HtmlElement = - div( - cls := "pt-5", - div( - cls := "flex justify-end", - button( - tpe := "button", - cls := "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - "Zrušit", - onClick.mapTo(Cancelled) --> updates - ), - button( - tpe := "submit", - cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", - disabled <-- files.signal.map(_.isEmpty), - "Autorizovat" - ) - ) - ) - - div( - filesStream.collect { case FilePicker.SelectionUpdated(files) => - files.to(List) - } --> files.writer, - filesStream.collect { case FilePicker.AvailableFilesRequested => - AvailableFilesRequested - } --> updates, - cls := "bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6", - Form( - Form.Body( - Form.Section( - FormHeader( - FormHeader.ViewModel( - "Doložení kritéria", - "Sestavte doklady poskytující důkaz kritéria a potvrďte odesláním formuláře. Případné limitace či jiné relevantní údaje vepište do pole Komentář." - ) - ), - FormFields( - FormRow( - "dokumenty", - "Dokumenty", - FilePicker(files.signal, availableFilesStream)(filesObserver) - .amend(idAttr := "dokumenty", cls("max-w-lg")) - ).toHtml, - FormRow( - "platnost", - "Platnost", - input( - idAttr := "platnost", - name := "platnost", - tpe := "date", - autoComplete := "date", - cls := "block max-w-lg w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" - ) - ).toHtml, - FormRow( - "komentar", - "Komentář", - div( - cls := "mt-1 sm:mt-0 sm:col-span-2", - textArea( - idAttr := "komentar", - name := "about", - rows := 3, - cls := "max-w-lg shadow-sm block w-full focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border border-gray-300 rounded-md" - ), - p( - cls := "mt-2 text-sm text-gray-500", - "Doplňte prosím potřebné informace související s doložením kritéria, včetně případných limitací." - ) - ) - ).toHtml - ) - ) - ), - submitButtons - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala deleted file mode 100644 index 41033f6..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/DirectoryPage.scala +++ /dev/null @@ -1,40 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -import components._ - -object DirectoryPage: - - type ViewModel = List[UserRow.ViewModel] - - def apply($m: Signal[ViewModel]): HtmlElement = - val (actionsStream, actionObserver) = - EventStream.withObserver[SearchForm.Action] - val $filter = actionsStream - .collect { - case SearchForm.ClearFilter => None - case SearchForm.SetFilter(t) => Some(t) - } - .startWith(None) - val byLetter = for { - d <- $m - f <- $filter - } yield for { - (letter, users) <- d - .filter { user => - f match - case None => true - case Some(t) => user.search.contains(t) - } - .groupBy(_.prijmeni.head) - .to(List) - .sortBy(_._1) - } yield (letter.toString, users.sortBy(_.prijmeni)) - - div( - cls := "h-full max-w-7xl mx-auto order-first flex flex-col flex-shrink-0", - SearchForm(actionObserver), - Directory(byLetter) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala deleted file mode 100644 index 73f915d..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/Directory.scala +++ /dev/null @@ -1,37 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.UserInfo - -object Directory: - - object Header: - type ViewModel = String - def apply($m: Signal[ViewModel]): HtmlElement = - div( - cls := "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500", - h3(child.text <-- $m) - ) - - object UserList: - type ViewModel = List[UserRow.ViewModel] - def apply($m: Signal[ViewModel]): HtmlElement = - ul( - role := "list", - cls := "relative z-0 divide-y divide-gray-200", - children <-- $m.split(_.osobniCislo)((_, _, s) => UserRow(s)) - ) - - type ViewModel = List[(String, List[UserRow.ViewModel])] - def apply($m: Signal[ViewModel]): HtmlElement = - nav( - cls := "flex-1 min-h-0 overflow-y-auto", - aria.label := "Directory", - children <-- $m.split(_._1)((_, _, s) => - div( - cls := "relative", - Header(s.map(_._1)), - UserList(s.map(_._2)) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala deleted file mode 100644 index e182910..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/SearchForm.scala +++ /dev/null @@ -1,54 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import fiftyforms.ui.components.tailwind.Icons - -object SearchForm: - sealed trait Action - case object SubmitFilter extends Action - sealed trait FilterAction extends Action - case object ClearFilter extends FilterAction - case class SetFilter(value: String) extends FilterAction - - def apply(actions: Observer[Action]): HtmlElement = - div( - cls := "px-6 pt-4 pb-4", - form( - cls := "flex space-x-4", - onSubmit.mapTo(SubmitFilter) --> actions, - div( - cls := "flex-1 min-w-0", - label( - forId := "search", - cls := "sr-only", - "Hledat" - ), - div( - cls := "relative rounded-md shadow-sm", - div( - cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", - Icons.solid.search().amend(svg.cls := "text-gray-400") - ), - input( - tpe := "search", - name := "search", - idAttr := "search", - cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", - placeholder := "Hledat", - composeEvents(onInput.mapToValue.setAsValue.map(SetFilter(_)))( - _.throttle(500) - ) --> actions - ) - ) - ), - button( - tpe := "submit", - cls := "inline-flex justify-center px-3.5 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500", - Icons.solid.filter().amend(svg.cls := "text-gray-400"), - span( - cls := "sr-only", - "Hledat" - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala deleted file mode 100644 index 82f7723..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/directory/components/UserRow.scala +++ /dev/null @@ -1,52 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.directory.components - -import com.raquo.laminar.api.L.{*, given} -import com.raquo.airstream.core.Signal -import fiftyforms.ui.components.tailwind.Avatar - -object UserRow: - case class ViewModel( - osobniCislo: String, - celeJmeno: String, - prijmeni: String, - hlavniFunkce: Option[String], - img: Option[String], - container: () => HtmlElement = () => div() - ) { - val search = prijmeni.toLowerCase - } - - def apply($m: Signal[ViewModel]): HtmlElement = - inline def avatarImage = - Avatar($m.map(_.img)).avatarImage(10) - - li( - div( - cls := "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500", - div( - cls := "flex-shrink-0", - child <-- avatarImage - ), - div( - cls := "flex-1 min-w-0", - child <-- $m.map { o => - o.container() - .amend( - cls := "focus:outline-none", - span( - cls := "absolute inset-0", - aria.hidden := true - ), - p( - cls := "text-sm font-medium text-gray-900", - o.celeJmeno - ), - p( - cls := "text-sm text-gray-500 truncate", - o.hlavniFunkce - ) - ) - } - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala deleted file mode 100644 index 467c5f5..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/ErrorPage.scala +++ /dev/null @@ -1,99 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import cz.e_bs.cmi.mdr.pdb.app.components.PageLink -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action -import com.raquo.waypoint.Router - -object ErrorPage: - case class ViewModel( - homePage: Page, - errorName: String, - title: String, - subTitle: String - ) - - def apply(m: ViewModel, actionBus: Observer[Action])(using - Router[Page] - ): HtmlElement = - val ViewModel(homePage, errorName, title, subTitle) = m - div( - cls := "min-h-full pt-16 pb-12 flex flex-col bg-white", - main( - cls := "flex-grow flex flex-col justify-center max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", - div( - cls := "flex-shrink-0 flex justify-center", - a( - href := "/", - cls := "inline-flex", - span( - cls := "sr-only", - """Workflow""" - ), - img( - cls := "h-12 w-auto", - src := "https://tailwindui.com/img/logos/workflow-mark.svg?color=indigo&shade=600", - alt := "" - ) - ) - ), - div( - cls := "py-16", - div( - cls := "text-center", - p( - cls := "text-sm font-semibold text-indigo-600 uppercase tracking-wide", - errorName - ), - h1( - cls := "mt-2 text-4xl font-extrabold text-gray-900 tracking-tight sm:text-5xl", - title - ), - p( - cls := "mt-2 text-base text-gray-500", - subTitle - ), - div( - cls := "mt-6", - PageLink - .container(homePage, actionBus) - .amend( - cls := "text-base font-medium text-indigo-600 hover:text-indigo-500", - """Go back home""" - ) - ) - ) - ) - ), - footer( - cls := "flex-shrink-0 max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", - nav( - cls := "flex justify-center space-x-4", - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Contact Support""" - ), - span( - cls := "inline-block border-l border-gray-300", - aria.hidden := true - ), - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Status""" - ), - span( - cls := "inline-block border-l border-gray-300", - aria.hidden := true - ), - a( - href := "#", - cls := "text-sm font-medium text-gray-500 hover:text-gray-600", - """Twitter""" - ) - ) - ) - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala deleted file mode 100644 index bd9baec..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/NotFoundPage.scala +++ /dev/null @@ -1,21 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action - -object NotFoundPage: - def apply(homePage: Page, url: String, actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - ErrorPage( - ErrorPage.ViewModel( - homePage, - "404 error", - "Page not found.", - s"Sorry, but page $url doesn't exist." - ), - actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala deleted file mode 100644 index 476fe3c..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala +++ /dev/null @@ -1,32 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.pages.errors - -import com.raquo.domtypes.generic.codecs.StringAsIsCodec -import com.raquo.laminar.api.L.{*, given} -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.app.Page -import cz.e_bs.cmi.mdr.pdb.app.Action - -object UnhandledErrorPage: - - case class ViewModel( - homePage: Page, - errorName: Option[String], - errorMessage: Option[String] - ) - - def apply(m: ViewModel, actionBus: Observer[Action])(using - router: Router[Page] - ): HtmlElement = - ErrorPage( - ErrorPage.ViewModel( - m.homePage, - "Unexpected error occurred", - m.errorName.getOrElse( - "Uh oh!" - ), // TODO: translations, better text than uh oh - m.errorMessage.getOrElse( - "This wasn't supposed to happen! Please try again." - ) - ), - actionBus - ) diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala deleted file mode 100644 index 25f059f..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/services/DataFetcher.scala +++ /dev/null @@ -1,6 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app.services - -import com.raquo.airstream.core.Observer - -trait DataFetcher[K, A]: - def fetch(id: K, sink: Observer[A]): Unit diff --git a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala deleted file mode 100644 index a469830..0000000 --- a/app/src/main/scala/cz/e_bs/cmi/mdr/pdb/app/state/AppState.scala +++ /dev/null @@ -1,120 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb.app -package state - -import com.raquo.airstream.core.EventStream -import cz.e_bs.cmi.mdr.pdb.{UserInfo, OsobniCislo} -import com.raquo.airstream.core.Observer -import scala.scalajs.js -import scala.scalajs.js.JSON -import zio.json._ -import com.raquo.airstream.eventbus.EventBus -import com.raquo.airstream.ownership.Owner -import com.raquo.waypoint.Router -import cz.e_bs.cmi.mdr.pdb.Parameter -import cz.e_bs.cmi.mdr.pdb.ParameterCriteria -import cz.e_bs.cmi.mdr.pdb.UserFunction -import cz.e_bs.cmi.mdr.pdb.UserContract -import fiftyforms.services.files.File - -trait AppState - extends connectors.DetailPageConnector.AppState - with connectors.DetailParametruPageConnector.AppState - with connectors.DetailKriteriaPageConnector.AppState - with pages.detail.UpravDukaz.State: - def users: EventStream[List[UserInfo]] - def details: EventStream[UserInfo] - def parameters: EventStream[List[Parameter]] - def actionBus: Observer[Action] - -class MockAppState(implicit owner: Owner, router: Router[Page]) - extends AppState: - - given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) - given JsonDecoder[UserFunction] = DeriveJsonDecoder.gen - given JsonDecoder[UserContract] = DeriveJsonDecoder.gen - given JsonDecoder[UserInfo] = DeriveJsonDecoder.gen - - given JsonDecoder[ParameterCriteria] = DeriveJsonDecoder.gen - given JsonDecoder[Parameter] = DeriveJsonDecoder.gen - - private val actions = EventBus[Action]() - private val (parametersStream, pushParameters) = - EventStream.withCallback[List[Parameter]] - private val (usersStream, pushUsers) = - EventStream.withCallback[List[UserInfo]] - private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] - private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] - - private val mockData: List[UserInfo] = - mockUsers - .asInstanceOf[js.Dictionary[js.Object]] - .values - // TODO: is there a more efficient way to parse from JS object directly? - .map { o => - val parsed = JSON.stringify(o).fromJson[UserInfo] - parsed.left.foreach(org.scalajs.dom.console.log(_)) - parsed - } - .collect { case Right(u) => - u - } - .toList - - private val mockParameters: List[Parameter] = - pdbParams - .asInstanceOf[js.Dictionary[js.Object]] - .values - .map(o => JSON.stringify(o).fromJson[Parameter]) - .collect { case Right(p) => p } - .toList - - // TODO: Extract to separate event handler - actions.events.foreach { - case FetchDirectory => pushUsers(mockData) - case FetchUserDetails(osc) => - mockData.find(_.personalNumber == osc).foreach { o => - pushDetails(o) - router.replaceState(Page.Detail(o)) - } - case FetchParameters(osc) => - pushParameters(mockParameters) - case FetchParameter(osc, paramId) => - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(Page.DetailParametru(o, p)) - case FetchParameterCriteria(osc, paramId, critId, page) => - for - o <- mockData.find(_.personalNumber == osc) - p <- mockParameters.find(_.id == paramId) - c <- p.criteria.find(_.id == critId) - do - pushDetails(o) - pushParameters(mockParameters) - router.replaceState(page(o, p, c)) - case NavigateTo(page) => router.pushState(page) - case FetchAvailableFiles(osc) => - pushFiles( - List( - File("https://tc163.cmi.cz/here", "Example file") - ) - ) - } - - override def users: EventStream[List[UserInfo]] = - usersStream.debugWithName("users") - - override def details: EventStream[UserInfo] = - detailsStream.debugWithName("details") - - override def parameters: EventStream[List[Parameter]] = - parametersStream.debugWithName("parameters") - - override def availableFiles: EventStream[List[File]] = - filesStream.debugWithName("available files") - - override def actionBus: Observer[Action] = - actions.writer.debugWithName("actions writer") diff --git a/app/src/main/scala/mdr/pdb/app/Main.scala b/app/src/main/scala/mdr/pdb/app/Main.scala new file mode 100644 index 0000000..081b4ee --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/Main.scala @@ -0,0 +1,103 @@ +package mdr.pdb.app + +import scala.scalajs.js.annotation.JSExportTopLevel +import scala.scalajs.js.annotation.JSExport +import scala.scalajs.js.annotation.JSImport +import scala.scalajs.js +import org.scalajs.dom +import com.raquo.laminar.api.L.{*, given} +import scala.scalajs.js.Date +import com.raquo.waypoint.Router +import com.raquo.waypoint.SplitRender +import mdr.pdb.app.services.DataFetcher +import scala.scalajs.js.JSON +import zio.json._ +import mdr.pdb.UserInfo +import mdr.pdb.app.state.AppState + +@js.native +@JSImport("stylesheets/main.css", JSImport.Namespace) +object Css extends js.Any + +@js.native +@JSImport("data/users.json", JSImport.Default) +object mockUsers extends js.Object + +@js.native +@JSImport("params/pdb-params.json", JSImport.Default) +object pdbParams extends js.Object + +@JSExportTopLevel("app") +object Main: + + @JSExport + def main(args: Array[String]): Unit = + import Routes.given + onLoad { + setupAirstream() + val appContainer = dom.document.querySelector("#app") + val _ = + render( + appContainer, + renderPage(state.MockAppState(using unsafeWindowOwner, router)) + ) + } + + private def onLoad(f: => Unit): Unit = + documentEvents.onDomContentLoaded.foreach(_ => f)(unsafeWindowOwner) + + private def setupAirstream()(using router: Router[Page]): Unit = + AirstreamError.registerUnhandledErrorCallback(err => + router.forcePage( + Page.UnhandledError( + Some(err.getClass.getName), // TODO: Fill only in dev mode + Some(err.getMessage) + ) + ) + ) + + def renderPage(state: AppState)(using + router: Router[Page] + ): HtmlElement = + val pageSplitter = SplitRender[Page, HtmlElement](router.$currentPage) + .collectSignal[Page.Detail]( + connectors + .DetailPageConnector(state)(_) + .apply + ) + .collectSignal[Page.DetailParametru]( + connectors + .DetailParametruPageConnector(state)(_) + .apply + ) + .collectSignal[Page.DetailKriteria]( + connectors + .DetailKriteriaPageConnector(state)(_) + .apply + ) + .collectSignal[Page.UpravDukazKriteria]( + pages.detail.UpravDukaz.Connector(state)(_).apply + ) + .collectStatic(Page.Dashboard)( + connectors.DashboardPageConnector(state.actionBus).apply + ) + .collect[Page.NotFound](pg => + pages.errors.NotFoundPage(Routes.homePage, pg.url, state.actionBus) + ) + .collect[Page.UnhandledError](pg => + pages.errors + .UnhandledErrorPage( + pages.errors.UnhandledErrorPage + .ViewModel(Routes.homePage, pg.errorName, pg.errorMessage), + state.actionBus + ) + ) + .collectStatic(Page.Directory)( + connectors + .DirectoryPageConnector(state.users, state.actionBus) + .apply + ) + div(cls := "h-full", child <-- pageSplitter.$view) + + // Pull in the stylesheet + val css: Css.type = Css diff --git a/app/src/main/scala/mdr/pdb/app/Routes.scala b/app/src/main/scala/mdr/pdb/app/Routes.scala new file mode 100644 index 0000000..2cc860c --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/Routes.scala @@ -0,0 +1,198 @@ +package mdr.pdb.app + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.* +import org.scalajs.dom +import zio.json.{*, given} +import mdr.pdb.OsobniCislo + +import scala.scalajs.js +import mdr.pdb.UserInfo +import mdr.pdb.Parameter +import mdr.pdb.ParameterCriteria +import mdr.pdb.app.Page.Titled + +// enum is not working with Waypoints' SplitRender collectStatic +sealed abstract class Page( + val id: String, + val title: String, + val parent: Option[Page] +) { + val path: Vector[Page] = + parent match + case None => Vector(this) + case Some(p) => p.path :+ this + + val isRoot: Boolean = parent.isEmpty +} + +object Page: + + case class Titled[V](value: V, title: Option[String] = None): + val show: String = title.getOrElse(value.toString) + + case object Directory extends Page("directory", "Adresář", None) + + case object Dashboard extends Page("dashboard", "Přehled", Some(Directory)) + + case class Detail(osobniCislo: Titled[OsobniCislo]) + extends Page("user", osobniCislo.show, Some(Directory)) + + object Detail { + def apply(o: UserInfo): Detail = Detail( + Titled(o.personalNumber, Some(o.name)) + ) + } + + case class DetailParametru( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String] + ) extends Page( + "parameter", + parametr.show, + Some(Detail(osobniCislo)) + ) + + object DetailParametru { + def apply(o: UserInfo, p: Parameter): DetailParametru = + DetailParametru( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)) + ) + } + + case class DetailKriteria( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String], + kriterium: Titled[String] + ) extends Page( + "criteria", + kriterium.show, + Some(DetailParametru(osobniCislo, parametr)) + ) + + object DetailKriteria { + def apply(o: UserInfo, p: Parameter, k: ParameterCriteria): DetailKriteria = + DetailKriteria( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)), + Titled(k.id, Some(k.id)) + ) + } + + case class UpravDukazKriteria( + osobniCislo: Titled[OsobniCislo], + parametr: Titled[String], + kriterium: Titled[String] + ) extends Page( + "addProof", + "Důkaz", + Some(DetailKriteria(osobniCislo, parametr, kriterium)) + ) + + object UpravDukazKriteria { + def apply( + o: UserInfo, + p: Parameter, + k: ParameterCriteria + ): UpravDukazKriteria = + UpravDukazKriteria( + Titled(o.personalNumber, Some(o.name)), + Titled(p.id, Some(p.name)), + Titled(k.id, Some(k.id)) + ) + } + + case class NotFound(url: String) extends Page("404", "404", Some(Directory)) + + case class UnhandledError( + errorName: Option[String], + errorMessage: Option[String] + ) extends Page("500", "Unexpected error", Some(Directory)) + +object Routes: + given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) + given JsonEncoder[OsobniCislo] = JsonEncoder.string.contramap(_.toString) + given [V: JsonEncoder]: JsonEncoder[Titled[V]] = + DeriveJsonEncoder.gen[Titled[V]] + given [V: JsonDecoder]: JsonDecoder[Titled[V]] = + DeriveJsonDecoder.gen[Titled[V]] + given JsonEncoder[Page] = DeriveJsonEncoder.gen[Page] + given JsonDecoder[Page] = DeriveJsonDecoder.gen[Page] + + val base = + js.`import`.meta.env.BASE_URL + .asInstanceOf[String] + "app" + + val homePage: Page = Page.Directory + + given router: Router[Page] = Router[Page]( + routes = List( + Route.static(homePage, root / endOfSegments, basePath = base), + Route.static( + Page.Dashboard, + root / "dashboard" / endOfSegments, + basePath = base + ), + Route[Page.Detail, String]( + encode = _.osobniCislo.value.toString, + decode = osc => Page.Detail(Titled(OsobniCislo(osc))), + root / "osoba" / segment[String] / endOfSegments, + basePath = base + ), + Route[Page.DetailParametru, (String, String)]( + encode = p => (p.osobniCislo.value.toString, p.parametr.value), + decode = + p => Page.DetailParametru(Titled(OsobniCislo(p._1)), Titled(p._2)), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / endOfSegments, + basePath = base + ), + Route[Page.DetailKriteria, (String, String, String)]( + encode = p => + ( + p.osobniCislo.value.toString, + p.parametr.value, + p.kriterium.value.replaceAll("\\.", "--") + ), + decode = p => + Page.DetailKriteria( + Titled(OsobniCislo(p._1)), + Titled(p._2), + Titled(p._3.replaceAll("--", ".")) + ), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / "kriterium" / segment[String] / endOfSegments, + basePath = base + ), + Route[Page.UpravDukazKriteria, (String, String, String)]( + encode = p => + ( + p.osobniCislo.value.toString, + p.parametr.value, + p.kriterium.value.replaceAll("\\.", "--") + ), + decode = p => + Page.UpravDukazKriteria( + Titled(OsobniCislo(p._1)), + Titled(p._2), + Titled(p._3.replaceAll("--", ".")) + ), + root / "osoba" / segment[String] / "parametr" / segment[ + String + ] / "kriterium" / segment[String] / "edit" / endOfSegments, + basePath = base + ) + ), + serializePage = _.toJson, + deserializePage = _.fromJson[Page] + .fold(s => throw IllegalStateException(s), identity), + getPageTitle = _.title, + routeFallback = url => Page.NotFound(url), + deserializeFallback = _ => Page.Dashboard + )( + $popStateEvent = windowEvents.onPopState, + owner = unsafeWindowOwner + ) diff --git a/app/src/main/scala/mdr/pdb/app/actions.scala b/app/src/main/scala/mdr/pdb/app/actions.scala new file mode 100644 index 0000000..d078b3a --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/actions.scala @@ -0,0 +1,21 @@ +package mdr.pdb.app + +import mdr.pdb.OsobniCislo +import mdr.pdb.UserInfo +import mdr.pdb.Parameter +import mdr.pdb.ParameterCriteria + +sealed trait Action + +case object FetchDirectory extends Action +case class FetchUserDetails(osc: OsobniCislo) extends Action +case class FetchParameters(osc: OsobniCislo) extends Action +case class FetchParameter(osc: OsobniCislo, paramId: String) extends Action +case class FetchParameterCriteria( + osc: OsobniCislo, + paramId: String, + critId: String, + page: (UserInfo, Parameter, ParameterCriteria) => Page +) extends Action +case class FetchAvailableFiles(osc: OsobniCislo) extends Action +case class NavigateTo(page: Page) extends Action diff --git a/app/src/main/scala/mdr/pdb/app/components/AppPage.scala b/app/src/main/scala/mdr/pdb/app/components/AppPage.scala new file mode 100644 index 0000000..130c817 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/AppPage.scala @@ -0,0 +1,82 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.Page +import mdr.pdb.{UserProfile, UserInfo, OsobniCislo} +import com.raquo.waypoint.Router +import mdr.pdb.app.Action +import mdr.pdb.app.NavigateTo +import mdr.pdb.UserFunction + +object AppPage: + // TODO: pages by logged in user + val pages: List[Page] = List(Page.Directory, Page.Dashboard) + + import NavigationBar.{Logo, MenuItem} + + val logo = Logo( + "https://tailwindui.com/img/logos/workflow-mark-indigo-300.svg", + "Workflow" + ) + + // TODO: menu items by user profile + val userMenu = + List( + MenuItem("Your Profile"), + MenuItem("Settings"), + MenuItem("Sign out") + ) + + // TODO: load user profile + val $userProfile = Var( + UserProfile( + "tom", + UserInfo( + OsobniCislo("1031"), + "tom", + "Tom", + "Cook", + None, + None, + Some("tom@example.com"), + Some("+420 222 866 180"), + Some( + UserFunction("administrátor zakázek", "ředitelství", "ČMI Medical") + ), + Nil, + None + ) + ) + ) + + val $userInfo = $userProfile.signal.map(_.userInfo) + + type ViewModel = Option[HtmlElement] + def apply( + actionBus: Observer[Action] + )($m: Signal[ViewModel], mods: Modifier[HtmlElement]*)(using + router: Router[Page] + ): HtmlElement = + PageLayout(actionBus)( + $m.combineWith($userInfo, router.$currentPage).map((c, u, cp) => + PageLayout.ViewModel( + NavigationBar.ViewModel( + u, + pages.map(p => + NavigationBar.Link( + () => + PageLink( + p, + actionBus + ), + p == cp + ) + ), + userMenu, + logo + ), + c + ) + ), + mods + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala b/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala new file mode 100644 index 0000000..9375398 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/Breadcrumbs.scala @@ -0,0 +1,143 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import fiftyforms.ui.components.tailwind.CustomAttrs.svg.ariaHidden +import fiftyforms.ui.components.tailwind.list.IconText.ViewModel +import fiftyforms.ui.components.tailwind.Icons +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.Page + +import com.raquo.laminar.api.L.{*, given} +import io.laminext.syntax.core.{*, given} +import mdr.pdb.app.Action + +object Breadcrumbs: + + private def slash = + import svg.{*, given} + svg( + cls := "flex-shrink-0 h-5 w-5 text-gray-300", + xmlns := "http://www.w3.org/2000/svg", + fill := "currentColor", + viewBox := "0 0 20 20", + ariaHidden := true, + path( + d := "M5.555 17.776l8-16 .894.448-8 16-.894-.448z" + ) + ) + + def backIcon = + Icons.solid + .`arrow-narrow-left`() + .amend( + svg.cls := "flex-shrink-0 text-gray-400 group-hover:text-gray-600" + ) + + object Link: + case class ViewModel( + page: Page, + icon: Option[SvgElement], + extraClasses: String, + text: String, + textClasses: Option[String] = None + ) + + val baseClasses = "text-sm font-medium text-gray-500 hover:text-gray-700" + + def shortHome(p: Page) = ViewModel( + p, + Some(backIcon), + "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", + "Zpět na úvodní stránku", + None + ) + + def fullHome(p: Page) = ViewModel( + p, + Some(Icons.solid.home().amend(svg.cls := "flex-shrink-0")), + "text-gray-400 hover:text-gray-500", + "Domů", + Some("sr-only") + ) + + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + PageLink + .container($m.map(_.page), actionBus) + .amend( + cls <-- $m.map(_.extraClasses), + child.maybe <-- $m.map(_.icon), + span( + cls <-- $m.map(_.textClasses.getOrElse("")), + child.text <-- $m.map(_.text) + ) + ) + + object FullBreadcrumbs: + type ViewModel = Page + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + ol( + role := "list", + cls := "flex items-center space-x-4", + children <-- $m.map(_.path).split(_.id)((_, _, $p) => + li( + div( + cls := "flex items-center", + child.maybe <-- $p.map(_.isRoot).switch(None, Some(slash)), + Link( + $p.map(p => + if (p.isRoot) then Link.fullHome(p) + else + Link.ViewModel( + p, + None, + s"ml-4 max-w-xs truncate ${Link.baseClasses}", + p.title + ) + ), + actionBus + ) + ) + ) + ) + ) + + object ShortBreadcrumbs: + type ViewModel = Page + def apply($m: Signal[ViewModel], actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + Link( + $m.map { p => + val target = p.parent.getOrElse(p) + if target.isRoot then Link.shortHome(target) + else + Link.ViewModel( + target, + Some(backIcon), + "group inline-flex space-x-3 text-sm text-gray-400 hover:text-gray-600", + s"Zpět na ${target.title}" + ) + }, + actionBus + ) + + def apply(actionBus: Observer[Action])(using + router: Router[Page] + ): HtmlElement = + nav( + cls := "flex", + aria.label := "Breadcrumb", + div( + cls := "flex sm:hidden", + ShortBreadcrumbs(router.$currentPage, actionBus) + ), + div( + cls := "hidden sm:block", + FullBreadcrumbs(router.$currentPage, actionBus) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala b/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala new file mode 100644 index 0000000..4bb8b8d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/NavigationBar.scala @@ -0,0 +1,242 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.CustomAttrs.ariaCurrent +import fiftyforms.ui.components.tailwind.Icons +import fiftyforms.ui.components.tailwind.Avatar +import mdr.pdb.UserInfo +import io.laminext.syntax.core.* + +object NavigationBar: + + case class Logo(img: String, name: String) + case class Link(a: () => Anchor, active: Boolean) + case class MenuItem(title: String) + + case class ViewModel( + userInfo: UserInfo, + pages: List[Link], + userMenu: List[MenuItem], + logo: Logo + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + val $userInfo = $m.map(_.userInfo) + val mobileMenuOpen = Var(false) + + val desktopOnly = cls("hidden md:block") + val mobileOnly = cls("md:hidden") + + inline def avatarImage(size: Int = 8) = + Avatar($userInfo.map(_.img)).avatarImage(size) + + def notificationButton = button( + tpe := "button", + cls := List( + "bg-indigo-600", + "focus:outline-none", + "focus:ring-2", + "focus:ring-offset-2", + "focus:ring-offset-indigo-600", + "focus:ring-white", + "hover:text-white", + "p-1", + "rounded-full", + "text-indigo-200" + ), + span(cls := "sr-only", "View notifications"), + Icons.outline.bell() + ) + + def userProfile: HtmlElement = + val menuOpen = Var(false) + + def menuItem(item: MenuItem, idx: Int): HtmlElement = + a( + href := "#", + cls := "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100", + role := "menuitem", + tabIndex := -1, + idAttr := s"user-menu-item-$idx", + item.title + ) + + div( + cls := "ml-3 relative", + div( + button( + tpe := "button", + cls := "max-w-xs bg-indigo-600 rounded-full flex items-center text-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", + idAttr := "user-menu-button", + aria.expanded <-- menuOpen.signal, + aria.hasPopup := true, + span(cls := "sr-only", "Open user menu"), + child <-- avatarImage(), + onClick.preventDefault.mapTo( + !menuOpen.now() + ) --> menuOpen.writer + ) + ), + /* + * */ + div( + cls := "origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none", + cls <-- menuOpen.signal.map { o => + if (o) "md:block" else "md:hidden" + }, + role := "menu", + aria.orientation := "vertical", + aria.labelledBy := "user-menu-button", + tabIndex := -1, + // : keyboard navigation + children <-- $m.map(_.userMenu.zipWithIndex.map(menuItem)) + ) + ) + + def mobileProfile = + def menuItem(item: MenuItem): HtmlElement = + a( + href := "#", + cls := "block px-3 py-2 rounded-md text-base font-medium text-white hover:bg-indigo-500 hover:bg-opacity-75", + item.title + ) + + div( + cls := "pt-4 pb-3 border-t border-indigo-700", + div( + cls := "flex items-center px-5", + div( + cls := "flex-shrink-0", + child <-- avatarImage(10) + ), + div( + cls := "ml-3", + div( + cls := "text-base font-medium text-white", + child.text <-- $userInfo.map(_.name) + ), + child.maybe <-- $userInfo.map( + _.email.map(e => + div( + cls := "text-sm font-medium text-indigo-300", + e + ) + ) + ) + ), + notificationButton.amend(cls := List("flex-shrink-0", "ml-auto")) + ), + div( + cls := "mt-3 px-2 space-y-1", + children <-- $m.map(_.userMenu.map(menuItem)) + ) + ) + + def pageLink(page: Link): Anchor = + page + .a() + .amend( + cls := "text-white px-3 py-2 rounded-md text-sm font-medium", + cls := Seq( + "bg-indigo-700" -> page.active, + "hover:bg-indigo-500 hover:bg-opacity-75" -> !page.active + ), + ariaCurrent := (if page.active then "page" else "false") + ) + + def logoImg: Image = + img( + cls := "h-8 w-8", + src <-- $m.map(_.logo.img), + alt <-- $m.map(_.logo.name) + ) + + def mobileMenuButton = button( + tpe := "button", + cls := "bg-indigo-600 inline-flex items-center justify-center p-2 rounded-md text-indigo-200 hover:text-white hover:bg-indigo-500 hover:bg-opacity-75 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-indigo-600 focus:ring-white", + aria.controls := "mobile-menu", + aria.expanded <-- mobileMenuOpen.signal, + span(cls := "sr-only", "Open main menu"), + Icons.outline + .menu() + .amend( + svg.cls <-- mobileMenuOpen.signal.switch("hidden", "block") + ), + Icons.outline + .x() + .amend( + svg.cls <-- mobileMenuOpen.signal.switch("block", "hidden") + ), + onClick.preventDefault.mapTo( + !mobileMenuOpen.now() + ) --> mobileMenuOpen.writer + ) + + def navBarLeft = + div( + cls := "flex items-center", + div(cls := "flex-shrink-0", logoImg), + div( + desktopOnly, + div( + cls := "ml-10 flex items-baseline space-x-4", + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) + ) + ) + ) + + def navBarRight = + div( + desktopOnly, + div( + cls := "ml-4 flex items-center md:ml-6", + notificationButton, + userProfile + ) + ) + + def navBarMobile = + div( + cls := "-mr-2 flex", + mobileOnly, + mobileMenuButton + ) + + def navBar = + div( + cls := "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8", + div( + cls := "flex items-center justify-between h-16", + navBarLeft, + navBarRight, + navBarMobile + ) + ) + + def mobileMenu = + div( + mobileOnly, + cls <-- mobileMenuOpen.signal.map { o => + if (o) "block" else "hidden" + }, + idAttr := "mobile-menu", + div( + cls := "px-2 pt-2 pb-3 space-y-1 sm:px-3", + children <-- $m.map( + _.pages.map(p => pageLink(p).amend(cls := "block")) + ) + ), + mobileProfile + ) + + nav(cls := "bg-indigo-600", navBar, mobileMenu) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala b/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala new file mode 100644 index 0000000..c4cb10d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageHeader.scala @@ -0,0 +1,19 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.Page +import com.raquo.waypoint.Router +import mdr.pdb.app.Action + +object PageHeader: + def apply(actionBus: Observer[Action])(using Router[Page]): HtmlElement = + header( + cls := "bg-white shadow-sm", + div( + cls := "max-w-7xl mx-auto py-4 px-4 sm:px-6 lg:px-8", + h1( + cls := "text-lg leading-6 font-semibold text-gray-900", + Breadcrumbs(actionBus) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala b/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala new file mode 100644 index 0000000..aae4539 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageLayout.scala @@ -0,0 +1,30 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.Page +import mdr.pdb.app.Action +import fiftyforms.ui.components.tailwind.Loading +import io.laminext.syntax.core.* + +object PageLayout: + case class ViewModel( + navigation: NavigationBar.ViewModel, + content: Option[HtmlElement] + ) + def apply(actionBus: Observer[Action])( + $m: Signal[ViewModel], + mods: Modifier[HtmlElement]* + )(using router: Router[Page]): HtmlElement = + val $maybeContent = $m.map(_.content).split(_ => ())((_, c, _) => c) + div( + cls := "h-full flex flex-col", + NavigationBar($m.map(_.navigation)), + child.maybe <-- router.$currentPage.map(_.isRoot) + .switch(None, Some(PageHeader(actionBus))), + main( + cls := "flex-grow-1 overflow-y-auto", + mods, + child <-- $maybeContent.map(_.getOrElse(Loading)) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/components/PageLink.scala b/app/src/main/scala/mdr/pdb/app/components/PageLink.scala new file mode 100644 index 0000000..e6e7052 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/components/PageLink.scala @@ -0,0 +1,40 @@ +package mdr.pdb.app.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.LinkSupport.* +import mdr.pdb.app.Page +import mdr.pdb.app.Action +import com.raquo.waypoint.Router +import mdr.pdb.app.NavigateTo + +object PageLink: + type ViewModel = Page + def apply($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = + a(mods($m, actions), child.text <-- $m.map(_.title)) + + def apply(m: ViewModel, actions: Observer[Action])(using + router: Router[Page] + ): Anchor = apply(Val(m), actions) + + def container($m: Signal[ViewModel], actions: Observer[Action])(using + router: Router[Page] + ): Anchor = a(mods($m, actions)) + + def container(m: ViewModel, actions: Observer[Action])(using + Router[Page] + ): Anchor = container(Val(m), actions) + + private def mods($m: Signal[Page], actions: Observer[Action])(using + router: Router[Page] + ): Modifier[Anchor] = + Seq( + href <-- $m.map(router.absoluteUrlForPage).recover { case _ => + Some("invalid url") + }, + composeEvents(onClick.noKeyMod.preventDefault)( + _.sample($m) + .map(NavigateTo.apply) + ) --> actions + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala new file mode 100644 index 0000000..7682772 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DashboardPageConnector.scala @@ -0,0 +1,13 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.pages.dashboard.DashboardPage +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage + +class DashboardPageConnector(actionBus: Observer[Action])(using + router: Router[Page] +): + def apply: HtmlElement = + AppPage(actionBus)(Val(Some(DashboardPage.render))) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala new file mode 100644 index 0000000..76b9133 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DetailKriteriaPageConnector.scala @@ -0,0 +1,76 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.Parameter +import mdr.pdb.UserInfo +import mdr.pdb.app.pages.detail.DetailParametruPage +import pages.detail.DetailKriteriaPage +import pages.detail.components.DukazyKriteria +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.ParameterCriteria + +object DetailKriteriaPageConnector { + trait AppState { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + } +} + +case class DetailKriteriaPageConnector( + state: DetailKriteriaPageConnector.AppState +)( + $page: Signal[Page.DetailKriteria] +)(using Router[Page]): + val $paramChangeSignal = + $page.splitOne(p => + (p.osobniCislo.value, p.parametr.value, p.kriterium.value) + )((x, _, _) => x) + val $pageChangeSignal = + $paramChangeSignal.map( + FetchParameterCriteria(_, _, _, Page.DetailKriteria(_, _, _)) + ) + + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + val $merged = + $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => + for { + da <- d + pa <- p + pb <- pa.find(_.id == pc._2) + ka <- pb.criteria.find(_.id == pc._3) + } yield (da, pb, ka) + ) + + def apply: HtmlElement = + AppPage(state.actionBus)( + $merged.map(_.map(buildModel)) + .split(_ => ())((_, s, $s) => + DetailKriteriaPage($s)(state.actionBus.contramap { + case DukazyKriteria.Add => + NavigateTo( + Page.UpravDukazKriteria( + Page.Titled(s.osoba.osobniCislo, Some(s.osoba.jmeno)), + Page.Titled(s.parametr.id, Some(s.parametr.nazev)), + Page.Titled(s.kriterium.id, Some(s.kriterium.id)) + ) + ) + }) + ), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: Parameter, + k: ParameterCriteria + ): DetailKriteriaPage.ViewModel = + DetailKriteriaPage.ViewModel( + o.toDetailOsoby, + p.toParametr(_ => a()), + k.toKriterium(_ => a()) + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DetailPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DetailPageConnector.scala new file mode 100644 index 0000000..df3a6b7 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DetailPageConnector.scala @@ -0,0 +1,57 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.UserInfo +import pages.detail.DetailPage +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.pages.detail.components.DetailOsoby +import mdr.pdb.Parameter +import mdr.pdb.app.pages.detail.components.SeznamParametru +import fiftyforms.ui.components.tailwind.Color + +object DetailPageConnector { + trait AppState { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + } +} + +case class DetailPageConnector(state: DetailPageConnector.AppState)( + $page: Signal[Page.Detail] +)(using router: Router[Page]): + val $oscChangeSignal = $page.splitOne(_.osobniCislo.value)((osc, _, _) => osc) + val $pageChangeSignal = + $oscChangeSignal.flatMap(osc => + EventStream.fromSeq(Seq(FetchUserDetails(osc), FetchParameters(osc))) + ) + // TODO: filter the value based on the current osc + // OSC change will fetch new data, but still + // - we need to be sure that what we got is really what we ought to display + // - we want to display stale data accordingly (at least with loading indicator) + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + def apply: HtmlElement = + AppPage(state.actionBus)( + $data.combineWithFn($params)(_ zip _) + .map(_.map(buildModel)) + .split(_ => ())((_, _, s) => DetailPage(s)), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: List[Parameter] + ): DetailPage.ViewModel = + DetailPage.ViewModel( + o.toDetailOsoby, + p.map( + _.toParametr(param => + PageLink.container(Page.DetailParametru(o, param), state.actionBus) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DetailParametruPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DetailParametruPageConnector.scala new file mode 100644 index 0000000..fa7193d --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DetailParametruPageConnector.scala @@ -0,0 +1,67 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.Parameter +import mdr.pdb.UserInfo +import mdr.pdb.app.pages.detail.DetailParametruPage +import pages.detail.DetailParametruPage +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.components.PageLink + +object DetailParametruPageConnector { + trait AppState { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + } +} + +case class DetailParametruPageConnector( + state: DetailParametruPageConnector.AppState +)( + $page: Signal[Page.DetailParametru] +)(using router: Router[Page]): + val $paramChangeSignal = + $page.splitOne(p => (p.osobniCislo.value, p.parametr.value))((x, _, _) => x) + val $pageChangeSignal = + $paramChangeSignal.map(FetchParameter(_, _)) + + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + val $merged = + $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => + for { + da <- d + pa <- p + pb <- pa.find(_.id == pc._2) + } yield (da, pb) + ) + + def apply: HtmlElement = + AppPage(state.actionBus)( + $merged.map(_.map(buildModel)) + .split(_ => ())((_, _, s) => DetailParametruPage(s)), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: Parameter + ): DetailParametruPage.ViewModel = + DetailParametruPage.ViewModel( + o.toDetailOsoby, + p.toParametr(p => + PageLink.container(Page.DetailParametru(o, p), state.actionBus) + ), + p.criteria.map( + _.toKriterium { c => + PageLink.container( + Page.DetailKriteria(o, p, c), + state.actionBus + ) + } + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/DirectoryPageConnector.scala b/app/src/main/scala/mdr/pdb/app/connectors/DirectoryPageConnector.scala new file mode 100644 index 0000000..ab2c045 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/DirectoryPageConnector.scala @@ -0,0 +1,34 @@ +package mdr.pdb.app +package connectors + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.UserInfo +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.components.AppPage + +case class DirectoryPageConnector( + $input: EventStream[List[UserInfo]], + actionBus: Observer[Action] +)(using router: Router[Page]): + val $data = $input.startWithNone + val $actionSignal = EventStream.fromValue(FetchDirectory) + + def apply: HtmlElement = + AppPage(actionBus)( + $data.split(_ => ())((_, _, s) => + pages.directory.DirectoryPage( + s.map( + _.map( + _.toUserRow(u => + PageLink.container( + Page.Detail(Page.Titled(u.personalNumber)), + actionBus + ) + ) + ) + ) + ) + ), + $actionSignal --> actionBus + ) diff --git a/app/src/main/scala/mdr/pdb/app/connectors/conversions.scala b/app/src/main/scala/mdr/pdb/app/connectors/conversions.scala new file mode 100644 index 0000000..f581796 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/connectors/conversions.scala @@ -0,0 +1,68 @@ +package mdr.pdb.app.connectors + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.UserInfo +import mdr.pdb.app.pages.detail.components.DetailOsoby +import mdr.pdb.Parameter +import mdr.pdb.app.pages.detail.components.SeznamParametru +import mdr.pdb.ParameterCriteria +import mdr.pdb.app.pages.detail.components.SeznamKriterii +import mdr.pdb.app.pages.directory.components.UserRow +import mdr.pdb.app.pages.detail.components.DetailParametru +import mdr.pdb.app.pages.detail.components.DetailKriteria +import fiftyforms.ui.components.tailwind.Color + +extension (o: UserInfo) + def toDetailOsoby: DetailOsoby.ViewModel = + DetailOsoby.ViewModel( + o.personalNumber, + o.name, + o.email, + o.phone, + o.img, + o.mainFunction.map(f => + DetailOsoby.Funkce.ViewModel(f.name, f.dept, f.ou) + ), + o.userContracts.headOption.map(c => + DetailOsoby.PracovniPomer.ViewModel(c.rel, c.startDate, c.endDate) + ) + ) + +extension (param: Parameter) + def toParametr(container: Parameter => Anchor): DetailParametru.ViewModel = + DetailParametru.ViewModel( + id = param.id, + nazev = param.name, + popis = param.description, + status = "Nesplněno", + statusColor = Color.red, + a = container(param) + ) + +extension (crit: ParameterCriteria) + def toKriterium( + container: ParameterCriteria => Anchor + ): DetailKriteria.ViewModel = + DetailKriteria.ViewModel( + nazev = crit.criteriumText, + kapitola = crit.chapterId, + bod = crit.itemId, + status = "Nesplněno", + statusColor = Color.red, + splneno = false, + dukazy = Nil, + container = container(crit) + ) + +extension (user: UserInfo) + def toUserRow( + container: UserInfo => HtmlElement = (_: UserInfo) => div() + ): UserRow.ViewModel = + UserRow.ViewModel( + osobniCislo = user.personalNumber.toString, + celeJmeno = user.name, + prijmeni = user.surname, + hlavniFunkce = user.mainFunction.map(_.name), + img = user.img, + container = () => container(user) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/dashboard/DashboardPage.scala b/app/src/main/scala/mdr/pdb/app/pages/dashboard/DashboardPage.scala new file mode 100644 index 0000000..0eb8f7a --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/dashboard/DashboardPage.scala @@ -0,0 +1,11 @@ +package mdr.pdb.app.pages.dashboard + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.Page + +object DashboardPage: + + def render: HtmlElement = + div("Dashboard page") diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala new file mode 100644 index 0000000..ff3a25f --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailKriteriaPage.scala @@ -0,0 +1,31 @@ +package mdr.pdb.app.pages.detail + +import com.raquo.laminar.api.L.{*, given} + +import components._ + +object DetailKriteriaPage: + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametr: DetailParametru.ViewModel, + kriterium: DetailKriteria.ViewModel + ) + + def apply($m: Signal[ViewModel])( + action: Observer[DukazyKriteria.Action] + ): HtmlElement = + div( + cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + div( + DetailOsoby.header($m.map(_.osoba)), + DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") + ), + div( + DetailKriteria($m.map(_.kriterium)), + DukazyKriteria($m.map(_.kriterium.dukazy))(action) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/DetailPage.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailPage.scala new file mode 100644 index 0000000..b591cc9 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailPage.scala @@ -0,0 +1,25 @@ +package mdr.pdb.app.pages.detail + +import com.raquo.laminar.api.L.{*, given} + +import components._ +import mdr.pdb.app.Page +import com.raquo.waypoint.Router +import mdr.pdb.app.Action + +object DetailPage: + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametry: SeznamParametru.ViewModel + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + DetailOsoby($m.map(_.osoba)), + SeznamParametru($m.map(_.parametry)) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/DetailParametruPage.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailParametruPage.scala new file mode 100644 index 0000000..3322279 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/DetailParametruPage.scala @@ -0,0 +1,24 @@ +package mdr.pdb.app.pages.detail + +import com.raquo.laminar.api.L.{*, given} + +import components._ + +object DetailParametruPage: + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametr: DetailParametru.ViewModel, + kriteria: SeznamKriterii.ViewModel + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "h-full overflow-y-auto max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + DetailOsoby.header($m.map(_.osoba)), + DetailParametru($m.map(_.parametr)), + SeznamKriterii($m.map(_.kriteria)) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala new file mode 100644 index 0000000..b58618a --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/UpravDukaz.scala @@ -0,0 +1,107 @@ +package mdr.pdb +package app +package pages.detail + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.components.AppPage +import mdr.pdb.app.pages.detail.components.UpravDukazForm +import fiftyforms.services.files.File + +object UpravDukaz: + + type ThisPage = Page.UpravDukazKriteria + type PageKey = (OsobniCislo, String, String) + + trait State { + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def availableFiles: EventStream[List[File]] + def actionBus: Observer[Action] + } + + def keyOfPage(page: ThisPage): PageKey = + (page.osobniCislo.value, page.parametr.value, page.kriterium.value) + + def onChangeAction(key: PageKey): Action = + FetchParameterCriteria( + key._1, + key._2, + key._3, + Page.UpravDukazKriteria(_, _, _) + ) + + class Connector(state: State)($page: Signal[ThisPage])(using Router[Page]): + val $paramChangeSignal = $page.splitOne(keyOfPage)((x, _, _) => x) + val $pageChangeSignal = $paramChangeSignal.map(onChangeAction) + + val $data = state.details.startWithNone + val $params = state.parameters.startWithNone + + val $merged = + $data.combineWithFn($params, $paramChangeSignal)((d, p, pc) => + for { + da <- d + pa <- p + pb <- pa.find(_.id == pc._2) + ka <- pb.criteria.find(_.id == pc._3) + } yield (da, pb, ka) + ) + + def apply: HtmlElement = + AppPage(state.actionBus)( + $merged.split(_ => ())((_, s, $s) => + PageComponent( + $s.map(buildModel), + state.availableFiles, + state.actionBus.contramap { + case UpravDukazForm.Cancelled => + NavigateTo(Page.DetailKriteria(s._1, s._2, s._3)) + case UpravDukazForm.AvailableFilesRequested => + FetchAvailableFiles(s._1.personalNumber) + } + ) + ), + $pageChangeSignal --> state.actionBus + ) + + private def buildModel( + o: UserInfo, + p: Parameter, + k: ParameterCriteria + ): PageComponent.ViewModel = + import connectors.* + PageComponent.ViewModel( + o.toDetailOsoby, + p.toParametr(_ => a()), + k.toKriterium(_ => a()) + ) + + object PageComponent: + import components.* + + case class ViewModel( + osoba: DetailOsoby.ViewModel, + parametr: DetailParametru.ViewModel, + kriterium: DetailKriteria.ViewModel + ) + + def apply( + $m: Signal[ViewModel], + availableFilesStream: EventStream[List[File]], + events: Observer[UpravDukazForm.Event] + ): HtmlElement = + div( + cls := "max-w-7xl mx-auto px-4 py-6 sm:px-6 lg:px-8", + div( + cls := "flex flex-col space-y-4", + div( + DetailOsoby.header($m.map(_.osoba)), + DetailParametru.header($m.map(_.parametr)).amend(cls := "mt-2") + ), + div( + DetailKriteria($m.map(_.kriterium)), + UpravDukazForm(availableFilesStream)(events) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailKriteria.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailKriteria.scala new file mode 100644 index 0000000..a3ab634 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailKriteria.scala @@ -0,0 +1,37 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.CustomAttrs +import fiftyforms.ui.components.tailwind.Icons +import java.time.LocalDate +import fiftyforms.ui.components.tailwind.Color + +object DetailKriteria: + case class ViewModel( + nazev: String, + kapitola: String, + bod: String, + status: String, + statusColor: Color, + splneno: Boolean, + dukazy: List[DukazKriteria.ViewModel], + container: HtmlElement = div() + ) { + val id = s"${kapitola}${bod}" + } + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "pb-5 border-b border-gray-200", + h3( + cls := "text-xl leading-6 font-bold text-gray-900", + child.text <-- $m.map(_.nazev) + ), + h3( + cls := "mt-2 max-w-4xl text-sm text-gray-500", + "Kapitola ", + child.text <-- $m.map(_.kapitola), + " bod ", + child <-- $m.map(_.bod) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailOsoby.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailOsoby.scala new file mode 100644 index 0000000..0fe2ba5 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailOsoby.scala @@ -0,0 +1,100 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.OsobniCislo + +import java.time.LocalDate +import fiftyforms.ui.components.tailwind.Avatar + +object DetailOsoby: + + object Funkce: + case class ViewModel( + nazev: String, + stredisko: String, + voj: String + ) + + def render($m: Signal[ViewModel]): HtmlElement = + p( + cls := "text-sm font-medium text-gray-500", + child.text <-- $m.map(_.nazev), + span( + cls := "hidden md:inline", + " @ ", + child.text <-- $m.map(_.stredisko), + ", ", + child.text <-- $m.map(_.voj) + ) + ) + + object PracovniPomer: + case class ViewModel( + druh: String, + pocatek: LocalDate, + konec: Option[LocalDate] + ) + + def render($m: Signal[ViewModel]): HtmlElement = + import fiftyforms.ui.components.tailwind.CustomAttrs.datetime + p( + cls := "text-sm font-medium text-gray-500", + child.text <-- $m.map(_.druh), + " od ", + time( + datetime <-- $m.map(_.pocatek.toString), + child.text <-- $m.map(_.pocatek.toString) + ) + ) + + case class ViewModel( + osobniCislo: OsobniCislo, + jmeno: String, + email: Option[String], + telefon: Option[String], + img: Option[String], + hlavniFunkce: Option[Funkce.ViewModel], + pracovniPomer: Option[PracovniPomer.ViewModel] + ) + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "md:flex md:items-center md:justify-between md:space-x-4", + div( + cls := "flex items-start space-x-4", + div( + cls := "flex-shrink-0", + Avatar($m.map(_.img)).avatar(16) + ), + div( + h1( + cls := "text-2xl font-bold text-gray-900", + child.text <-- $m.map(_.jmeno) + ), + child.maybe <-- $m.map(_.hlavniFunkce).split(_ => ())((_, _, d) => + Funkce.render(d) + ), + child.maybe <-- $m.map(_.pracovniPomer).split(_ => ())((_, _, d) => + PracovniPomer.render(d) + ) + ) + ) + ) + + def header($m: Signal[ViewModel]): HtmlElement = + div( + cls := "md:flex md:items-center md:justify-between md:space-x-5", + div( + cls := "flex items-start space-x-4", + div( + cls := "flex-shrink-0", + Avatar($m.map(_.img)).avatar(8) + ), + div( + h1( + cls := "text-lg font-medium text-gray-800", + child.text <-- $m.map(_.jmeno) + ) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailParametru.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailParametru.scala new file mode 100644 index 0000000..6310dde --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DetailParametru.scala @@ -0,0 +1,34 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.Color + +object DetailParametru: + case class ViewModel( + id: String, + nazev: String, + popis: String, + status: String, + statusColor: Color, + a: Anchor + ) + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "pb-5 border-b border-gray-200", + h2( + cls := "text-2xl leading-6 font-medium text-gray-900", + child.text <-- $m.map(_.nazev) + ), + p( + cls := "mt-2 max-w-4xl text-sm text-gray-500", + child.text <-- $m.map(_.popis) + ) + ) + + def header($m: Signal[ViewModel]): HtmlElement = + div( + h2( + cls := "text-lg leading-6 font-medium text-gray-600", + child.text <-- $m.map(_.nazev) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazKriteria.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazKriteria.scala new file mode 100644 index 0000000..c292919 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazKriteria.scala @@ -0,0 +1,177 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import java.time.LocalDate +import fiftyforms.ui.components.tailwind.CustomAttrs + +object DukazKriteria: + case class Osoba(osobniCislo: String, jmeno: String) + case class Udalost(osoba: Osoba, datum: LocalDate) + case class Dokument( + url: String, + nazev: String, + pridal: Udalost, + odebral: Option[Udalost] + ) + case class ViewModel( + dokumenty: List[Dokument], + autorizoval: Option[Udalost], + platiDo: Option[LocalDate], + poznámka: Option[String] + ) + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "bg-white shadow overflow-hidden sm:rounded-lg", + div( + cls := "px-4 py-5 sm:px-6", + h3( + cls := "text-lg leading-6 font-medium text-gray-900", + """Applicant Information""" + ), + p( + cls := "mt-1 max-w-2xl text-sm text-gray-500", + """Personal details and application.""" + ) + ), + div( + cls := "border-t border-gray-200 px-4 py-5 sm:px-6", + dl( + cls := "grid grid-cols-1 gap-x-4 gap-y-8 sm:grid-cols-2", + div( + cls := "sm:col-span-1", + dt( + cls := "text-sm font-medium text-gray-500", + """Full name""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """Margot Foster""" + ) + ), + div( + cls := "sm:col-span-1", + dt( + cls := "text-sm font-medium text-gray-500", + """Application for""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """Backend Developer""" + ) + ), + div( + cls := "sm:col-span-1", + dt( + cls := "text-sm font-medium text-gray-500", + """Email address""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """margotfoster@example.com""" + ) + ), + div( + cls := "sm:col-span-1", + dt( + cls := "text-sm font-medium text-gray-500", + """Salary expectation""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """$120,000""" + ) + ), + div( + cls := "sm:col-span-2", + dt( + cls := "text-sm font-medium text-gray-500", + """About""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + """Fugiat ipsum ipsum deserunt culpa aute sint do nostrud anim incididunt cillum culpa consequat. Excepteur qui ipsum aliquip consequat sint. Sit id mollit nulla mollit nostrud in ea officia proident. Irure nostrud pariatur mollit ad adipisicing reprehenderit deserunt qui eu.""" + ) + ), + div( + cls := "sm:col-span-2", + dt( + cls := "text-sm font-medium text-gray-500", + """Attachments""" + ), + dd( + cls := "mt-1 text-sm text-gray-900", + ul( + role := "list", + cls := "border border-gray-200 rounded-md divide-y divide-gray-200", + li( + cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", + div( + cls := "w-0 flex-1 flex items-center", { + import svg.* + import CustomAttrs.svg.ariaHidden + svg( + cls := "flex-shrink-0 h-5 w-5 text-gray-400", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + ariaHidden := true, + path( + fillRule := "evenodd", + d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", + clipRule := "evenodd" + ) + ) + }, + span( + cls := "ml-2 flex-1 w-0 truncate", + """resume_back_end_developer.pdf""" + ) + ), + div( + cls := "ml-4 flex-shrink-0", + a( + href := "#", + cls := "font-medium text-indigo-600 hover:text-indigo-500", + """Download""" + ) + ) + ), + li( + cls := "pl-3 pr-4 py-3 flex items-center justify-between text-sm", + div( + cls := "w-0 flex-1 flex items-center", { + import svg.* + import CustomAttrs.svg.ariaHidden + svg( + cls := "flex-shrink-0 h-5 w-5 text-gray-400", + xmlns := "http://www.w3.org/2000/svg", + viewBox := "0 0 20 20", + fill := "currentColor", + ariaHidden := true, + path( + fillRule := "evenodd", + d := "M8 4a3 3 0 00-3 3v4a5 5 0 0010 0V7a1 1 0 112 0v4a7 7 0 11-14 0V7a5 5 0 0110 0v4a3 3 0 11-6 0V7a1 1 0 012 0v4a1 1 0 102 0V7a3 3 0 00-3-3z", + clipRule := "evenodd" + ) + ) + }, + span( + cls := "ml-2 flex-1 w-0 truncate", + """coverletter_back_end_developer.pdf""" + ) + ), + div( + cls := "ml-4 flex-shrink-0", + a( + href := "#", + cls := "font-medium text-indigo-600 hover:text-indigo-500", + """Download""" + ) + ) + ) + ) + ) + ) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala new file mode 100644 index 0000000..d604860 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/DukazyKriteria.scala @@ -0,0 +1,35 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.Icons + +object DukazyKriteria: + sealed trait Action + case object Add extends Action + + type ViewModel = List[DukazKriteria.ViewModel] + + def apply($m: Signal[ViewModel])(actions: Observer[Action]): HtmlElement = + div( + child.maybe <-- $m.map(m => + if (m.isEmpty) then Some(prazdnyDukaz(actions)) + else None + ), + children <-- $m.map(_.zipWithIndex).split(_._2)((_, _, $s) => + DukazKriteria($s.map(_._1)) + ) + ) + + private def prazdnyDukaz(actions: Observer[Action]): HtmlElement = + button( + tpe := "button", + onClick.preventDefault.mapTo(Add) --> actions, + cls := "relative block w-full border-2 border-gray-300 border-dashed rounded-lg p-12 text-center hover:border-gray-400 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + Icons.outline + .`document-add`(12) + .amend(svg.cls := "mx-auto text-gray-400"), + span( + cls := "mt-2 block text-sm font-medium text-gray-900", + "Přidat důkaz" + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala new file mode 100644 index 0000000..7e77a7b --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamKriterii.scala @@ -0,0 +1,34 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.Color +import fiftyforms.ui.components.tailwind.list.StackedList +import fiftyforms.ui.components.tailwind.list.ListRow +import fiftyforms.ui.components.tailwind.list.RowTag +import fiftyforms.ui.components.tailwind.list.RowNext +import java.time.LocalDate + +object SeznamKriterii: + type ViewModel = List[DetailKriteria.ViewModel] + + private val kritList = new StackedList[DetailKriteria.ViewModel] + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "bg-white shadow overflow-hidden sm:rounded-md", + kritList($m, _.id) { $i => + $i.map { i => + ListRow.ViewModel( + title = i.nazev, + topRight = RowTag.render( + $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) + ), + bottomLeft = + p(cls := "text-sm text-gray-500", s"${i.kapitola}${i.bod}"), + bottomRight = emptyNode, + farRight = RowNext.render, + containerElement = i.container + ) + } + } + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamParametru.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamParametru.scala new file mode 100644 index 0000000..0760b32 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/SeznamParametru.scala @@ -0,0 +1,37 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.list.{ + StackedList, + ListRow, + RowTag, + PropList, + IconText, + RowNext +} +import fiftyforms.ui.components.tailwind.Color +import fiftyforms.ui.components.tailwind.LinkSupport.* + +object SeznamParametru: + type ViewModel = List[DetailParametru.ViewModel] + + private val parametrList = new StackedList[DetailParametru.ViewModel] + + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "bg-white shadow overflow-hidden sm:rounded-md", + parametrList($m, _.id) { $i => + $i.map { i => + ListRow.ViewModel( + title = i.nazev, + topRight = RowTag.render( + $i.map(x => RowTag.ViewModel(x.status, x.statusColor)) + ), + bottomLeft = emptyNode, + bottomRight = emptyNode, + farRight = RowNext.render, + containerElement = i.a + ) + } + } + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala b/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala new file mode 100644 index 0000000..28753c9 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/detail/components/UpravDukazForm.scala @@ -0,0 +1,101 @@ +package mdr.pdb.app.pages.detail.components + +import com.raquo.laminar.api.L.{*, given} +import io.laminext.syntax.core.* +import fiftyforms.ui.components.tailwind.CustomAttrs +import fiftyforms.ui.components.tailwind.form.* +import org.scalajs.dom +import com.raquo.laminar.nodes.ReactiveHtmlElement +import fiftyforms.services.files.components.tailwind.FilePicker +import mdr.pdb.frontend.AutorizujDukaz +import mdr.pdb.frontend.DocumentRef +import fiftyforms.services.files.File + +object UpravDukazForm: + sealed trait Event + case object Cancelled extends Event + case object AvailableFilesRequested extends Event + def apply(availableFilesStream: EventStream[List[File]])( + updates: Observer[Event] + ): HtmlElement = + val (filesStream, filesObserver) = + EventStream.withObserver[FilePicker.Event] + val files = Var[List[File]](Nil) + def submitButtons: HtmlElement = + div( + cls := "pt-5", + div( + cls := "flex justify-end", + button( + tpe := "button", + cls := "bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + "Zrušit", + onClick.mapTo(Cancelled) --> updates + ), + button( + tpe := "submit", + cls := "disabled:bg-indigo-300 ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500", + disabled <-- files.signal.map(_.isEmpty), + "Autorizovat" + ) + ) + ) + + div( + filesStream.collect { case FilePicker.SelectionUpdated(files) => + files.to(List) + } --> files.writer, + filesStream.collect { case FilePicker.AvailableFilesRequested => + AvailableFilesRequested + } --> updates, + cls := "bg-white shadow px-4 py-5 sm:rounded-lg sm:p-6", + Form( + Form.Body( + Form.Section( + FormHeader( + FormHeader.ViewModel( + "Doložení kritéria", + "Sestavte doklady poskytující důkaz kritéria a potvrďte odesláním formuláře. Případné limitace či jiné relevantní údaje vepište do pole Komentář." + ) + ), + FormFields( + FormRow( + "dokumenty", + "Dokumenty", + FilePicker(files.signal, availableFilesStream)(filesObserver) + .amend(idAttr := "dokumenty", cls("max-w-lg")) + ).toHtml, + FormRow( + "platnost", + "Platnost", + input( + idAttr := "platnost", + name := "platnost", + tpe := "date", + autoComplete := "date", + cls := "block max-w-lg w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" + ) + ).toHtml, + FormRow( + "komentar", + "Komentář", + div( + cls := "mt-1 sm:mt-0 sm:col-span-2", + textArea( + idAttr := "komentar", + name := "about", + rows := 3, + cls := "max-w-lg shadow-sm block w-full focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border border-gray-300 rounded-md" + ), + p( + cls := "mt-2 text-sm text-gray-500", + "Doplňte prosím potřebné informace související s doložením kritéria, včetně případných limitací." + ) + ) + ).toHtml + ) + ) + ), + submitButtons + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/directory/DirectoryPage.scala b/app/src/main/scala/mdr/pdb/app/pages/directory/DirectoryPage.scala new file mode 100644 index 0000000..7a72ca3 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/directory/DirectoryPage.scala @@ -0,0 +1,40 @@ +package mdr.pdb.app.pages.directory + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.UserInfo + +import components._ + +object DirectoryPage: + + type ViewModel = List[UserRow.ViewModel] + + def apply($m: Signal[ViewModel]): HtmlElement = + val (actionsStream, actionObserver) = + EventStream.withObserver[SearchForm.Action] + val $filter = actionsStream + .collect { + case SearchForm.ClearFilter => None + case SearchForm.SetFilter(t) => Some(t) + } + .startWith(None) + val byLetter = for { + d <- $m + f <- $filter + } yield for { + (letter, users) <- d + .filter { user => + f match + case None => true + case Some(t) => user.search.contains(t) + } + .groupBy(_.prijmeni.head) + .to(List) + .sortBy(_._1) + } yield (letter.toString, users.sortBy(_.prijmeni)) + + div( + cls := "h-full max-w-7xl mx-auto order-first flex flex-col flex-shrink-0", + SearchForm(actionObserver), + Directory(byLetter) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/directory/components/Directory.scala b/app/src/main/scala/mdr/pdb/app/pages/directory/components/Directory.scala new file mode 100644 index 0000000..f7d6f99 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/directory/components/Directory.scala @@ -0,0 +1,37 @@ +package mdr.pdb.app.pages.directory.components + +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.UserInfo + +object Directory: + + object Header: + type ViewModel = String + def apply($m: Signal[ViewModel]): HtmlElement = + div( + cls := "z-10 sticky top-0 border-t border-b border-gray-200 bg-gray-50 px-6 py-1 text-sm font-medium text-gray-500", + h3(child.text <-- $m) + ) + + object UserList: + type ViewModel = List[UserRow.ViewModel] + def apply($m: Signal[ViewModel]): HtmlElement = + ul( + role := "list", + cls := "relative z-0 divide-y divide-gray-200", + children <-- $m.split(_.osobniCislo)((_, _, s) => UserRow(s)) + ) + + type ViewModel = List[(String, List[UserRow.ViewModel])] + def apply($m: Signal[ViewModel]): HtmlElement = + nav( + cls := "flex-1 min-h-0 overflow-y-auto", + aria.label := "Directory", + children <-- $m.split(_._1)((_, _, s) => + div( + cls := "relative", + Header(s.map(_._1)), + UserList(s.map(_._2)) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/directory/components/SearchForm.scala b/app/src/main/scala/mdr/pdb/app/pages/directory/components/SearchForm.scala new file mode 100644 index 0000000..fb9ecc4 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/directory/components/SearchForm.scala @@ -0,0 +1,54 @@ +package mdr.pdb.app.pages.directory.components + +import com.raquo.laminar.api.L.{*, given} +import fiftyforms.ui.components.tailwind.Icons + +object SearchForm: + sealed trait Action + case object SubmitFilter extends Action + sealed trait FilterAction extends Action + case object ClearFilter extends FilterAction + case class SetFilter(value: String) extends FilterAction + + def apply(actions: Observer[Action]): HtmlElement = + div( + cls := "px-6 pt-4 pb-4", + form( + cls := "flex space-x-4", + onSubmit.mapTo(SubmitFilter) --> actions, + div( + cls := "flex-1 min-w-0", + label( + forId := "search", + cls := "sr-only", + "Hledat" + ), + div( + cls := "relative rounded-md shadow-sm", + div( + cls := "absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none", + Icons.solid.search().amend(svg.cls := "text-gray-400") + ), + input( + tpe := "search", + name := "search", + idAttr := "search", + cls := "focus:ring-pink-500 focus:border-pink-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md", + placeholder := "Hledat", + composeEvents(onInput.mapToValue.setAsValue.map(SetFilter(_)))( + _.throttle(500) + ) --> actions + ) + ) + ), + button( + tpe := "submit", + cls := "inline-flex justify-center px-3.5 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-pink-500", + Icons.solid.filter().amend(svg.cls := "text-gray-400"), + span( + cls := "sr-only", + "Hledat" + ) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/directory/components/UserRow.scala b/app/src/main/scala/mdr/pdb/app/pages/directory/components/UserRow.scala new file mode 100644 index 0000000..4aa1672 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/directory/components/UserRow.scala @@ -0,0 +1,52 @@ +package mdr.pdb.app.pages.directory.components + +import com.raquo.laminar.api.L.{*, given} +import com.raquo.airstream.core.Signal +import fiftyforms.ui.components.tailwind.Avatar + +object UserRow: + case class ViewModel( + osobniCislo: String, + celeJmeno: String, + prijmeni: String, + hlavniFunkce: Option[String], + img: Option[String], + container: () => HtmlElement = () => div() + ) { + val search = prijmeni.toLowerCase + } + + def apply($m: Signal[ViewModel]): HtmlElement = + inline def avatarImage = + Avatar($m.map(_.img)).avatarImage(10) + + li( + div( + cls := "bg-white relative px-6 py-5 flex items-center space-x-3 hover:bg-gray-50 focus-within:ring-2 focus-within:ring-inset focus-within:ring-pink-500", + div( + cls := "flex-shrink-0", + child <-- avatarImage + ), + div( + cls := "flex-1 min-w-0", + child <-- $m.map { o => + o.container() + .amend( + cls := "focus:outline-none", + span( + cls := "absolute inset-0", + aria.hidden := true + ), + p( + cls := "text-sm font-medium text-gray-900", + o.celeJmeno + ), + p( + cls := "text-sm text-gray-500 truncate", + o.hlavniFunkce + ) + ) + } + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/errors/ErrorPage.scala b/app/src/main/scala/mdr/pdb/app/pages/errors/ErrorPage.scala new file mode 100644 index 0000000..6b195ef --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/errors/ErrorPage.scala @@ -0,0 +1,99 @@ +package mdr.pdb.app.pages.errors + +import com.raquo.domtypes.generic.codecs.StringAsIsCodec +import com.raquo.laminar.api.L.{*, given} +import mdr.pdb.app.components.PageLink +import mdr.pdb.app.Page +import mdr.pdb.app.Action +import com.raquo.waypoint.Router + +object ErrorPage: + case class ViewModel( + homePage: Page, + errorName: String, + title: String, + subTitle: String + ) + + def apply(m: ViewModel, actionBus: Observer[Action])(using + Router[Page] + ): HtmlElement = + val ViewModel(homePage, errorName, title, subTitle) = m + div( + cls := "min-h-full pt-16 pb-12 flex flex-col bg-white", + main( + cls := "flex-grow flex flex-col justify-center max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", + div( + cls := "flex-shrink-0 flex justify-center", + a( + href := "/", + cls := "inline-flex", + span( + cls := "sr-only", + """Workflow""" + ), + img( + cls := "h-12 w-auto", + src := "https://tailwindui.com/img/logos/workflow-mark.svg?color=indigo&shade=600", + alt := "" + ) + ) + ), + div( + cls := "py-16", + div( + cls := "text-center", + p( + cls := "text-sm font-semibold text-indigo-600 uppercase tracking-wide", + errorName + ), + h1( + cls := "mt-2 text-4xl font-extrabold text-gray-900 tracking-tight sm:text-5xl", + title + ), + p( + cls := "mt-2 text-base text-gray-500", + subTitle + ), + div( + cls := "mt-6", + PageLink + .container(homePage, actionBus) + .amend( + cls := "text-base font-medium text-indigo-600 hover:text-indigo-500", + """Go back home""" + ) + ) + ) + ) + ), + footer( + cls := "flex-shrink-0 max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8", + nav( + cls := "flex justify-center space-x-4", + a( + href := "#", + cls := "text-sm font-medium text-gray-500 hover:text-gray-600", + """Contact Support""" + ), + span( + cls := "inline-block border-l border-gray-300", + aria.hidden := true + ), + a( + href := "#", + cls := "text-sm font-medium text-gray-500 hover:text-gray-600", + """Status""" + ), + span( + cls := "inline-block border-l border-gray-300", + aria.hidden := true + ), + a( + href := "#", + cls := "text-sm font-medium text-gray-500 hover:text-gray-600", + """Twitter""" + ) + ) + ) + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/errors/NotFoundPage.scala b/app/src/main/scala/mdr/pdb/app/pages/errors/NotFoundPage.scala new file mode 100644 index 0000000..97760e1 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/errors/NotFoundPage.scala @@ -0,0 +1,21 @@ +package mdr.pdb.app.pages.errors + +import com.raquo.domtypes.generic.codecs.StringAsIsCodec +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.Page +import mdr.pdb.app.Action + +object NotFoundPage: + def apply(homePage: Page, url: String, actionBus: Observer[Action])(using + router: Router[Page] + ): HtmlElement = + ErrorPage( + ErrorPage.ViewModel( + homePage, + "404 error", + "Page not found.", + s"Sorry, but page $url doesn't exist." + ), + actionBus + ) diff --git a/app/src/main/scala/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala b/app/src/main/scala/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala new file mode 100644 index 0000000..77b0af8 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/pages/errors/UnhandledErrorPage.scala @@ -0,0 +1,32 @@ +package mdr.pdb.app.pages.errors + +import com.raquo.domtypes.generic.codecs.StringAsIsCodec +import com.raquo.laminar.api.L.{*, given} +import com.raquo.waypoint.Router +import mdr.pdb.app.Page +import mdr.pdb.app.Action + +object UnhandledErrorPage: + + case class ViewModel( + homePage: Page, + errorName: Option[String], + errorMessage: Option[String] + ) + + def apply(m: ViewModel, actionBus: Observer[Action])(using + router: Router[Page] + ): HtmlElement = + ErrorPage( + ErrorPage.ViewModel( + m.homePage, + "Unexpected error occurred", + m.errorName.getOrElse( + "Uh oh!" + ), // TODO: translations, better text than uh oh + m.errorMessage.getOrElse( + "This wasn't supposed to happen! Please try again." + ) + ), + actionBus + ) diff --git a/app/src/main/scala/mdr/pdb/app/services/DataFetcher.scala b/app/src/main/scala/mdr/pdb/app/services/DataFetcher.scala new file mode 100644 index 0000000..134c221 --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/services/DataFetcher.scala @@ -0,0 +1,6 @@ +package mdr.pdb.app.services + +import com.raquo.airstream.core.Observer + +trait DataFetcher[K, A]: + def fetch(id: K, sink: Observer[A]): Unit diff --git a/app/src/main/scala/mdr/pdb/app/state/AppState.scala b/app/src/main/scala/mdr/pdb/app/state/AppState.scala new file mode 100644 index 0000000..e44b2df --- /dev/null +++ b/app/src/main/scala/mdr/pdb/app/state/AppState.scala @@ -0,0 +1,120 @@ +package mdr.pdb.app +package state + +import com.raquo.airstream.core.EventStream +import mdr.pdb.{UserInfo, OsobniCislo} +import com.raquo.airstream.core.Observer +import scala.scalajs.js +import scala.scalajs.js.JSON +import zio.json._ +import com.raquo.airstream.eventbus.EventBus +import com.raquo.airstream.ownership.Owner +import com.raquo.waypoint.Router +import mdr.pdb.Parameter +import mdr.pdb.ParameterCriteria +import mdr.pdb.UserFunction +import mdr.pdb.UserContract +import fiftyforms.services.files.File + +trait AppState + extends connectors.DetailPageConnector.AppState + with connectors.DetailParametruPageConnector.AppState + with connectors.DetailKriteriaPageConnector.AppState + with pages.detail.UpravDukaz.State: + def users: EventStream[List[UserInfo]] + def details: EventStream[UserInfo] + def parameters: EventStream[List[Parameter]] + def actionBus: Observer[Action] + +class MockAppState(implicit owner: Owner, router: Router[Page]) + extends AppState: + + given JsonDecoder[OsobniCislo] = JsonDecoder.string.map(OsobniCislo.apply) + given JsonDecoder[UserFunction] = DeriveJsonDecoder.gen + given JsonDecoder[UserContract] = DeriveJsonDecoder.gen + given JsonDecoder[UserInfo] = DeriveJsonDecoder.gen + + given JsonDecoder[ParameterCriteria] = DeriveJsonDecoder.gen + given JsonDecoder[Parameter] = DeriveJsonDecoder.gen + + private val actions = EventBus[Action]() + private val (parametersStream, pushParameters) = + EventStream.withCallback[List[Parameter]] + private val (usersStream, pushUsers) = + EventStream.withCallback[List[UserInfo]] + private val (detailsStream, pushDetails) = EventStream.withCallback[UserInfo] + private val (filesStream, pushFiles) = EventStream.withCallback[List[File]] + + private val mockData: List[UserInfo] = + mockUsers + .asInstanceOf[js.Dictionary[js.Object]] + .values + // TODO: is there a more efficient way to parse from JS object directly? + .map { o => + val parsed = JSON.stringify(o).fromJson[UserInfo] + parsed.left.foreach(org.scalajs.dom.console.log(_)) + parsed + } + .collect { case Right(u) => + u + } + .toList + + private val mockParameters: List[Parameter] = + pdbParams + .asInstanceOf[js.Dictionary[js.Object]] + .values + .map(o => JSON.stringify(o).fromJson[Parameter]) + .collect { case Right(p) => p } + .toList + + // TODO: Extract to separate event handler + actions.events.foreach { + case FetchDirectory => pushUsers(mockData) + case FetchUserDetails(osc) => + mockData.find(_.personalNumber == osc).foreach { o => + pushDetails(o) + router.replaceState(Page.Detail(o)) + } + case FetchParameters(osc) => + pushParameters(mockParameters) + case FetchParameter(osc, paramId) => + for + o <- mockData.find(_.personalNumber == osc) + p <- mockParameters.find(_.id == paramId) + do + pushDetails(o) + pushParameters(mockParameters) + router.replaceState(Page.DetailParametru(o, p)) + case FetchParameterCriteria(osc, paramId, critId, page) => + for + o <- mockData.find(_.personalNumber == osc) + p <- mockParameters.find(_.id == paramId) + c <- p.criteria.find(_.id == critId) + do + pushDetails(o) + pushParameters(mockParameters) + router.replaceState(page(o, p, c)) + case NavigateTo(page) => router.pushState(page) + case FetchAvailableFiles(osc) => + pushFiles( + List( + File("https://tc163.cmi.cz/here", "Example file") + ) + ) + } + + override def users: EventStream[List[UserInfo]] = + usersStream.debugWithName("users") + + override def details: EventStream[UserInfo] = + detailsStream.debugWithName("details") + + override def parameters: EventStream[List[Parameter]] = + parametersStream.debugWithName("parameters") + + override def availableFiles: EventStream[List[File]] = + filesStream.debugWithName("available files") + + override def actionBus: Observer[Action] = + actions.writer.debugWithName("actions writer") diff --git a/build.sbt b/build.sbt index 2afc879..0daf796 100644 --- a/build.sbt +++ b/build.sbt @@ -11,6 +11,12 @@ lazy val core = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Pure) .in(file("core")) + .settings( + IWDeps.useZIO(Test), + IWDeps.tapirCore, + IWDeps.tapirZIO, + IWDeps.tapirZIOJson + ) lazy val ui = (project in file("ui")) .enablePlugins(ScalaJSPlugin) diff --git a/core/src/main/scala/cz/e_bs/cmi/mdr/pdb/Parameter.scala b/core/src/main/scala/cz/e_bs/cmi/mdr/pdb/Parameter.scala deleted file mode 100644 index 9d68240..0000000 --- a/core/src/main/scala/cz/e_bs/cmi/mdr/pdb/Parameter.scala +++ /dev/null @@ -1,27 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb - -import java.math.BigInteger -import java.security.MessageDigest - -object ParameterCriteria { - type Id = String -} - -case class ParameterCriteria( - chapterId: String, - itemId: String, - criteriumText: String -) { - val id: ParameterCriteria.Id = s"${chapterId}${itemId}" -} - -object Parameter { - type Id = String -} - -case class Parameter( - id: Parameter.Id, - name: String, - description: String, - criteria: List[ParameterCriteria] -) diff --git a/core/src/main/scala/cz/e_bs/cmi/mdr/pdb/UserProfile.scala b/core/src/main/scala/cz/e_bs/cmi/mdr/pdb/UserProfile.scala deleted file mode 100644 index 455a445..0000000 --- a/core/src/main/scala/cz/e_bs/cmi/mdr/pdb/UserProfile.scala +++ /dev/null @@ -1,44 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb - -import java.time.LocalDate - -opaque type OsobniCislo = String - -object OsobniCislo: - // TODO: validation - def apply(osc: String): OsobniCislo = osc - -extension (osc: OsobniCislo) def toString: String = osc - -case class UserContract( - rel: String, - startDate: LocalDate, - endDate: Option[LocalDate] -) -case class UserFunction(name: String, dept: String, ou: String) - -case class UserInfo( - personalNumber: OsobniCislo, - username: String, - givenName: String, - surname: String, - titlesBeforeName: Option[String] = None, - titlesAfterName: Option[String] = None, - email: Option[String] = None, - phone: Option[String] = None, - mainFunction: Option[UserFunction] = None, - userContracts: List[UserContract] = Nil, - img: Option[String] = None -) { - val name = - List( - Some( - List(titlesBeforeName, Some(givenName), Some(surname)).flatten.mkString( - " " - ) - ), - titlesAfterName - ).flatten.mkString(", ") -} - -case class UserProfile(username: String, userInfo: UserInfo) diff --git a/core/src/main/scala/cz/e_bs/cmi/mdr/pdb/frontend/domain.scala b/core/src/main/scala/cz/e_bs/cmi/mdr/pdb/frontend/domain.scala deleted file mode 100644 index 10980c5..0000000 --- a/core/src/main/scala/cz/e_bs/cmi/mdr/pdb/frontend/domain.scala +++ /dev/null @@ -1,17 +0,0 @@ -package cz.e_bs.cmi.mdr.pdb -package frontend - -import java.time.LocalDate -import java.time.Instant - -sealed trait Command - -type DocumentRef = String - -case class AutorizujDukaz( - osoba: OsobniCislo, - parametr: Parameter.Id, - kriterium: ParameterCriteria.Id, - dukaz: List[DocumentRef], - platiDo: Option[LocalDate] -) extends Command diff --git a/core/src/main/scala/mdr/pdb/Parameter.scala b/core/src/main/scala/mdr/pdb/Parameter.scala new file mode 100644 index 0000000..0b89f6c --- /dev/null +++ b/core/src/main/scala/mdr/pdb/Parameter.scala @@ -0,0 +1,27 @@ +package mdr.pdb + +import java.math.BigInteger +import java.security.MessageDigest + +object ParameterCriteria { + type Id = String +} + +case class ParameterCriteria( + chapterId: String, + itemId: String, + criteriumText: String +) { + val id: ParameterCriteria.Id = s"${chapterId}${itemId}" +} + +object Parameter { + type Id = String +} + +case class Parameter( + id: Parameter.Id, + name: String, + description: String, + criteria: List[ParameterCriteria] +) diff --git a/core/src/main/scala/mdr/pdb/UserProfile.scala b/core/src/main/scala/mdr/pdb/UserProfile.scala new file mode 100644 index 0000000..8373b82 --- /dev/null +++ b/core/src/main/scala/mdr/pdb/UserProfile.scala @@ -0,0 +1,44 @@ +package mdr.pdb + +import java.time.LocalDate + +opaque type OsobniCislo = String + +object OsobniCislo: + // TODO: validation + def apply(osc: String): OsobniCislo = osc + +extension (osc: OsobniCislo) def toString: String = osc + +case class UserContract( + rel: String, + startDate: LocalDate, + endDate: Option[LocalDate] +) +case class UserFunction(name: String, dept: String, ou: String) + +case class UserInfo( + personalNumber: OsobniCislo, + username: String, + givenName: String, + surname: String, + titlesBeforeName: Option[String] = None, + titlesAfterName: Option[String] = None, + email: Option[String] = None, + phone: Option[String] = None, + mainFunction: Option[UserFunction] = None, + userContracts: List[UserContract] = Nil, + img: Option[String] = None +) { + val name = + List( + Some( + List(titlesBeforeName, Some(givenName), Some(surname)).flatten.mkString( + " " + ) + ), + titlesAfterName + ).flatten.mkString(", ") +} + +case class UserProfile(username: String, userInfo: UserInfo) diff --git a/core/src/main/scala/mdr/pdb/frontend/domain.scala b/core/src/main/scala/mdr/pdb/frontend/domain.scala new file mode 100644 index 0000000..9a12381 --- /dev/null +++ b/core/src/main/scala/mdr/pdb/frontend/domain.scala @@ -0,0 +1,17 @@ +package mdr.pdb +package frontend + +import java.time.LocalDate +import java.time.Instant + +sealed trait Command + +type DocumentRef = String + +case class AutorizujDukaz( + osoba: OsobniCislo, + parametr: Parameter.Id, + kriterium: ParameterCriteria.Id, + dukaz: List[DocumentRef], + platiDo: Option[LocalDate] +) extends Command