From 6f7e95e233f1c8c03975ff8380a5c560218b3c21 Mon Sep 17 00:00:00 2001
From: Andrey Sobolev <haiodo@users.noreply.github.com>
Date: Wed, 23 Mar 2022 16:03:41 +0700
Subject: [PATCH] Upcoming events (#1195)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
---
 models/recruit/src/index.ts                   |  14 ++-
 models/recruit/src/review.ts                  |  78 ++++++------
 packages/core/src/storage.ts                  |  13 +-
 packages/core/src/tx.ts                       |   2 +-
 packages/query/src/__tests__/minmodel.ts      |  12 +-
 packages/query/src/__tests__/query.test.ts    |  55 ++++++++-
 packages/query/src/index.ts                   |  12 +-
 plugins/calendar-assets/lang/en.json          |   7 +-
 plugins/calendar-assets/lang/ru.json          |   7 +-
 .../src/components/CalendarView.svelte        |  86 +++++++++----
 .../src/components/DateTimePresenter.svelte   |  59 +++++++++
 .../src/components/UpcomingEvents.svelte      | 116 ++++++++++++++++++
 plugins/calendar-resources/src/index.ts       |   6 +-
 plugins/calendar-resources/src/plugin.ts      |   7 +-
 plugins/calendar/src/index.ts                 |   4 +-
 .../src/components/review/CreateReview.svelte |   9 +-
 .../src/components/review/Reviews.svelte      |   5 +-
 17 files changed, 408 insertions(+), 84 deletions(-)
 create mode 100644 plugins/calendar-resources/src/components/DateTimePresenter.svelte
 create mode 100644 plugins/calendar-resources/src/components/UpcomingEvents.svelte

diff --git a/models/recruit/src/index.ts b/models/recruit/src/index.ts
index b87603a794..37cafe75a0 100644
--- a/models/recruit/src/index.ts
+++ b/models/recruit/src/index.ts
@@ -41,7 +41,8 @@ import workbench from '@anticrm/model-workbench'
 import { Applicant, Candidate, Candidates, Vacancy } from '@anticrm/recruit'
 import { TOpinion, TReview, TReviewCategory } from './review-model'
 import recruit from './plugin'
-import { createReviewModel } from './review'
+import { createReviewModel, reviewTableConfig, reviewTableOptions } from './review'
+import calendar from '@anticrm/model-calendar'
 
 @Model(recruit.class.Vacancy, task.class.SpaceWithStates)
 @UX(recruit.string.Vacancy, recruit.icon.Vacancy)
@@ -192,7 +193,18 @@ export function createModel (builder: Builder): void {
             componentProps: {
               labelTasks: recruit.string.Applications,
               _class: recruit.class.Applicant
+            }
+          },
+          {
+            id: 'upcoming',
+            component: calendar.component.UpcomingEvents,
+            componentProps: {
+              _class: recruit.class.Review,
+              options: reviewTableOptions,
+              config: reviewTableConfig
             },
+            icon: calendar.icon.Calendar,
+            label: calendar.string.UpcomingEvents,
             position: 'top'
           }
         ]
diff --git a/models/recruit/src/review.ts b/models/recruit/src/review.ts
index 934f4751bf..d5e34645c1 100644
--- a/models/recruit/src/review.ts
+++ b/models/recruit/src/review.ts
@@ -1,12 +1,38 @@
-import { Doc, FindOptions } from '@anticrm/core'
+import { FindOptions } from '@anticrm/core'
 import { Builder } from '@anticrm/model'
+import calendar from '@anticrm/model-calendar'
 import contact from '@anticrm/model-contact'
 import core from '@anticrm/model-core'
 import task from '@anticrm/model-task'
 import view from '@anticrm/model-view'
 import workbench from '@anticrm/model-workbench'
+import { Review } from '@anticrm/recruit'
+import { BuildModelKey } from '@anticrm/view'
 import recruit from './plugin'
