From fc2668feee3d5c36747002196b441922fbc8f960 Mon Sep 17 00:00:00 2001 From: Eike Kettner Date: Mon, 30 Nov 2020 21:04:29 +0100 Subject: [PATCH 1/5] Allow to connect a person to an organization --- .../docspell/backend/ops/OOrganization.scala | 12 ++++-- .../src/main/resources/docspell-openapi.yml | 2 + .../restserver/conv/Conversions.scala | 29 +++++++------ .../db/migration/h2/V1.14.0__person_org.sql | 7 ++++ .../migration/mariadb/V1.14.0__person_org.sql | 7 ++++ .../postgresql/V1.14.0__person_org.sql | 7 ++++ .../store/queries/QOrganization.scala | 42 ++++++++++++------- .../docspell/store/records/RPerson.scala | 10 +++-- 8 files changed, 81 insertions(+), 35 deletions(-) create mode 100644 modules/store/src/main/resources/db/migration/h2/V1.14.0__person_org.sql create mode 100644 modules/store/src/main/resources/db/migration/mariadb/V1.14.0__person_org.sql create mode 100644 modules/store/src/main/resources/db/migration/postgresql/V1.14.0__person_org.sql diff --git a/modules/backend/src/main/scala/docspell/backend/ops/OOrganization.scala b/modules/backend/src/main/scala/docspell/backend/ops/OOrganization.scala index e007b1af..eba07e84 100644 --- a/modules/backend/src/main/scala/docspell/backend/ops/OOrganization.scala +++ b/modules/backend/src/main/scala/docspell/backend/ops/OOrganization.scala @@ -28,8 +28,10 @@ trait OOrganization[F[_]] { def findAllPersonRefs(account: AccountId, nameQuery: Option[String]): F[Vector[IdRef]] + /** Add a new person with their contacts. The additional organization is ignored. */ def addPerson(s: PersonAndContacts): F[AddResult] + /** Update a person with their contacts. The additional organization is ignored. */ def updatePerson(s: PersonAndContacts): F[AddResult] def deleteOrg(orgId: Ident, collective: Ident): F[AddResult] @@ -41,7 +43,11 @@ object OOrganization { case class OrgAndContacts(org: ROrganization, contacts: Seq[RContact]) - case class PersonAndContacts(person: RPerson, contacts: Seq[RContact]) + case class PersonAndContacts( + person: RPerson, + org: Option[ROrganization], + contacts: Seq[RContact] + ) def apply[F[_]: Effect](store: Store[F]): Resource[F, OOrganization[F]] = Resource.pure[F, OOrganization[F]](new OOrganization[F] { @@ -79,14 +85,14 @@ object OOrganization { ): F[Vector[PersonAndContacts]] = store .transact(QOrganization.findPersonAndContact(account.collective, query, _.name)) - .map({ case (person, cont) => PersonAndContacts(person, cont) }) + .map({ case (person, org, cont) => PersonAndContacts(person, org, cont) }) .compile .toVector def findPerson(account: AccountId, persId: Ident): F[Option[PersonAndContacts]] = store .transact(QOrganization.getPersonAndContact(account.collective, persId)) - .map(_.map({ case (org, cont) => PersonAndContacts(org, cont) })) + .map(_.map({ case (pers, org, cont) => PersonAndContacts(pers, org, cont) })) def findAllPersonRefs( account: AccountId, diff --git a/modules/restapi/src/main/resources/docspell-openapi.yml b/modules/restapi/src/main/resources/docspell-openapi.yml index db8a440d..70d7c979 100644 --- a/modules/restapi/src/main/resources/docspell-openapi.yml +++ b/modules/restapi/src/main/resources/docspell-openapi.yml @@ -4833,6 +4833,8 @@ components: format: ident name: type: string + organization: + $ref: "#/components/schemas/IdName" address: $ref: "#/components/schemas/Address" contacts: diff --git a/modules/restserver/src/main/scala/docspell/restserver/conv/Conversions.scala b/modules/restserver/src/main/scala/docspell/restserver/conv/Conversions.scala index b17ca5f7..bdba7bf6 100644 --- a/modules/restserver/src/main/scala/docspell/restserver/conv/Conversions.scala +++ b/modules/restserver/src/main/scala/docspell/restserver/conv/Conversions.scala @@ -414,15 +414,16 @@ trait Conversions { } def mkPerson(v: OOrganization.PersonAndContacts): Person = { - val ro = v.person + val rp = v.person Person( - ro.pid, - ro.name, - Address(ro.street, ro.zip, ro.city, ro.country), + rp.pid, + rp.name, + v.org.map(o => IdName(o.oid, o.name)), + Address(rp.street, rp.zip, rp.city, rp.country), v.contacts.map(mkContact).toList, - ro.notes, - ro.concerning, - ro.created + rp.notes, + rp.concerning, + rp.created ) } @@ -433,7 +434,7 @@ trait Conversions { now <- Timestamp.current[F] pid <- Ident.randomId[F] cont <- contacts(pid) - org = RPerson( + pers = RPerson( pid, cid, v.name, @@ -444,9 +445,10 @@ trait Conversions { v.notes, v.concerning, now, - now + now, + v.organization.map(_.id) ) - } yield OOrganization.PersonAndContacts(org, cont) + } yield OOrganization.PersonAndContacts(pers, None, cont) } def changePerson[F[_]: Sync]( @@ -458,7 +460,7 @@ trait Conversions { for { now <- Timestamp.current[F] cont <- contacts(v.id) - org = RPerson( + pers = RPerson( v.id, cid, v.name, @@ -469,9 +471,10 @@ trait Conversions { v.notes, v.concerning, v.created, - now + now, + v.organization.map(_.id) ) - } yield OOrganization.PersonAndContacts(org, cont) + } yield OOrganization.PersonAndContacts(pers, None, cont) } // contact diff --git a/modules/store/src/main/resources/db/migration/h2/V1.14.0__person_org.sql b/modules/store/src/main/resources/db/migration/h2/V1.14.0__person_org.sql new file mode 100644 index 00000000..82832cc8 --- /dev/null +++ b/modules/store/src/main/resources/db/migration/h2/V1.14.0__person_org.sql @@ -0,0 +1,7 @@ +ALTER TABLE "person" +ADD COLUMN "oid" varchar(254); + +ALTER TABLE "person" +ADD CONSTRAINT fk_person_organization +FOREIGN KEY ("oid") +REFERENCES "organization"("oid"); diff --git a/modules/store/src/main/resources/db/migration/mariadb/V1.14.0__person_org.sql b/modules/store/src/main/resources/db/migration/mariadb/V1.14.0__person_org.sql new file mode 100644 index 00000000..86b867ad --- /dev/null +++ b/modules/store/src/main/resources/db/migration/mariadb/V1.14.0__person_org.sql @@ -0,0 +1,7 @@ +ALTER TABLE `person` +ADD COLUMN `oid` varchar(254); + +ALTER TABLE `person` +ADD CONSTRAINT fk_person_organization +FOREIGN KEY (`oid`) +REFERENCES `organization`(`oid`); diff --git a/modules/store/src/main/resources/db/migration/postgresql/V1.14.0__person_org.sql b/modules/store/src/main/resources/db/migration/postgresql/V1.14.0__person_org.sql new file mode 100644 index 00000000..82832cc8 --- /dev/null +++ b/modules/store/src/main/resources/db/migration/postgresql/V1.14.0__person_org.sql @@ -0,0 +1,7 @@ +ALTER TABLE "person" +ADD COLUMN "oid" varchar(254); + +ALTER TABLE "person" +ADD CONSTRAINT fk_person_organization +FOREIGN KEY ("oid") +REFERENCES "organization"("oid"); diff --git a/modules/store/src/main/scala/docspell/store/queries/QOrganization.scala b/modules/store/src/main/scala/docspell/store/queries/QOrganization.scala index 50c0d2f2..d0234973 100644 --- a/modules/store/src/main/scala/docspell/store/queries/QOrganization.scala +++ b/modules/store/src/main/scala/docspell/store/queries/QOrganization.scala @@ -41,7 +41,7 @@ object QOrganization { Seq.empty }) - (selectSimple(cols, from, and(q)) ++ orderBy(order(OC).f)) + (selectSimple(cols, from, and(q)) ++ orderBy(order(OC).prefix("o").f)) .query[(ROrganization, Option[RContact])] .stream .groupAdjacentBy(_._1) @@ -82,17 +82,21 @@ object QOrganization { coll: Ident, query: Option[String], order: PC.type => Column - ): Stream[ConnectionIO, (RPerson, Vector[RContact])] = { + ): Stream[ConnectionIO, (RPerson, Option[ROrganization], Vector[RContact])] = { val pColl = PC.cid.prefix("p") val pName = RPerson.Columns.name.prefix("p") val pNotes = RPerson.Columns.notes.prefix("p") val pId = RPerson.Columns.pid.prefix("p") val cPers = RContact.Columns.personId.prefix("c") val cVal = RContact.Columns.value.prefix("c") + val oId = ROrganization.Columns.oid.prefix("o") + val pOid = RPerson.Columns.oid.prefix("p") - val cols = RPerson.Columns.all.map(_.prefix("p")) ++ RContact.Columns.all - .map(_.prefix("c")) + val cols = RPerson.Columns.all.map(_.prefix("p")) ++ + ROrganization.Columns.all.map(_.prefix("o")) ++ + RContact.Columns.all.map(_.prefix("c")) val from = RPerson.table ++ fr"p LEFT JOIN" ++ + ROrganization.table ++ fr"o ON" ++ pOid.is(oId) ++ fr"LEFT JOIN" ++ RContact.table ++ fr"c ON" ++ cPers.is(pId) val q = Seq(pColl.is(coll)) ++ (query match { @@ -103,38 +107,44 @@ object QOrganization { Seq.empty }) - (selectSimple(cols, from, and(q)) ++ orderBy(order(PC).f)) - .query[(RPerson, Option[RContact])] + (selectSimple(cols, from, and(q)) ++ orderBy(order(PC).prefix("p").f)) + .query[(RPerson, Option[ROrganization], Option[RContact])] .stream .groupAdjacentBy(_._1) - .map({ case (ro, chunk) => - val cs = chunk.toVector.flatMap(_._2) - (ro, cs) + .map({ case (rp, chunk) => + val cs = chunk.toVector.flatMap(_._3) + val ro = chunk.map(_._2).head.flatten + (rp, ro, cs) }) } def getPersonAndContact( coll: Ident, persId: Ident - ): ConnectionIO[Option[(RPerson, Vector[RContact])]] = { + ): ConnectionIO[Option[(RPerson, Option[ROrganization], Vector[RContact])]] = { val pColl = PC.cid.prefix("p") val pId = RPerson.Columns.pid.prefix("p") val cPers = RContact.Columns.personId.prefix("c") + val oId = ROrganization.Columns.oid.prefix("o") + val pOid = RPerson.Columns.oid.prefix("p") - val cols = RPerson.Columns.all.map(_.prefix("p")) ++ RContact.Columns.all - .map(_.prefix("c")) + val cols = RPerson.Columns.all.map(_.prefix("p")) ++ + ROrganization.Columns.all.map(_.prefix("o")) ++ + RContact.Columns.all.map(_.prefix("c")) val from = RPerson.table ++ fr"p LEFT JOIN" ++ + ROrganization.table ++ fr"o ON" ++ pOid.is(oId) ++ fr"LEFT JOIN" ++ RContact.table ++ fr"c ON" ++ cPers.is(pId) val q = and(pColl.is(coll), pId.is(persId)) selectSimple(cols, from, q) - .query[(RPerson, Option[RContact])] + .query[(RPerson, Option[ROrganization], Option[RContact])] .stream .groupAdjacentBy(_._1) - .map({ case (ro, chunk) => - val cs = chunk.toVector.flatMap(_._2) - (ro, cs) + .map({ case (rp, chunk) => + val cs = chunk.toVector.flatMap(_._3) + val ro = chunk.map(_._2).head.flatten + (rp, ro, cs) }) .compile .last diff --git a/modules/store/src/main/scala/docspell/store/records/RPerson.scala b/modules/store/src/main/scala/docspell/store/records/RPerson.scala index 87abb00c..b59d4cf4 100644 --- a/modules/store/src/main/scala/docspell/store/records/RPerson.scala +++ b/modules/store/src/main/scala/docspell/store/records/RPerson.scala @@ -21,7 +21,8 @@ case class RPerson( notes: Option[String], concerning: Boolean, created: Timestamp, - updated: Timestamp + updated: Timestamp, + oid: Option[Ident] ) {} object RPerson { @@ -42,6 +43,7 @@ object RPerson { val concerning = Column("concerning") val created = Column("created") val updated = Column("updated") + val oid = Column("oid") val all = List( pid, cid, @@ -53,7 +55,8 @@ object RPerson { notes, concerning, created, - updated + updated, + oid ) } @@ -63,7 +66,7 @@ object RPerson { val sql = insertRow( table, all, - fr"${v.pid},${v.cid},${v.name},${v.street},${v.zip},${v.city},${v.country},${v.notes},${v.concerning},${v.created},${v.updated}" + fr"${v.pid},${v.cid},${v.name},${v.street},${v.zip},${v.city},${v.country},${v.notes},${v.concerning},${v.created},${v.updated},${v.oid}" ) sql.update.run } @@ -82,6 +85,7 @@ object RPerson { country.setTo(v.country), concerning.setTo(v.concerning), notes.setTo(v.notes), + oid.setTo(v.oid), updated.setTo(now) ) ) From 2e28c8e57bed5bb0215f3ae58be124599046e52c Mon Sep 17 00:00:00 2001 From: Eike Kettner Date: Mon, 30 Nov 2020 21:26:31 +0100 Subject: [PATCH 2/5] Harmonize table views --- .../src/main/elm/Comp/CustomFieldTable.elm | 4 +--- .../src/main/elm/Comp/EquipmentTable.elm | 18 ++++++++++---- .../webapp/src/main/elm/Comp/FolderTable.elm | 14 +++++------ modules/webapp/src/main/elm/Comp/OrgTable.elm | 24 +++++++++++++------ .../webapp/src/main/elm/Comp/PersonTable.elm | 20 ++++++++++++---- modules/webapp/src/main/elm/Comp/TagTable.elm | 18 ++++++++++---- 6 files changed, 68 insertions(+), 30 deletions(-) diff --git a/modules/webapp/src/main/elm/Comp/CustomFieldTable.elm b/modules/webapp/src/main/elm/Comp/CustomFieldTable.elm index 2dc28534..71f59ef9 100644 --- a/modules/webapp/src/main/elm/Comp/CustomFieldTable.elm +++ b/modules/webapp/src/main/elm/Comp/CustomFieldTable.elm @@ -8,11 +8,9 @@ module Comp.CustomFieldTable exposing ) import Api.Model.CustomField exposing (CustomField) -import Api.Model.CustomFieldList exposing (CustomFieldList) import Html exposing (..) import Html.Attributes exposing (..) import Html.Events exposing (onClick) -import Util.Html import Util.Time @@ -44,7 +42,7 @@ update msg model = view : Model -> List CustomField -> Html Msg view _ items = div [] - [ table [ class "ui very basic center aligned table" ] + [ table [ class "ui very basic aligned table" ] [ thead [] [ tr [] [ th [ class "collapsing" ] [] diff --git a/modules/webapp/src/main/elm/Comp/EquipmentTable.elm b/modules/webapp/src/main/elm/Comp/EquipmentTable.elm index f89d3afa..097983e8 100644 --- a/modules/webapp/src/main/elm/Comp/EquipmentTable.elm +++ b/modules/webapp/src/main/elm/Comp/EquipmentTable.elm @@ -47,10 +47,11 @@ update _ msg model = view : Model -> Html Msg view model = - table [ class "ui selectable table" ] + table [ class "ui very basic aligned table" ] [ thead [] [ tr [] - [ th [] [ text "Name" ] + [ th [ class "collapsing" ] [] + , th [] [ text "Name" ] ] ] , tbody [] @@ -62,9 +63,18 @@ renderEquipmentLine : Model -> Equipment -> Html Msg renderEquipmentLine model equip = tr [ classList [ ( "active", model.selected == Just equip ) ] - , onClick (Select equip) ] - [ td [] + [ td [ class "collapsing" ] + [ a + [ href "#" + , class "ui basic small blue label" + , onClick (Select equip) + ] + [ i [ class "edit icon" ] [] + , text "Edit" + ] + ] + , td [] [ text equip.name ] ] diff --git a/modules/webapp/src/main/elm/Comp/FolderTable.elm b/modules/webapp/src/main/elm/Comp/FolderTable.elm index 46fb55f2..2727eb99 100644 --- a/modules/webapp/src/main/elm/Comp/FolderTable.elm +++ b/modules/webapp/src/main/elm/Comp/FolderTable.elm @@ -43,15 +43,15 @@ update msg model = view : Model -> List FolderItem -> Html Msg view _ items = div [] - [ table [ class "ui very basic center aligned table" ] + [ table [ class "ui very basic aligned table" ] [ thead [] [ tr [] [ th [ class "collapsing" ] [] , th [] [ text "Name" ] , th [] [ text "Owner" ] - , th [] [ text "Owner or Member" ] - , th [] [ text "#Member" ] - , th [] [ text "Created" ] + , th [ class "collapsing" ] [ text "Owner or Member" ] + , th [ class "collapsing" ] [ text "#Member" ] + , th [ class "collapsing" ] [ text "Created" ] ] ] , tbody [] @@ -79,14 +79,14 @@ viewItem item = , td [] [ text item.owner.name ] - , td [] + , td [ class "center aligned" ] [ Util.Html.checkbox item.isMember ] - , td [] + , td [ class "center aligned" ] [ String.fromInt item.memberCount |> text ] - , td [] + , td [ class "center aligned" ] [ Util.Time.formatDateShort item.created |> text ] diff --git a/modules/webapp/src/main/elm/Comp/OrgTable.elm b/modules/webapp/src/main/elm/Comp/OrgTable.elm index e78e408a..7c1d3f09 100644 --- a/modules/webapp/src/main/elm/Comp/OrgTable.elm +++ b/modules/webapp/src/main/elm/Comp/OrgTable.elm @@ -16,14 +16,14 @@ import Util.Contact type alias Model = - { equips : List Organization + { orgs : List Organization , selected : Maybe Organization } emptyModel : Model emptyModel = - { equips = [] + { orgs = [] , selected = Nothing } @@ -38,7 +38,7 @@ update : Flags -> Msg -> Model -> ( Model, Cmd Msg ) update _ msg model = case msg of SetOrgs list -> - ( { model | equips = list, selected = Nothing }, Cmd.none ) + ( { model | orgs = list, selected = Nothing }, Cmd.none ) Select equip -> ( { model | selected = Just equip }, Cmd.none ) @@ -49,16 +49,17 @@ update _ msg model = view : Model -> Html Msg view model = - table [ class "ui selectable table" ] + table [ class "ui very basic aligned table" ] [ thead [] [ tr [] - [ th [ class "collapsing" ] [ text "Name" ] + [ th [ class "collapsing" ] [] + , th [ class "collapsing" ] [ text "Name" ] , th [] [ text "Address" ] , th [] [ text "Contact" ] ] ] , tbody [] - (List.map (renderOrgLine model) model.equips) + (List.map (renderOrgLine model) model.orgs) ] @@ -66,9 +67,18 @@ renderOrgLine : Model -> Organization -> Html Msg renderOrgLine model org = tr [ classList [ ( "active", model.selected == Just org ) ] - , onClick (Select org) ] [ td [ class "collapsing" ] + [ a + [ href "#" + , class "ui basic small blue label" + , onClick (Select org) + ] + [ i [ class "edit icon" ] [] + , text "Edit" + ] + ] + , td [ class "collapsing" ] [ text org.name ] , td [] diff --git a/modules/webapp/src/main/elm/Comp/PersonTable.elm b/modules/webapp/src/main/elm/Comp/PersonTable.elm index 0a97598e..d94a7a5b 100644 --- a/modules/webapp/src/main/elm/Comp/PersonTable.elm +++ b/modules/webapp/src/main/elm/Comp/PersonTable.elm @@ -49,11 +49,12 @@ update _ msg model = view : Model -> Html Msg view model = - table [ class "ui selectable table" ] + table [ class "ui very basic aligned table" ] [ thead [] [ tr [] - [ th [ class "collapsing" ] [ text "Name" ] - , th [ class "collapsing" ] [ text "Concerning" ] + [ th [ class "collapsing" ] [] + , th [ class "collapsing" ] [ text "Name" ] + , th [ class "collapsing center aligned" ] [ text "Concerning" ] , th [] [ text "Address" ] , th [] [ text "Contact" ] ] @@ -67,12 +68,21 @@ renderPersonLine : Model -> Person -> Html Msg renderPersonLine model person = tr [ classList [ ( "active", model.selected == Just person ) ] - , onClick (Select person) ] [ td [ class "collapsing" ] - [ text person.name + [ a + [ href "#" + , class "ui basic small blue label" + , onClick (Select person) + ] + [ i [ class "edit icon" ] [] + , text "Edit" + ] ] , td [ class "collapsing" ] + [ text person.name + ] + , td [ class "center aligned" ] [ if person.concerning then i [ class "check square outline icon" ] [] diff --git a/modules/webapp/src/main/elm/Comp/TagTable.elm b/modules/webapp/src/main/elm/Comp/TagTable.elm index 97aa35cc..a191f5cc 100644 --- a/modules/webapp/src/main/elm/Comp/TagTable.elm +++ b/modules/webapp/src/main/elm/Comp/TagTable.elm @@ -47,10 +47,11 @@ update _ msg model = view : Model -> Html Msg view model = - table [ class "ui selectable table" ] + table [ class "ui very basic aligned table" ] [ thead [] [ tr [] - [ th [] [ text "Name" ] + [ th [ class "collapsing" ] [] + , th [ class "eight wide" ] [ text "Name" ] , th [] [ text "Category" ] ] ] @@ -63,9 +64,18 @@ renderTagLine : Model -> Tag -> Html Msg renderTagLine model tag = tr [ classList [ ( "active", model.selected == Just tag ) ] - , onClick (Select tag) ] - [ td [] + [ td [ class "collapsing" ] + [ a + [ href "#" + , class "ui basic small blue label" + , onClick (Select tag) + ] + [ i [ class "edit icon" ] [] + , text "Edit" + ] + ] + , td [] [ text tag.name ] , td [] From a6dd71af9f11959c9cde865650a431712505f0a6 Mon Sep 17 00:00:00 2001 From: Eike Kettner Date: Mon, 30 Nov 2020 22:07:16 +0100 Subject: [PATCH 3/5] Set an organization to a person in webapp --- .../webapp/src/main/elm/Comp/DetailEdit.elm | 60 +++++++++++-- modules/webapp/src/main/elm/Comp/Dropdown.elm | 13 +++ .../src/main/elm/Comp/ItemDetail/Update.elm | 40 +++++---- .../src/main/elm/Comp/ItemDetail/View.elm | 1 - .../webapp/src/main/elm/Comp/PersonForm.elm | 56 ++++++++++-- .../webapp/src/main/elm/Comp/PersonManage.elm | 88 ++++++++++++++----- .../webapp/src/main/elm/Comp/PersonTable.elm | 14 ++- .../webapp/src/main/elm/Comp/SearchMenu.elm | 8 +- 8 files changed, 215 insertions(+), 65 deletions(-) diff --git a/modules/webapp/src/main/elm/Comp/DetailEdit.elm b/modules/webapp/src/main/elm/Comp/DetailEdit.elm index 3e84ab4a..e666482d 100644 --- a/modules/webapp/src/main/elm/Comp/DetailEdit.elm +++ b/modules/webapp/src/main/elm/Comp/DetailEdit.elm @@ -30,6 +30,7 @@ import Api.Model.Equipment exposing (Equipment) import Api.Model.NewCustomField exposing (NewCustomField) import Api.Model.Organization exposing (Organization) import Api.Model.Person exposing (Person) +import Api.Model.ReferenceList exposing (ReferenceList) import Api.Model.Tag exposing (Tag) import Comp.CustomFieldForm import Comp.EquipmentForm @@ -134,7 +135,10 @@ editPerson flags persId pm = , loading = True , result = Nothing } - , Api.getPersonFull persId flags GetPersonResp + , Cmd.batch + [ Api.getPersonFull persId flags GetPersonResp + , Api.getOrgLight flags GetOrgsResp + ] ) @@ -150,14 +154,18 @@ editEquip flags equipId em = ) -initCorrPerson : String -> Comp.PersonForm.Model -> Model -initCorrPerson itemId pm = - init itemId (PMR pm) +initCorrPerson : Flags -> String -> Comp.PersonForm.Model -> ( Model, Cmd Msg ) +initCorrPerson flags itemId pm = + ( init itemId (PMR pm) + , Api.getOrgLight flags GetOrgsResp + ) -initConcPerson : String -> Comp.PersonForm.Model -> Model -initConcPerson itemId pm = - init itemId (PMC pm) +initConcPerson : Flags -> String -> Comp.PersonForm.Model -> ( Model, Cmd Msg ) +initConcPerson flags itemId pm = + ( init itemId (PMC pm) + , Api.getOrgLight flags GetOrgsResp + ) initTag : String -> Comp.TagForm.Model -> Model @@ -198,6 +206,7 @@ type Msg | GetOrgResp (Result Http.Error Organization) | GetPersonResp (Result Http.Error Person) | GetEquipResp (Result Http.Error Equipment) + | GetOrgsResp (Result Http.Error ReferenceList) type Value @@ -304,6 +313,43 @@ update flags msg model = , Nothing ) + GetOrgsResp (Ok list) -> + case model.form of + PMC pm -> + let + ( p_, c_ ) = + Comp.PersonForm.update flags (Comp.PersonForm.SetOrgs list.items) pm + in + ( { model + | loading = False + , form = PMC p_ + } + , Cmd.map PersonMsg c_ + , Nothing + ) + + PMR pm -> + let + ( p_, c_ ) = + Comp.PersonForm.update flags (Comp.PersonForm.SetOrgs list.items) pm + in + ( { model + | loading = False + , form = PMR p_ + } + , Cmd.map PersonMsg c_ + , Nothing + ) + + _ -> + ( { model | loading = False }, Cmd.none, Nothing ) + + GetOrgsResp (Err err) -> + ( { model | loading = False, result = Just (BasicResult False (Util.Http.errorToString err)) } + , Cmd.none + , Nothing + ) + GetEquipResp (Ok equip) -> case model.form of EM em -> diff --git a/modules/webapp/src/main/elm/Comp/Dropdown.elm b/modules/webapp/src/main/elm/Comp/Dropdown.elm index 8798abbe..936c0a55 100644 --- a/modules/webapp/src/main/elm/Comp/Dropdown.elm +++ b/modules/webapp/src/main/elm/Comp/Dropdown.elm @@ -10,6 +10,7 @@ module Comp.Dropdown exposing , makeSingleList , mkOption , notSelected + , orgDropdown , setMkOption , update , view @@ -19,6 +20,7 @@ module Comp.Dropdown exposing {-| This needs to be rewritten from scratch! -} +import Api.Model.IdName exposing (IdName) import Data.UiSettings exposing (UiSettings) import Html exposing (..) import Html.Attributes exposing (..) @@ -28,6 +30,17 @@ import Util.Html exposing (onKeyUp) import Util.List +orgDropdown : Model IdName +orgDropdown = + makeModel + { multiple = False + , searchable = \n -> n > 0 + , makeOption = \e -> { value = e.id, text = e.name, additional = "" } + , labelColor = \_ -> \_ -> "" + , placeholder = "Choose an organization" + } + + type alias Option = { value : String , text : String diff --git a/modules/webapp/src/main/elm/Comp/ItemDetail/Update.elm b/modules/webapp/src/main/elm/Comp/ItemDetail/Update.elm index 6d0341c9..0dcb4dd8 100644 --- a/modules/webapp/src/main/elm/Comp/ItemDetail/Update.elm +++ b/modules/webapp/src/main/elm/Comp/ItemDetail/Update.elm @@ -1113,26 +1113,30 @@ update key flags inav settings msg model = resultModel model StartCorrPersonModal -> - resultModel - { model - | modalEdit = - Just - (Comp.DetailEdit.initCorrPerson - model.item.id - Comp.PersonForm.emptyModel - ) - } + let + ( pm, pc ) = + Comp.DetailEdit.initCorrPerson + flags + model.item.id + Comp.PersonForm.emptyModel + in + resultModelCmd + ( { model | modalEdit = Just pm } + , Cmd.map ModalEditMsg pc + ) StartConcPersonModal -> - resultModel - { model - | modalEdit = - Just - (Comp.DetailEdit.initConcPerson - model.item.id - Comp.PersonForm.emptyModel - ) - } + let + ( p, c ) = + Comp.DetailEdit.initConcPerson + flags + model.item.id + Comp.PersonForm.emptyModel + in + resultModelCmd + ( { model | modalEdit = Just p } + , Cmd.map ModalEditMsg c + ) StartEditPersonModal pm -> let diff --git a/modules/webapp/src/main/elm/Comp/ItemDetail/View.elm b/modules/webapp/src/main/elm/Comp/ItemDetail/View.elm index 3f1dc968..f8a8f370 100644 --- a/modules/webapp/src/main/elm/Comp/ItemDetail/View.elm +++ b/modules/webapp/src/main/elm/Comp/ItemDetail/View.elm @@ -17,7 +17,6 @@ import Comp.LinkTarget import Comp.MarkdownInput import Comp.SentMails import Comp.YesNoDimmer -import Data.CustomFieldType import Data.Direction import Data.Fields import Data.Icons as Icons diff --git a/modules/webapp/src/main/elm/Comp/PersonForm.elm b/modules/webapp/src/main/elm/Comp/PersonForm.elm index 2fba1b03..c41c8721 100644 --- a/modules/webapp/src/main/elm/Comp/PersonForm.elm +++ b/modules/webapp/src/main/elm/Comp/PersonForm.elm @@ -9,9 +9,11 @@ module Comp.PersonForm exposing , view1 ) +import Api.Model.IdName exposing (IdName) import Api.Model.Person exposing (Person) import Comp.AddressForm import Comp.ContactField +import Comp.Dropdown import Data.Flags exposing (Flags) import Data.UiSettings exposing (UiSettings) import Html exposing (..) @@ -20,23 +22,25 @@ import Html.Events exposing (onCheck, onInput) type alias Model = - { org : Person + { person : Person , name : String , addressModel : Comp.AddressForm.Model , contactModel : Comp.ContactField.Model , notes : Maybe String , concerning : Bool + , orgModel : Comp.Dropdown.Model IdName } emptyModel : Model emptyModel = - { org = Api.Model.Person.empty + { person = Api.Model.Person.empty , name = "" , addressModel = Comp.AddressForm.emptyModel , contactModel = Comp.ContactField.emptyModel , notes = Nothing , concerning = False + , orgModel = Comp.Dropdown.orgDropdown } @@ -48,15 +52,20 @@ isValid model = getPerson : Model -> Person getPerson model = let - o = - model.org + person = + model.person + + org = + Comp.Dropdown.getSelected model.orgModel + |> List.head in - { o + { person | name = model.name , address = Comp.AddressForm.getAddress model.addressModel , contacts = Comp.ContactField.getContacts model.contactModel , notes = model.notes , concerning = model.concerning + , organization = org } @@ -67,6 +76,8 @@ type Msg | ContactMsg Comp.ContactField.Msg | SetNotes String | SetConcerning Bool + | SetOrgs (List IdName) + | OrgDropdownMsg (Comp.Dropdown.Msg IdName) update : Flags -> Msg -> Model -> ( Model, Cmd Msg ) @@ -79,16 +90,32 @@ update flags msg model = ( m2, c2 ) = update flags (ContactMsg (Comp.ContactField.SetItems t.contacts)) m1 + + ( m3, c3 ) = + update flags + (OrgDropdownMsg + (Comp.Dropdown.SetSelection + (List.filterMap identity [ t.organization ]) + ) + ) + m2 in - ( { m2 - | org = t + ( { m3 + | person = t , name = t.name , notes = t.notes , concerning = t.concerning } - , Cmd.batch [ c1, c2 ] + , Cmd.batch [ c1, c2, c3 ] ) + SetOrgs orgs -> + let + opts = + Comp.Dropdown.SetOptions orgs + in + update flags (OrgDropdownMsg opts) model + AddressMsg am -> let ( m1, c1 ) = @@ -121,6 +148,15 @@ update flags msg model = SetConcerning _ -> ( { model | concerning = not model.concerning }, Cmd.none ) + OrgDropdownMsg lm -> + let + ( dm_, cmd_ ) = + Comp.Dropdown.update lm model.orgModel + in + ( { model | orgModel = dm_ } + , Cmd.map OrgDropdownMsg cmd_ + ) + view : UiSettings -> Model -> Html Msg view settings model = @@ -156,6 +192,10 @@ view1 settings compact model = , label [] [ text "Use for concerning person suggestion only" ] ] ] + , div [ class "field" ] + [ label [] [ text "Organization" ] + , Html.map OrgDropdownMsg (Comp.Dropdown.view settings model.orgModel) + ] , h3 [ class "ui dividing header" ] [ text "Address" ] diff --git a/modules/webapp/src/main/elm/Comp/PersonManage.elm b/modules/webapp/src/main/elm/Comp/PersonManage.elm index beb3dd52..14241bf0 100644 --- a/modules/webapp/src/main/elm/Comp/PersonManage.elm +++ b/modules/webapp/src/main/elm/Comp/PersonManage.elm @@ -10,6 +10,7 @@ import Api import Api.Model.BasicResult exposing (BasicResult) import Api.Model.Person import Api.Model.PersonList exposing (PersonList) +import Api.Model.ReferenceList exposing (ReferenceList) import Comp.PersonForm import Comp.PersonTable import Comp.YesNoDimmer @@ -28,7 +29,7 @@ type alias Model = , formModel : Comp.PersonForm.Model , viewMode : ViewMode , formError : Maybe String - , loading : Bool + , loading : Int , deleteConfirm : Comp.YesNoDimmer.Model , query : String } @@ -45,7 +46,7 @@ emptyModel = , formModel = Comp.PersonForm.emptyModel , viewMode = Table , formError = Nothing - , loading = False + , loading = 0 , deleteConfirm = Comp.YesNoDimmer.emptyModel , query = "" } @@ -63,6 +64,7 @@ type Msg | YesNoMsg Comp.YesNoDimmer.Msg | RequestDelete | SetQuery String + | GetOrgResp (Result Http.Error ReferenceList) update : Flags -> Msg -> Model -> ( Model, Cmd Msg ) @@ -105,17 +107,35 @@ update flags msg model = ( { model | formModel = m2 }, Cmd.map FormMsg c2 ) LoadPersons -> - ( { model | loading = True }, Api.getPersons flags model.query PersonResp ) + ( { model | loading = model.loading + 2 } + , Cmd.batch + [ Api.getPersons flags model.query PersonResp + , Api.getOrgLight flags GetOrgResp + ] + ) - PersonResp (Ok orgs) -> + PersonResp (Ok persons) -> let m2 = - { model | viewMode = Table, loading = False } + { model + | viewMode = Table + , loading = Basics.max 0 (model.loading - 1) + } in - update flags (TableMsg (Comp.PersonTable.SetPersons orgs.items)) m2 + update flags (TableMsg (Comp.PersonTable.SetPersons persons.items)) m2 PersonResp (Err _) -> - ( { model | loading = False }, Cmd.none ) + ( { model | loading = Basics.max 0 (model.loading - 1) }, Cmd.none ) + + GetOrgResp (Ok list) -> + let + m2 = + { model | loading = Basics.max 0 (model.loading - 1) } + in + update flags (FormMsg (Comp.PersonForm.SetOrgs list.items)) m2 + + GetOrgResp (Err _) -> + ( { model | loading = Basics.max 0 (model.loading - 1) }, Cmd.none ) SetViewMode m -> let @@ -148,7 +168,9 @@ update flags msg model = Comp.PersonForm.isValid model.formModel in if valid then - ( { model | loading = True }, Api.postPerson flags person SubmitResp ) + ( { model | loading = model.loading + 1 } + , Api.postPerson flags person SubmitResp + ) else ( { model | formError = Just "Please correct the errors in the form." }, Cmd.none ) @@ -162,13 +184,23 @@ update flags msg model = ( m3, c3 ) = update flags LoadPersons m2 in - ( { m3 | loading = False }, Cmd.batch [ c2, c3 ] ) + ( { m3 | loading = Basics.max 0 (model.loading - 1) }, Cmd.batch [ c2, c3 ] ) else - ( { model | formError = Just res.message, loading = False }, Cmd.none ) + ( { model + | formError = Just res.message + , loading = Basics.max 0 (model.loading - 1) + } + , Cmd.none + ) SubmitResp (Err err) -> - ( { model | formError = Just (Util.Http.errorToString err), loading = False }, Cmd.none ) + ( { model + | formError = Just (Util.Http.errorToString err) + , loading = Basics.max 0 (model.loading - 1) + } + , Cmd.none + ) RequestDelete -> update flags (YesNoMsg Comp.YesNoDimmer.activate) model @@ -198,6 +230,11 @@ update flags msg model = ( m, Api.getPersons flags str PersonResp ) +isLoading : Model -> Bool +isLoading model = + model.loading /= 0 + + view : UiSettings -> Model -> Html Msg view settings model = if model.viewMode == Table then @@ -241,7 +278,7 @@ viewTable model = , div [ classList [ ( "ui dimmer", True ) - , ( "active", model.loading ) + , ( "active", isLoading model ) ] ] [ div [ class "ui loader" ] [] @@ -253,9 +290,9 @@ viewForm : UiSettings -> Model -> Html Msg viewForm settings model = let newPerson = - model.formModel.org.id == "" + model.formModel.person.id == "" in - Html.form [ class "ui segment", onSubmit Submit ] + div [ class "ui segment" ] [ Html.map YesNoMsg (Comp.YesNoDimmer.view model.deleteConfirm) , if newPerson then h3 [ class "ui dividing header" ] @@ -264,10 +301,10 @@ viewForm settings model = else h3 [ class "ui dividing header" ] - [ text ("Edit person: " ++ model.formModel.org.name) + [ text ("Edit person: " ++ model.formModel.person.name) , div [ class "sub header" ] [ text "Id: " - , text model.formModel.org.id + , text model.formModel.person.id ] ] , Html.map FormMsg (Comp.PersonForm.view settings model.formModel) @@ -280,14 +317,25 @@ viewForm settings model = [ Maybe.withDefault "" model.formError |> text ] , div [ class "ui horizontal divider" ] [] - , button [ class "ui primary button", type_ "submit" ] + , button + [ class "ui primary button" + , onClick Submit + ] [ text "Submit" ] - , a [ class "ui secondary button", onClick (SetViewMode Table), href "" ] + , a + [ class "ui secondary button" + , onClick (SetViewMode Table) + , href "" + ] [ text "Cancel" ] , if not newPerson then - a [ class "ui right floated red button", href "", onClick RequestDelete ] + a + [ class "ui right floated red button" + , href "" + , onClick RequestDelete + ] [ text "Delete" ] else @@ -295,7 +343,7 @@ viewForm settings model = , div [ classList [ ( "ui dimmer", True ) - , ( "active", model.loading ) + , ( "active", isLoading model ) ] ] [ div [ class "ui loader" ] [] diff --git a/modules/webapp/src/main/elm/Comp/PersonTable.elm b/modules/webapp/src/main/elm/Comp/PersonTable.elm index d94a7a5b..2296be79 100644 --- a/modules/webapp/src/main/elm/Comp/PersonTable.elm +++ b/modules/webapp/src/main/elm/Comp/PersonTable.elm @@ -53,8 +53,9 @@ view model = [ thead [] [ tr [] [ th [ class "collapsing" ] [] - , th [ class "collapsing" ] [ text "Name" ] , th [ class "collapsing center aligned" ] [ text "Concerning" ] + , th [] [ text "Name" ] + , th [] [ text "Organization" ] , th [] [ text "Address" ] , th [] [ text "Contact" ] ] @@ -79,9 +80,6 @@ renderPersonLine model person = , text "Edit" ] ] - , td [ class "collapsing" ] - [ text person.name - ] , td [ class "center aligned" ] [ if person.concerning then i [ class "check square outline icon" ] [] @@ -89,6 +87,14 @@ renderPersonLine model person = else i [ class "minus square outline icon" ] [] ] + , td [] + [ text person.name + ] + , td [] + [ Maybe.map .name person.organization + |> Maybe.withDefault "-" + |> text + ] , td [] [ Util.Address.toString person.address |> text ] diff --git a/modules/webapp/src/main/elm/Comp/SearchMenu.elm b/modules/webapp/src/main/elm/Comp/SearchMenu.elm index 9f677053..8c65c6fb 100644 --- a/modules/webapp/src/main/elm/Comp/SearchMenu.elm +++ b/modules/webapp/src/main/elm/Comp/SearchMenu.elm @@ -93,13 +93,7 @@ init = , selected = Nothing } , orgModel = - Comp.Dropdown.makeModel - { multiple = False - , searchable = \n -> n > 0 - , makeOption = \e -> { value = e.id, text = e.name, additional = "" } - , labelColor = \_ -> \_ -> "" - , placeholder = "Choose an organization" - } + Comp.Dropdown.orgDropdown , corrPersonModel = Comp.Dropdown.makeSingle { makeOption = \e -> { value = e.id, text = e.name, additional = "" } From d4470ab5fda2d5ecdfd6af127c518ceedca10fef Mon Sep 17 00:00:00 2001 From: Eike Kettner Date: Tue, 1 Dec 2020 00:28:58 +0100 Subject: [PATCH 4/5] Restrict person dropdown options to the associated organization --- .../src/main/elm/Comp/ItemDetail/Model.elm | 20 ++++++++ .../src/main/elm/Comp/ItemDetail/Update.elm | 48 +++++++++++++++++-- .../src/main/elm/Comp/ItemDetail/View.elm | 18 ++++++- 3 files changed, 80 insertions(+), 6 deletions(-) diff --git a/modules/webapp/src/main/elm/Comp/ItemDetail/Model.elm b/modules/webapp/src/main/elm/Comp/ItemDetail/Model.elm index ca659619..6345bbb3 100644 --- a/modules/webapp/src/main/elm/Comp/ItemDetail/Model.elm +++ b/modules/webapp/src/main/elm/Comp/ItemDetail/Model.elm @@ -7,6 +7,7 @@ module Comp.ItemDetail.Model exposing , UpdateResult , emptyModel , isEditNotes + , personMatchesOrg , resultModel , resultModelCmd , resultModelCmdSub @@ -20,6 +21,7 @@ import Api.Model.FolderList exposing (FolderList) import Api.Model.IdName exposing (IdName) import Api.Model.ItemDetail exposing (ItemDetail) import Api.Model.ItemProposals exposing (ItemProposals) +import Api.Model.Person exposing (Person) import Api.Model.PersonList exposing (PersonList) import Api.Model.ReferenceList exposing (ReferenceList) import Api.Model.SentMails exposing (SentMails) @@ -100,6 +102,7 @@ type alias Model = , customFieldSavingIcon : Dict String String , customFieldThrottle : Throttle Msg , allTags : List Tag + , allPersons : Dict String Person } @@ -205,6 +208,7 @@ emptyModel = , customFieldSavingIcon = Dict.empty , customFieldThrottle = Throttle.create 1 , allTags = [] + , allPersons = Dict.empty } @@ -322,3 +326,19 @@ resultModelCmd ( model, cmd ) = resultModelCmdSub : ( Model, Cmd Msg, Sub Msg ) -> UpdateResult resultModelCmdSub ( model, cmd, sub ) = UpdateResult model cmd sub Comp.LinkTarget.LinkNone + + +personMatchesOrg : Model -> Bool +personMatchesOrg model = + let + org = + Comp.Dropdown.getSelected model.corrOrgModel + |> List.head + + persOrg = + Comp.Dropdown.getSelected model.corrPersonModel + |> List.head + |> Maybe.andThen (\idref -> Dict.get idref.id model.allPersons) + |> Maybe.andThen .organization + in + org == Nothing || org == persOrg diff --git a/modules/webapp/src/main/elm/Comp/ItemDetail/Update.elm b/modules/webapp/src/main/elm/Comp/ItemDetail/Update.elm index 0dcb4dd8..00813cea 100644 --- a/modules/webapp/src/main/elm/Comp/ItemDetail/Update.elm +++ b/modules/webapp/src/main/elm/Comp/ItemDetail/Update.elm @@ -252,6 +252,7 @@ update key flags inav settings msg model = , getOptions flags , proposalCmd , Api.getSentMails flags item.id SentMailsResp + , Api.getPersons flags "" GetPersonResp , Cmd.map CustomFieldMsg (Comp.CustomFieldMultiInput.initCmd flags) ] , sub = @@ -347,12 +348,14 @@ update key flags inav settings msg model = ( m2, c2 ) = Comp.Dropdown.update m model.corrOrgModel - newModel = - { model | corrOrgModel = m2 } - idref = Comp.Dropdown.getSelected m2 |> List.head + newModel = + { model + | corrOrgModel = m2 + } + save = if isDropdownChangeMsg m then setCorrOrg flags newModel idref @@ -609,11 +612,46 @@ update key flags inav settings msg model = ( conc, corr ) = List.partition .concerning ps.items + personDict = + List.map (\p -> ( p.id, p )) ps.items + |> Dict.fromList + + corrOrg = + Comp.Dropdown.getSelected model.corrOrgModel + |> List.head + + personFilter = + case corrOrg of + Just n -> + \p -> p.organization == Just n + + Nothing -> + \_ -> True + concRefs = List.map (\e -> IdName e.id e.name) conc corrRefs = - List.map (\e -> IdName e.id e.name) corr + List.filter personFilter corr + |> List.map (\e -> IdName e.id e.name) + + mkPersonOption idref = + let + org = + Dict.get idref.id personDict + |> Maybe.andThen .organization + |> Maybe.map .name + |> Maybe.map (Util.String.ellipsis 15) + |> Maybe.withDefault "" + in + Comp.Dropdown.Option idref.id idref.name org + + model_ = + { model + | corrPersonModel = Comp.Dropdown.setMkOption mkPersonOption model.corrPersonModel + , concPersonModel = Comp.Dropdown.setMkOption mkPersonOption model.concPersonModel + , allPersons = personDict + } res1 = update key @@ -621,7 +659,7 @@ update key flags inav settings msg model = inav settings (CorrPersonMsg (Comp.Dropdown.SetOptions corrRefs)) - model + model_ res2 = update key diff --git a/modules/webapp/src/main/elm/Comp/ItemDetail/View.elm b/modules/webapp/src/main/elm/Comp/ItemDetail/View.elm index f8a8f370..2529b6b7 100644 --- a/modules/webapp/src/main/elm/Comp/ItemDetail/View.elm +++ b/modules/webapp/src/main/elm/Comp/ItemDetail/View.elm @@ -10,7 +10,14 @@ import Comp.DetailEdit import Comp.Dropdown import Comp.Dropzone import Comp.ItemDetail.AttachmentTabMenu -import Comp.ItemDetail.Model exposing (Model, Msg(..), NotesField(..), SaveNameState(..)) +import Comp.ItemDetail.Model + exposing + ( Model + , Msg(..) + , NotesField(..) + , SaveNameState(..) + , personMatchesOrg + ) import Comp.ItemMail import Comp.KeyInput import Comp.LinkTarget @@ -882,6 +889,15 @@ item visible. This message will disappear then. ] , Html.map CorrPersonMsg (Comp.Dropdown.view settings model.corrPersonModel) , renderCorrPersonSuggestions model + , div + [ classList + [ ( "ui warning message", True ) + , ( "invisible hidden", personMatchesOrg model ) + ] + ] + [ i [ class "info icon" ] [] + , text "The selected person doesn't belong to the selected organization." + ] ] , optional [ Data.Fields.ConcPerson, Data.Fields.ConcEquip ] <| h4 [ class "ui dividing header" ] From 290989f67fe562f848015f61e30523f2b9572a81 Mon Sep 17 00:00:00 2001 From: Eike Kettner Date: Tue, 1 Dec 2020 21:57:01 +0100 Subject: [PATCH 5/5] Reorder correspondent person suggestion based on org relationship --- .../scala/docspell/common/PersonRef.scala | 18 +++++ .../docspell/joex/process/EvalProposals.scala | 65 +++++++++++++++---- .../docspell/store/records/RPerson.scala | 12 ++++ 3 files changed, 82 insertions(+), 13 deletions(-) create mode 100644 modules/common/src/main/scala/docspell/common/PersonRef.scala diff --git a/modules/common/src/main/scala/docspell/common/PersonRef.scala b/modules/common/src/main/scala/docspell/common/PersonRef.scala new file mode 100644 index 00000000..e5624548 --- /dev/null +++ b/modules/common/src/main/scala/docspell/common/PersonRef.scala @@ -0,0 +1,18 @@ +package docspell.common + +import io.circe._ +import io.circe.generic.semiauto._ + +case class PersonRef(id: Ident, name: String, organization: Option[Ident]) { + + def toIdRef: IdRef = + IdRef(id, name) +} + +object PersonRef { + + implicit val jsonEncoder: Encoder[PersonRef] = + deriveEncoder[PersonRef] + implicit val jsonDecoder: Decoder[PersonRef] = + deriveDecoder[PersonRef] +} diff --git a/modules/joex/src/main/scala/docspell/joex/process/EvalProposals.scala b/modules/joex/src/main/scala/docspell/joex/process/EvalProposals.scala index 170958c9..772e9c03 100644 --- a/modules/joex/src/main/scala/docspell/joex/process/EvalProposals.scala +++ b/modules/joex/src/main/scala/docspell/joex/process/EvalProposals.scala @@ -6,8 +6,8 @@ import cats.effect.Sync import cats.implicits._ import docspell.common._ -import docspell.joex.scheduler.Task -import docspell.store.records.RAttachmentMeta +import docspell.joex.scheduler.{Context, Task} +import docspell.store.records.{RAttachmentMeta, RPerson} /** Calculate weights for candidates that adds the most likely * candidate a lower number. @@ -15,21 +15,40 @@ import docspell.store.records.RAttachmentMeta object EvalProposals { def apply[F[_]: Sync](data: ItemData): Task[F, ProcessItemArgs, ItemData] = - Task { _ => - Timestamp - .current[F] - .map { now => - val metas = data.metas.map(calcCandidateWeight(now.toUtcDate)) - data.copy(metas = metas) - } + Task { ctx => + for { + now <- Timestamp.current[F] + personRefs <- findOrganizationRelation[F](data, ctx) + metas = data.metas.map(calcCandidateWeight(now.toUtcDate, personRefs)) + } yield data.copy(metas = metas) } - def calcCandidateWeight(now: LocalDate)(rm: RAttachmentMeta): RAttachmentMeta = { - val list = rm.proposals.change(mp => mp.addWeights(weight(rm, mp, now))) + def findOrganizationRelation[F[_]: Sync]( + data: ItemData, + ctx: Context[F, _] + ): F[Map[Ident, PersonRef]] = { + val corrPersIds = data.metas + .flatMap(_.proposals.find(MetaProposalType.CorrPerson)) + .flatMap(_.values.toList.map(_.ref.id)) + .toSet + ctx.store + .transact(RPerson.findOrganization(corrPersIds)) + .map(_.map(p => (p.id, p)).toMap) + } + + def calcCandidateWeight(now: LocalDate, personRefs: Map[Ident, PersonRef])( + rm: RAttachmentMeta + ): RAttachmentMeta = { + val list = rm.proposals.change(mp => mp.addWeights(weight(rm, mp, now, personRefs))) rm.copy(proposals = list.sortByWeights) } - def weight(rm: RAttachmentMeta, mp: MetaProposal, ref: LocalDate)( + def weight( + rm: RAttachmentMeta, + mp: MetaProposal, + ref: LocalDate, + personRefs: Map[Ident, PersonRef] + )( cand: MetaProposal.Candidate ): Double = mp.proposalType match { @@ -51,7 +70,27 @@ object EvalProposals { val words = cand.origin.map(_.label.split(' ').length).max.toDouble val nerFac = cand.origin.map(label => nerTagFactor(label.tag, mp.proposalType)).min - (1 / words) * (1 / tagCount) * positionWeight(pos, textLen) * nerFac + val corrPerFac = corrOrgPersonFactor(rm, mp, personRefs, cand) + (1 / words) * (1 / tagCount) * positionWeight(pos, textLen) * nerFac * corrPerFac + } + + def corrOrgPersonFactor( + rm: RAttachmentMeta, + mp: MetaProposal, + personRefs: Map[Ident, PersonRef], + cand: MetaProposal.Candidate + ): Double = + mp.proposalType match { + case MetaProposalType.CorrPerson => + (for { + currentOrg <- rm.proposals + .find(MetaProposalType.CorrOrg) + .map(_.values.head.ref.id) + personOrg <- personRefs.get(cand.ref.id).flatMap(_.organization) + fac = if (currentOrg == personOrg) 0.5 else 1 + } yield fac).getOrElse(1) + case _ => + 1 } def positionWeight(pos: Int, total: Int): Double = diff --git a/modules/store/src/main/scala/docspell/store/records/RPerson.scala b/modules/store/src/main/scala/docspell/store/records/RPerson.scala index b59d4cf4..c7df6fed 100644 --- a/modules/store/src/main/scala/docspell/store/records/RPerson.scala +++ b/modules/store/src/main/scala/docspell/store/records/RPerson.scala @@ -1,6 +1,8 @@ package docspell.store.records import cats.Eq +import cats.data.NonEmptyList +import cats.effect._ import fs2.Stream import docspell.common.{IdRef, _} @@ -167,4 +169,14 @@ object RPerson { def delete(personId: Ident, coll: Ident): ConnectionIO[Int] = deleteFrom(table, and(pid.is(personId), cid.is(coll))).update.run + + def findOrganization(ids: Set[Ident]): ConnectionIO[Vector[PersonRef]] = { + val cols = Seq(pid, name, oid) + NonEmptyList.fromList(ids.toList) match { + case Some(nel) => + selectSimple(cols, table, pid.isIn(nel)).query[PersonRef].to[Vector] + case None => + Sync[ConnectionIO].pure(Vector.empty) + } + } }