TSK-425: Supported team settings (#2406)

Signed-off-by: Anton Brechka <anton.brechka@ezthera.com>
This commit is contained in:
mrsadman99 2022-12-07 16:40:19 +07:00 committed by GitHub
parent a843530bb2
commit d2345cd931
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 500 additions and 252 deletions

View File

@ -11,7 +11,14 @@ import core, {
SortingOrder,
WorkspaceId
} from '@hcengineering/core'
import tracker, { calcRank, Issue, IssuePriority, IssueStatus } from '../../../plugins/tracker/lib'
import tracker, {
calcRank,
Issue,
IssuePriority,
IssueStatus,
TimeReportDayType,
WorkDayLength
} from '../../../plugins/tracker/lib'
import { connect } from './connect'
@ -35,7 +42,9 @@ const object: AttachedData<Issue> = {
reportedTime: 0,
estimation: 0,
reports: 0,
childInfo: []
childInfo: [],
workDayLength: WorkDayLength.EIGHT_HOURS,
defaultTimeReportDay: TimeReportDayType.PreviousWorkDay
}
export interface IssueOptions {
@ -97,7 +106,9 @@ async function genIssue (client: TxOperations): Promise<void> {
estimation: object.estimation,
reports: 0,
relations: [],
childInfo: []
childInfo: [],
workDayLength: object.workDayLength,
defaultTimeReportDay: object.defaultTimeReportDay
}
await client.addCollection(
tracker.class.Issue,

View File

@ -68,8 +68,10 @@ import {
Sprint,
SprintStatus,
Team,
TimeReportDayType,
TimeSpendReport,
trackerId
trackerId,
WorkDayLength
} from '@hcengineering/tracker'
import { KeyBinding } from '@hcengineering/view'
import tracker from './plugin'
@ -173,6 +175,9 @@ export class TTeam extends TSpace implements Team {
@Prop(TypeRef(tracker.class.IssueStatus), tracker.string.DefaultIssueStatus)
defaultIssueStatus!: Ref<IssueStatus>
declare workDayLength: WorkDayLength
declare defaultTimeReportDay: TimeReportDayType
}
/**
@ -255,6 +260,9 @@ export class TIssue extends TAttachedDoc implements Issue {
reports!: number
declare childInfo: IssueChildInfo[]
declare workDayLength: WorkDayLength
declare defaultTimeReportDay: TimeReportDayType
}
/**
@ -858,6 +866,26 @@ export function createModel (builder: Builder): void {
tracker.action.EditWorkflowStatuses
)
createAction(
builder,
{
action: tracker.actionImpl.EditTeam,
label: tracker.string.EditTeam,
icon: contact.icon.Edit,
input: 'focus',
category: tracker.category.Tracker,
target: tracker.class.Team,
query: {
archived: false
},
context: {
mode: ['context', 'browser'],
group: 'edit'
}
},
tracker.action.EditTeam
)
builder.createDoc(
view.class.ActionCategory,
core.space.Model,

View File

@ -15,7 +15,15 @@
import core, { Doc, DocumentUpdate, generateId, Ref, SortingOrder, TxOperations, TxResult } from '@hcengineering/core'
import { createOrUpdate, MigrateOperation, MigrationClient, MigrationUpgradeClient } from '@hcengineering/model'
import { IssueStatus, IssueStatusCategory, Team, genRanks, Issue } from '@hcengineering/tracker'
import {
IssueStatus,
IssueStatusCategory,
Team,
genRanks,
Issue,
TimeReportDayType,
WorkDayLength
} from '@hcengineering/tracker'
import tags from '@hcengineering/tags'
import { DOMAIN_TRACKER } from '.'
import tracker from './plugin'
@ -99,7 +107,9 @@ async function createDefaultTeam (tx: TxOperations): Promise<void> {
identifier: 'TSK',
sequence: 0,
issueStatuses: 0,
defaultIssueStatus: defaultStatusId
defaultIssueStatus: defaultStatusId,
defaultTimeReportDay: TimeReportDayType.PreviousWorkDay,
workDayLength: WorkDayLength.EIGHT_HOURS
},
tracker.team.DefaultTeam
)
@ -127,6 +137,21 @@ async function fixTeamsIssueStatusesOrder (tx: TxOperations): Promise<void> {
await Promise.all(teams.map((team) => fixTeamIssueStatusesOrder(tx, team)))
}
async function upgradeTeamSettings (tx: TxOperations): Promise<void> {
const teams = await tx.findAll(tracker.class.Team, {
defaultTimeReportDay: { $exists: false },
workDayLength: { $exists: false }
})
await Promise.all(
teams.map((team) =>
tx.update(team, {
defaultTimeReportDay: TimeReportDayType.PreviousWorkDay,
workDayLength: WorkDayLength.EIGHT_HOURS
})
)
)
}
async function upgradeTeamIssueStatuses (tx: TxOperations): Promise<void> {
const teams = await tx.findAll(tracker.class.Team, { issueStatuses: undefined })
@ -178,6 +203,25 @@ async function upgradeIssueStatuses (tx: TxOperations): Promise<void> {
}
}
async function upgradeIssueTimeReportSettings (tx: TxOperations): Promise<void> {
const issues = await tx.findAll(tracker.class.Issue, {
defaultTimeReportDay: { $exists: false },
workDayLength: { $exists: false }
})
const teams = await tx.findAll(tracker.class.Team, {
_id: { $in: Array.from(new Set(issues.map((issue) => issue.space))) }
})
const teamsById = new Map(teams.map((team) => [team._id, team]))
await Promise.all(
issues.map((issue) => {
const team = teamsById.get(issue.space)
return tx.update(issue, { defaultTimeReportDay: team?.defaultTimeReportDay, workDayLength: team?.workDayLength })
})
)
}
async function migrateParentIssues (client: MigrationClient): Promise<void> {
let { updated } = await client.update(
DOMAIN_TRACKER,
@ -305,10 +349,12 @@ async function createDefaults (tx: TxOperations): Promise<void> {
async function upgradeTeams (tx: TxOperations): Promise<void> {
await upgradeTeamIssueStatuses(tx)
await fixTeamsIssueStatusesOrder(tx)
await upgradeTeamSettings(tx)
}
async function upgradeIssues (tx: TxOperations): Promise<void> {
await upgradeIssueStatuses(tx)
await upgradeIssueTimeReportSettings(tx)
const issues = await tx.findAll(tracker.class.Issue, {
$or: [{ blockedBy: { $exists: true } }, { relatedIssue: { $exists: true } }]

View File

@ -56,6 +56,7 @@ export default mergeIds(trackerId, tracker, {
actionImpl: {
CopyToClipboard: '' as ViewAction,
EditWorkflowStatuses: '' as ViewAction,
EditTeam: '' as ViewAction,
DeleteSprint: '' as ViewAction
},
action: {

View File

@ -191,7 +191,7 @@ export interface DropdownTextItem {
}
export interface DropdownIntlItem {
id: string
id: string | number
label: IntlString
}

View File

@ -174,6 +174,7 @@
"EditIssue": "Edit {title}",
"EditWorkflowStatuses": "Edit issue statuses",
"EditTeam": "Edit team",
"ManageWorkflowStatuses": "Manage issue statuses within team",
"AddWorkflowStatus": "Add issue status",
"EditWorkflowStatus": "Edit issue status",
@ -230,6 +231,7 @@
"TimeSpendReportValueTooltip": "Reported time in man days",
"TimeSpendReportDescription": "Description",
"TimeSpendValue": "{value}d",
"TimeSpendHours": "{value}h",
"SprintPassed": "{from}d/{to}d",
"ChildEstimation": "Subissues Estimation",
"ChildReportedTime": "Subissues Time",
@ -249,9 +251,14 @@
"TemplateReplace": "You you replace applied process?",
"TemplateReplaceConfirm": "All changes to template values will be lost.",
"WorkDayCurrent": "Current Working Day",
"WorkDayPrevious": "Previous Working Day",
"WorkDayLabel": "Select Working Day Type"
"CurrentWorkDay": "Current Working Day",
"PreviousWorkDay": "Previous Working Day",
"TimeReportDayTypeLabel": "Select time report day type",
"DefaultTimeReportDay": "Select default day for time report",
"WorkDayLength": "Select length of working day",
"SevenHoursLength": "Seven Hours",
"EightHoursLength": "Eight Hours"
},
"status": {}
}

View File

@ -174,6 +174,7 @@
"EditIssue": "Редактирование {title}",
"EditWorkflowStatuses": "Редактировать статусы задач",
"EditTeam": "Редактировать команду",
"ManageWorkflowStatuses": "Управлять статусами задач для команды",
"AddWorkflowStatus": "Добавить статус задачи",
"EditWorkflowStatus": "Редактировать статус задачи",
@ -230,6 +231,7 @@
"TimeSpendReportValueTooltip": "Затраченное время в человеко днях",
"TimeSpendReportDescription": "Описание",
"TimeSpendValue": "{value}d",
"TimeSpendHours": "{value}h",
"SprintPassed": "{from}d/{to}d",
"ChildEstimation": "Оценка подзадач",
"ChildReportedTime": "Время водзадач",
@ -249,9 +251,14 @@
"TemplateReplace": "Вы хотите заменить выбранный процесс?",
"TemplateReplaceConfirm": "Все внесенные изменения в задачу будут потеряны",
"WorkDayCurrent": "Текущий Рабочий День",
"WorkDayPrevious": "Предыдущий Рабочий День",
"WorkDayLabel": "Выберите Тип Рабочего Дня"
"CurrentWorkDay": "Текущий Рабочий День",
"PreviousWorkDay": "Предыдущий Рабочий День",
"TimeReportDayTypeLabel": "Выберите тип дня для временного отчета",
"DefaultTimeReportDay": "Выберите дeнь для временного отчета по умолчанию",
"WorkDayLength": "Выберите длину рабочего дня",
"SevenHoursLength": "Семь Часов",
"EightHoursLength": "Восемь Часов"
},
"status": {}
}

View File

@ -40,7 +40,9 @@
IssueTemplateChild,
Project,
Sprint,
Team
Team,
TimeReportDayType,
WorkDayLength
} from '@hcengineering/tracker'
import {
ActionIcon,
@ -91,6 +93,7 @@
let issueStatuses: WithLookup<IssueStatus>[] | undefined
let labels: TagReference[] = draft?.labels || []
let objectId: Ref<Issue> = draft?.issueId || generateId()
let currentTeam: Team | undefined
function toIssue (initials: AttachedData<Issue>, draft: IssueDraft | null): AttachedData<Issue> {
if (draft == null) {
@ -100,7 +103,29 @@
return { ...initials, ...issue }
}
let object: AttachedData<Issue> = originalIssue
const defaultIssue = {
title: '',
description: '',
assignee,
project,
sprint,
number: 0,
rank: '',
status: '' as Ref<IssueStatus>,
priority,
dueDate: null,
comments: 0,
subIssues: 0,
parents: [],
reportedTime: 0,
estimation: 0,
reports: 0,
childInfo: [],
workDayLength: currentTeam?.workDayLength ?? WorkDayLength.EIGHT_HOURS,
defaultTimeReportDay: currentTeam?.defaultTimeReportDay ?? TimeReportDayType.PreviousWorkDay
}
let object = originalIssue
? {
...originalIssue,
title: `${originalIssue.title} (copy)`,
@ -110,51 +135,19 @@
reports: 0,
childInfo: []
}
: toIssue(
{
title: '',
description: '',
assignee,
project,
sprint,
number: 0,
rank: '',
status: '' as Ref<IssueStatus>,
priority,
dueDate: null,
comments: 0,
subIssues: 0,
parents: [],
reportedTime: 0,
estimation: 0,
reports: 0,
childInfo: []
},
draft
)
: toIssue(defaultIssue, draft)
$: {
defaultIssue.workDayLength = currentTeam?.workDayLength ?? WorkDayLength.EIGHT_HOURS
defaultIssue.defaultTimeReportDay = currentTeam?.defaultTimeReportDay ?? TimeReportDayType.PreviousWorkDay
object.workDayLength = defaultIssue.workDayLength
object.defaultTimeReportDay = defaultIssue.defaultTimeReportDay
}
function resetObject (): void {
templateId = undefined
template = undefined
object = {
title: '',
description: '',
assignee,
project,
sprint,
number: 0,
rank: '',
status: '' as Ref<IssueStatus>,
priority,
dueDate: null,
comments: 0,
subIssues: 0,
parents: [],
reportedTime: 0,
estimation: 0,
reports: 0,
childInfo: []
}
object = { ...defaultIssue }
subIssues = []
}
@ -221,6 +214,7 @@
const dispatch = createEventDispatcher()
const client = getClient()
const statusesQuery = createQuery()
const spaceQuery = createQuery()
let descriptionBox: AttachmentStyledBox
@ -244,6 +238,9 @@
sort: { rank: SortingOrder.Ascending }
}
)
$: spaceQuery.query(tracker.class.Team, { _id: _space }, (res) => {
currentTeam = res.shift()
})
async function setPropsFromOriginalIssue () {
if (!originalIssue) {
@ -418,7 +415,9 @@
estimation: object.estimation,
reports: 0,
relations: relatedTo !== undefined ? [{ _id: relatedTo._id, _class: relatedTo._class }] : [],
childInfo: []
childInfo: [],
workDayLength: object.workDayLength,
defaultTimeReportDay: object.defaultTimeReportDay
}
await client.addCollection(
@ -490,7 +489,9 @@
estimation: subIssue.estimation,
reports: 0,
relations: [],
childInfo: []
childInfo: [],
workDayLength: object.workDayLength,
defaultTimeReportDay: object.defaultTimeReportDay
}
await client.addCollection(

View File

@ -65,7 +65,9 @@
estimation: 0,
reportedTime: 0,
reports: 0,
childInfo: []
childInfo: [],
workDayLength: currentTeam.workDayLength,
defaultTimeReportDay: currentTeam.defaultTimeReportDay
}
}

View File

@ -46,6 +46,7 @@
let currentTeam: Team | undefined
let issueStatuses: WithLookup<IssueStatus>[] | undefined
$: defaultTimeReportDay = object.defaultTimeReportDay
$: query.query(
object._class,
{ _id: object._id },
@ -140,7 +141,13 @@
on:click={(event) => {
showPopup(
TimeSpendReportPopup,
{ issueId: object._id, issueClass: object._class, space: object.space, assignee: object.assignee },
{
issueId: object._id,
issueClass: object._class,
space: object.space,
assignee: object.assignee,
defaultTimeReportDay
},
eventToHTMLElement(event)
)
}}

View File

@ -1,20 +0,0 @@
<!--
// Copyright © 2020, 2021 Anticrm Platform Contributors.
// Copyright © 2021 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">
export let value: number
</script>
<span class="lines-limit-2 select-text">{value}d</span>

View File

@ -16,12 +16,13 @@
import { AttachedData } from '@hcengineering/core'
import { Issue } from '@hcengineering/tracker'
import { floorFractionDigits, Label } from '@hcengineering/ui'
import tracker from '../../../plugin'
import { floorFractionDigits } from '@hcengineering/ui'
import EstimationProgressCircle from './EstimationProgressCircle.svelte'
import TimePresenter from './TimePresenter.svelte'
export let value: Issue | AttachedData<Issue>
$: workDayLength = value.workDayLength
$: childReportTime = floorFractionDigits(
value.reportedTime + (value.childInfo ?? []).map((it) => it.reportedTime).reduce((a, b) => a + b, 0),
3
@ -43,21 +44,18 @@
{@const reportDiff = floorFractionDigits(rchildReportTime - value.reportedTime, 3)}
{#if reportDiff !== 0 && value.reportedTime !== 0}
<div class="flex flex-nowrap mr-1" class:showError={reportDiff > 0}>
<Label label={tracker.string.TimeSpendValue} params={{ value: rchildReportTime }} />
<TimePresenter value={rchildReportTime} {workDayLength} />
</div>
<div class="romColor">
(<Label
label={tracker.string.TimeSpendValue}
params={{ value: floorFractionDigits(value.reportedTime, 3) }}
/>)
(<TimePresenter value={value.reportedTime} {workDayLength} />)
</div>
{:else if value.reportedTime === 0}
<Label label={tracker.string.TimeSpendValue} params={{ value: childReportTime }} />
<TimePresenter value={childReportTime} {workDayLength} />
{:else}
<Label label={tracker.string.TimeSpendValue} params={{ value: floorFractionDigits(value.reportedTime, 3) }} />
<TimePresenter value={value.reportedTime} {workDayLength} />
{/if}
{:else}
<Label label={tracker.string.TimeSpendValue} params={{ value: floorFractionDigits(value.reportedTime, 3) }} />
<TimePresenter value={value.reportedTime} {workDayLength} />
{/if}
<div class="p-1">/</div>
{/if}
@ -66,21 +64,18 @@
{@const estimationDiff = childEstTime - Math.round(value.estimation)}
{#if estimationDiff !== 0}
<div class="flex flex-nowrap mr-1" class:showWarning={estimationDiff !== 0}>
<Label label={tracker.string.TimeSpendValue} params={{ value: childEstTime }} />
<TimePresenter value={childEstTime} {workDayLength} />
</div>
{#if value.estimation !== 0}
<div class="romColor">
(<Label
label={tracker.string.TimeSpendValue}
params={{ value: floorFractionDigits(value.estimation, 3) }}
/>)
(<TimePresenter value={value.estimation} {workDayLength} />)
</div>
{/if}
{:else}
<Label label={tracker.string.TimeSpendValue} params={{ value: floorFractionDigits(value.estimation, 3) }} />
<TimePresenter value={value.estimation} {workDayLength} />
{/if}
{:else}
<Label label={tracker.string.TimeSpendValue} params={{ value: floorFractionDigits(value.estimation, 3) }} />
<TimePresenter value={value.estimation} {workDayLength} />
{/if}
</span>
</div>

View File

@ -19,6 +19,7 @@
import { ActionIcon, eventToHTMLElement, floorFractionDigits, IconAdd, Label, showPopup } from '@hcengineering/ui'
import ReportsPopup from './ReportsPopup.svelte'
import TimeSpendReportPopup from './TimeSpendReportPopup.svelte'
import TimePresenter from './TimePresenter.svelte'
// export let label: IntlString
export let placeholder: IntlString
@ -26,10 +27,19 @@
export let value: number
export let kind: 'no-border' | 'link' = 'no-border'
$: defaultTimeReportDay = object.defaultTimeReportDay
$: workDayLength = object.workDayLength
function addTimeReport (event: MouseEvent): void {
showPopup(
TimeSpendReportPopup,
{ issueId: object._id, issueClass: object._class, space: object.space, assignee: object.assignee },
{
issueId: object._id,
defaultTimeReportDay,
issueClass: object._class,
space: object.space,
assignee: object.assignee
},
eventToHTMLElement(event)
)
}
@ -46,9 +56,9 @@
<div id="ReportedTimeEditor" class="link-container flex-between" on:click={showReports}>
{#if value !== undefined}
<span class="overflow-label">
{floorFractionDigits(value, 3)}
<TimePresenter {value} {workDayLength} />
{#if childTime !== 0}
/ {childTime}
/ <TimePresenter value={childTime} {workDayLength} />
{/if}
</span>
{:else}
@ -60,9 +70,9 @@
</div>
{:else if value !== undefined}
<span class="overflow-label">
{floorFractionDigits(value, 3)}
<TimePresenter {value} {workDayLength} />
{#if childTime !== 0}
/ {childTime}
/ <TimePresenter value={childTime} {workDayLength} />
{/if}
</span>
{:else}

View File

@ -25,6 +25,8 @@
import TimeSpendReportPopup from './TimeSpendReportPopup.svelte'
export let issue: Issue
$: defaultTimeReportDay = issue.defaultTimeReportDay
export function canClose (): boolean {
return true
}
@ -37,7 +39,13 @@
function addReport (event: MouseEvent): void {
showPopup(
TimeSpendReportPopup,
{ issueId: issue._id, issueClass: issue._class, space: issue.space, assignee: issue.assignee },
{
issueId: issue._id,
issueClass: issue._class,
space: issue.space,
assignee: issue.assignee,
defaultTimeReportDay
},
eventToHTMLElement(event)
)
}

View File

@ -0,0 +1,54 @@
<!--
// 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 { WorkDayLength } from '@hcengineering/tracker'
import { floorFractionDigits, Label, tooltip } from '@hcengineering/ui'
import tracker from '../../../plugin'
export let id: string | undefined = undefined
export let kind: 'link' | undefined = undefined
export let workDayLength: WorkDayLength = WorkDayLength.EIGHT_HOURS
export let value: number
</script>
<span
{id}
class:link={kind === 'link'}
on:click
use:tooltip={{
component: Label,
props: { label: tracker.string.TimeSpendHours, params: { value: floorFractionDigits(value * workDayLength, 2) } }
}}
>
<Label label={tracker.string.TimeSpendValue} params={{ value: floorFractionDigits(value, 3) }} />
</span>
<style lang="scss">
.link {
white-space: nowrap;
font-size: 0.8125rem;
color: var(--content-color);
cursor: pointer;
&:hover {
color: var(--caption-color);
text-decoration: underline;
}
&:active {
color: var(--accent-color);
}
}
</style>

View File

@ -13,34 +13,32 @@
// limitations under the License.
-->
<script lang="ts">
import { TimeReportDayType } from '@hcengineering/tracker'
import { DropdownIntlItem, DropdownLabelsIntl } from '@hcengineering/ui'
import tracker from '../../../plugin'
import { WorkDaysType } from '../../../types'
import { getWorkDate, getWorkDayType } from '../../../utils'
import WorkDaysIcon from './WorkDaysIcon.svelte'
import TimeReportDayIcon from './TimeReportDayIcon.svelte'
export let dateTimestamp: number
export let label = tracker.string.TimeReportDayTypeLabel
export let selected: TimeReportDayType | undefined
const workDaysDropdownItems: DropdownIntlItem[] = [
{
id: WorkDaysType.CURRENT,
label: tracker.string.WorkDayCurrent
id: TimeReportDayType.CurrentWorkDay,
label: tracker.string.CurrentWorkDay
},
{
id: WorkDaysType.PREVIOUS,
label: tracker.string.WorkDayPrevious
id: TimeReportDayType.PreviousWorkDay,
label: tracker.string.PreviousWorkDay
}
]
$: selectedWorkDayType = dateTimestamp ? getWorkDayType(dateTimestamp) : undefined
</script>
<DropdownLabelsIntl
kind="link-bordered"
icon={WorkDaysIcon}
icon={TimeReportDayIcon}
shouldUpdateUndefined={false}
label={tracker.string.WorkDayLabel}
{label}
items={workDaysDropdownItems}
bind:selected={selectedWorkDayType}
on:selected={({ detail }) => (dateTimestamp = getWorkDate(detail))}
bind:selected
on:selected
/>

View File

@ -13,75 +13,40 @@
// limitations under the License.
-->
<script lang="ts">
import contact, { Employee } from '@hcengineering/contact'
import { WithLookup } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation'
import type { TimeSpendReport } from '@hcengineering/tracker'
import { eventToHTMLElement, floorFractionDigits, Label, showPopup, tooltip } from '@hcengineering/ui'
import view, { AttributeModel } from '@hcengineering/view'
import { getObjectPresenter } from '@hcengineering/view-resources'
import tracker from '../../../plugin'
import { Issue, TimeSpendReport } from '@hcengineering/tracker'
import { eventToHTMLElement, showPopup } from '@hcengineering/ui'
import TimePresenter from './TimePresenter.svelte'
import TimeSpendReportPopup from './TimeSpendReportPopup.svelte'
export let value: WithLookup<TimeSpendReport>
const client = getClient()
let presenter: AttributeModel
getObjectPresenter(client, contact.class.Employee, { key: '' }).then((p) => {
presenter = p
})
$: issue = value.$lookup?.attachedTo
$: if (!issue) {
client.findOne(value.attachedToClass, { _id: value.attachedTo }).then((r) => {
issue = r as Issue
})
}
$: workDayLength = issue?.workDayLength
$: defaultTimeReportDay = issue?.defaultTimeReportDay
function editSpendReport (event: MouseEvent): void {
showPopup(
TimeSpendReportPopup,
{ issue: value.attachedTo, issueClass: value.attachedToClass, value, assignee: value.employee },
{
issue: value.attachedTo,
issueClass: value.attachedToClass,
value,
assignee: value.employee,
defaultTimeReportDay
},
eventToHTMLElement(event)
)
}
let employee: Employee | undefined | null = value.$lookup?.employee ?? null
$: if (employee === undefined) {
client.findOne(value.attachedToClass, { _id: value.attachedTo }).then((r) => {
employee = r as Employee
})
}
</script>
{#if value && value.value}
<span
id="TimeSpendReportValue"
class="issuePresenterRoot flex-row-center"
on:click={editSpendReport}
use:tooltip={value.employee
? {
label: tracker.string.TimeSpendReport,
component: view.component.ObjectPresenter,
props: {
objectId: value.employee,
_class: contact.class.Employee,
value: value.$lookup?.employee
}
}
: undefined}
>
<Label label={tracker.string.TimeSpendValue} params={{ value: floorFractionDigits(value.value, 3) }} />
</span>
<TimePresenter id="TimeSpendReportValue" kind="link" value={value.value} {workDayLength} on:click={editSpendReport} />
{/if}
<style lang="scss">
.issuePresenterRoot {
white-space: nowrap;
font-size: 0.8125rem;
color: var(--content-color);
cursor: pointer;
&:hover {
color: var(--caption-color);
text-decoration: underline;
}
&:active {
color: var(--accent-color);
}
}
</style>

View File

@ -17,11 +17,11 @@
import { AttachedData, Class, DocumentUpdate, Ref, Space } from '@hcengineering/core'
import type { IntlString } from '@hcengineering/platform'
import presentation, { Card, getClient, UserBox } from '@hcengineering/presentation'
import { Issue, TimeSpendReport } from '@hcengineering/tracker'
import { Issue, TimeReportDayType, TimeSpendReport } from '@hcengineering/tracker'
import { DatePresenter, EditBox } from '@hcengineering/ui'
import tracker from '../../../plugin'
import { getWorkDate, WorkDaysType } from '../../../utils'
import WorkDaysDropdown from './WorkDaysDropdown.svelte'
import { getTimeReportDate, getTimeReportDayType } from '../../../utils'
import TimeReportDayDropdown from './TimeReportDayDropdown.svelte'
export let issueId: Ref<Issue>
export let issueClass: Ref<Class<Issue>>
@ -30,17 +30,21 @@
export let value: TimeSpendReport | undefined
export let placeholder: IntlString = tracker.string.TimeSpendReportValue
export let defaultTimeReportDay = TimeReportDayType.PreviousWorkDay
const data = {
date: value?.date ?? getTimeReportDate(defaultTimeReportDay),
description: value?.description ?? '',
value: value?.value,
employee: value?.employee ?? assignee ?? null
}
let selectedTimeReportDay = getTimeReportDayType(data.date)
export function canClose (): boolean {
return true
}
const data = {
date: value?.date ?? getWorkDate(WorkDaysType.PREVIOUS),
description: value?.description ?? '',
value: value?.value,
employee: value?.employee ?? assignee ?? null
}
async function create (): Promise<void> {
if (value === undefined) {
getClient().addCollection(
@ -88,8 +92,16 @@
bind:value={data.employee}
showNavigate={false}
/>
<WorkDaysDropdown bind:dateTimestamp={data.date} />
<DatePresenter kind={'link'} bind:value={data.date} editable />
<TimeReportDayDropdown
bind:selected={selectedTimeReportDay}
on:selected={({ detail }) => (data.date = getTimeReportDate(detail))}
/>
<DatePresenter
kind={'link'}
bind:value={data.date}
editable
on:change={({ detail }) => (selectedTimeReportDay = getTimeReportDayType(detail))}
/>
</div>
<EditBox bind:value={data.description} placeholder={tracker.string.TimeSpendReportDescription} kind={'editbox'} />
</Card>

View File

@ -18,6 +18,7 @@
import { Issue, Team, TimeSpendReport } from '@hcengineering/tracker'
import { floorFractionDigits, Label, Scroller, Spinner } from '@hcengineering/ui'
import tracker from '../../../plugin'
import TimePresenter from './TimePresenter.svelte'
import TimeSpendReportsList from './TimeSpendReportsList.svelte'
export let issue: Issue
@ -28,6 +29,7 @@
let reports: TimeSpendReport[] | undefined
$: workDayLength = issue.workDayLength
$: subIssuesQuery.query(tracker.class.TimeSpendReport, query, async (result) => (reports = result), {
sort: { modifiedOn: SortingOrder.Descending },
lookup: {
@ -40,8 +42,10 @@
</script>
{#if reports}
<Label label={tracker.string.ReportedTime} />: {reportedTime}
<Label label={tracker.string.TimeSpendReports} />: {total}
<span class="overflow-label flex-nowrap">
<Label label={tracker.string.ReportedTime} />: <TimePresenter value={reportedTime} {workDayLength} />
<Label label={tracker.string.TimeSpendReports} />: <TimePresenter value={total} {workDayLength} />
</span>
<div class="h-50">
<Scroller>
<TimeSpendReportsList {reports} {teams} />

View File

@ -16,19 +16,13 @@
import contact from '@hcengineering/contact'
import { Doc, Ref, Space, WithLookup } from '@hcengineering/core'
import UserBox from '@hcengineering/presentation/src/components/UserBox.svelte'
import { Team, TimeSpendReport } from '@hcengineering/tracker'
import {
eventToHTMLElement,
floorFractionDigits,
getEventPositionElement,
ListView,
showPopup
} from '@hcengineering/ui'
import { Team, TimeReportDayType, TimeSpendReport } from '@hcengineering/tracker'
import { eventToHTMLElement, getEventPositionElement, ListView, showPopup } from '@hcengineering/ui'
import DatePresenter from '@hcengineering/ui/src/components/calendar/DatePresenter.svelte'
import { ContextMenu, FixedColumn, ListSelectionProvider, SelectDirection } from '@hcengineering/view-resources'
import { getIssueId } from '../../../issues'
import tracker from '../../../plugin'
import EstimationPresenter from './EstimationPresenter.svelte'
import TimePresenter from './TimePresenter.svelte'
import TimeSpendReportPopup from './TimeSpendReportPopup.svelte'
export let reports: WithLookup<TimeSpendReport>[]
@ -52,10 +46,20 @@
}
const toTeamId = (ref: Ref<Space>) => ref as Ref<Team>
function editSpendReport (event: MouseEvent, value: TimeSpendReport): void {
function editSpendReport (
event: MouseEvent,
value: TimeSpendReport,
defaultTimeReportDay: TimeReportDayType | undefined
): void {
showPopup(
TimeSpendReportPopup,
{ issue: value.attachedTo, issueClass: value.attachedToClass, value, assignee: value.employee },
{
issue: value.attachedTo,
issueClass: value.attachedToClass,
value,
assignee: value.employee,
defaultTimeReportDay
},
eventToHTMLElement(event)
)
}
@ -75,7 +79,7 @@
on:focus={() => {
listProvider.updateFocus(report)
}}
on:click={(evt) => editSpendReport(evt, report)}
on:click={(evt) => editSpendReport(evt, report, currentTeam?.defaultTimeReportDay)}
>
<div class="flex-row-center clear-mins gap-2 p-2">
<span class="issuePresenter">
@ -104,7 +108,7 @@
readonly
showNavigate={false}
/>
<EstimationPresenter value={floorFractionDigits(report.value, 3)} />
<TimePresenter value={report.value} workDayLength={currentTeam?.workDayLength} />
<DatePresenter value={report.date} />
</div>
</div>

View File

@ -16,7 +16,7 @@
import { Ref, WithLookup } from '@hcengineering/core'
import { IntlString } from '@hcengineering/platform'
import { createQuery, getClient } from '@hcengineering/presentation'
import { Issue, IssueStatus, IssueTemplate, Sprint } from '@hcengineering/tracker'
import { Issue, IssueStatus, IssueTemplate, Sprint, Team } from '@hcengineering/tracker'
import { ButtonKind, ButtonSize, ButtonShape, floorFractionDigits } from '@hcengineering/ui'
import { Label, deviceOptionsStore as deviceInfo } from '@hcengineering/ui'
import DatePresenter from '@hcengineering/ui/src/components/calendar/DatePresenter.svelte'
@ -24,6 +24,7 @@
import tracker from '../../plugin'
import { getDayOfSprint } from '../../utils'
import EstimationProgressCircle from '../issues/timereport/EstimationProgressCircle.svelte'
import TimePresenter from '../issues/timereport/TimePresenter.svelte'
import SprintSelector from './SprintSelector.svelte'
export let value: Issue | IssueTemplate
@ -42,6 +43,13 @@
export let enlargedText: boolean = false
const client = getClient()
const spaceQuery = createQuery()
let currentTeam: Team | undefined
$: spaceQuery.query(tracker.class.Team, { _id: value.space }, (res) => {
currentTeam = res.shift()
})
$: workDayLength = currentTeam?.workDayLength
const handleSprintIdChanged = async (newSprintId: Ref<Sprint> | null | undefined) => {
if (!isEditable || newSprintId === undefined || value.sprint === newSprintId) {
@ -157,23 +165,21 @@
<div class="flex-row-center" class:minus-margin-space={kind === 'list-header'} class:text-sm={twoRows}>
{#if sprint}
{@const now = Date.now()}
{@const sprintDaysFrom =
now < sprint.startDate
? 0
: now > sprint.targetDate
? getDayOfSprint(sprint.startDate, sprint.targetDate)
: getDayOfSprint(sprint.startDate, now)}
{@const sprintDaysTo = getDayOfSprint(sprint.startDate, sprint.targetDate)}
<DatePresenter value={sprint.startDate} kind={'transparent'} />
<span class="p-1"> / </span>
<DatePresenter value={sprint.targetDate} kind={'transparent'} />
<div class="w-2 min-w-2" />
<!-- Active sprint in time -->
<Label
label={tracker.string.SprintPassed}
params={{
from:
now < sprint.startDate
? 0
: now > sprint.targetDate
? getDayOfSprint(sprint.startDate, sprint.targetDate)
: getDayOfSprint(sprint.startDate, now),
to: getDayOfSprint(sprint.startDate, sprint.targetDate)
}}
/>
<TimePresenter value={sprintDaysFrom} {workDayLength} />
/
<TimePresenter value={sprintDaysTo} {workDayLength} />
{/if}
{#if issues}
<!-- <Label label={tracker.string.SprintDay} value={}/> -->
@ -186,10 +192,10 @@
<EstimationProgressCircle value={totalReported} max={totalEstimation} />
<div class="w-2 min-w-2" />
{#if totalReported > 0}
<Label label={tracker.string.TimeSpendValue} params={{ value: totalReported }} />
<TimePresenter value={totalReported} {workDayLength} />
/
{/if}
<Label label={tracker.string.TimeSpendValue} params={{ value: totalEstimation }} />
<TimePresenter value={totalEstimation} {workDayLength} />
{#if sprint?.capacity}
<Label label={tracker.string.CapacityValue} params={{ value: sprint?.capacity }} />
{/if}

View File

@ -14,25 +14,56 @@
-->
<script lang="ts">
import { createEventDispatcher } from 'svelte'
import { Button, EditBox, eventToHTMLElement, Label, showPopup, ToggleWithLabel } from '@hcengineering/ui'
import { getClient, SpaceCreateCard } from '@hcengineering/presentation'
import {
Button,
DropdownIntlItem,
DropdownLabelsIntl,
EditBox,
eventToHTMLElement,
Label,
showPopup,
ToggleWithLabel
} from '@hcengineering/ui'
import presentation, { Card, getClient } from '@hcengineering/presentation'
import core, { getCurrentAccount, Ref } from '@hcengineering/core'
import { IssueStatus } from '@hcengineering/tracker'
import { IssueStatus, Team, TimeReportDayType, WorkDayLength } from '@hcengineering/tracker'
import { StyledTextBox } from '@hcengineering/text-editor'
import { Asset } from '@hcengineering/platform'
import tracker from '../../plugin'
import TeamIconChooser from './TeamIconChooser.svelte'
import TimeReportDayDropdown from '../issues/timereport/TimeReportDayDropdown.svelte'
let name: string = ''
let description: string = ''
let isPrivate: boolean = false
let icon: Asset | undefined = undefined
export let team: Team | undefined = undefined
let name: string = team?.name ?? ''
let description: string = team?.description ?? ''
let isPrivate: boolean = team?.private ?? false
let icon: Asset | undefined = team?.icon ?? undefined
let selectedWorkDayType: TimeReportDayType | undefined =
team?.defaultTimeReportDay ?? TimeReportDayType.PreviousWorkDay
let selectedWorkDayLength: WorkDayLength | undefined = team?.workDayLength ?? WorkDayLength.EIGHT_HOURS
const dispatch = createEventDispatcher()
const client = getClient()
const workDayLengthItems: DropdownIntlItem[] = [
{
id: WorkDayLength.SEVEN_HOURS,
label: tracker.string.SevenHoursLength
},
{
id: WorkDayLength.EIGHT_HOURS,
label: tracker.string.EightHoursLength
}
]
async function createTeam () {
await client.createDoc(tracker.class.Team, core.space.Space, {
$: isNew = !team
async function handleSave () {
isNew ? createTeam() : updateTeam()
}
function getTeamData () {
return {
name,
description,
private: isPrivate,
@ -42,8 +73,31 @@
sequence: 0,
issueStatuses: 0,
defaultIssueStatus: '' as Ref<IssueStatus>,
icon
})
icon,
defaultTimeReportDay: selectedWorkDayType ?? TimeReportDayType.PreviousWorkDay,
workDayLength: selectedWorkDayLength ?? WorkDayLength.EIGHT_HOURS
}
}
async function updateTeam () {
const teamData = getTeamData()
// update team doc
await client.update(team!, teamData)
// update issues related to team
const issuesByTeam = await client.findAll(tracker.class.Issue, { space: team!._id })
await Promise.all(
issuesByTeam.map((issue) =>
client.update(issue, {
defaultTimeReportDay: teamData.defaultTimeReportDay,
workDayLength: teamData.workDayLength
})
)
)
}
async function createTeam () {
await client.createDoc(tracker.class.Team, core.space.Space, getTeamData())
}
function chooseIcon (ev: MouseEvent) {
@ -55,10 +109,11 @@
}
</script>
<SpaceCreateCard
label={tracker.string.NewTeam}
okAction={createTeam}
canSave={name.length > 0}
<Card
label={isNew ? tracker.string.NewTeam : tracker.string.EditTeam}
okLabel={isNew ? presentation.string.Create : presentation.string.Edit}
okAction={handleSave}
canSave={name.length > 0 && !!selectedWorkDayType && !!selectedWorkDayLength}
on:close={() => {
dispatch('close')
}}
@ -81,4 +136,24 @@
</div>
<Button icon={icon ?? tracker.icon.Home} kind="no-border" size="medium" on:click={chooseIcon} />
</div>
</SpaceCreateCard>
<div class="flex-between">
<div class="caption">
<Label label={tracker.string.DefaultTimeReportDay} />
</div>
<TimeReportDayDropdown bind:selected={selectedWorkDayType} label={tracker.string.DefaultTimeReportDay} />
</div>
<div class="flex-between">
<div class="caption">
<Label label={tracker.string.WorkDayLength} />
</div>
<DropdownLabelsIntl
kind="link-bordered"
label={tracker.string.WorkDayLength}
items={workDayLengthItems}
shouldUpdateUndefined={false}
bind:selected={selectedWorkDayLength}
/>
</div>
</Card>

View File

@ -164,6 +164,12 @@ async function editWorkflowStatuses (team: Team | undefined): Promise<void> {
}
}
async function editTeam (team: Team | undefined): Promise<void> {
if (team !== undefined) {
showPopup(CreateTeam, { team })
}
}
async function moveAndDeleteSprint (client: TxOperations, oldSprint: Sprint, newSprint?: Sprint): Promise<void> {
const noSprintLabel = await translate(tracker.string.NoSprint, {})
@ -282,6 +288,7 @@ export default async (): Promise<Resources> => ({
},
actionImpl: {
EditWorkflowStatuses: editWorkflowStatuses,
EditTeam: editTeam,
DeleteSprint: deleteSprint
},
resolver: {

View File

@ -90,6 +90,7 @@ export default mergeIds(trackerId, tracker, {
DefaultIssueStatus: '' as IntlString,
IssueStatuses: '' as IntlString,
EditWorkflowStatuses: '' as IntlString,
EditTeam: '' as IntlString,
ManageWorkflowStatuses: '' as IntlString,
AddWorkflowStatus: '' as IntlString,
EditWorkflowStatus: '' as IntlString,
@ -248,6 +249,7 @@ export default mergeIds(trackerId, tracker, {
TimeSpendReportValue: '' as IntlString,
TimeSpendReportDescription: '' as IntlString,
TimeSpendValue: '' as IntlString,
TimeSpendHours: '' as IntlString,
SprintPassed: '' as IntlString,
ChildEstimation: '' as IntlString,
@ -265,9 +267,14 @@ export default mergeIds(trackerId, tracker, {
TemplateReplace: '' as IntlString,
TemplateReplaceConfirm: '' as IntlString,
WorkDayCurrent: '' as IntlString,
WorkDayPrevious: '' as IntlString,
WorkDayLabel: '' as IntlString
CurrentWorkDay: '' as IntlString,
PreviousWorkDay: '' as IntlString,
TimeReportDayTypeLabel: '' as IntlString,
DefaultTimeReportDay: '' as IntlString,
WorkDayLength: '' as IntlString,
SevenHoursLength: '' as IntlString,
EightHoursLength: '' as IntlString
},
component: {
NopeComponent: '' as AnyComponent,

View File

@ -104,8 +104,3 @@ export const issuesGroupBySorting: Record<IssuesGrouping, SortingQuery<Issue>> =
[IssuesGrouping.Sprint]: { '$lookup.sprint.label': SortingOrder.Ascending },
[IssuesGrouping.NoGrouping]: {}
}
export enum WorkDaysType {
CURRENT = 'current',
PREVIOUS = 'previous'
}

View File

@ -27,7 +27,8 @@ import {
ProjectStatus,
Sprint,
SprintStatus,
Team
Team,
TimeReportDayType
} from '@hcengineering/tracker'
import { ViewOptionModel } from '@hcengineering/view-resources'
import {
@ -39,13 +40,7 @@ import {
MILLISECONDS_IN_WEEK
} from '@hcengineering/ui'
import tracker from './plugin'
import {
defaultPriorities,
defaultProjectStatuses,
defaultSprintStatuses,
issuePriorities,
WorkDaysType
} from './types'
import { defaultPriorities, defaultProjectStatuses, defaultSprintStatuses, issuePriorities } from './types'
export * from './types'
@ -649,13 +644,14 @@ export async function moveIssuesToAnotherSprint (
}
}
export function getWorkDate (type: WorkDaysType): number {
export function getTimeReportDate (type: TimeReportDayType): number {
const date = new Date(Date.now())
if (type === WorkDaysType.PREVIOUS) {
if (type === TimeReportDayType.PreviousWorkDay) {
date.setDate(date.getDate() - 1)
}
// if currentDate is day off then set date to last working day
// if date is day off then set date to last working day
while (isWeekend(date)) {
date.setDate(date.getDate() - 1)
}
@ -663,14 +659,14 @@ export function getWorkDate (type: WorkDaysType): number {
return date.valueOf()
}
export function getWorkDayType (timestamp: number): WorkDaysType | undefined {
export function getTimeReportDayType (timestamp: number): TimeReportDayType | undefined {
const date = new Date(timestamp)
const currentWorkDate = new Date(getWorkDate(WorkDaysType.CURRENT))
const previousWorkDate = new Date(getWorkDate(WorkDaysType.PREVIOUS))
const currentWorkDate = new Date(getTimeReportDate(TimeReportDayType.CurrentWorkDay))
const previousWorkDate = new Date(getTimeReportDate(TimeReportDayType.PreviousWorkDay))
if (areDatesEqual(date, currentWorkDate)) {
return WorkDaysType.CURRENT
return TimeReportDayType.CurrentWorkDay
} else if (areDatesEqual(date, previousWorkDate)) {
return WorkDaysType.PREVIOUS
return TimeReportDayType.PreviousWorkDay
}
}

View File

@ -53,6 +53,24 @@ export interface Team extends Space {
issueStatuses: number
defaultIssueStatus: Ref<IssueStatus>
icon?: Asset
workDayLength: WorkDayLength
defaultTimeReportDay: TimeReportDayType
}
/**
* @public
*/
export enum TimeReportDayType {
CurrentWorkDay = 'CurrentWorkDay',
PreviousWorkDay = 'PreviousWorkDay'
}
/**
* @public
*/
export enum WorkDayLength {
SEVEN_HOURS = 7,
EIGHT_HOURS = 8
}
/**
@ -175,6 +193,9 @@ export interface Issue extends AttachedDoc {
childInfo: IssueChildInfo[]
workDayLength: WorkDayLength
defaultTimeReportDay: TimeReportDayType
template?: {
// A template issue is based on
template: Ref<IssueTemplate>
@ -465,6 +486,7 @@ export default plugin(trackerId, {
Relations: '' as Ref<Action>,
NewSubIssue: '' as Ref<Action>,
EditWorkflowStatuses: '' as Ref<Action>,
EditTeam: '' as Ref<Action>,
SetSprint: '' as Ref<Action>
},
team: {