-import calendar from '@anticrm/model-calendar'
+
+export const reviewTableOptions: FindOptions<Review> = {
+  lookup: {
+    attachedTo: recruit.mixin.Candidate,
+    participants: contact.class.Employee,
+    company: contact.class.Organization
+  }
+}
+export const reviewTableConfig: (BuildModelKey | string)[] = [
+  '',
+  'title',
+  '$lookup.attachedTo',
+  // 'verdict',
+  { key: '', presenter: recruit.component.OpinionsPresenter, label: recruit.string.Opinions, sortingKey: 'opinions' },
+  {
+    key: '$lookup.participants',
+    presenter: calendar.component.PersonsPresenter,
+    label: calendar.string.Participants,
+    sortingKey: '$lookup.participants'
+  },
+  '$lookup.company',
+  { key: '', presenter: calendar.component.DateTimePresenter, label: calendar.string.Date, sortingKey: 'date' },
+  'modifiedOn'
+]
 
 export function createReviewModel (builder: Builder): void {
   builder.mixin(recruit.class.ReviewCategory, core.class.Class, workbench.mixin.SpaceView, {
@@ -82,27 +108,20 @@ export function createReviewModel (builder: Builder): void {
     }
   })
 
+  const reviewOptions: FindOptions<Review> = {
+    lookup: {
+      attachedTo: recruit.mixin.Candidate,
+      participants: contact.class.Employee,
+      company: contact.class.Organization
+    }
+  }
+
   builder.createDoc(view.class.Viewlet, core.space.Model, {
     attachTo: recruit.class.Review,
     descriptor: calendar.viewlet.Calendar,
     // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
-    options: {
-      lookup: {
-        attachedTo: recruit.mixin.Candidate,
-        participants: contact.class.Employee,
-        company: contact.class.Organization
-      }
-    } as FindOptions<Doc>,
-    config: [
-      '',
-      'title',
-      '$lookup.attachedTo',
-      'verdict',
-      { key: '', presenter: recruit.component.OpinionsPresenter, label: recruit.string.Opinions, sortingKey: 'opinions' },
-      { key: '$lookup.participants', presenter: calendar.component.PersonsPresenter, label: calendar.string.Participants, sortingKey: '$lookup.participants' },
-      '$lookup.company',
-      'modifiedOn'
-    ]
+    options: reviewOptions,
+    config: reviewTableConfig
   })
 }
 
