Fix estimation issues (#2256)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev 2022-08-23 16:39:21 +07:00 committed by GitHub
parent f1fad745bd
commit 43e6d19403
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 112 additions and 23 deletions

View File

@ -361,6 +361,9 @@ export class TSprint extends TDoc implements Sprint {
targetDate!: Timestamp
declare space: Ref<Team>
@Prop(TypeNumber(), tracker.string.Capacity)
capacity!: number
}
@UX(core.string.Number)

View File

@ -210,7 +210,9 @@
"TimeSpendValue": "{value}d",
"SprintPassed": "{from}d/{to}d",
"ChildEstimation": "Subissues Estimation",
"ChildReportedTime": "Subissues Time"
"ChildReportedTime": "Subissues Time",
"Capacity": "Capacity",
"CapacityValue": "of {value}d"
},
"status": {}
}

View File

@ -209,8 +209,10 @@
"TimeSpendReportDescription": "Описание",
"TimeSpendValue": "{value}d",
"SprintPassed": "{from}d/{to}d",
"ChildEstimation": "Subissues Estimation",
"ChildReportedTime": "Subissues Time"
"ChildEstimation": "Оценка подзадач",
"ChildReportedTime": "Время водзадач",
"Capacity": "Вместимость",
"CapacityValue": "из {value}d"
},
"status": {}
}

View File

@ -75,7 +75,19 @@
let personPresenter: AttributeModel
$: isCollapsedMap = Object.fromEntries(categories.map((category) => [category, false]))
const isCollapsedMap: Record<any, boolean> = {}
$: {
const exkeys = new Set(Object.keys(isCollapsedMap))
for (const c of categories) {
if (!exkeys.delete(c)) {
isCollapsedMap[c] = false
}
}
for (const k of exkeys) {
delete isCollapsedMap[k]
}
}
$: combinedGroupedIssues = Object.values(groupedIssues).flat(1)
$: options = { ...baseOptions, sort: { [orderBy]: issuesSortOrderMap[orderBy] } } as FindOptions<Issue>
$: headerComponent = groupByKey === undefined || groupByKey === 'assignee' ? null : issuesGroupEditorMap[groupByKey]

View File

