Allow person to be correspondent, concerning or both

This commit is contained in:
Eike Kettner 2021-02-16 22:37:56 +01:00
parent 567bfb3e69
commit 48eee00c0b
19 changed files with 321 additions and 94 deletions

View File

@ -198,6 +198,9 @@ val openapiScalaSettings = Seq(
case "listtype" =>
field =>
field.copy(typeDef = TypeDef("ListType", Imports("docspell.common.ListType")))
case "personuse" =>
field =>
field.copy(typeDef = TypeDef("PersonUse", Imports("docspell.common.PersonUse")))
}))
)

View File

@ -0,0 +1,50 @@
package docspell.common
import cats.data.NonEmptyList
import io.circe.Decoder
import io.circe.Encoder
sealed trait PersonUse { self: Product =>
final def name: String =
self.productPrefix.toLowerCase()
}
object PersonUse {
case object Correspondent extends PersonUse
case object Concerning extends PersonUse
case object Both extends PersonUse
def concerning: PersonUse = Concerning
def correspondent: PersonUse = Correspondent
def both: PersonUse = Both
val concerningAndBoth: NonEmptyList[PersonUse] =
NonEmptyList.of(Concerning, Both)
val correspondentAndBoth: NonEmptyList[PersonUse] =
NonEmptyList.of(Correspondent, Both)
def fromString(str: String): Either[String, PersonUse] =
str.toLowerCase() match {
case "correspondent" =>
Right(Correspondent)
case "concerning" =>
Right(Concerning)
case "both" =>
Right(Both)
case _ =>
Left(s"Unknown person-use: $str")
}
def unsafeFromString(str: String): PersonUse =
fromString(str).fold(sys.error, identity)
implicit val jsonDecoder: Decoder[PersonUse] =
Decoder.decodeString.emap(fromString)
implicit val jsonEncoder: Encoder[PersonUse] =
Encoder.encodeString.contramap(_.name)
}

View File

