From 813797756cffe1d0f89b7618639b2dabf2123601 Mon Sep 17 00:00:00 2001
From: eikek <eike.kettner@posteo.de>
Date: Tue, 5 Oct 2021 13:50:31 +0200
Subject: [PATCH] Extend search stats to fully populate search menu

Refs: #856
---
 .../src/main/resources/docspell-openapi.yml   | 34 +++++++++++++
 .../restserver/conv/Conversions.scala         | 14 ++++--
 .../docspell/store/queries/IdRefCount.scala   |  5 ++
 .../scala/docspell/store/queries/QItem.scala  | 50 ++++++++++++++++++-
 .../store/queries/SearchSummary.scala         | 12 ++++-
 .../main/elm/Comp/CustomFieldMultiInput.elm   |  6 +++
 .../webapp/src/main/elm/Comp/SearchMenu.elm   | 42 ++++++++++++++++
 .../webapp/src/main/elm/Util/CustomField.elm  | 12 +++++
 8 files changed, 167 insertions(+), 8 deletions(-)
 create mode 100644 modules/store/src/main/scala/docspell/store/queries/IdRefCount.scala

diff --git a/modules/restapi/src/main/resources/docspell-openapi.yml b/modules/restapi/src/main/resources/docspell-openapi.yml
index 46a4766a..0c0dd351 100644
--- a/modules/restapi/src/main/resources/docspell-openapi.yml
+++ b/modules/restapi/src/main/resources/docspell-openapi.yml
@@ -5305,6 +5305,10 @@ components:
         - tagCategoryCloud
         - fieldStats
         - folderStats
+        - corrOrgStats
+        - corrPersStats
+        - concPersStats
+        - concEquipStats
       properties:
         count:
           type: integer
@@ -5321,6 +5325,23 @@ components:
           type: array
           items:
             $ref: "#/components/schemas/FolderStats"
+        corrOrgStats:
+          type: array
+          items:
+            $ref: "#/components/schemas/IdRefStats"
+        corrPersStats:
+          type: array
+          items:
+            $ref: "#/components/schemas/IdRefStats"
+        concPersStats:
+          type: array
+          items:
+            $ref: "#/components/schemas/IdRefStats"
+        concEquipStats:
+          type: array
+          items:
+            $ref: "#/components/schemas/IdRefStats"
+
     ItemInsights:
       description: |
         Information about the items in docspell.
@@ -5454,6 +5475,19 @@ components:
           type: integer
           format: int32
 
+    IdRefStats:
+      description: |
+        Counting some objects that have an id and a name.
+      required:
+        - ref
+        - count
+      properties:
+        ref:
+          $ref: "#/components/schemas/IdName"
+        count:
+          type: integer
+          format: int32
+
     AttachmentMeta:
       description: |
         Extracted meta data of an attachment.
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 7cc03c6b..be89e955 100644
--- a/modules/restserver/src/main/scala/docspell/restserver/conv/Conversions.scala
+++ b/modules/restserver/src/main/scala/docspell/restserver/conv/Conversions.scala
@@ -7,11 +7,9 @@
 package docspell.restserver.conv
 
 import java.time.{LocalDate, ZoneId}
-
 import cats.effect.{Async, Sync}
 import cats.implicits._
 import fs2.Stream
-
 import docspell.backend.ops.OCollective.{InsightData, PassChangeResult}
 import docspell.backend.ops.OCustomFields.SetValueResult
 import docspell.backend.ops.OJob.JobCancelResult
@@ -22,10 +20,9 @@ import docspell.common.syntax.all._
 import docspell.ftsclient.FtsResult
 import docspell.restapi.model._
 import docspell.restserver.conv.Conversions._
-import docspell.store.queries.{AttachmentLight => QAttachmentLight}
+import docspell.store.queries.{AttachmentLight => QAttachmentLight, IdRefCount}
 import docspell.store.records._
 import docspell.store.{AddResult, UpdateResult}
-
 import org.http4s.headers.`Content-Type`
 import org.http4s.multipart.Multipart
 import org.log4s.Logger
@@ -38,9 +35,16 @@ trait Conversions {
       mkTagCloud(sum.tags),
       mkTagCategoryCloud(sum.cats),
       sum.fields.map(mkFieldStats),
-      sum.folders.map(mkFolderStats)
+      sum.folders.map(mkFolderStats),
+      sum.corrOrgs.map(mkIdRefStats),
+      sum.corrPers.map(mkIdRefStats),
+      sum.concPers.map(mkIdRefStats),
+      sum.concEquip.map(mkIdRefStats)
     )
 
+  def mkIdRefStats(s: IdRefCount): IdRefStats =
+    IdRefStats(mkIdName(s.ref), s.count)
+
   def mkFolderStats(fs: docspell.store.queries.FolderCount): FolderStats =
     FolderStats(fs.id, fs.name, mkIdName(fs.owner), fs.count)
 
