platform/plugins/time-resources/src/components/ToDos.svelte
Alexander Platov 128f5f55ff
Fixed Planner layout and Separator ()
Signed-off-by: Alexander Platov <alexander.platov@hardcoreeng.com>
2024-04-24 13:59:32 +07:00

331 lines
8.8 KiB
Svelte

<!--
// Copyright © 2024 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 type { DocumentQuery, Ref, WithLookup, IdMap } from '@hcengineering/core'
import type { ToDo, WorkSlot } from '@hcengineering/time'
import type { PersonAccount } from '@hcengineering/contact'
import type { IntlString } from '@hcengineering/platform'
import type { TagElement } from '@hcengineering/tags'
import type { Project } from '@hcengineering/tracker'
import type { ToDosMode } from '..'
import { Scroller, areDatesEqual, todosSP, defaultSP, Header, ButtonIcon, Label } from '@hcengineering/ui'
import { getCurrentAccount, toIdMap, SortingOrder } from '@hcengineering/core'
import { createQuery } from '@hcengineering/presentation'
import tracker from '@hcengineering/tracker'
import tags from '@hcengineering/tags'
import view from '@hcengineering/view-resources/src/plugin'
import { getNearest } from '../utils'
import CreateToDo from './CreateToDo.svelte'
import ToDoGroup from './ToDoGroup.svelte'
import MenuClose from './icons/MenuClose.svelte'
import MenuOpen from './icons/MenuOpen.svelte'
import IconDiff from './icons/Diff.svelte'
import time from '../plugin'
export let mode: ToDosMode
export let tag: Ref<TagElement> | undefined
export let currentDate: Date
export let visibleNav: boolean = true
const acc = getCurrentAccount() as PersonAccount
const user = acc.person
const doneQuery = createQuery()
const inboxQuery = createQuery()
const activeQuery = createQuery()
const tagsQuery = createQuery()
const projectsQuery = createQuery()
let projects: IdMap<Project> = new Map()
projectsQuery.query(tracker.class.Project, { archived: false }, (result) => {
projects = toIdMap(result)
})
let ids: Ref<ToDo>[] = []
$: updateTags(mode, tag)
function togglePlannerNav (): void {
visibleNav = !visibleNav
}
function updateTags (mode: ToDosMode, tag: Ref<TagElement> | undefined): void {
if (mode !== 'tag' || tag === undefined) {
tagsQuery.unsubscribe()
ids = []
return
}
tagsQuery.query(
tags.class.TagReference,
{
tag
},
(res) => {
ids = res.map((p) => p.attachedTo as Ref<ToDo>)
}
)
}
function update (mode: ToDosMode, currentDate: Date, ids: Ref<ToDo>[]): void {
let activeQ: DocumentQuery<ToDo> | undefined = undefined
let doneQ: DocumentQuery<ToDo> | undefined = undefined
let inboxQ: DocumentQuery<ToDo> | undefined = undefined
if (mode === 'unplanned') {
activeQ = undefined
doneQ = undefined
inboxQ = {
user,
doneOn: null,
workslots: 0
}
} else if (mode === 'planned') {
inboxQ = undefined
doneQ = {
doneOn: { $gte: currentDate.setHours(0, 0, 0, 0), $lte: currentDate.setHours(23, 59, 59, 999) },
user
}
activeQ = {
user,
doneOn: null,
workslots: { $gt: 0 }
}
} else if (mode === 'all') {
inboxQ = {
doneOn: null,
workslots: 0,
user
}
doneQ = {
doneOn: { $ne: null },
user
}
activeQ = {
user,
doneOn: null,
workslots: { $gt: 0 }
}
} else if (mode === 'tag') {
inboxQ = {
doneOn: null,
workslots: 0,
user,
_id: { $in: ids }
}
doneQ = {
doneOn: { $ne: null },
user,
_id: { $in: ids }
}
activeQ = {
user,
doneOn: null,
workslots: { $gt: 0 },
_id: { $in: ids }
}
}
if (activeQ !== undefined) {
activeQuery.query(
time.class.ToDo,
activeQ,
(res) => {
rawActive = res
},
{
limit: 200,
sort: { rank: SortingOrder.Ascending },
lookup: { _id: { workslots: time.class.WorkSlot } }
}
)
} else {
activeQuery.unsubscribe()
rawActive = []
}
if (inboxQ !== undefined) {
inboxQuery.query(
time.class.ToDo,
inboxQ,
(res) => {
inbox = res
},
{
limit: 200,
sort: { rank: SortingOrder.Ascending }
}
)
} else {
inboxQuery.unsubscribe()
inbox = []
}
if (doneQ !== undefined) {
doneQuery.query(
time.class.ToDo,
doneQ,
(res) => {
done = res
},
{ limit: 200, sort: { doneOn: SortingOrder.Descending }, lookup: { _id: { workslots: time.class.WorkSlot } } }
)
} else {
doneQuery.unsubscribe()
done = []
}
}
$: update(mode, currentDate, ids)
let inbox: WithLookup<ToDo>[] = []
let done: WithLookup<ToDo>[] = []
let rawActive: WithLookup<ToDo>[] = []
$: active = filterActive(mode, rawActive, currentDate)
$: groups = group(inbox, done, active)
function filterActive (mode: ToDosMode, raw: WithLookup<ToDo>[], currentDate: Date): WithLookup<ToDo>[] {
if (mode === 'planned') {
const today = areDatesEqual(new Date(), currentDate)
const res: WithLookup<ToDo>[] = []
const endDay = new Date().setHours(23, 59, 59, 999)
for (const todo of raw) {
const nearest = getNearest(getWorkslots(todo))
if (nearest === undefined) {
res.push(todo)
} else {
if (today) {
if (nearest.dueDate < endDay) {
res.push(todo)
}
} else if (areDatesEqual(new Date(nearest.date), currentDate)) {
res.push(todo)
}
}
}
return res
} else {
return raw
}
}
function getWorkslots (todo: WithLookup<ToDo>): WorkSlot[] {
return (todo.$lookup?.workslots ?? []) as WorkSlot[]
}
function group (
unplanned: WithLookup<ToDo>[],
done: WithLookup<ToDo>[],
active: WithLookup<ToDo>[]
): [IntlString, WithLookup<ToDo>[]][] {
const groups = new Map<IntlString, WithLookup<ToDo>[]>([
[time.string.Unplanned, unplanned],
[time.string.ToDos, []],
[time.string.Scheduled, []],
[time.string.Done, done]
])
const now = Date.now()
const todos: {
nearest: WorkSlot | undefined
todo: WithLookup<ToDo>
}[] = []
const scheduled: {
nearest: WorkSlot | undefined
todo: WithLookup<ToDo>
}[] = []
for (const todo of active) {
if (todo.$lookup?.workslots !== undefined) {
todo.$lookup.workslots = getWorkslots(todo).sort((a, b) => a.date - b.date)
}
const nearest = getNearest(getWorkslots(todo))
if (nearest === undefined || nearest.dueDate < now) {
todos.push({
nearest,
todo
})
} else {
scheduled.push({
nearest,
todo
})
}
}
groups.set(
time.string.ToDos,
todos.map((p) => p.todo)
)
groups.set(
time.string.Scheduled,
scheduled.map((p) => p.todo)
)
return Array.from(groups)
}
const getDateStr = (date: Date): string => {
return date.toLocaleDateString('default', { month: 'long', day: 'numeric', year: 'numeric' })
}
</script>
<div class="toDos-container">
<Header type={'type-panel'} hideSeparator>
<ButtonIcon
icon={visibleNav ? MenuClose : MenuOpen}
kind={'tertiary'}
size={'small'}
pressed={!visibleNav}
on:click={togglePlannerNav}
/>
<div class="heading-bold-20 ml-4">
<Label label={time.string.ToDoColon} />
{#if mode === 'date'}
{getDateStr(currentDate)}
{:else}
<Label
label={mode === 'all'
? time.string.All
: mode === 'planned'
? time.string.Planned
: mode === 'unplanned'
? time.string.Unplanned
: view.string.Labels}
/>
{/if}
</div>
</Header>
<CreateToDo fullSize />
<Scroller fade={groups.length > 1 ? todosSP : defaultSP} noStretch>
{#each groups as group}
<ToDoGroup
todos={group[1]}
title={group[0]}
showTitle={groups.length > 1}
showDuration={group[0] !== time.string.Unplanned}
{mode}
{projects}
/>
{/each}
</Scroller>
</div>
<style lang="scss">
/* Global styles in components.scss */
.toDos-container {
display: flex;
flex-direction: column;
width: 100%;
min-width: 0;
min-height: 0;
}
</style>