@ -57,7 +57,11 @@ object FindProposal {
ctx.store
.transact(
RPerson
.findLike(coll, mp.values.head.ref.name.toLowerCase, false)
.findLike(
coll,
mp.values.head.ref.name.toLowerCase,
PersonUse.correspondentAndBoth
)
.map(_.headOption)
)
.flatTap(oref =>
@ -67,7 +71,11 @@ object FindProposal {
ctx.store
.transact(
RPerson
.findLike(coll, mp.values.head.ref.name.toLowerCase, true)
.findLike(
coll,
mp.values.head.ref.name.toLowerCase,
PersonUse.concerningAndBoth
)
.map(_.headOption)
)
.flatTap(oref =>
@ -231,10 +239,16 @@ object FindProposal {
case NerTag.Person =>
val s1 = ctx.store
.transact(RPerson.findLike(ctx.args.meta.collective, value, true))
.transact(
RPerson
.findLike(ctx.args.meta.collective, value, PersonUse.concerningAndBoth)
)
.map(MetaProposalList.from(MetaProposalType.ConcPerson, nt))
val s2 = ctx.store
.transact(RPerson.findLike(ctx.args.meta.collective, value, false))
.transact(
RPerson
.findLike(ctx.args.meta.collective, value, PersonUse.correspondentAndBoth)
)
.map(MetaProposalList.from(MetaProposalType.CorrPerson, nt))
val s3 =
ctx.store
@ -288,10 +302,16 @@ object FindProposal {
.transact(ROrganization.findLike(ctx.args.meta.collective, kind, value))
.map(MetaProposalList.from(MetaProposalType.CorrOrg, nt))
val corrP = ctx.store
.transact(RPerson.findLike(ctx.args.meta.collective, kind, value, false))
.transact(
RPerson
.findLike(ctx.args.meta.collective, kind, value, PersonUse.correspondentAndBoth)
)
.map(MetaProposalList.from(MetaProposalType.CorrPerson, nt))
val concP = ctx.store
.transact(RPerson.findLike(ctx.args.meta.collective, kind, value, true))
.transact(
RPerson
.findLike(ctx.args.meta.collective, kind, value, PersonUse.concerningAndBoth)
)
.map(MetaProposalList.from(MetaProposalType.CorrPerson, nt))
ctx.logger.debug(s"Looking with $kind: $value") *>

View File

@ -236,7 +236,7 @@ object ScanMailboxTask {
ctx.args.account.collective,
from.address,
Some(ContactKind.Email),
Some(true)
Some(NonEmptyList.of(PersonUse.concerning))
)
.take(1)
.compile

View File

@ -4999,7 +4999,7 @@ components:
- address
- contacts
- created
- concerning
- use
properties:
id:
type: string
@ -5016,11 +5016,16 @@ components:
$ref: "#/components/schemas/Contact"
notes:
type: string
concerning:
type: boolean
use:
type: string
format: personuse
enum:
- concerning
- correspondent
- both
description: |
Whether this person should be used to create suggestions
for the "concerning person" association.
for the "concerning person", "correspondent" or both.
created:
description: DateTime
type: integer

View File

@ -445,7 +445,7 @@ trait Conversions {
Address(rp.street, rp.zip, rp.city, rp.country),
v.contacts.map(mkContact).toList,
rp.notes,
rp.concerning,
rp.use,
rp.created
)
}
@ -466,10 +466,10 @@ trait Conversions {
v.address.city,
v.address.country,
v.notes,
v.concerning,
now,
now,
v.organization.map(_.id)
v.organization.map(_.id),
v.use
)
} yield OOrganization.PersonAndContacts(pers, None, cont)
}
@ -492,10 +492,10 @@ trait Conversions {
v.address.city,
v.address.country,
v.notes,
v.concerning,
v.created,
now,
v.organization.map(_.id)
v.organization.map(_.id),
v.use
)
} yield OOrganization.PersonAndContacts(pers, None, cont)
}

View File

@ -0,0 +1,9 @@
ALTER TABLE "person"
ADD COLUMN "person_use" varchar(254);
UPDATE "person" SET "person_use" = 'concerning' where "concerning" = true;
UPDATE "person" SET "person_use" = 'correspondent' where "concerning" = false;
UPDATE "person" SET "person_use" = 'both' where "concerning" is null;
ALTER TABLE "person"
DROP COLUMN "concerning";

View File

@ -0,0 +1,9 @@
ALTER TABLE `person`
ADD COLUMN `person_use` varchar(254);
UPDATE `person` SET `person_use` = 'concerning' where `concerning` = true;
UPDATE `person` SET `person_use` = 'correspondent' where `concerning` = false;
UPDATE `person` SET `person_use` = 'both' where `concerning` is null;
ALTER TABLE `person`
DROP COLUMN `concerning`;

View File

@ -0,0 +1,9 @@
ALTER TABLE "person"
ADD COLUMN "person_use" varchar(254);
UPDATE "person" SET "person_use" = 'concerning' where "concerning" = true;
UPDATE "person" SET "person_use" = 'correspondent' where "concerning" = false;
UPDATE "person" SET "person_use" = 'both' where "concerning" is null;
ALTER TABLE "person"
DROP COLUMN "concerning";

View File

@ -103,6 +103,9 @@ trait DoobieMeta extends EmilDoobieMeta {
implicit val metaListType: Meta[ListType] =
Meta[String].timap(ListType.unsafeFromString)(_.name)
implicit val metaPersonUse: Meta[PersonUse] =
Meta[String].timap(PersonUse.unsafeFromString)(_.name)
}
object DoobieMeta extends DoobieMeta {

View File

@ -1,5 +1,6 @@
package docspell.store.queries
import cats.data.NonEmptyList
import cats.implicits._
import fs2._
@ -121,13 +122,13 @@ object QOrganization {
coll: Ident,
value: String,
ck: Option[ContactKind],
concerning: Option[Boolean]
use: Option[NonEmptyList[PersonUse]]
): Stream[ConnectionIO, RPerson] =
runDistinct(
select(p.all),
from(p).innerJoin(c, c.personId === p.pid),
c.value.like(s"%${value.toLowerCase}%") && p.cid === coll &&?
concerning.map(c => p.concerning === c) &&?
use.map(u => p.use.in(u)) &&?
ck.map(k => c.kind === k)
).query[RPerson].stream

View File

@ -21,10 +21,10 @@ case class RPerson(
city: String,
country: String,
notes: Option[String],
concerning: Boolean,
created: Timestamp,
updated: Timestamp,
oid: Option[Ident]
oid: Option[Ident],
use: PersonUse
) {}
object RPerson {
@ -34,18 +34,18 @@ object RPerson {
final case class Table(alias: Option[String]) extends TableDef {
val tableName = "person"
val pid = Column[Ident]("pid", this)
val cid = Column[Ident]("cid", this)
val name = Column[String]("name", this)
val street = Column[String]("street", this)
val zip = Column[String]("zip", this)
val city = Column[String]("city", this)
val country = Column[String]("country", this)
val notes = Column[String]("notes", this)
val concerning = Column[Boolean]("concerning", this)
val created = Column[Timestamp]("created", this)
val updated = Column[Timestamp]("updated", this)
val oid = Column[Ident]("oid", this)
val pid = Column[Ident]("pid", this)
val cid = Column[Ident]("cid", this)
val name = Column[String]("name", this)
val street = Column[String]("street", this)
val zip = Column[String]("zip", this)
val city = Column[String]("city", this)
val country = Column[String]("country", this)
val notes = Column[String]("notes", this)
val created = Column[Timestamp]("created", this)
val updated = Column[Timestamp]("updated", this)
val oid = Column[Ident]("oid", this)
val use = Column[PersonUse]("person_use", this)
val all = NonEmptyList.of[Column[_]](
pid,
cid,
@ -55,10 +55,10 @@ object RPerson {
city,
country,
notes,
concerning,
created,
updated,
oid
oid,
use
)
}
@ -70,7 +70,7 @@ object RPerson {
DML.insert(
T,
T.all,
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}"
fr"${v.pid},${v.cid},${v.name},${v.street},${v.zip},${v.city},${v.country},${v.notes},${v.created},${v.updated},${v.oid},${v.use}"
)
def update(v: RPerson): ConnectionIO[Int] = {
@ -85,7 +85,7 @@ object RPerson {
T.zip.setTo(v.zip),
T.city.setTo(v.city),
T.country.setTo(v.country),
T.concerning.setTo(v.concerning),
T.use.setTo(v.use),
T.notes.setTo(v.notes),
T.oid.setTo(v.oid),
T.updated.setTo(now)
@ -116,19 +116,19 @@ object RPerson {
def findLike(
coll: Ident,
personName: String,
concerningOnly: Boolean
use: NonEmptyList[PersonUse]
): ConnectionIO[Vector[IdRef]] =
run(
select(T.pid, T.name),
from(T),
where(T.cid === coll, T.concerning === concerningOnly, T.name.like(personName))
where(T.cid === coll, T.use.in(use), T.name.like(personName))
).query[IdRef].to[Vector]
def findLike(
coll: Ident,
contactKind: ContactKind,
value: String,
concerningOnly: Boolean
use: NonEmptyList[PersonUse]
): ConnectionIO[Vector[IdRef]] = {
val p = RPerson.as("p")
val c = RContact.as("c")
@ -139,7 +139,7 @@ object RPerson {
where(
p.cid === coll,
c.kind === contactKind,
p.concerning === concerningOnly,
p.use.in(use),
c.value.like(value)
)
).query[IdRef].to[Vector]

View File

@ -0,0 +1,4 @@
module Comp.DateInput exposing (..)
import Html exposing (..)
import Html.Attributes exposing (..)

View File

@ -32,6 +32,7 @@ import Data.DropdownStyle
import Data.Fields
import Data.Flags exposing (Flags)
import Data.Icons as Icons
import Data.PersonUse
import Data.UiSettings exposing (UiSettings)
import DatePicker exposing (DatePicker)
import Html exposing (..)
@ -429,14 +430,14 @@ update flags msg model =
GetPersonResp (Ok ps) ->
let
( conc, corr ) =
List.partition .concerning ps.items
{ concerning, correspondent } =
Data.PersonUse.spanPersonList ps.items
concRefs =
List.map (\e -> IdName e.id e.name) conc
List.map (\e -> IdName e.id e.name) concerning
corrRefs =
List.map (\e -> IdName e.id e.name) corr
List.map (\e -> IdName e.id e.name) correspondent
res1 =
update flags (CorrPersonMsg (Comp.Dropdown.SetOptions corrRefs)) model

View File

@ -49,6 +49,7 @@ import Data.Direction
import Data.Fields exposing (Field)
import Data.Flags exposing (Flags)
import Data.ItemNav exposing (ItemNav)
import Data.PersonUse
import Data.UiSettings exposing (UiSettings)
import DatePicker
import Dict
@ -612,8 +613,8 @@ update key flags inav settings msg model =
GetPersonResp (Ok ps) ->
let
( conc, corr ) =
List.partition .concerning ps.items
{ concerning, correspondent } =
Data.PersonUse.spanPersonList ps.items
personDict =
List.map (\p -> ( p.id, p )) ps.items
@ -632,10 +633,10 @@ update key flags inav settings msg model =
\_ -> True
concRefs =
List.map (\e -> IdName e.id e.name) conc
List.map (\e -> IdName e.id e.name) concerning
corrRefs =
List.filter personFilter corr
List.filter personFilter correspondent
|> List.map (\e -> IdName e.id e.name)
mkPersonOption idref =

View File

@ -16,12 +16,14 @@ import Comp.AddressForm
import Comp.Basic as B
import Comp.ContactField
import Comp.Dropdown
import Comp.FixedDropdown
import Data.DropdownStyle as DS
import Data.Flags exposing (Flags)
import Data.PersonUse exposing (PersonUse)
import Data.UiSettings exposing (UiSettings)
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (onCheck, onInput)
import Html.Events exposing (onInput)
import Styles as S
@ -31,7 +33,8 @@ type alias Model =
, addressModel : Comp.AddressForm.Model
, contactModel : Comp.ContactField.Model
, notes : Maybe String
, concerning : Bool
, use : PersonUse
, useModel : Comp.FixedDropdown.Model PersonUse
, orgModel : Comp.Dropdown.Model IdName
}
@ -43,7 +46,11 @@ emptyModel =
, addressModel = Comp.AddressForm.emptyModel
, contactModel = Comp.ContactField.emptyModel
, notes = Nothing
, concerning = False
, use = Data.PersonUse.Both
, useModel =
Comp.FixedDropdown.initMap
Data.PersonUse.label
Data.PersonUse.all
, orgModel = Comp.Dropdown.orgDropdown
}
@ -68,7 +75,7 @@ getPerson model =
, address = Comp.AddressForm.getAddress model.addressModel
, contacts = Comp.ContactField.getContacts model.contactModel
, notes = model.notes
, concerning = model.concerning
, use = Data.PersonUse.asString model.use
, organization = org
}
@ -79,9 +86,9 @@ type Msg
| AddressMsg Comp.AddressForm.Msg
| ContactMsg Comp.ContactField.Msg
| SetNotes String
| SetConcerning Bool
| SetOrgs (List IdName)
| OrgDropdownMsg (Comp.Dropdown.Msg IdName)
| UseDropdownMsg (Comp.FixedDropdown.Msg PersonUse)
update : Flags -> Msg -> Model -> ( Model, Cmd Msg )
@ -108,7 +115,9 @@ update flags msg model =
| person = t
, name = t.name
, notes = t.notes
, concerning = t.concerning
, use =
Data.PersonUse.fromString t.use
|> Maybe.withDefault Data.PersonUse.Both
}
, Cmd.batch [ c1, c2, c3 ]
)
@ -149,8 +158,15 @@ update flags msg model =
, Cmd.none
)
SetConcerning _ ->
( { model | concerning = not model.concerning }, Cmd.none )
UseDropdownMsg lm ->
let
( nm, mu ) =
Comp.FixedDropdown.update lm model.useModel
newUse =
Maybe.withDefault model.use mu
in
( { model | useModel = nm, use = newUse }, Cmd.none )
OrgDropdownMsg lm ->
let
@ -185,16 +201,10 @@ view1 settings compact model =
]
[]
]
, div [ class "inline field" ]
[ div [ class "ui checkbox" ]
[ input
[ type_ "checkbox"
, checked model.concerning
, onCheck SetConcerning
]
[]
, label [] [ text "Use for concerning person suggestion only" ]
]
, div [ class "field" ]
[ label [] [ text "Use" ]
, Html.map UseDropdownMsg (Comp.FixedDropdown.view (makeUseItem model) model.useModel)
, label [] [ text "Use for concerning person suggestion only" ]
]
, div [ class "field" ]
[ label [] [ text "Organization" ]
@ -221,6 +231,12 @@ view1 settings compact model =
]
makeUseItem : Model -> Maybe (Comp.FixedDropdown.Item PersonUse)
makeUseItem model =
Just <|
Comp.FixedDropdown.Item model.use (Data.PersonUse.label model.use)
--- View2
@ -253,21 +269,21 @@ view2 mobile settings model =
]
, div [ class "mb-4" ]
[ label
[ class "inline-flex items-center"
, for "concerning"
[ class S.inputLabel
]
[ input
[ type_ "checkbox"
, checked model.concerning
, onCheck SetConcerning
, class S.checkboxInput
, name "concerning"
, id "concerning"
]
[]
, span [ class "ml-2" ]
[ text "Use for concerning person suggestion only"
]
[ text "Use of this person" ]
, Html.map UseDropdownMsg
(Comp.FixedDropdown.view2 (makeUseItem model) model.useModel)
, span [ class "opacity-50 text-sm" ]
[ case model.use of
Data.PersonUse.Concerning ->
text "Use as concerning person only"
Data.PersonUse.Correspondent ->
text "Use as correspondent person only"
Data.PersonUse.Both ->
text "Use as both concerning or correspondent person"
]
]
, div [ class "mb-4" ]

View File

@ -10,6 +10,7 @@ module Comp.PersonTable exposing
import Api.Model.Person exposing (Person)
import Comp.Basic as B
import Data.Flags exposing (Flags)
import Data.PersonUse
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (onClick)
@ -57,7 +58,7 @@ view model =
[ thead []
[ tr []
[ th [ class "collapsing" ] []
, th [ class "collapsing center aligned" ] [ text "Concerning" ]
, th [ class "collapsing center aligned" ] [ text "Use" ]
, th [] [ text "Name" ]
, th [] [ text "Organization" ]
, th [] [ text "Address" ]
@ -85,11 +86,10 @@ renderPersonLine model person =
]
]
, td [ class "center aligned" ]
[ if person.concerning then
i [ class "check square outline icon" ] []
else
i [ class "minus square outline icon" ] []
[ Data.PersonUse.fromString person.use
|> Maybe.withDefault Data.PersonUse.Both
|> Data.PersonUse.label
|> text
]
, td []
[ text person.name
@ -118,8 +118,8 @@ view2 model =
[ thead []
[ tr []
[ th [ class "w-px whitespace-nowrap" ] []
, th [ class "w-px whitespace-nowrap text-center pr-1 md:px-2" ]
[ text "Concerning"
, th [ class "text-left pr-1 md:px-2" ]
[ text "Use"
]
, th [ class "text-left" ] [ text "Name" ]
, th [ class "text-left hidden sm:table-cell" ] [ text "Organization" ]
@ -138,8 +138,13 @@ renderPersonLine2 model person =
, class S.tableRow
]
[ B.editLinkTableCell (Select person)
, td [ class "w-px whitespace-nowrap text-center" ]
[ Util.Html.checkbox2 person.concerning
, td [ class "text-left pr-1 md:px-2" ]
[ div [ class "label inline-flex text-sm" ]
[ Data.PersonUse.fromString person.use
|> Maybe.withDefault Data.PersonUse.Both
|> Data.PersonUse.label
|> text
]
]
, td []
[ text person.name

View File

@ -38,6 +38,7 @@ import Data.DropdownStyle as DS
import Data.Fields
import Data.Flags exposing (Flags)
import Data.Icons as Icons
import Data.PersonUse
import Data.UiSettings exposing (UiSettings)
import DatePicker exposing (DatePicker)
import Html exposing (..)
@ -561,14 +562,14 @@ updateDrop ddm flags settings msg model =
GetPersonResp (Ok ps) ->
let
( conc, corr ) =
List.partition .concerning ps.items
{ concerning, correspondent } =
Data.PersonUse.spanPersonList ps.items
concRefs =
List.map (\e -> IdName e.id e.name) conc
List.map (\e -> IdName e.id e.name) concerning
corrRefs =
List.map (\e -> IdName e.id e.name) corr
List.map (\e -> IdName e.id e.name) correspondent
next1 =
updateDrop ddm

View File

@ -0,0 +1,90 @@
module Data.PersonUse exposing
( PersonUse(..)
, all
, asString
, fromString
, label
, spanPersonList
)
import Api.Model.Person exposing (Person)
type PersonUse
= Correspondent
| Concerning
| Both
fromString : String -> Maybe PersonUse
fromString str =
case String.toLower str of
"concerning" ->
Just Concerning
"correspondent" ->
Just Correspondent
"both" ->
Just Both
_ ->
Nothing
asString : PersonUse -> String
asString pu =
case pu of
Correspondent ->
"correspondent"
Concerning ->
"concerning"
Both ->
"both"
label : PersonUse -> String
label pu =
case pu of
Correspondent ->
"Correspondent"
Concerning ->
"Concerning"
Both ->
"Both"
all : List PersonUse
all =
[ Correspondent, Concerning, Both ]
spanPersonList : List Person -> { concerning : List Person, correspondent : List Person }
spanPersonList input =
let
init =
{ concerning = [], correspondent = [] }
parseUse p =
fromString p.use
|> Maybe.withDefault Both
merge p res =
case parseUse p of
Concerning ->
{ res | concerning = p :: res.concerning }
Correspondent ->
{ res | correspondent = p :: res.correspondent }
Both ->
{ res
| correspondent = p :: res.correspondent
, concerning = p :: res.concerning
}
in
List.foldl merge init input