src/main/scala/fis/pm/ui/ProjectSnippet.scala
author Tomas Zeman <tzeman@volny.cz>
Tue, 23 Apr 2013 10:36:04 +0200
changeset 108 ef4e3e0ef83f
parent 107 26e9d0e4a619
permissions -rw-r--r--
84a94fa29a67504b Task/Project notifications

/*
 * Copyright 2012 Tomas Zeman <tzeman@volny.cz>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package fis.pm.ui

import fis.aaa.ui.IfLoggedIn
import fis.base.model.ReadOnlyField
import fis.base.ui._
import fis.cl.model._
import fis.crm.model._
import fis.fs.model._
import fis.fs.ui._
import fis.geo.model._
import fis.pm.model._
import net.liftweb.common._
import net.liftweb.http._
import net.liftweb.record.field._
import net.liftweb.sitemap._
import net.liftweb.sitemap.Loc._
import net.liftweb.squerylrecord.RecordTypeMode._
import net.liftweb.util._
import net.liftweb.util.Helpers._
import net.tz.lift.model._
import net.tz.lift.snippet._
import scala.xml.{Elem, NodeSeq, Text}

object ProjectSnippet extends ProjectCrud with EntitySnippet[Project] {

  val prefix = "project"

  private val listPre = Menu("project.list", l10n("Projects")) / prefix >>
    Title(_ => i18n("Projects")) >>
    locTpl("project/list") >> Snippet("list", list)

  private val createPre = Menu("project.create", l10n("Create")) / prefix / ADD >>
    Title(_ => i18n("Create project")) >> IfLoggedIn.test >>
    locTpl("project/form") >> Snippet("form", form) >> Hidden

  private val viewPre = Menu.param[Project]("project.view", l10n("Project"), parse,
    encode) / prefix / * >> Title(p => i18n("Project %s", p.linkName)) >>
    locTpl("project/view") >> Snippet("panel", panel) >>
    Snippet("tasks", tasks) >> Snippet("attachments", attachmentsTable) >>
    Hidden

  private val editPre = Menu.param[Project]("project.edit", l10n("Edit"), parse,
    encode) / prefix / * / EDIT >>
    Title(p => i18n("Edit project %s", p.linkName)) >> IfLoggedIn.testVal >>
    locTpl("project/form") >> Snippet("form", form) >> Hidden

  private val deletePre = Menu.param[Project]("project.delete", l10n("Delete"),
    parse, encode) / prefix / * / DELETE >>
    Title(p => i18n("Delete project %s", p.linkName)) >> IfLoggedIn.testVal >>
    locTpl("entity/delete") >> Snippet("form", deleteF) >> Hidden

  private val createTaskPre = Menu.param[Project]("project.create-task",
    l10n("Create task"), parse, encode) / prefix / * / "create-task" >>
    Title(p => i18n("Create task for project %s", p.linkName)) >>
    IfLoggedIn.testVal >>
    locTpl("entity/form") >> Snippet("form", taskF) >> Hidden

  private val locationsPre = Menu.param[Project]("project.locations",
    l10n("Edit locations"), parse, encode) / prefix / * / "locations" >>
    Title(p => i18n("Edit locations of project %s", p.linkName)) >>
    IfLoggedIn.testVal >>
    locTpl("entity/form") >> Snippet("form", locationsF) >> Hidden


  private val listM = listPre >> SecNav(createPre).build
  private val createM = createPre >> SecNav(listPre).build
  private val viewM = viewPre >> (SecNav(editPre) + deletePre +
    locationsPre + createTaskPre + attachments.addPre).build
  private val editM = editPre >> SecNav(viewPre).build
  private val deleteM = deletePre >> SecNav(viewPre).build
  private val createTaskM = createTaskPre >> SecNav(viewPre).build
  private val locationsM = locationsPre >> SecNav(viewPre).build

  private lazy val viewLoc = viewM.toLoc
  private lazy val editLoc = editM.toLoc
  private lazy val deleteLoc = deleteM.toLoc

  val menu = listM submenus(viewM, editM, createM, deleteM, createTaskM,
    locationsM, attachments.menu)

  private def cur = viewLoc.currentValue or editLoc.currentValue or
    deleteLoc.currentValue

  private def allQp: Boolean = (S param "all") flatMap(asBoolean(_)) openOr false
  private def allQp(url: String, all: Boolean) =
    all.box(appendQueryParameters(url, List("all" -> "1"))) openOr url

  private def list: CssTr = {
    "li" #> (List(false, true) map { b =>
      val html = <li><a href={allQp(url.list, b)}>{
        l10n(if (b) "projects.all" else "projects.unfinished")}</a></li>
      (b == allQp).box(html % ("class" -> "active")) openOr html
    }) &
    ".content *" #> ProjectTable(allQp.box(PmSchema.projects) openOr
      from(PmSchema.projectT, CodeListSchema.cli)((p, i) =>
      where(p.stateFld === i.id and ProjectState.unfinishedClause(i))
      select(p) orderBy(p.deadline asc)))
  }

  private def panel: CssTr = "*" #> cur.map { p => ViewPanel(fields(p)) }

  private def tasks: CssTr = "*" #> cur.map { p => TaskTable(ProjectTasks(p)) }

  private def attachmentsTable: CssTr = "*" #> cur.map { p =>
    attachments.table(p)(ProjectAttachments(p)) }

  object url {
    def view: Project => Box[String] = (viewLoc.calcHref _) andThen (Box !! _)
    def list: String = listM.loc.calcDefaultHref
  }

  private def fields(p: Project) = List(p.name, p.identS, p.phaseFld,
    p.stateFld, p.createdBy, p.createdAt, p.deadline, p.responsible,
    p.productLine, ProjectCompanyField(p), ProjectLocationsField(p),
    p.description, p.note)

  private case class ProjectLink(c: Project) extends EntityLink[Project](c, url.view)

  EntityLink.register[Project](ProjectLink(_))

  private case class ProjectCompanyField(p: Project) extends OptionalLongField(p)
    with CompanyField with FieldLabel {
    override def name = "company"
    override def defaultValueBox =
      PmSchema.projectCompany.left(p).headOption.map(_.id)
  }

  private case class ProjectLocationsField(p: Project) extends ReadOnlyField(
    "locations", l10n("project.locations"), NodeSeq.Empty, Empty) {
    override def asHtml = <span>{
      ((List(p.locationA, p.locationB) filterNot(_.get.isEmpty) map(_.asHtml)) ++
      (ProjectLocations(p) flatMap(EntityLink(_)) map(_.asHtml))) map(x =>
      <div>{x}</div>)
    }</span>
  }

  private object form extends HorizontalScreen with CancelButton with
    SaveButton {

    private object project extends ScreenVar[Project](Project.createRecord)
    private object company extends ScreenVar[ProjectCompanyField](
      ProjectCompanyField(project))
    private object companyRec extends ScreenVar[Company](Company.createRecord)
    private object address extends ScreenVar[Address](Address.createRecord)

    private val createNewCompany = builder(l10n("Create new company?"), false,
      FormFieldId("create_new_company")).
      help(i18n("Create new company instead of choosing an existing one.")).make

    private def formFields(p: Project) = {
      val pFlds = List[FieldContainer](p.name, p.identS, p.phaseFld, p.stateFld,
        p.deadline, p.responsible, p.productLine, p.locationA, p.locationB,
        p.description, p.note)
      val companySelect = field(company.get, FormFieldId("company_select"))
      p.id == p.idField.defaultValue match {
        case false => pFlds :+ companySelect
        case true =>
          val c = companyRec.get
          val cFlds = field(c.name, FormFieldId("comp_name")) ::
            field(c.ico, FormFieldId("comp_ico")) ::
            field(c.dic, FormFieldId("comp_dic")) ::
            field(ReadOnlyField.labelOnly(c.address), FormFieldId("comp_addr")) ::
            (address.formFields map { f => field(f,
              FormFieldId(f.uniqueFieldId.map("comp_" + _) openOr nextFuncName)) })
          (pFlds :+ createNewCompany :+ companySelect) ::: cFlds
      }
    }

    override def screenFields = formFields(project) flatMap(_.allFields)

    protected override def decorateLine(f: ScreenFieldInfo): CssTr =
      (f.field.uniqueFieldId.filter { _.startsWith("comp_") } map { _ =>
      ".control-group [class+]" #> "company-field" }) or
      (f.field.uniqueFieldId.filter { _.startsWith("company_select") } map { _ =>
      ".control-group [class+]" #> "company-select" }) openOr PassThru

    override def localSetup() {
      cur.foreach { p => project(p); company(ProjectCompanyField(p)) }
    }

    def finish() {
      save(project) foreach { p =>
        val fk = PmSchema.projectCompany.left(p)
        createNewCompany.get match {
          case false => company.vend match {
            case Full(c) if fk.exists(_.id == c.id) => // empty, no update
            case Full(c) =>
              fk.dissociateAll
              fk.associate(c)
            case _ => fk.dissociateAll
          }
          case true => AddressCrud.save(address).foreach { a =>
            CompanyCrud.save(companyRec.address(a.id)).foreach { c =>
              S notice l10n("Company %s saved.", c.linkName)
              fk.dissociateAll
              fk.associate(c)
            }
          }
        }
        S notice l10n("Project %s saved.", p.linkName)
        S.redirectTo(viewLoc.calcHref(p))
      }
    }
  }

  private object deleteF extends HorizontalScreen with CancelButton with
    DeleteButton {

    val confirm = field(l10n("Really delete this project?"), false)

    def finish() {
      for {
        c <- deleteLoc.currentValue if confirm
        r <- delete(c)
        n <- r.box(c.linkName)
      } {
        S notice l10n("Project %s deleted.", n)
        S redirectTo listM.loc.calcDefaultHref
      }
    }
  }

  private object taskF extends TaskForm {

    override def localSetup() {
      createTaskM.currentValue.foreach { p => task.project(p.id) }
    }

    protected def onSuccess(t: Task) {
      TaskSnippet.url.view(t).foreach { u => S redirectTo u }
    }
  }

  private object locationsF extends HorizontalScreen with CancelButton with
    SaveButton {

    private class V(iv: Boolean, val l: Location) { var v: Boolean = iv }

    private object locs extends ScreenVar[Iterable[V]](Nil)

    def v2r(v: V): Box[List[NodeSeq]] = for {
      ll <- EntityLink(v.l)
      a <- v.l.address.vend
      c <- a.city.vend
      cl <- EntityLink(c)
    } yield {
      List(SHtml.checkbox(v.v, v.v = _), ll.asHtml, v.l.address.asHtml,
        cl.asHtml)
    }

    def locsTbl =
      (new DataTable(List(l10n("Check"), l10n("Location"),
        Location.address.displayName, l10n("City")),
        locs flatMap { v2r _ } toList))(NodeSeq.Empty)

    addFields(() => new ReadOnlyField("locations", l10n("project.locations"),
      NodeSeq.Empty, Full(locsTbl)))

    override def localSetup() {
      locationsM.toLoc.currentValue.foreach { p =>
        val ids = ProjectLocations(p) map(_.id) toSet
        val ls = Location.locations() map { l => new V(ids contains l.id, l) }
        locs(ls)
      }
    }

    def finish() { locationsM.toLoc.currentValue.foreach { p =>
      val fk = PmSchema.projectLocation.left(p)
      val curIds = fk map(_.id) toSet
      val newLocs = locs filter(_.v) map(_.l)
      val newIds = newLocs map(_.id) toSet

      fk filterNot(newIds contains _.id) foreach { l => fk.dissociate(l) }
      newLocs filterNot(curIds contains _.id) foreach { l => fk.associate(l) }
      S notice l10n("Locations saved.")
      S redirectTo viewLoc.calcHref(p)
    }}
  }

  /** Project attachments. */
  private object attachments {

    val addPre = Menu.param[Project]("project.add-attachment",
      l10n("Add attachment"), parse, encode) / prefix / * / "attachment" / ADD >>
      Title(_ => i18n("Add attachment")) >> IfLoggedIn.testVal >>
      locTpl("entity/form") >> Snippet("form", attachmentF) >> Hidden

    type PA = (Project, Attachment)
    private object paMemo extends RequestMemoize[List[String], Box[PA]]()
    private def parsePA(ids: List[String]): Box[PA] = paMemo(ids, ids match {
      case AsLong(pid) :: AsLong(aid) :: Nil => for {
        p <- get(pid)
        a <- AttachmentCrud.get(aid)
      } yield (p, a)
      case _ => Empty
    })
    private def encodePA(pa: PA) = List(pa._1, pa._2) map(_.id.toString)

    private val downloadPre = Menu.params[PA]("project.attachment",
      LinkText(pa => Text(pa._2.linkName)),
      parsePA, encodePA) / prefix / * / "attachment" / * >>
      EarlyResponse(download _)

    private val deleteM = Menu.params[PA]("project.delete-attachment",
      l10n("Delete"), parsePA, encodePA) / prefix / * / "attachment" / * / DELETE >>
      Title(pa => i18n("Delete attachment %s", pa._2.linkName)) >>
      IfLoggedIn.testVal >> locTpl("entity/form") >>
      Snippet("form", attachmentD) >> Hidden

    private val addM = addPre >> SecNav(viewPre).build

    val downloadM = downloadPre submenus(addM, deleteM)
    val menu = downloadM

    private lazy val downloadLoc = downloadM.toLoc

    private def download(): Box[LiftResponse] = for {
      (p, a) <- downloadLoc.currentValue
      is <- a.getContent
    } yield
      StreamingResponse(is, () => is.close, a.contentSize, a.headers, Nil, 200)

    private object attachmentF extends AttachmentForm {

      override protected def onSuccess(a: Attachment) =
        addM.toLoc.currentValue.foreach { p =>
          PmSchema.projectAttachment.left(p).associate(a)
          S notice l10n("Attachment %s saved.", a.name.get)
          S redirectTo viewLoc.calcHref(p)
        }
    }

    private object attachmentD extends DeleteAttachmentForm {
      protected def getAttachment = deleteM.toLoc.currentValue map(_._2)
      protected def onSuccess(d: Boolean, rm: Boolean) {
        S notice l10n("Attachment deleted.")
        deleteM.toLoc.currentValue map(_._1) foreach { p =>
          S redirectTo viewLoc.calcHref(p) }
      }
    }

    def table(p: Project) = AttachmentTable(
      { a => NavLink(downloadLoc)((p, a)) },
      { a => NavLink(deleteM.toLoc)((p, a)) }) _
  }

  /** Project notifications. */
  private object notifications extends ProjectNotification {
    lazy val viewLoc = ProjectSnippet.this.viewLoc
    def panel(p: Project) = ViewPanel(fields(p))
  }

  override protected def afterSave(orig: Box[Project], proj: Box[Project]) =
    proj $ { _ => notifications(orig, proj) }
}

// vim: set ts=2 sw=2 et: