From 36ddf8477a3eac1360094adf5bf3debe5d14b187 Mon Sep 17 00:00:00 2001
From: Andrey Sobolev <haiodo@users.noreply.github.com>
Date: Tue, 15 Mar 2022 16:01:49 +0700
Subject: [PATCH] Vacancies sorting (#1143)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
---
 .../src/components/Vacancies.svelte           | 30 ++++++++++++++-----
 .../components/VacancyCountPresenter.svelte   |  6 ++--
 .../VacancyModifiedPresenter.svelte           | 25 ++++++++++++++++
 plugins/recruit-resources/src/index.ts        |  2 ++
 plugins/recruit-resources/src/plugin.ts       |  3 +-
 .../src/components/Table.svelte               |  6 ++++
 plugins/view/src/index.ts                     |  3 ++
 7 files changed, 63 insertions(+), 12 deletions(-)
 create mode 100644 plugins/recruit-resources/src/components/VacancyModifiedPresenter.svelte

diff --git a/plugins/recruit-resources/src/components/Vacancies.svelte b/plugins/recruit-resources/src/components/Vacancies.svelte
index dba95f6964..65a322e79d 100644
--- a/plugins/recruit-resources/src/components/Vacancies.svelte
+++ b/plugins/recruit-resources/src/components/Vacancies.svelte
@@ -13,7 +13,7 @@
 // limitations under the License.
 -->
 <script lang="ts">
-  import { Doc, DocumentQuery, Ref } from '@anticrm/core'
+  import core, { Doc, DocumentQuery, Ref } from '@anticrm/core'
   import { createQuery } from '@anticrm/presentation'
   import { Applicant, Vacancy } from '@anticrm/recruit'
   import { Button, getCurrentLocation, Icon, Label, navigate, Scroller, showPopup, IconAdd } from '@anticrm/ui'
@@ -42,7 +42,7 @@
     vacancies = res
   })
 
-  function lowerIncludes (a?: string, b: string): boolean {
+  function lowerIncludes (a: string | undefined, b: string): boolean {
     return (a ?? '').toLowerCase().includes(b)
   }
 
@@ -63,8 +63,8 @@
   }
 
   $: resultQuery = vquery === '' ? {} : { $search: vquery }
-
-  let applications: Map<Ref<Vacancy>, number> | undefined
+  type ApplicationInfo = { count: number, modifiedOn: number }
+  let applications: Map<Ref<Vacancy>, ApplicationInfo> | undefined
 
   const applicantQuery = createQuery()
   $: if (vacancies.length > 0) {
@@ -73,10 +73,13 @@
       recruit.class.Applicant,
       { ...(resultQuery as DocumentQuery<Applicant>), space: { $in: vacancies.map((it) => it._id) } },
       (res) => {
-        const result = new Map<Ref<Vacancy>, number>()
+        const result = new Map<Ref<Vacancy>, ApplicationInfo>()
 
         for (const d of res) {
-          result.set(d.space, (result.get(d.space) ?? 0) + 1)
+          const v = result.get(d.space) ?? { count: 0, modifiedOn: 0 }
+          v.count++
+          v.modifiedOn = Math.max(v.modifiedOn, d.modifiedOn)
+          result.set(d.space, v)
         }
 
         applications = result
@@ -88,6 +91,8 @@
   function showCreateDialog (ev: Event) {
     showPopup(CreateVacancy, { space: recruit.space.CandidatesPublic }, ev.target as HTMLElement)
   }
+  const applicationSorting = (a:Doc, b:Doc) => ((applications?.get(b._id as Ref<Vacancy>)?.count ?? 0) - (applications?.get(a._id as Ref<Vacancy>)?.count ?? 0)) ?? 0
+  const modifiedSorting = (a:Doc, b:Doc) => ((applications?.get(b._id as Ref<Vacancy>)?.modifiedOn ?? 0) - (applications?.get(a._id as Ref<Vacancy>)?.modifiedOn ?? 0)) ?? 0
 </script>
 
 <div class="ac-header full">
@@ -118,12 +123,21 @@
         key: '',
         presenter: recruit.component.VacancyCountPresenter,
         label: recruit.string.Applications,
-        props: { applications, resultQuery }
+        props: { applications, resultQuery },
+        sortingKey: '@applications',
+        sortingFunction: applicationSorting
       },
       'company',
       'location',
       'description',
-      'modifiedOn'
+      {
+        key: '',
+        presenter: recruit.component.VacancyModifiedPresenter,
+        label: core.string.Modified,
+        props: { applications },
+        sortingKey: 'modifiedOn',
+        sortingFunction: modifiedSorting
+      }
     ]}
     options={{}}
     query={{
diff --git a/plugins/recruit-resources/src/components/VacancyCountPresenter.svelte b/plugins/recruit-resources/src/components/VacancyCountPresenter.svelte
index 5bf977252f..c50d250586 100644
--- a/plugins/recruit-resources/src/components/VacancyCountPresenter.svelte
+++ b/plugins/recruit-resources/src/components/VacancyCountPresenter.svelte
@@ -21,14 +21,14 @@ import recruit from '../plugin'
 import VacancyApplicationsPopup from './VacancyApplicationsPopup.svelte'
 
 export let value: Vacancy
-export let applications: Map<Ref<Vacancy>, number> | undefined
+export let applications: Map<Ref<Vacancy>, {count: number, modifiedOn: number}> | undefined
 export let resultQuery: DocumentQuery<Doc>
 </script>
 
-{#if (applications?.get(value._id) ?? 0) > 0}
+{#if (applications?.get(value._id)?.count ?? 0) > 0}
   <Tooltip label={recruit.string.Applications} component={VacancyApplicationsPopup} props={{ value: value._id, resultQuery }}>
     <div class="sm-tool-icon">
-      <span class="icon"><Icon icon={recruit.icon.Application} size={'small'} /></span>&nbsp;{(applications?.get(value._id) ?? 0)}
+      <span class="icon"><Icon icon={recruit.icon.Application} size={'small'} /></span>&nbsp;{(applications?.get(value._id)?.count ?? 0)}
     </div>
   </Tooltip>
 {/if}
\ No newline at end of file
diff --git a/plugins/recruit-resources/src/components/VacancyModifiedPresenter.svelte b/plugins/recruit-resources/src/components/VacancyModifiedPresenter.svelte
new file mode 100644
index 0000000000..19d0adee34
--- /dev/null
+++ b/plugins/recruit-resources/src/components/VacancyModifiedPresenter.svelte
@@ -0,0 +1,25 @@
+<!--
+// Copyright © 2022 Hardcore Engineering Inc.
+//
+// Licensed under the Eclipse Public License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License. You may
+// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//
+// See the License for the specific language governing permissions and
+// limitations under the License.
+-->
+
+<script lang="ts">
+import { Ref } from '@anticrm/core'
+import { Vacancy } from '@anticrm/recruit'
+import { TimeSince } from '@anticrm/ui'
+
+export let value: Vacancy
+export let applications: Map<Ref<Vacancy>, {count: number, modifiedOn: number}> | undefined
+</script>
+
+<TimeSince value={Math.max(applications?.get(value._id)?.modifiedOn ?? 0, value.modifiedOn)}/>
diff --git a/plugins/recruit-resources/src/index.ts b/plugins/recruit-resources/src/index.ts
index b3563ee3f9..57b5828eee 100644
--- a/plugins/recruit-resources/src/index.ts
+++ b/plugins/recruit-resources/src/index.ts
@@ -47,6 +47,7 @@ import Vacancies from './components/Vacancies.svelte'
 import VacancyItemPresenter from './components/VacancyItemPresenter.svelte'
 import VacancyPresenter from './components/VacancyPresenter.svelte'
 import VacancyCountPresenter from './components/VacancyCountPresenter.svelte'
+import VacancyModifiedPresenter from './components/VacancyModifiedPresenter.svelte'
 import recruit from './plugin'
 import PersonsPresenter from './components/review/PersonsPresenter.svelte'
 
@@ -155,6 +156,7 @@ export default async (): Promise<Resources> => ({
     Vacancies,
     VacancyItemPresenter,
     VacancyCountPresenter,
+    VacancyModifiedPresenter,
 
     CreateReviewCategory,
     EditReviewCategory,
diff --git a/plugins/recruit-resources/src/plugin.ts b/plugins/recruit-resources/src/plugin.ts
index db73aba0ab..d0765b3b04 100644
--- a/plugins/recruit-resources/src/plugin.ts
+++ b/plugins/recruit-resources/src/plugin.ts
@@ -116,6 +116,7 @@ export default mergeIds(recruitId, recruit, {
     VacancyItemPresenter: '' as AnyComponent,
     VacancyCountPresenter: '' as AnyComponent,
     OpinionsPresenter: '' as AnyComponent,
-    PersonsPresenter: '' as AnyComponent
+    PersonsPresenter: '' as AnyComponent,
+    VacancyModifiedPresenter: '' as AnyComponent
   }
 })
diff --git a/plugins/view-resources/src/components/Table.svelte b/plugins/view-resources/src/components/Table.svelte
index 82d7ab7c36..a27ad69e76 100644
--- a/plugins/view-resources/src/components/Table.svelte
+++ b/plugins/view-resources/src/components/Table.svelte
@@ -45,6 +45,8 @@
 
   const q = createQuery()
 
+  $: sortingFunction = (config.find(it => (typeof it !== 'string') && it.sortingKey === sortKey) as BuildModelKey)?.sortingFunction
+
   async function update (
     _class: Ref<Class<Doc>>,
     query: DocumentQuery<Doc>,
@@ -58,6 +60,10 @@
       query,
       (result) => {
         objects = result
+        if (sortingFunction !== undefined) {
+          const sf = sortingFunction
+          objects.sort((a, b) => -1 * sortOrder * sf(a, b))
+        }
         loading = false
       },
       { sort: { [sortKey]: sortOrder }, ...options, limit: 200 }
diff --git a/plugins/view/src/index.ts b/plugins/view/src/index.ts
index 700b132a61..d48a059c1b 100644
--- a/plugins/view/src/index.ts
+++ b/plugins/view/src/index.ts
@@ -124,6 +124,9 @@ export interface BuildModelKey {
 
   label?: IntlString
   sortingKey?: string
+
+  // On client sorting function
+  sortingFunction?: (a: Doc, b: Doc) => number
 }
 
 /**