Company contacts
authorTomas Zeman <tzeman@volny.cz>
Fri, 20 Apr 2012 08:44:36 +0200
changeset 72 077b875a2a0c
parent 71 b177060246a9
child 73 4bcb7deedd3f
Company contacts
db/db-schema.sql
src/main/scala/fis/aaa/ui/UserSnippet.scala
src/main/scala/fis/base/ui/FieldTable.scala
src/main/scala/fis/crm/model/CrmSchema.scala
src/main/scala/fis/crm/ui/CompanySnippet.scala
src/main/webapp/company/add-contact.html
src/main/webapp/company/view.html
src/main/webapp/css/base.css
src/main/webapp/templates-hidden/_resources.html
src/main/webapp/templates-hidden/_resources_cs.html
--- a/db/db-schema.sql	Fri Apr 20 08:44:28 2012 +0200
+++ b/db/db-schema.sql	Fri Apr 20 08:44:36 2012 +0200
@@ -138,11 +138,19 @@
     "updated_by" bigint
   );
 create sequence "bank_account_id_seq";
+create table "company_contact" (
+    "contact" bigint not null,
+    "entity" bigint not null
+  );
 -- foreign key constraints :
 alter table "address" add constraint "addressFK1" foreign key ("city_id") references "city"("id");
 alter table "city" add constraint "cityFK2" foreign key ("country_id") references "country"("id");
 alter table "company" add constraint "companyFK3" foreign key ("address_id") references "address"("id");
 alter table "company" add constraint "companyFK4" foreign key ("post_adress_id") references "address"("id") on delete set null;
 alter table "bank_account" add constraint "bank_accountFK5" foreign key ("company_id") references "company"("id") on delete cascade;
+alter table "company_contact" add constraint "company_contactFK6" foreign key ("entity") references "company"("id") on delete cascade;
+alter table "company_contact" add constraint "company_contactFK7" foreign key ("contact") references "contact"("id") on delete cascade;
+-- composite key indexes :
+alter table "company_contact" add constraint "company_contactCPK" unique("entity","contact");
 -- column group indexes :
 create index "user_deleted_active_idx" on "user" ("deleted","active");
--- a/src/main/scala/fis/aaa/ui/UserSnippet.scala	Fri Apr 20 08:44:28 2012 +0200
+++ b/src/main/scala/fis/aaa/ui/UserSnippet.scala	Fri Apr 20 08:44:36 2012 +0200
@@ -15,6 +15,7 @@
  */
 package fis.aaa.ui
 
+import fis.base.model._
 import fis.base.ui._
 import fis.aaa.model._
 import net.liftweb.common._
@@ -92,19 +93,10 @@
     private def validatePass: List[FieldError] = (pass1.get != pass2.get).box {
       FieldError(pass2, i18n("Passwords do not match.")) } toList
 
-    private def roText(f: BaseField) = new Field {
-      type ValueType = String 
-      def default = ""
-      override def name = f.name
-      override def displayName = f.displayName
-      override def toForm = Full(<span class="uneditable-input">{f.asHtml}</span>)
-      override implicit def manifest = buildIt[String] 
-    }
-
     private def fields(u: User): List[FieldContainer] = {
       val n_l = List(u.name, u.login)
-      val n_l_f = (u.id != u.idField.defaultValue).box(n_l map { roText _ }).
-        openOr(n_l)
+      val n_l_f = (u.id != u.idField.defaultValue).box(
+        n_l map(ReadOnlyField(_))).openOr(n_l)
 
       n_l_f ++ List[FieldContainer](pass1, pass2, u.active, u.note)
     }
--- a/src/main/scala/fis/base/ui/FieldTable.scala	Fri Apr 20 08:44:28 2012 +0200
+++ b/src/main/scala/fis/base/ui/FieldTable.scala	Fri Apr 20 08:44:36 2012 +0200
@@ -40,4 +40,11 @@
     (cols(fields(header)) & cells(rows))(load)
 }
 
+object FieldTable {
+  def apply[T](fieldsF: T => Iterable[ReadableField], header: T)(rows: Iterable[T]):
+    NodeSeq = (new FieldTable[T] {
+      def fields(v: T) = fieldsF(v)
+    }).build(header, rows)
+}
+
 // vim: set ts=2 sw=2 et:
