mirror of
https://github.com/hcengineering/platform.git
synced 2024-12-26 21:15:32 +03:00
Fix estimation issues (#2256)
Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
parent
f1fad745bd
commit
43e6d19403
@ -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)
|
||||
|
@ -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": {}
|
||||
}
|
||||
|
@ -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": {}
|
||||
}
|
||||
|
@ -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]
|
||||
|
@ -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 },
|
||||
|
@ -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={[
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -125,6 +125,9 @@ export interface Sprint extends Doc {
|
||||
|
||||
startDate: Timestamp
|
||||
targetDate: Timestamp
|
||||
|
||||
// Capacity in man days.
|
||||
capacity: number
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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}
|
||||
|
Loading…
Reference in New Issue
Block a user