@ -75,6 +75,7 @@
<Label label={tracker.string.ChildEstimation} />:
<Scroller tableFade>
<TableBrowser
showFilterBar={false}
_class={tracker.class.Issue}
query={{ _id: { $in: childIds } }}
config={['', { key: '$lookup.attachedTo', presenter: ParentNamesPresenter }, 'estimation']}
@ -86,6 +87,7 @@
<TableBrowser
_class={tracker.class.TimeSpendReport}
query={{ attachedTo: { $in: [object._id, ...childIds] } }}
showFilterBar={false}
config={[
'$lookup.attachedTo',
{ key: '$lookup.attachedTo', presenter: ParentNamesPresenter },

View File

@ -54,6 +54,7 @@
</svelte:fragment>
<Scroller tableFade>
<TableBrowser
showFilterBar={false}
_class={tracker.class.TimeSpendReport}
query={{ attachedTo: { $in: [issue._id, ...issue.childInfo.map((it) => it.childId)] } }}
config={[

View File

@ -2,11 +2,12 @@
import { getClient } from '@anticrm/presentation'
import { StyledTextBox } from '@anticrm/text-editor'
import { Sprint } from '@anticrm/tracker'
import { Button, EditBox, Icon, showPopup } from '@anticrm/ui'
import { Button, DatePresenter, EditBox, Icon, Label, showPopup } from '@anticrm/ui'
import { DocAttributeBar } from '@anticrm/view-resources'
import { onDestroy } from 'svelte'
import { activeSprint } from '../../issues'
import tracker from '../../plugin'
import { getDayOfSprint } from '../../utils'
import Expanded from '../icons/Expanded.svelte'
import IssuesView from '../issues/IssuesView.svelte'
import SprintPopup from './SprintPopup.svelte'
@ -46,6 +47,33 @@
</Button>
</div>
</svelte:fragment>
<svelte:fragment slot="afterHeader">
{@const now = Date.now()}
<div class="p-1 ml-6 flex-row-center">
<div class="flex-row-center">
<DatePresenter value={sprint.startDate} kind={'transparent'} />
<span class="p-1"> / </span><DatePresenter value={sprint.targetDate} kind={'transparent'} />
</div>
<div class="flex-row-center ml-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)
}}
/>
{#if sprint?.capacity}
<Label label={tracker.string.CapacityValue} params={{ value: sprint?.capacity }} />
{/if}
</div>
</div>
</svelte:fragment>
<svelte:fragment slot="aside">
<div class="flex-grow p-4 w-60 left-divider">
<div class="fs-title text-xl">
@ -64,3 +92,9 @@
</div>
</svelte:fragment>
</IssuesView>
<style lang="scss">
.showWarning {
color: var(--warning-color) !important;
}
</style>

View File

@ -17,10 +17,11 @@
import { IntlString } from '@anticrm/platform'
import { createQuery, getClient } from '@anticrm/presentation'
import { Issue, Sprint } from '@anticrm/tracker'
import { ButtonKind, ButtonShape, ButtonSize, isWeekend, Label, tooltip } from '@anticrm/ui'
import { ButtonKind, ButtonShape, ButtonSize, Label, tooltip } from '@anticrm/ui'
import DatePresenter from '@anticrm/ui/src/components/calendar/DatePresenter.svelte'
import { activeSprint } from '../../issues'
import tracker from '../../plugin'
import { getDayOfSprint } from '../../utils'
import EstimationProgressCircle from '../issues/timereport/EstimationProgressCircle.svelte'
import SprintSelector from './SprintSelector.svelte'
@ -93,14 +94,6 @@
sprint = res.shift()
})
}
function getDayOfSprint (startDate: number, now: number): number {
const days = Math.floor(Math.abs((1 + now - startDate) / 1000 / 60 / 60 / 24))
const stDate = new Date(startDate)
const stDateDate = stDate.getDate()
const stTime = stDate.getTime()
const ds = Array.from(Array(days).keys()).map((it) => stDateDate + it)
return ds.filter((it) => !isWeekend(new Date(new Date(stTime).setDate(it)))).length
}
</script>
{#if (value.sprint && value.sprint !== $activeSprint && groupBy !== 'sprint') || shouldShowPlaceholder}
@ -148,7 +141,7 @@
{/if}
{#if issues}
<!-- <Label label={tracker.string.SprintDay} value={}/> -->
<div class="ml-4 flex-row-center">
<div class="ml-4 flex-row-center" class:showWarning={totalEstimation > (sprint?.capacity ?? 0)}>
<div class="mr-2">
<EstimationProgressCircle value={totalReported} max={totalEstimation} />
</div>
@ -157,5 +150,14 @@
/
{/if}
<Label label={tracker.string.TimeSpendValue} params={{ value: totalEstimation }} />
{#if sprint?.capacity}
<Label label={tracker.string.CapacityValue} params={{ value: sprint?.capacity }} />
{/if}
</div>
{/if}
<style lang="scss">
.showWarning {
color: var(--warning-color) !important;
}
</style>

View File

@ -15,9 +15,10 @@
<script lang="ts">
import type { Class, Doc, DocumentQuery, Ref } from '@anticrm/core'
import { ObjectCreate, ObjectPopup } from '@anticrm/presentation'
import { Sprint } from '@anticrm/tracker'
import { Sprint, SprintStatus } from '@anticrm/tracker'
import { Icon, Label } from '@anticrm/ui'
import { sprintStatusAssets } from '../../utils'
import SprintTitlePresenter from './SprintTitlePresenter.svelte'
export let _class: Ref<Class<Sprint>>
export let selected: Ref<Sprint> | undefined
export let sprintQuery: DocumentQuery<Sprint> = {}
@ -31,6 +32,7 @@
update: (doc: Doc) => (doc as Sprint).label
}
: undefined
const getStatus = (sprint: Sprint): SprintStatus => sprint.status
</script>
<ObjectPopup
@ -44,8 +46,21 @@
create={_create}
on:update
on:close
groupBy={'status'}
>
<svelte:fragment slot="item" let:item={sprint}>
<SprintTitlePresenter value={sprint} />
</svelte:fragment>
<svelte:fragment slot="category" let:item={sprint}>
{@const status = sprintStatusAssets[getStatus(sprint)]}
{#if status}
<div class="flex-row-center p-1">
<Icon icon={status.icon} size={'small'} />
<div class="ml-2">
<Label label={status.label} />
</div>
</div>
{/if}
</svelte:fragment>
</ObjectPopup>

View File

@ -227,7 +227,9 @@ export default mergeIds(trackerId, tracker, {
SprintPassed: '' as IntlString,
ChildEstimation: '' as IntlString,
ChildReportedTime: '' as IntlString
ChildReportedTime: '' as IntlString,
Capacity: '' as IntlString,
CapacityValue: '' as IntlString
},
component: {
NopeComponent: '' as AnyComponent,

View File

@ -29,7 +29,7 @@ import {
Team
} from '@anticrm/tracker'
import { ViewOptionModel } from '@anticrm/view-resources'
import { AnyComponent, AnySvelteComponent, getMillisecondsInMonth, MILLISECONDS_IN_WEEK } from '@anticrm/ui'
import { AnyComponent, AnySvelteComponent, getMillisecondsInMonth, isWeekend, MILLISECONDS_IN_WEEK } from '@anticrm/ui'
import tracker from './plugin'
import { defaultPriorities, defaultProjectStatuses, defaultSprintStatuses, issuePriorities } from './types'
@ -555,8 +555,17 @@ export function getSprintDays (value: Sprint): string {
const st = new Date(value.startDate).getDate()
const days = Math.floor(Math.abs((1 + value.targetDate - value.startDate) / 1000 / 60 / 60 / 24)) + 1
const stDate = new Date(value.startDate)
const stTime = stDate.getTime()
let ds = Array.from(Array(days).keys()).map((it) => st + it)
ds = ds.filter((it) => ![0, 6].includes(new Date(stDate.setDate(it)).getDay()))
ds = ds.filter((it) => ![0, 6].includes(new Date(new Date(stTime).setDate(it)).getDay()))
return ds.join(' ')
}
export function getDayOfSprint (startDate: number, now: number): number {
const days = Math.floor(Math.abs((1 + now - startDate) / 1000 / 60 / 60 / 24))
const stDate = new Date(startDate)
const stDateDate = stDate.getDate()
const stTime = stDate.getTime()
const ds = Array.from(Array(days).keys()).map((it) => stDateDate + it)
return ds.filter((it) => !isWeekend(new Date(new Date(stTime).setDate(it)))).length
}

View File

@ -125,6 +125,9 @@ export interface Sprint extends Doc {
startDate: Timestamp
targetDate: Timestamp
// Capacity in man days.
capacity: number
}
/**

View File

@ -29,6 +29,7 @@
export let options: FindOptions<Doc> | undefined = undefined
export let baseMenuClass: Ref<Class<Doc>> | undefined = undefined
export let config: (BuildModelKey | string)[]
export let showFilterBar = true
// If defined, will show a number of dummy items before real data will appear.
export let loadingProps: LoadingProps | undefined = undefined
@ -53,8 +54,9 @@
mode: 'browser'
}}
/>
<FilterBar {_class} {query} on:change={(e) => (resultQuery = e.detail)} />
{#if showFilterBar}
<FilterBar {_class} {query} on:change={(e) => (resultQuery = e.detail)} />
{/if}
<Scroller tableFade>
<Table
bind:this={table}