--- a/src/main/scala/fis/crm/model/CrmSchema.scala	Fri Apr 20 08:44:28 2012 +0200
+++ b/src/main/scala/fis/crm/model/CrmSchema.scala	Fri Apr 20 08:44:36 2012 +0200
@@ -55,6 +55,14 @@
     via((c, a) => c.id === a.company)
   companyBankAccounts.foreignKeyDeclaration.constrainReference(onDelete cascade)
 
+  val companyContacts = manyToManyRelation(companyT, contactT).
+    via[CompanyContact]((comp, cnt, cc) => (
+      comp.id === cc.entity,
+      cnt.id  === cc.contact
+    ))
+  companyContacts.leftForeignKeyDeclaration.constrainReference(onDelete cascade)
+  companyContacts.rightForeignKeyDeclaration.constrainReference(onDelete cascade)
+
   def allCompanies: Iterable[Company] = from(companyT) (c =>
     select(c) orderBy(c.name asc))
 
@@ -62,14 +70,24 @@
 
 object CrmSchema extends CrmSchema
 
+object CompanyContacts {
+  def apply(company: Company): Iterable[Contact] =
+    from(CrmSchema.companyContacts.left(company)) (c =>
+      select(c) orderBy(c.lastName asc, c.firstName asc)
+    )
+}
+
 import org.squeryl.KeyedEntity
 import org.squeryl.dsl._
 