diff --git a/modules/store/src/main/scala/docspell/store/queries/IdRefCount.scala b/modules/store/src/main/scala/docspell/store/queries/IdRefCount.scala
new file mode 100644
index 00000000..20c2fbdf
--- /dev/null
+++ b/modules/store/src/main/scala/docspell/store/queries/IdRefCount.scala
@@ -0,0 +1,5 @@
+package docspell.store.queries
+
+import docspell.common._
+
+final case class IdRefCount(ref: IdRef, count: Int) {}
diff --git a/modules/store/src/main/scala/docspell/store/queries/QItem.scala b/modules/store/src/main/scala/docspell/store/queries/QItem.scala
index 2671fcaa..623b68e0 100644
--- a/modules/store/src/main/scala/docspell/store/queries/QItem.scala
+++ b/modules/store/src/main/scala/docspell/store/queries/QItem.scala
@@ -192,7 +192,21 @@ object QItem {
       cats <- searchTagCategorySummary(today)(q)
       fields <- searchFieldSummary(today)(q)
       folders <- searchFolderSummary(today)(q)
-    } yield SearchSummary(count, tags, cats, fields, folders)
+      orgs <- searchCorrOrgSummary(today)(q)
+      corrPers <- searchCorrPersonSummary(today)(q)
+      concPers <- searchConcPersonSummary(today)(q)
+      concEquip <- searchConcEquipSummary(today)(q)
+    } yield SearchSummary(
+      count,
+      tags,
+      cats,
+      fields,
+      folders,
+      orgs,
+      corrPers,
+      concPers,
+      concEquip
+    )
 
   def searchTagCategorySummary(
       today: LocalDate
@@ -251,6 +265,40 @@ object QItem {
       .query[Int]
       .unique
 
+  def searchCorrOrgSummary(today: LocalDate)(q: Query): ConnectionIO[List[IdRefCount]] =
+    searchIdRefSummary(org.oid, org.name, i.corrOrg, today)(q)
+
+  def searchCorrPersonSummary(today: LocalDate)(
+      q: Query
+  ): ConnectionIO[List[IdRefCount]] =
+    searchIdRefSummary(pers0.pid, pers0.name, i.corrPerson, today)(q)
+
+  def searchConcPersonSummary(today: LocalDate)(
+      q: Query
+  ): ConnectionIO[List[IdRefCount]] =
+    searchIdRefSummary(pers1.pid, pers1.name, i.concPerson, today)(q)
+
+  def searchConcEquipSummary(today: LocalDate)(
+      q: Query
+  ): ConnectionIO[List[IdRefCount]] =
+    searchIdRefSummary(equip.eid, equip.name, i.concEquipment, today)(q)
+
+  private def searchIdRefSummary(
+      idCol: Column[Ident],
+      nameCol: Column[String],
+      fkCol: Column[Ident],
+      today: LocalDate
+  )(q: Query): ConnectionIO[List[IdRefCount]] =
+    findItemsBase(q.fix, today, 0).unwrap
+      .withSelect(select(idCol, nameCol).append(count(idCol).as("num")))
+      .changeWhere(c =>
+        c && fkCol.isNotNull && queryCondition(today, q.fix.account.collective, q.cond)
+      )
+      .groupBy(idCol, nameCol)
+      .build
+      .query[IdRefCount]
+      .to[List]
+
   def searchFolderSummary(today: LocalDate)(q: Query): ConnectionIO[List[FolderCount]] = {
     val fu = RUser.as("fu")
     findItemsBase(q.fix, today, 0).unwrap
diff --git a/modules/store/src/main/scala/docspell/store/queries/SearchSummary.scala b/modules/store/src/main/scala/docspell/store/queries/SearchSummary.scala
index 1eeaef2e..c6bff383 100644
--- a/modules/store/src/main/scala/docspell/store/queries/SearchSummary.scala
+++ b/modules/store/src/main/scala/docspell/store/queries/SearchSummary.scala
@@ -11,7 +11,11 @@ case class SearchSummary(
     tags: List[TagCount],
     cats: List[CategoryCount],
     fields: List[FieldStats],
-    folders: List[FolderCount]
+    folders: List[FolderCount],
+    corrOrgs: List[IdRefCount],
+    corrPers: List[IdRefCount],
+    concPers: List[IdRefCount],
+    concEquip: List[IdRefCount]
 ) {
 
   def onlyExisting: SearchSummary =
@@ -20,6 +24,10 @@ case class SearchSummary(
       tags.filter(_.count > 0),
       cats.filter(_.count > 0),
       fields.filter(_.count > 0),
-      folders.filter(_.count > 0)
+      folders.filter(_.count > 0),
+      corrOrgs = corrOrgs.filter(_.count > 0),
+      corrPers = corrPers.filter(_.count > 0),
+      concPers = concPers.filter(_.count > 0),
+      concEquip = concEquip.filter(_.count > 0)
     )
 }
diff --git a/modules/webapp/src/main/elm/Comp/CustomFieldMultiInput.elm b/modules/webapp/src/main/elm/Comp/CustomFieldMultiInput.elm
index 27d11480..6a60260e 100644
--- a/modules/webapp/src/main/elm/Comp/CustomFieldMultiInput.elm
+++ b/modules/webapp/src/main/elm/Comp/CustomFieldMultiInput.elm
@@ -16,6 +16,7 @@ module Comp.CustomFieldMultiInput exposing
     , isEmpty
     , nonEmpty
     , reset
+    , setOptions
     , setValues
     , update
     , updateSearch
@@ -125,6 +126,11 @@ setValues values =
     SetValues values
 
 
+setOptions : List CustomField -> Msg
+setOptions fields =
+    CustomFieldResp (Ok (CustomFieldList fields))
+
+
 reset : Model -> Model
 reset model =
     let
diff --git a/modules/webapp/src/main/elm/Comp/SearchMenu.elm b/modules/webapp/src/main/elm/Comp/SearchMenu.elm
index c70a1c3d..890cf25b 100644
--- a/modules/webapp/src/main/elm/Comp/SearchMenu.elm
+++ b/modules/webapp/src/main/elm/Comp/SearchMenu.elm
@@ -60,6 +60,7 @@ import Http
 import Messages.Comp.SearchMenu exposing (Texts)
 import Set exposing (Set)
 import Styles as S
+import Util.CustomField
 import Util.Html exposing (KeyCode(..))
 import Util.ItemDragDrop as DD
 import Util.Maybe
@@ -564,6 +565,42 @@ updateDrop ddm flags settings msg model =
                 selectModel =
                     Comp.TagSelect.modifyCount model.tagSelectModel tagCount catCount
 
+                orgOpts =
+                    Comp.Dropdown.update (Comp.Dropdown.SetOptions (List.map .ref stats.corrOrgStats))
+                        model.orgModel
+                        |> Tuple.first
+
+                corrPersOpts =
+                    Comp.Dropdown.update (Comp.Dropdown.SetOptions (List.map .ref stats.corrPersStats))
+                        model.corrPersonModel
+                        |> Tuple.first
+
+                concPersOpts =
+                    Comp.Dropdown.update (Comp.Dropdown.SetOptions (List.map .ref stats.concPersStats))
+                        model.concPersonModel
+                        |> Tuple.first
+
+                concEquipOpts =
+                    let
+                        mkEquip ref =
+                            Equipment ref.id ref.name 0 Nothing ""
+                    in
+                    Comp.Dropdown.update
+                        (Comp.Dropdown.SetOptions
+                            (List.map (.ref >> mkEquip) stats.concEquipStats)
+                        )
+                        model.concEquipmentModel
+                        |> Tuple.first
+
+                fields =
+                    Util.CustomField.statsToFields stats
+
+                fieldOpts =
+                    Comp.CustomFieldMultiInput.update flags
+                        (Comp.CustomFieldMultiInput.setOptions fields)
+                        model.customFieldModel
+                        |> .model
+
                 model_ =
                     { model
                         | tagSelectModel = selectModel
@@ -571,6 +608,11 @@ updateDrop ddm flags settings msg model =
                             Comp.FolderSelect.modify model.selectedFolder
                                 model.folderList
                                 stats.folderStats
+                        , orgModel = orgOpts
+                        , corrPersonModel = corrPersOpts
+                        , concPersonModel = concPersOpts
+                        , concEquipmentModel = concEquipOpts
+                        , customFieldModel = fieldOpts
                     }
             in
             { model = model_
diff --git a/modules/webapp/src/main/elm/Util/CustomField.elm b/modules/webapp/src/main/elm/Util/CustomField.elm
index fc121f62..cfe58d92 100644
--- a/modules/webapp/src/main/elm/Util/CustomField.elm
+++ b/modules/webapp/src/main/elm/Util/CustomField.elm
@@ -10,9 +10,12 @@ module Util.CustomField exposing
     , nameOrLabel
     , renderValue
     , renderValue2
+    , statsToFields
     )
 
+import Api.Model.CustomField exposing (CustomField)
 import Api.Model.ItemFieldValue exposing (ItemFieldValue)
+import Api.Model.SearchStats exposing (SearchStats)
 import Data.CustomFieldType
 import Data.Icons as Icons
 import Html exposing (..)
@@ -20,6 +23,15 @@ import Html.Attributes exposing (..)
 import Html.Events exposing (onClick)
 
 
+statsToFields : SearchStats -> List CustomField
+statsToFields stats =
+    let
+        mkField fs =
+            CustomField fs.id fs.name fs.label fs.ftype fs.count 0
+    in
+    List.map mkField stats.fieldStats
+
+
 {-| This is how the server wants the value to a bool custom field
 -}
 boolValue : Bool -> String