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

View File

@ -68,8 +68,10 @@ import {
Sprint, Sprint,
SprintStatus, SprintStatus,
Team, Team,
TimeReportDayType,
TimeSpendReport, TimeSpendReport,
trackerId trackerId,
WorkDayLength
} from '@hcengineering/tracker' } from '@hcengineering/tracker'
import { KeyBinding } from '@hcengineering/view' import { KeyBinding } from '@hcengineering/view'
import tracker from './plugin' import tracker from './plugin'
@ -173,6 +175,9 @@ export class TTeam extends TSpace implements Team {
@Prop(TypeRef(tracker.class.IssueStatus), tracker.string.DefaultIssueStatus) @Prop(TypeRef(tracker.class.IssueStatus), tracker.string.DefaultIssueStatus)
defaultIssueStatus!: Ref<IssueStatus> defaultIssueStatus!: Ref<IssueStatus>
declare workDayLength: WorkDayLength
declare defaultTimeReportDay: TimeReportDayType
} }
/** /**
@ -255,6 +260,9 @@ export class TIssue extends TAttachedDoc implements Issue {
reports!: number reports!: number
declare childInfo: IssueChildInfo[] declare childInfo: IssueChildInfo[]
declare workDayLength: WorkDayLength
declare defaultTimeReportDay: TimeReportDayType
} }
/** /**
@ -858,6 +866,26 @@ export function createModel (builder: Builder): void {
tracker.action.EditWorkflowStatuses 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( builder.createDoc(
view.class.ActionCategory, view.class.ActionCategory,
core.space.Model, core.space.Model,

View File

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

View File

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

View File

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

View File

@ -174,6 +174,7 @@
"EditIssue": "Edit {title}", "EditIssue": "Edit {title}",
"EditWorkflowStatuses": "Edit issue statuses", "EditWorkflowStatuses": "Edit issue statuses",
"EditTeam": "Edit team",
"ManageWorkflowStatuses": "Manage issue statuses within team", "ManageWorkflowStatuses": "Manage issue statuses within team",
"AddWorkflowStatus": "Add issue status", "AddWorkflowStatus": "Add issue status",
"EditWorkflowStatus": "Edit issue status", "EditWorkflowStatus": "Edit issue status",
@ -230,6 +231,7 @@
"TimeSpendReportValueTooltip": "Reported time in man days", "TimeSpendReportValueTooltip": "Reported time in man days",
"TimeSpendReportDescription": "Description", "TimeSpendReportDescription": "Description",
"TimeSpendValue": "{value}d", "TimeSpendValue": "{value}d",
"TimeSpendHours": "{value}h",
"SprintPassed": "{from}d/{to}d", "SprintPassed": "{from}d/{to}d",
"ChildEstimation": "Subissues Estimation", "ChildEstimation": "Subissues Estimation",
"ChildReportedTime": "Subissues Time", "ChildReportedTime": "Subissues Time",
@ -249,9 +251,14 @@
"TemplateReplace": "You you replace applied process?", "TemplateReplace": "You you replace applied process?",
"TemplateReplaceConfirm": "All changes to template values will be lost.", "TemplateReplaceConfirm": "All changes to template values will be lost.",
"WorkDayCurrent": "Current Working Day", "CurrentWorkDay": "Current Working Day",
"WorkDayPrevious": "Previous Working Day", "PreviousWorkDay": "Previous Working Day",
"WorkDayLabel": "Select Working Day Type" "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": {} "status": {}
} }

View File

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

View File

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

View File

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

View File

@ -46,6 +46,7 @@
let currentTeam: Team | undefined let currentTeam: Team | undefined
let issueStatuses: WithLookup<IssueStatus>[] | undefined let issueStatuses: WithLookup<IssueStatus>[] | undefined
$: defaultTimeReportDay = object.defaultTimeReportDay
$: query.query( $: query.query(
object._class, object._class,
{ _id: object._id }, { _id: object._id },
@ -140,7 +141,13 @@
on:click={(event) => { on:click={(event) => {
showPopup( showPopup(
TimeSpendReportPopup, 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) 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 { AttachedData } from '@hcengineering/core'
import { Issue } from '@hcengineering/tracker' import { Issue } from '@hcengineering/tracker'
import { floorFractionDigits, Label } from '@hcengineering/ui' import { floorFractionDigits } from '@hcengineering/ui'
import tracker from '../../../plugin'
import EstimationProgressCircle from './EstimationProgressCircle.svelte' import EstimationProgressCircle from './EstimationProgressCircle.svelte'
import TimePresenter from './TimePresenter.svelte'
export let value: Issue | AttachedData<Issue> export let value: Issue | AttachedData<Issue>
$: workDayLength = value.workDayLength
$: childReportTime = floorFractionDigits( $: childReportTime = floorFractionDigits(
value.reportedTime + (value.childInfo ?? []).map((it) => it.reportedTime).reduce((a, b) => a + b, 0), value.reportedTime + (value.childInfo ?? []).map((it) => it.reportedTime).reduce((a, b) => a + b, 0),
3 3
@ -43,21 +44,18 @@
{@const reportDiff = floorFractionDigits(rchildReportTime - value.reportedTime, 3)} {@const reportDiff = floorFractionDigits(rchildReportTime - value.reportedTime, 3)}
{#if reportDiff !== 0 && value.reportedTime !== 0} {#if reportDiff !== 0 && value.reportedTime !== 0}
<div class="flex flex-nowrap mr-1" class:showError={reportDiff > 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>
<div class="romColor"> <div class="romColor">
(<Label (<TimePresenter value={value.reportedTime} {workDayLength} />)
label={tracker.string.TimeSpendValue}
params={{ value: floorFractionDigits(value.reportedTime, 3) }}
/>)
</div> </div>
{:else if value.reportedTime === 0} {:else if value.reportedTime === 0}
<Label label={tracker.string.TimeSpendValue} params={{ value: childReportTime }} /> <TimePresenter value={childReportTime} {workDayLength} />
{:else} {:else}
<Label label={tracker.string.TimeSpendValue} params={{ value: floorFractionDigits(value.reportedTime, 3) }} /> <TimePresenter value={value.reportedTime} {workDayLength} />
{/if} {/if}
{:else} {:else}
<Label label={tracker.string.TimeSpendValue} params={{ value: floorFractionDigits(value.reportedTime, 3) }} /> <TimePresenter value={value.reportedTime} {workDayLength} />
{/if} {/if}
<div class="p-1">/</div> <div class="p-1">/</div>
{/if} {/if}
@ -66,21 +64,18 @@
{@const estimationDiff = childEstTime - Math.round(value.estimation)} {@const estimationDiff = childEstTime - Math.round(value.estimation)}
{#if estimationDiff !== 0} {#if estimationDiff !== 0}
<div class="flex flex-nowrap mr-1" class:showWarning={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> </div>
{#if value.estimation !== 0} {#if value.estimation !== 0}
<div class="romColor"> <div class="romColor">
(<Label (<TimePresenter value={value.estimation} {workDayLength} />)
label={tracker.string.TimeSpendValue}
params={{ value: floorFractionDigits(value.estimation, 3) }}
/>)
</div> </div>
{/if} {/if}
{:else} {:else}
<Label label={tracker.string.TimeSpendValue} params={{ value: floorFractionDigits(value.estimation, 3) }} /> <TimePresenter value={value.estimation} {workDayLength} />
{/if} {/if}
{:else} {:else}
<Label label={tracker.string.TimeSpendValue} params={{ value: floorFractionDigits(value.estimation, 3) }} /> <TimePresenter value={value.estimation} {workDayLength} />
{/if} {/if}
</span> </span>
</div> </div>

View File

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

View File

@ -25,6 +25,8 @@
import TimeSpendReportPopup from './TimeSpendReportPopup.svelte' import TimeSpendReportPopup from './TimeSpendReportPopup.svelte'
export let issue: Issue export let issue: Issue
$: defaultTimeReportDay = issue.defaultTimeReportDay
export function canClose (): boolean { export function canClose (): boolean {
return true return true
} }
@ -37,7 +39,13 @@
function addReport (event: MouseEvent): void { function addReport (event: MouseEvent): void {
showPopup( showPopup(
TimeSpendReportPopup, 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) 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. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import { TimeReportDayType } from '@hcengineering/tracker'
import { DropdownIntlItem, DropdownLabelsIntl } from '@hcengineering/ui' import { DropdownIntlItem, DropdownLabelsIntl } from '@hcengineering/ui'
import tracker from '../../../plugin' import tracker from '../../../plugin'
import { WorkDaysType } from '../../../types' import TimeReportDayIcon from './TimeReportDayIcon.svelte'
import { getWorkDate, getWorkDayType } from '../../../utils'
import WorkDaysIcon from './WorkDaysIcon.svelte'
export let dateTimestamp: number export let label = tracker.string.TimeReportDayTypeLabel
export let selected: TimeReportDayType | undefined
const workDaysDropdownItems: DropdownIntlItem[] = [ const workDaysDropdownItems: DropdownIntlItem[] = [
{ {
id: WorkDaysType.CURRENT, id: TimeReportDayType.CurrentWorkDay,
label: tracker.string.WorkDayCurrent label: tracker.string.CurrentWorkDay
}, },
{ {
id: WorkDaysType.PREVIOUS, id: TimeReportDayType.PreviousWorkDay,
label: tracker.string.WorkDayPrevious label: tracker.string.PreviousWorkDay
} }
] ]
$: selectedWorkDayType = dateTimestamp ? getWorkDayType(dateTimestamp) : undefined
</script> </script>
<DropdownLabelsIntl <DropdownLabelsIntl
kind="link-bordered" kind="link-bordered"
icon={WorkDaysIcon} icon={TimeReportDayIcon}
shouldUpdateUndefined={false} shouldUpdateUndefined={false}
label={tracker.string.WorkDayLabel} {label}
items={workDaysDropdownItems} items={workDaysDropdownItems}
bind:selected={selectedWorkDayType} bind:selected
on:selected={({ detail }) => (dateTimestamp = getWorkDate(detail))} on:selected
/> />

View File

@ -13,75 +13,40 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import contact, { Employee } from '@hcengineering/contact'
import { WithLookup } from '@hcengineering/core' import { WithLookup } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation' import { getClient } from '@hcengineering/presentation'
import type { TimeSpendReport } from '@hcengineering/tracker' import { Issue, TimeSpendReport } from '@hcengineering/tracker'
import { eventToHTMLElement, floorFractionDigits, Label, showPopup, tooltip } from '@hcengineering/ui' import { eventToHTMLElement, showPopup } from '@hcengineering/ui'
import view, { AttributeModel } from '@hcengineering/view' import TimePresenter from './TimePresenter.svelte'
import { getObjectPresenter } from '@hcengineering/view-resources'
import tracker from '../../../plugin'
import TimeSpendReportPopup from './TimeSpendReportPopup.svelte' import TimeSpendReportPopup from './TimeSpendReportPopup.svelte'
export let value: WithLookup<TimeSpendReport> export let value: WithLookup<TimeSpendReport>
const client = getClient() const client = getClient()
let presenter: AttributeModel
getObjectPresenter(client, contact.class.Employee, { key: '' }).then((p) => { $: issue = value.$lookup?.attachedTo
presenter = p $: 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 { function editSpendReport (event: MouseEvent): void {
showPopup( showPopup(
TimeSpendReportPopup, TimeSpendReportPopup,
{ issue: value.attachedTo, issueClass: value.attachedToClass, value, assignee: value.employee }, {
issue: value.attachedTo,
issueClass: value.attachedToClass,
value,
assignee: value.employee,
defaultTimeReportDay
},
eventToHTMLElement(event) 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> </script>
{#if value && value.value} {#if value && value.value}
<span <TimePresenter id="TimeSpendReportValue" kind="link" value={value.value} {workDayLength} on:click={editSpendReport} />
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>
{/if} {/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 { AttachedData, Class, DocumentUpdate, Ref, Space } from '@hcengineering/core'
import type { IntlString } from '@hcengineering/platform' import type { IntlString } from '@hcengineering/platform'
import presentation, { Card, getClient, UserBox } from '@hcengineering/presentation' 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 { DatePresenter, EditBox } from '@hcengineering/ui'
import tracker from '../../../plugin' import tracker from '../../../plugin'
import { getWorkDate, WorkDaysType } from '../../../utils' import { getTimeReportDate, getTimeReportDayType } from '../../../utils'
import WorkDaysDropdown from './WorkDaysDropdown.svelte' import TimeReportDayDropdown from './TimeReportDayDropdown.svelte'
export let issueId: Ref<Issue> export let issueId: Ref<Issue>
export let issueClass: Ref<Class<Issue>> export let issueClass: Ref<Class<Issue>>
@ -30,17 +30,21 @@
export let value: TimeSpendReport | undefined export let value: TimeSpendReport | undefined
export let placeholder: IntlString = tracker.string.TimeSpendReportValue 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 { export function canClose (): boolean {
return true 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> { async function create (): Promise<void> {
if (value === undefined) { if (value === undefined) {
getClient().addCollection( getClient().addCollection(
@ -88,8 +92,16 @@
bind:value={data.employee} bind:value={data.employee}
showNavigate={false} showNavigate={false}
/> />
<WorkDaysDropdown bind:dateTimestamp={data.date} /> <TimeReportDayDropdown
<DatePresenter kind={'link'} bind:value={data.date} editable /> 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> </div>
<EditBox bind:value={data.description} placeholder={tracker.string.TimeSpendReportDescription} kind={'editbox'} /> <EditBox bind:value={data.description} placeholder={tracker.string.TimeSpendReportDescription} kind={'editbox'} />
</Card> </Card>

View File

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

View File

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

View File

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

View File

@ -14,25 +14,56 @@
--> -->
<script lang="ts"> <script lang="ts">
import { createEventDispatcher } from 'svelte' import { createEventDispatcher } from 'svelte'
import { Button, EditBox, eventToHTMLElement, Label, showPopup, ToggleWithLabel } from '@hcengineering/ui' import {
import { getClient, SpaceCreateCard } from '@hcengineering/presentation' 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 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 { StyledTextBox } from '@hcengineering/text-editor'
import { Asset } from '@hcengineering/platform' import { Asset } from '@hcengineering/platform'
import tracker from '../../plugin' import tracker from '../../plugin'
import TeamIconChooser from './TeamIconChooser.svelte' import TeamIconChooser from './TeamIconChooser.svelte'
import TimeReportDayDropdown from '../issues/timereport/TimeReportDayDropdown.svelte'
let name: string = '' export let team: Team | undefined = undefined
let description: string = ''
let isPrivate: boolean = false let name: string = team?.name ?? ''
let icon: Asset | undefined = undefined 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 dispatch = createEventDispatcher()
const client = getClient() 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 () { $: isNew = !team
await client.createDoc(tracker.class.Team, core.space.Space, {
async function handleSave () {
isNew ? createTeam() : updateTeam()
}
function getTeamData () {
return {
name, name,
description, description,
private: isPrivate, private: isPrivate,
@ -42,8 +73,31 @@
sequence: 0, sequence: 0,
issueStatuses: 0, issueStatuses: 0,
defaultIssueStatus: '' as Ref<IssueStatus>, 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) { function chooseIcon (ev: MouseEvent) {
@ -55,10 +109,11 @@
} }
</script> </script>
<SpaceCreateCard <Card
label={tracker.string.NewTeam} label={isNew ? tracker.string.NewTeam : tracker.string.EditTeam}
okAction={createTeam} okLabel={isNew ? presentation.string.Create : presentation.string.Edit}
canSave={name.length > 0} okAction={handleSave}
canSave={name.length > 0 && !!selectedWorkDayType && !!selectedWorkDayLength}
on:close={() => { on:close={() => {
dispatch('close') dispatch('close')
}} }}
@ -81,4 +136,24 @@
</div> </div>
<Button icon={icon ?? tracker.icon.Home} kind="no-border" size="medium" on:click={chooseIcon} /> <Button icon={icon ?? tracker.icon.Home} kind="no-border" size="medium" on:click={chooseIcon} />
</div> </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> { async function moveAndDeleteSprint (client: TxOperations, oldSprint: Sprint, newSprint?: Sprint): Promise<void> {
const noSprintLabel = await translate(tracker.string.NoSprint, {}) const noSprintLabel = await translate(tracker.string.NoSprint, {})
@ -282,6 +288,7 @@ export default async (): Promise<Resources> => ({
}, },
actionImpl: { actionImpl: {
EditWorkflowStatuses: editWorkflowStatuses, EditWorkflowStatuses: editWorkflowStatuses,
EditTeam: editTeam,
DeleteSprint: deleteSprint DeleteSprint: deleteSprint
}, },
resolver: { resolver: {

View File

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

View File

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

View File

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

View File

@ -53,6 +53,24 @@ export interface Team extends Space {
issueStatuses: number issueStatuses: number
defaultIssueStatus: Ref<IssueStatus> defaultIssueStatus: Ref<IssueStatus>
icon?: Asset 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[] childInfo: IssueChildInfo[]
workDayLength: WorkDayLength
defaultTimeReportDay: TimeReportDayType
template?: { template?: {
// A template issue is based on // A template issue is based on
template: Ref<IssueTemplate> template: Ref<IssueTemplate>
@ -465,6 +486,7 @@ export default plugin(trackerId, {
Relations: '' as Ref<Action>, Relations: '' as Ref<Action>,
NewSubIssue: '' as Ref<Action>, NewSubIssue: '' as Ref<Action>,
EditWorkflowStatuses: '' as Ref<Action>, EditWorkflowStatuses: '' as Ref<Action>,
EditTeam: '' as Ref<Action>,
SetSprint: '' as Ref<Action> SetSprint: '' as Ref<Action>
}, },
team: { team: {