-case class EntityContact[T](val entityId: Long, val contactId: Long) extends
-  KeyedEntity[CompositeKey2[Long, Long]] {
+trait EntityContact[T] extends KeyedEntity[CompositeKey2[Long, Long]] {
+  def entity: Long
+  def contact: Long
+  def id = CompositeKey2(entity, contact)
+}
 
-  def id = CompositeKey2(entityId, contactId)
-}
+case class CompanyContact(entity: Long, contact: Long) extends
+  EntityContact[Company]
 
 case class EntityContactWithType[T](val entityId: Long, val contactId: Long,
   val typeId: Long) extends KeyedEntity[CompositeKey3[Long, Long, Long]] {
--- a/src/main/scala/fis/crm/ui/CompanySnippet.scala	Fri Apr 20 08:44:28 2012 +0200
+++ b/src/main/scala/fis/crm/ui/CompanySnippet.scala	Fri Apr 20 08:44:36 2012 +0200
@@ -15,11 +15,13 @@
  */
 package fis.crm.ui
 
+import fis.base.model._
 import fis.base.ui._
 import fis.crm.model._
 import fis.geo.model._
 import net.liftweb.common._
 import net.liftweb.http._
+import net.liftweb.http.js.JsCmds.RedirectTo
 import net.liftweb.sitemap._
 import net.liftweb.sitemap.Loc._
 import net.liftweb.util._
@@ -41,7 +43,8 @@
 
   private val viewPre = Menu.param[Company]("company.view", l10n("Company"), parse,
     encode) / prefix / * >> Title(c => i18n("Company %s", c.linkName)) >>
-    locTpl("entity/view") >> Snippet("panel", panel) >> Hidden
+    locTpl("company/view") >> Snippet("panel", panel) >>
+    Snippet("contacts", contacts.contacts) >> Hidden
 
   private val editPre = Menu.param[Company]("company.edit", l10n("Edit"), parse,
     encode) / prefix / * / EDIT >>
@@ -55,7 +58,8 @@
 
   private val listM = listPre >> SecNav(createPre).build
   private val createM = createPre >> SecNav(listPre).build
-  private val viewM = viewPre >> (SecNav(editPre) + deletePre).build
+  private val viewM = viewPre >>
+    (SecNav(editPre) + deletePre + contacts.chooseM).build
   private val editM = editPre >> SecNav(viewPre).build
   private val deleteM = deletePre >> SecNav(viewPre).build
 
@@ -63,7 +67,8 @@
   private lazy val editLoc = editM.toLoc
   private lazy val deleteLoc = deleteM.toLoc
 
-  val menu = listM submenus(viewM, editM, createM, deleteM)
+  val menu = listM submenus(viewM, editM, createM, deleteM,
+    contacts.chooseM, contacts.confirmM)
 
   private def cur = viewLoc.currentValue or editLoc.currentValue or
     deleteLoc.currentValue
@@ -94,17 +99,9 @@
     override def screenFields: List[BaseField] =
       fields(company) flatMap(_.allFields)
 
-    private def labelField(f: BaseField) = new Field {
-      type ValueType = String
-      def default = ""
-      override def name = f.name
-      override def displayName = f.displayName
-      override def toForm = Empty
-      override implicit def manifest = buildIt[String] 
-    }
-
     private def fields(c: Company): List[FieldContainer] = {
-      List[FieldContainer](c.name, c.ico, c.dic, labelField(c.address)) ++
+      List[FieldContainer](c.name, c.ico, c.dic,
+        ReadOnlyField.labelOnly(c.address)) ++
       address.formFields ++
       List[FieldContainer](hasPostAddr) ++
       postAddress.formFields.map { f => field(f,
@@ -167,6 +164,79 @@
     }
   }
 
+  /* Contacts view + add contact op. */
+  private object contacts {
+
+    private val choosePreM = Menu.param[Company]("company.addContact",
+      l10n("Add contact"), parse, encode) / prefix / * / "add-contact" >>
+      Title(c => i18n("Add contact to %s", c.linkName)) >>
+      locTpl("company/add-contact") >> Snippet("panel", chooseContact) >> Hidden
+
+    val confirmM = Menu.params[(Company, Contact)](
+      "company.addContactId", l10n("Add contact"), { _ match {
+        case AsLong(cmpId) :: AsLong(cntId) :: Nil => for {
+            comp <- get(cmpId)
+            cnt <- ContactCrud.get(cntId)
+          } yield (comp, cnt)
+        case _ => Empty
+      }},
+      { p => List(p._1, p._2) map(_.id.toString) }) / prefix /
+        * / "add-contact" / * >>
+      Title(p => i18n("Add contact %s to %s", p._2.linkName, p._1.linkName)) >>
+      locTpl("entity/form") >> Snippet("form", addContact) >> Hidden
+
+    val chooseM = choosePreM >> SecNav(viewPre).build
+
+    private case class RemoveContactLink(comp: Company, cnt: Contact) extends
+      ReadOnlyField("actions", "", ConfirmationLink(i18n("Remove"),
+        l10n("Really remove contact?"), {
+          CrmSchema.companyContacts.left(comp).dissociate(cnt)
+          RedirectTo(viewLoc.calcHref(comp))
+        }), Empty)
+
+    def contacts: CssTr = "*" #> cur.map { c =>
+      FieldTable[Contact]({ cnt => ContactTable.fields(cnt).toSeq :+
+        RemoveContactLink(c, cnt) }, Contact)(CompanyContacts(c)) }
+
+    private case class AddContactLink(comp: Company, cnt: Contact) extends
+      EntityLink[Contact](cnt,
+        { c => Full(confirmM.toLoc.calcHref((comp, cnt))) })
+
+    private def chooseContact: CssTr = "*" #> chooseM.toLoc.currentValue.map {
+      comp =>
+        FieldTable[Contact]({ c =>
+          List(AddContactLink(comp, c)) }, Contact)(CrmSchema.allContacts)
+    }
+
+    private object addContact extends HorizontalScreen with CancelButton
+      with SaveButton {
+
+      override def screenFields: List[BaseField] = (for {
+        (comp, cnt) <- confirmM.toLoc.currentValue
+        cl <- EntityLink(comp)
+        cntl <- EntityLink(cnt)
+      } yield {
+        List(ReadOnlyField(l10n("Company"), cl),
+          ReadOnlyField(l10n("Contact"), cntl)) flatMap(_.allFields)
+      }) openOr Nil
+
+      def finish() { for {
+        (comp, cnt) <- confirmM.toLoc.currentValue
+      } {
+        val fk = CrmSchema.companyContacts.left(comp)
+        fk.exists(_.id == cnt.id) match {
+          case true =>
+            S notice l10n("Contact %s is already associated with %s",
+              cnt.linkName, comp.linkName)
+          case false =>
+            fk.associate(cnt)
+            S notice l10n("Added contact %s", cnt.linkName)
+        }
+        S redirectTo viewLoc.calcHref(comp)
+      }}
+    }
+  }
+
 }
 
 // vim: set ts=2 sw=2 et:
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/webapp/company/add-contact.html	Fri Apr 20 08:44:36 2012 +0200
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta content="text/html; charset=UTF-8" http-equiv="content-type" />
+    <title>Entity View</title>
+  </head>
+  <body class="lift:content_id=main">
+    <div id="main" class="lift:surround?with=default;at=content">
+      <div class="row">
+        <div class="span5">
+          <span class="lift:panel"></span>
+        </div>
+      </div> <!-- /row -->
+    </div>
+  </body>
+</html>
+
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/main/webapp/company/view.html	Fri Apr 20 08:44:36 2012 +0200
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <meta content="text/html; charset=UTF-8" http-equiv="content-type" />
+    <title>Entity View</title>
+  </head>
+  <body class="lift:content_id=main">
+    <div id="main" class="lift:surround?with=default;at=content">
+      <div class="row">
+        <div class="span12">
+          <span class="lift:panel"></span>
+        </div>
+      </div> <!-- /row -->
+      <div class="row section">
+        <div class="span12">
+          <h3><span class="lift:loc?locid=Contacts"></span></h3>
+          <span class="lift:contacts"></span>
+        </div>
+      </div> <!-- /row -->
+    </div>
+  </body>
+</html>
+
+
--- a/src/main/webapp/css/base.css	Fri Apr 20 08:44:28 2012 +0200
+++ b/src/main/webapp/css/base.css	Fri Apr 20 08:44:36 2012 +0200
@@ -77,3 +77,7 @@
 #user-label {
   font-size: 12px;
 }