@@ -111,25 +130,8 @@ function createTableViewlet (builder: Builder): void {
     attachTo: recruit.class.Review,
     descriptor: view.viewlet.Table,
     // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
-    options: {
-      lookup: {
-        attachedTo: recruit.mixin.Candidate,
-        participants: contact.class.Employee,
-        company: contact.class.Organization
-      }
-    } as FindOptions<Doc>,
-    config: [
-      '',
-      'title',
-      '$lookup.attachedTo',
-      'verdict',
-      { key: '', presenter: recruit.component.OpinionsPresenter, label: recruit.string.Opinions, sortingKey: 'opinions' },
-      { key: '$lookup.participants', presenter: calendar.component.PersonsPresenter, label: calendar.string.Participants, sortingKey: '$lookup.participants' },
-      '$lookup.company',
-      'date',
-      'dueDate',
-      'modifiedOn'
-    ]
+    options: reviewTableOptions,
+    config: reviewTableConfig
   })
 
   builder.mixin(recruit.class.Opinion, core.class.Class, view.mixin.AttributeEditor, {
diff --git a/packages/core/src/storage.ts b/packages/core/src/storage.ts
index d36c7d016a..90c9a48949 100644
--- a/packages/core/src/storage.ts
+++ b/packages/core/src/storage.ts
@@ -32,7 +32,7 @@ export type QuerySelector<T> = {
 /**
  * @public
  */
-export type ObjQueryType<T> = T | QuerySelector<T>
+export type ObjQueryType<T> = (T extends Array<infer U> ? U | U[] : T) | QuerySelector<T>
 
 /**
  * @public
@@ -50,23 +50,26 @@ export type DocumentQuery<T extends Doc> = {
  * @public
  */
 export type ToClassRefT<T extends object, P extends keyof T> = T[P] extends Ref<infer X> | null | undefined ? Ref<Class<X>> | [Ref<Class<X>>, Lookup<X>] : never
-
+/**
+ * @public
+ */
+export type ToClassRefTA<T extends object, P extends keyof T> = T[P] extends Array<Ref<infer X>> | null | undefined ? Ref<Class<X>> | [Ref<Class<X>>, Lookup<X>] : never
 /**
  * @public
  */
 export type ToClassRef<T extends object> = {
-  [P in keyof T]?: ToClassRefT<T, P>
+  [P in keyof T]?: ToClassRefT<T, P> | ToClassRefTA<T, P>
 }
 
 /**
  * @public
  */
-export type RefKeys<T extends Doc> = Pick<T, KeysByType<T, NullableRef>>
+export type NullableRef = Ref<Doc> | Array<Ref<Doc>> | null | undefined
 
 /**
  * @public
  */
-export type NullableRef = Ref<Doc> | null | undefined
+export type RefKeys<T extends Doc> = Pick<T, KeysByType<T, NullableRef>>
 
 /**
  * @public
diff --git a/packages/core/src/tx.ts b/packages/core/src/tx.ts
index 924e286321..0aef96dabc 100644
--- a/packages/core/src/tx.ts
+++ b/packages/core/src/tx.ts
@@ -116,7 +116,7 @@ export interface MoveDescriptor<X extends PropertyType> {
  * @public
  */
 export type ArrayAsElementPosition<T extends object> = {
-  [P in keyof T]: T[P] extends Arr<infer X> ? X | Position<X> : never
+  [P in keyof T]-?: T[P] extends Arr<infer X> ? X | Position<X> : never
 }
 
 /**
diff --git a/packages/query/src/__tests__/minmodel.ts b/packages/query/src/__tests__/minmodel.ts
index d2d2309856..93bdc84f8e 100644
--- a/packages/query/src/__tests__/minmodel.ts
+++ b/packages/query/src/__tests__/minmodel.ts
@@ -58,10 +58,18 @@ export const test = plugin('test' as Plugin, {
     TestMixin: '' as Ref<Mixin<TestMixin>>
   },
   class: {
-    TestComment: '' as Ref<Class<AttachedComment>>
+    TestComment: '' as Ref<Class<AttachedComment>>,
+    ParticipantsHolder: '' as Ref<Class<ParticipantsHolder>>
   }
 })
 
+/**
+ * @public
+ */
+export interface ParticipantsHolder extends Doc {
+  participants?: Ref<Doc>[]
+}
+
 const DOMAIN_TEST: Domain = 'test' as Domain
 
 /**
@@ -90,6 +98,8 @@ export function genMinModel (): TxCUD<Doc>[] {
 
   txes.push(createClass(test.class.TestComment, { label: 'TestComment' as IntlString, extends: core.class.AttachedDoc, kind: ClassifierKind.CLASS, domain: DOMAIN_TEST }))
 
+  txes.push(createClass(test.class.ParticipantsHolder, { label: 'ParticipantsHolder' as IntlString, extends: core.class.Doc, kind: ClassifierKind.CLASS, domain: DOMAIN_TEST }))
+
   const u1 = 'User1' as Ref<Account>
   const u2 = 'User2' as Ref<Account>
   txes.push(
diff --git a/packages/query/src/__tests__/query.test.ts b/packages/query/src/__tests__/query.test.ts
index c532a956f1..7a48a2d239 100644
--- a/packages/query/src/__tests__/query.test.ts
+++ b/packages/query/src/__tests__/query.test.ts
@@ -14,9 +14,9 @@
 //
 
 import core, { createClient, Doc, generateId, Ref, SortingOrder, Space, Tx, TxCreateDoc, TxOperations, WithLookup } from '@anticrm/core'
-import { AttachedComment, test, genMinModel } from './minmodel'
 import { LiveQuery } from '..'
 import { connect } from './connection'
+import { AttachedComment, genMinModel, ParticipantsHolder, test } from './minmodel'
 
 interface Channel extends Space {
   x: number
@@ -49,12 +49,12 @@ describe('query', () => {
       }
     }
 
-    await new Promise((resolve) => {
+    const result = await new Promise((resolve) => {
       liveQuery.query<Space>(core.class.Space, { private: false }, (result) => {
-        expect(result).toHaveLength(expectedLength)
-        resolve(null)
+        resolve(result)
       })
     })
+    expect(result).toHaveLength(expectedLength)
   })
 
   it('query should be live', async () => {
@@ -743,4 +743,51 @@ describe('query', () => {
   //   }
   //   await pp
   // })
+
+  it('update-array-value', async () => {
+    const { liveQuery, factory } = await getClient()
+
+    const spaces = await liveQuery.findAll(core.class.Space, {})
+    await factory.createDoc(test.class.ParticipantsHolder, spaces[0]._id, {
+      participants: ['a' as Ref<Doc>]
+    })
+    const a2 = await factory.createDoc(test.class.ParticipantsHolder, spaces[0]._id, {
+      participants: ['b' as Ref<Doc>]
+    })
+
+    const holderBefore = await liveQuery.findAll(test.class.ParticipantsHolder, { participants: 'a' as Ref<Doc> })
+    expect(holderBefore.length).toEqual(1)
+
+    let attempt = 0
+    let resolvePpv: (value: Doc[] | PromiseLike<Doc[]>) => void
+
+    const resolveP = new Promise<Doc[]>((resolve) => {
+      resolvePpv = resolve
+    })
+    const pp = await new Promise((resolve) => {
+      liveQuery.query<Space>(
+        test.class.ParticipantsHolder,
+        { participants: 'a' as Ref<Doc> },
+        (result) => {
+          if (attempt > 0) {
+            resolvePpv(result)
+          } else {
+            resolve(null)
+          }
+        },
+        { sort: { private: SortingOrder.Ascending } }
+      )
+    })
+
+    await pp // We have first value returned
+
+    attempt++
+    await factory.updateDoc<ParticipantsHolder>(test.class.ParticipantsHolder, spaces[0]._id, a2, {
+      $push: {
+        participants: 'a' as Ref<Doc>
+      }
+    })
+    const result = await resolveP
+    expect(result.length).toEqual(2)
+  })
 })
diff --git a/packages/query/src/index.ts b/packages/query/src/index.ts
index da83b669c7..d7f86c77c6 100644
--- a/packages/query/src/index.ts
+++ b/packages/query/src/index.ts
@@ -338,9 +338,19 @@ export class LiveQuery extends TxProcessor implements Client {
       return false
     }
 
+    const doc: Doc = {
+      _id: tx.objectId,
+      _class: tx.objectClass,
+      modifiedBy: tx.modifiedBy,
+      modifiedOn: tx.modifiedOn,
+      space: tx.objectSpace
+    }
+
+    TxProcessor.updateDoc2Doc(doc, tx)
+
     for (const key in q.query) {
       const value = (q.query as any)[key]
-      const res = findProperty([tx.operations as unknown as Doc], key, value)
+      const res = findProperty([doc], key, value)
       if (res.length === 1) {
         return true
       }
diff --git a/plugins/calendar-assets/lang/en.json b/plugins/calendar-assets/lang/en.json
index e28eca5466..c590a3dbbb 100644
--- a/plugins/calendar-assets/lang/en.json
+++ b/plugins/calendar-assets/lang/en.json
@@ -20,6 +20,11 @@
     "ModeWeek": "Week",
     "ModeMonth": "Month",
     "ModeYear": "Year",
-    "Today": "Today"
+    "Today": "Today",
+    "UpcomingEvents": "Upcoming events",
+    "TableView": "Table",
+    "DueMinutes": "{minutes, plural, =0 {less than a minute} =1 {a minute} other {# minutes}}",
+    "DueHours": "{hours, plural, =0 {less than an hour} =1 {1 hour} other {# hours}}",
+    "DueDays": "{days, plural, =0 {today} =1 {1 day} other {# days}}"
   }
 }
\ No newline at end of file
diff --git a/plugins/calendar-assets/lang/ru.json b/plugins/calendar-assets/lang/ru.json
index 19be55c791..a0eae53111 100644
--- a/plugins/calendar-assets/lang/ru.json
+++ b/plugins/calendar-assets/lang/ru.json
@@ -20,6 +20,11 @@
     "ModeWeek": "Неделя",
     "ModeMonth": "Месяц",
     "ModeYear": "Год",
-    "Today": "Сегодня"
+    "Today": "Сегодня",
+    "UpcomingEvents": "Предстоящие события",
+    "TableView": "Таблица",
+    "DueMinutes": "{minutes, plural, =0 {меньше минуты} =1 {минута} other {# минут}}",
+    "DueHours": "{hours, plural, =0 {меньше часа} =1 {1 час} other {# часы}}",
+    "DueDays": "{days, plural, =0 {сегодня} =1 {1 день} other {# дня}}"
   }
 }
\ No newline at end of file
diff --git a/plugins/calendar-resources/src/components/CalendarView.svelte b/plugins/calendar-resources/src/components/CalendarView.svelte
index 7ef1f1108d..3738c28320 100644
--- a/plugins/calendar-resources/src/components/CalendarView.svelte
+++ b/plugins/calendar-resources/src/components/CalendarView.svelte
@@ -38,7 +38,8 @@
 
   let loading = false
   let resultQuery: DocumentQuery<Event>
-  $: resultQuery = search === '' ? { ...query, space } : { ...query, $search: search, space }
+  $: spaceOpt = (space ? { space } : {})
+  $: resultQuery = search === '' ? { ...query, ...spaceOpt } : { ...query, $search: search, ...spaceOpt }
 
   let objects: Event[] = []
 
@@ -73,14 +74,16 @@
   }
 
   function findEvents (events: Event[], date: Date): Event[] {
-    return events.filter((it) => areDatesLess(new Date(it.date), date) && areDatesLess(date, new Date(it.dueDate ?? it.date)))
+    return events.filter(
+      (it) => areDatesLess(new Date(it.date), date) && areDatesLess(date, new Date(it.dueDate ?? it.date))
+    )
   }
 
   interface ShiftType {
     yearShift: number
     monthShift: number
     dayShift: number
-    weekShift:number
+    weekShift: number
   }
 
   let shifts: ShiftType = {
@@ -134,7 +137,12 @@
     return res
   }
 
-  enum CalendarMode { Day, Week, Month, Year }
+  enum CalendarMode {
+    Day,
+    Week,
+    Month,
+    Year
+  }
 
   let mode: CalendarMode = CalendarMode.Year
 
@@ -156,7 +164,7 @@
   }
 </script>
 
-<div class='fs-title ml-2 mb-2 flex-row-center'>
+<div class="fs-title ml-2 mb-2 flex-row-center">
   {label(currentDate(date, shifts), mode)}
 </div>
 
@@ -202,29 +210,63 @@
       }
       mode = CalendarMode.Year
     }}
-  />  
+  />
   <div class="flex ml-4 gap-1">
-    <Button icon={IconBack} size={'small'}  on:click={() => { inc(-1) } }/>
-    <Button size={'small'} label={calendar.string.Today} on:click={() => { inc(0) }}/>
-    <Button icon={IconForward} size={'small'} on:click={() => { inc(1) }}/>
+    <Button
+      icon={IconBack}
+      size={'small'}
+      on:click={() => {
+        inc(-1)
+      }}
+    />
+    <Button
+      size={'small'}
+      label={calendar.string.Today}
+      on:click={() => {
+        inc(0)
+      }}
+    />
+    <Button
+      icon={IconForward}
+      size={'small'}
+      on:click={() => {
+        inc(1)
+      }}
+    />
   </div>
 </div>
 
-
 {#if mode === CalendarMode.Year}
-<ScrollBox bothScroll>
-    <YearCalendar {mondayStart} cellHeight={'2.5rem'} bind:value={value} currentDate={currentDate(date, shifts)}>
+  <ScrollBox bothScroll>
+    <YearCalendar {mondayStart} cellHeight={'2.5rem'} bind:value currentDate={currentDate(date, shifts)}>
       <svelte:fragment slot="cell" let:date>
-        <Day events={findEvents(objects, date)} {date} {_class} {baseMenuClass} {options} {config} query={resultQuery} />
+        <Day
+          events={findEvents(objects, date)}
+          {date}
+          {_class}
+          {baseMenuClass}
+          {options}
+          {config}
+          query={resultQuery}
+        />
       </svelte:fragment>
     </YearCalendar>
   </ScrollBox>
-  {:else if mode === CalendarMode.Month}
-    <div class='flex flex-grow'>
-      <MonthCalendar {mondayStart} cellHeight={'8.5rem'} bind:value={value} currentDate={currentDate(date, shifts)}>
-        <svelte:fragment slot="cell" let:date={date}>        
-          <Day events={findEvents(objects, date)} {date} size={'huge'} {_class} {baseMenuClass} {options} {config} query={resultQuery}/>
-        </svelte:fragment>
-      </MonthCalendar>  
-    </div>
-  {/if}
+{:else if mode === CalendarMode.Month}
+  <div class="flex flex-grow">
+    <MonthCalendar {mondayStart} cellHeight={'8.5rem'} bind:value currentDate={currentDate(date, shifts)}>
+      <svelte:fragment slot="cell" let:date>
+        <Day
+          events={findEvents(objects, date)}
+          {date}
+          size={'huge'}
+          {_class}
+          {baseMenuClass}
+          {options}
+          {config}
+          query={resultQuery}
+        />
+      </svelte:fragment>
+    </MonthCalendar>
+  </div>
+{/if}
diff --git a/plugins/calendar-resources/src/components/DateTimePresenter.svelte b/plugins/calendar-resources/src/components/DateTimePresenter.svelte
new file mode 100644
index 0000000000..044e112bd1
--- /dev/null
+++ b/plugins/calendar-resources/src/components/DateTimePresenter.svelte
@@ -0,0 +1,59 @@
+<!--
+// 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 { Event } from '@anticrm/calendar'
+  import { translate } from '@anticrm/platform'
+  import { DatePresenter } from '@anticrm/ui'
+  import calendar from '../plugin'
+
+  export let value: Event
+  
+  $: date = value ? new Date(value.date) : undefined
+  $: dueDate = value ? new Date(value.dueDate ?? value.date) : undefined
+
+  $: interval = (value.dueDate ?? value.date) - value.date
+
+  const SECOND = 1000
+  const MINUTE = SECOND * 60
+  const HOUR = MINUTE * 60
+  const DAY = HOUR * 24
+
+  async function formatDueDate (interval: number): Promise<string> {
+    let passed = interval
+    if (interval < 0) passed = 0
+    if (passed < HOUR) {
+      return await translate(calendar.string.DueMinutes, { minutes: Math.floor(passed / MINUTE) })
+    } else if (passed < DAY) {
+      return await translate(calendar.string.DueHours, { hours: Math.floor(passed / HOUR) })
+    } else {
+      return await translate(calendar.string.DueDays, { days: Math.floor(passed / DAY) })
+    }
+  }
+
+</script>
+
+<div class="antiSelect">
+  {#if date}
+    <DatePresenter value={date} withTime={date.getMinutes() !== 0 && date.getHours() !== 0 && interval < DAY} />
+    {#if interval > 0}
+    {#await formatDueDate(interval) then t}
+        <span class='ml-2 mr-1 whitespace-nowrap'>({t})</span>         
+      {/await}
+    {/if}
+  {:else}
+    No date
+  {/if}
+</div>
\ No newline at end of file
diff --git a/plugins/calendar-resources/src/components/UpcomingEvents.svelte b/plugins/calendar-resources/src/components/UpcomingEvents.svelte
new file mode 100644
index 0000000000..b8a8fce62d
--- /dev/null
+++ b/plugins/calendar-resources/src/components/UpcomingEvents.svelte
@@ -0,0 +1,116 @@
+<!--
+// 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 { Event } from '@anticrm/calendar'
+  import { EmployeeAccount } from '@anticrm/contact'
+  import { Class, DocumentQuery, FindOptions, getCurrentAccount, Ref } from '@anticrm/core'
+  import { Asset, IntlString } from '@anticrm/platform'
+  import { AnySvelteComponent, Icon, Label, SearchEdit, Tooltip } from '@anticrm/ui'
+  import { Table } from '@anticrm/view-resources'
+  import view from '@anticrm/view'
+  import calendar from '../plugin'
+  import CalendarView from './CalendarView.svelte'
+
+  export let _class: Ref<Class<Event>>
+  export let query: DocumentQuery<Event> = {}
+  export let options: FindOptions<Event> | undefined = undefined
+  export let baseMenuClass: Ref<Class<Event>> | undefined = undefined
+  export let config: string[]
+
+  const currentUser = getCurrentAccount() as EmployeeAccount
+
+  let search = ''
+  let resultQuery: DocumentQuery<Event> = {}
+
+  function updateResultQuery (search: string): void {
+    resultQuery = search === '' ? { ...query } : { ...query, $search: search }
+
+    resultQuery.participants = currentUser.employee
+  }
+
+  $: updateResultQuery(search)
+
+  interface CalendarViewlet {
+    component: AnySvelteComponent
+    props: Record<string, any>
+    label: IntlString
+    icon: Asset
+  }
+
+  $: viewlets = [{
+    component: CalendarView,
+    icon: calendar.icon.Calendar,
+    label: calendar.string.Calendar,
+    props: {
+      _class,
+      space: undefined,
+      query: resultQuery,
+      options,
+      baseMenuClass,
+      config,
+      search
+    }
+  },
+  {
+    component: Table,
+    icon: view.icon.Table,
+    label: calendar.string.TableView,
+    props: {
+      _class,
+      query: resultQuery,
+      options,
+      baseMenuClass,
+      config,
+      search
+    }
+  }] as CalendarViewlet[]
+  let selectedViewlet = 0
+</script>
+
+<div class="ac-header full">
+  <div class="ac-header__wrap-title">
+    <div class="ac-header__icon"><Icon icon={calendar.icon.Calendar} size={'small'} /></div>
+    <span class="ac-header__title"><Label label={calendar.string.UpcomingEvents} /></span>
+  </div>
+
+  {#if viewlets.length > 1}
+    <div class="flex">
+      {#each viewlets as viewlet, i}
+        <Tooltip label={viewlet.label} direction={'top'}>
+          <button
+            class="ac-header__icon-button"
+            class:selected={selectedViewlet === i}
+            on:click={() => {
+              selectedViewlet = i
+            }}
+          >
+            <Icon icon={viewlet.icon} size={'small'} />
+          </button>
+        </Tooltip>
+      {/each}
+    </div>
+  {/if}
+
+  <SearchEdit
+    bind:value={search}
+    on:change={() => {
+      updateResultQuery(search)
+    }}
+  />
+</div>
+
+{#if viewlets[selectedViewlet]}
+  <svelte:component this={viewlets[selectedViewlet].component} {...(viewlets[selectedViewlet].props)} />
+{/if}
diff --git a/plugins/calendar-resources/src/index.ts b/plugins/calendar-resources/src/index.ts
index 4e28eb0e1d..060b7eeda6 100644
--- a/plugins/calendar-resources/src/index.ts
+++ b/plugins/calendar-resources/src/index.ts
@@ -17,10 +17,14 @@ import { Resources } from '@anticrm/platform'
 
 import PersonsPresenter from './components/PersonsPresenter.svelte'
 import CalendarView from './components/CalendarView.svelte'
+import UpcomingEvents from './components/UpcomingEvents.svelte'
+import DateTimePresenter from './components/DateTimePresenter.svelte'
 
 export default async (): Promise<Resources> => ({
   component: {
     PersonsPresenter,
-    CalendarView
+    CalendarView,
+    UpcomingEvents,
+    DateTimePresenter
   }
 })
diff --git a/plugins/calendar-resources/src/plugin.ts b/plugins/calendar-resources/src/plugin.ts
index dfa411cc62..87b7e1eaf2 100644
--- a/plugins/calendar-resources/src/plugin.ts
+++ b/plugins/calendar-resources/src/plugin.ts
@@ -25,6 +25,11 @@ export default mergeIds(calendarId, calendar, {
     ModeWeek: '' as IntlString,
     ModeMonth: '' as IntlString,
     ModeYear: '' as IntlString,
-    Today: '' as IntlString
+    Today: '' as IntlString,
+    UpcomingEvents: '' as IntlString,
+    TableView: '' as IntlString,
+    DueMinutes: '' as IntlString,
+    DueHours: '' as IntlString,
+    DueDays: '' as IntlString
   }
 })
diff --git a/plugins/calendar/src/index.ts b/plugins/calendar/src/index.ts
index acaec40e3e..8a158aff7e 100644
--- a/plugins/calendar/src/index.ts
+++ b/plugins/calendar/src/index.ts
@@ -69,7 +69,9 @@ const calendarPlugin = plugin(calendarId, {
     Calendar: '' as Ref<Doc>
   },
   component: {
-    PersonsPresenter: '' as AnyComponent
+    PersonsPresenter: '' as AnyComponent,
+    UpcomingEvents: '' as AnyComponent,
+    DateTimePresenter: '' as AnyComponent
   },
   string: {
     Title: '' as IntlString,
diff --git a/plugins/recruit-resources/src/components/review/CreateReview.svelte b/plugins/recruit-resources/src/components/review/CreateReview.svelte
index 7726e27786..0a71605825 100644
--- a/plugins/recruit-resources/src/components/review/CreateReview.svelte
+++ b/plugins/recruit-resources/src/components/review/CreateReview.svelte
@@ -13,10 +13,10 @@
 // limitations under the License.
 -->
 <script lang="ts">
-  import type { Contact, Organization, Person } from '@anticrm/contact'
+  import type { Contact, EmployeeAccount, Organization, Person } from '@anticrm/contact'
   import contact from '@anticrm/contact'
   import { OrganizationSelector } from '@anticrm/contact-resources'
-  import { Account, Class, Client, Doc, generateId, Ref } from '@anticrm/core'
+  import { Account, Class, Client, Doc, generateId, getCurrentAccount, Ref } from '@anticrm/core'
   import { getResource, OK, Resource, Severity, Status } from '@anticrm/platform'
   import { Card, getClient, UserBox } from '@anticrm/presentation'
   import type { Candidate, Review } from '@anticrm/recruit'
@@ -32,6 +32,8 @@
 
   export let preserveCandidate = false
 
+  const currentUser = getCurrentAccount() as EmployeeAccount
+
   let status: Status = OK
 
   let title: string = ''
@@ -56,7 +58,8 @@
     description,
     company,
     verdict: '',
-    title
+    title,
+    participants: [currentUser.employee]
   }
 
   const dispatch = createEventDispatcher()
diff --git a/plugins/recruit-resources/src/components/review/Reviews.svelte b/plugins/recruit-resources/src/components/review/Reviews.svelte
index 9719883cfe..8c194cc58c 100644
--- a/plugins/recruit-resources/src/components/review/Reviews.svelte
+++ b/plugins/recruit-resources/src/components/review/Reviews.svelte
@@ -16,7 +16,7 @@
   import type { Doc, Ref } from '@anticrm/core'
   import core from '@anticrm/core'
   import { IntlString } from '@anticrm/platform'
-  import task from '@anticrm/task'
+  import calendar from '@anticrm/calendar'
   import { CircleButton, IconAdd, Label, showPopup } from '@anticrm/ui'
   import { Table } from '@anticrm/view-resources'
   import recruit from '../../plugin'
@@ -50,8 +50,7 @@
           label: recruit.string.Opinions,
           sortingKey: 'opinions'
         },
-        'date',
-        'dueDate'
+        { key: '', presenter: calendar.component.DateTimePresenter, label: calendar.string.Date, sortingKey: 'date' },
       ]}
       options={{
         lookup: {