--- 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>