+
+.section {
+  margin-top: 1em;
+}
--- a/src/main/webapp/templates-hidden/_resources.html	Fri Apr 20 08:44:28 2012 +0200
+++ b/src/main/webapp/templates-hidden/_resources.html	Fri Apr 20 08:44:36 2012 +0200
@@ -8,6 +8,9 @@
   <res name="Edit" lang="en" default="true">Edit</res>
   <res name="Add" lang="en" default="true">Add</res>
   <res name="linkName" lang="en" default="true">Name</res>
+  <!--
+    Remove
+  -->
 
   <!-- authn -->
   <res name="login.title" lang="en" default="true">Log In</res>
@@ -23,7 +26,15 @@
   <res name="error.invalid-password" lang="en" default="true">Invalid password.</res>
 
 
-  <-- contact -->
+  <-- contact
+    default strings:
+      Add contact
+      Add contact to %s
+      Add contact %s to %s
+      Really remove contact?
+      Contact %s is already associated with %s
+      Added contact %s
+  -->
   <res name="Contact" lang="en" default="true">Contact</res>
   <res name="Contact %s" lang="en" default="true">Contact %s</res>
   <res name="Contacts" lang="en" default="true">Contacts</res>
--- a/src/main/webapp/templates-hidden/_resources_cs.html	Fri Apr 20 08:44:28 2012 +0200
+++ b/src/main/webapp/templates-hidden/_resources_cs.html	Fri Apr 20 08:44:36 2012 +0200
@@ -8,6 +8,7 @@
   <res name="Edit" lang="cs">Upravit</res>
   <res name="Add" lang="cs">Přidat</res>
   <res name="linkName" lang="cs">Název</res>
+  <res name="Remove" lang="cs">Odebrat</res>
 
 
   <!-- authn -->
@@ -34,6 +35,12 @@
   <res name="Really delete this contact?" lang="cs">Skutečně smazat kontakt?</res>
   <res name="Contact %s deleted." lang="cs">Kontakt %s smazán.</res>
   <res name="invalid.email.address" lang="cs">Nesprávná emailová adresa.</res>
+  <res name="Add contact" lang="cs">Přidat kontakt</res>
+  <res name="Add contact to %s" lang="cs">Přidat kontakt k %s</res>
+  <res name="Add contact %s to %s" lang="cs">Přidat kontakt %s k %s</res>
+  <res name="Really remove contact?" lang="cs">Skutečně odebrat kontakt?</res>
+  <res name="Contact %s is already associated with %s" lang="cs">Kontakt %s je již asociován s %s.</res>
+  <res name="Added contact %s" lang="cs">Přidán kontakt %s.</res>
 
   <!-- contact fields -->
   <res name="contact.name" lang="cs">Jméno</res>