Subissue estimations (#2254)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev
2022-08-22 08:03:32 +07:00
committed by GitHub
parent 854331ae5f
commit ccd2048ad0
30 changed files with 428 additions and 115 deletions
@@ -42,6 +42,7 @@
import SetParentIssueActionPopup from './SetParentIssueActionPopup.svelte'
import SprintSelector from './sprints/SprintSelector.svelte'
import { activeProject, activeSprint } from '../issues'
import EstimationEditor from './issues/timereport/EstimationEditor.svelte'
export let space: Ref<Team>
export let status: Ref<IssueStatus> | undefined = undefined
@@ -71,7 +72,8 @@
parents: [],
reportedTime: 0,
estimation: 0,
reports: 0
reports: 0,
childInfo: []
}
const dispatch = createEventDispatcher()
@@ -156,8 +158,9 @@
? [{ parentId: parentIssue._id, parentTitle: parentIssue.title }, ...parentIssue.parents]
: [],
reportedTime: 0,
estimation: 0,
reports: 0
estimation: object.estimation,
reports: 0,
childInfo: []
}
await client.addCollection(
@@ -344,6 +347,7 @@
labels = labels.filter((it) => it._id !== evt.detail)
}}
/>
<EstimationEditor kind={'no-border'} size={'small'} value={object} />
<ProjectSelector value={object.project} onProjectIdChange={handleProjectIdChanged} />
<SprintSelector value={object.sprint} onSprintIdChange={handleSprintIdChanged} />
{#if object.dueDate !== null}
@@ -21,8 +21,12 @@
export let value: WithLookup<Issue>
// export let inline: boolean = false
export let disableClick = false
function handleIssueEditorOpened () {
if (disableClick) {
return
}
showPanel(tracker.component.EditIssue, value._id, value._class, 'content')
}
@@ -39,7 +43,7 @@
</script>
{#if value}
<span class="issuePresenterRoot" title="title" on:click={handleIssueEditorOpened}>
<span class="issuePresenterRoot" class:noPointer={disableClick} title="title" on:click={handleIssueEditorOpened}>
{title}
</span>
{/if}
@@ -56,6 +60,10 @@
color: var(--content-color);
cursor: pointer;
&.noPointer {
cursor: default;
}
&:hover {
color: var(--caption-color);
text-decoration: underline;
@@ -24,6 +24,7 @@
import AssigneeEditor from '../AssigneeEditor.svelte'
import StatusEditor from '../StatusEditor.svelte'
import PriorityEditor from '../PriorityEditor.svelte'
import EstimationEditor from '../timereport/EstimationEditor.svelte'
export let parentIssue: Issue
export let issueStatuses: WithLookup<IssueStatus>[]
@@ -55,7 +56,12 @@
dueDate: null,
comments: 0,
subIssues: 0,
parents: []
parents: [],
sprint: parentIssue.sprint,
estimation: 0,
reportedTime: 0,
reports: 0,
childInfo: []
}
}
@@ -209,6 +215,7 @@
labels = labels.filter((it) => it._id !== evt.detail)
}}
/>
<EstimationEditor kind={'no-border'} size={'small'} value={newIssue} />
</div>
<div class="buttons-group small-gap">
<Button label={presentation.string.Cancel} size="small" kind="transparent" on:click={close} />
@@ -279,7 +279,9 @@
<SubIssues {issue} {issueStatuses} {currentTeam} />
{/key}
</div>
<AttachmentDocList value={issue} />
<div class="mt-6">
<AttachmentDocList value={issue} />
</div>
{/if}
<span slot="actions-label">
@@ -32,6 +32,7 @@
import DueDateEditor from '../DueDateEditor.svelte'
import PriorityEditor from '../PriorityEditor.svelte'
import StatusEditor from '../StatusEditor.svelte'
import EstimationEditor from '../timereport/EstimationEditor.svelte'
export let issues: Issue[]
export let issueStatuses: WithLookup<IssueStatus>[]
@@ -148,6 +149,7 @@
</span>
</div>
<div class="flex-center flex-no-shrink">
<EstimationEditor value={issue} kind={'list'} />
{#if issue.dueDate !== null}
<DueDateEditor value={issue} />
{/if}
@@ -13,15 +13,18 @@
// limitations under the License.
-->
<script lang="ts">
import { AttachedData } from '@anticrm/core'
import { getClient } from '@anticrm/presentation'
import { Issue } from '@anticrm/tracker'
import { Button, ButtonKind, ButtonSize, eventToHTMLElement, Label, showPopup } from '@anticrm/ui'
import EditBoxPopup from '@anticrm/view-resources/src/components/EditBoxPopup.svelte'
import { createEventDispatcher } from 'svelte'
import tracker from '../../../plugin'
import EstimationPopup from './EstimationPopup.svelte'
import EstimationProgressCircle from './EstimationProgressCircle.svelte'
export let value: Issue
export let value: Issue | AttachedData<Issue>
export let isEditable: boolean = true
export let kind: ButtonKind = 'link'
@@ -39,16 +42,24 @@
return
}
showPopup(
EstimationPopup,
{ value: value.estimation, format: 'number', object: value },
eventToHTMLElement(event),
(res) => {
if (res != null) {
if (kind === 'list') {
showPopup(
EstimationPopup,
{ value: value.estimation, format: 'number', object: value },
eventToHTMLElement(event),
(res) => {
if (res != null) {
changeEstimation(res)
}
}
)
} else {
showPopup(EditBoxPopup, { value, format: 'number' }, eventToHTMLElement(event), (res) => {
if (res !== undefined) {
changeEstimation(res)
}
}
)
})
}
}
const changeEstimation = async (newEstimation: number | undefined) => {
@@ -60,22 +71,67 @@
if ('_id' in value) {
await client.update(value, { estimation: newEstimation })
} else {
value.estimation = newEstimation
}
}
$: childReportTime = (value.childInfo ?? []).map((it) => it.reportedTime).reduce((a, b) => a + b, 0)
$: childEstimationTime = (value.childInfo ?? []).map((it) => it.estimation).reduce((a, b) => a + b, 0)
function hourFloor (value: number): number {
const days = Math.ceil(value)
const hours = value - days
return days + Math.floor(hours * 10) / 10
}
</script>
{#if value}
{#if kind === 'list'}
<div class="estimation-container" on:click={handleestimationEditorOpened}>
<div class="icon">
<EstimationProgressCircle value={value.reportedTime} max={value.estimation} />
<EstimationProgressCircle value={Math.max(value.reportedTime, childReportTime)} max={value.estimation} />
</div>
<span class="overflow-label label">
{#if value.reportedTime > 0}
<Label label={tracker.string.TimeSpendValue} params={{ value: value.reportedTime }} />
/
<span class="overflow-label label flex-row-center flex-nowrap text-md">
{#if value.reportedTime > 0 || childReportTime > 0}
{#if childReportTime}
{@const rchildReportTime = hourFloor(childReportTime)}
{@const reportDiff = rchildReportTime - hourFloor(value.reportedTime)}
{#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 }} />
</div>
<div class="romColor">
(<Label label={tracker.string.TimeSpendValue} params={{ value: hourFloor(value.reportedTime) }} />)
</div>
{:else if value.reportedTime === 0}
<Label label={tracker.string.TimeSpendValue} params={{ value: hourFloor(childReportTime) }} />
{:else}
<Label label={tracker.string.TimeSpendValue} params={{ value: hourFloor(value.reportedTime) }} />
{/if}
{:else}
<Label label={tracker.string.TimeSpendValue} params={{ value: hourFloor(value.reportedTime) }} />
{/if}
<div class="p-1">/</div>
{/if}
{#if childEstimationTime}
{@const childEstTime = Math.round(childEstimationTime)}
{@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 }} />
</div>
{#if value.estimation !== 0}
<div class="romColor">
(<Label label={tracker.string.TimeSpendValue} params={{ value: hourFloor(value.estimation) }} />)
</div>
{/if}
{:else}
<Label label={tracker.string.TimeSpendValue} params={{ value: hourFloor(value.estimation) }} />
{/if}
{:else}
<Label label={tracker.string.TimeSpendValue} params={{ value: hourFloor(value.estimation) }} />
{/if}
<Label label={tracker.string.TimeSpendValue} params={{ value: value.estimation }} />
</span>
</div>
{:else}
@@ -122,5 +178,15 @@
color: var(--caption-color) !important;
}
}
.showError {
color: var(--error-color) !important;
}
.showWarning {
color: var(--warning-color) !important;
}
.romColor {
color: var(--content-color) !important;
}
}
</style>
@@ -16,14 +16,15 @@
<script lang="ts">
import contact from '@anticrm/contact'
import { FindOptions } from '@anticrm/core'
import { Card } from '@anticrm/presentation'
import presentation, { Card } from '@anticrm/presentation'
import { Issue, TimeSpendReport } from '@anticrm/tracker'
import { Button, EditBox, EditStyle, eventToHTMLElement, IconAdd, Scroller, showPopup } from '@anticrm/ui'
import { Button, EditBox, EditStyle, eventToHTMLElement, IconAdd, Label, Scroller, showPopup } from '@anticrm/ui'
import { TableBrowser } from '@anticrm/view-resources'
import { createEventDispatcher } from 'svelte'
import tracker from '../../../plugin'
import IssuePresenter from '../IssuePresenter.svelte'
import ParentNamesPresenter from '../ParentNamesPresenter.svelte'
import TimeSpendReportPopup from './TimeSpendReportPopup.svelte'
import presentation from '@anticrm/presentation'
export let value: string | number | undefined
export let format: 'text' | 'password' | 'number'
@@ -39,8 +40,9 @@
if (ev.key === 'Enter') dispatch('close', _value)
}
const options: FindOptions<TimeSpendReport> = {
lookup: { employee: contact.class.Employee }
lookup: { employee: contact.class.Employee, attachedTo: tracker.class.Issue }
}
$: childIds = Array.from((object.childInfo ?? []).map((it) => it.childId))
</script>
<Card
@@ -54,6 +56,9 @@
dispatch('close', null)
}}
>
<svelte:fragment slot="header">
<IssuePresenter value={object} disableClick />
</svelte:fragment>
<div class="header no-border flex-col p-1">
<div class="flex-row-center flex-between">
<EditBox
@@ -67,11 +72,28 @@
/>
</div>
</div>
<Label label={tracker.string.ChildEstimation} />:
<Scroller tableFade>
<TableBrowser
_class={tracker.class.Issue}
query={{ _id: { $in: childIds } }}
config={['', { key: '$lookup.attachedTo', presenter: ParentNamesPresenter }, 'estimation']}
{options}
/>
</Scroller>
<Label label={tracker.string.ReportedTime} />:
<Scroller tableFade>
<TableBrowser
_class={tracker.class.TimeSpendReport}
query={{ attachedTo: object._id }}
config={['', '$lookup.employee', 'date', 'description']}
query={{ attachedTo: { $in: [object._id, ...childIds] } }}
config={[
'$lookup.attachedTo',
{ key: '$lookup.attachedTo', presenter: ParentNamesPresenter },
'',
'$lookup.employee',
'date',
'description'
]}
{options}
/>
</Scroller>
@@ -25,9 +25,6 @@
export let greenColor: string = FernColor
export let overdueColor = FlamingoColor
if (value > max) value = max
if (value < min) value = min
const lenghtC: number = Math.PI * 14 - 1
$: procC = lenghtC / (max - min)
$: dashOffset = (Math.min(value, max) - min) * procC
@@ -36,12 +36,18 @@
function showReports (event: MouseEvent): void {
showPopup(ReportsPopup, { issue: object }, eventToHTMLElement(event))
}
$: childTime = (object.childInfo ?? []).map((it) => it.reportedTime).reduce((a, b) => a + b, 0)
</script>
{#if kind === 'link'}
<div class="link-container flex-between" on:click={showReports}>
{#if value !== undefined}
<span class="overflow-label">{value}</span>
<span class="overflow-label">
{value}
{#if childTime !== 0}
/ {childTime}
{/if}
</span>
{:else}
<span class="dark-color"><Label label={placeholder} /></span>
{/if}
@@ -50,7 +56,12 @@
</div>
</div>
{:else if value !== undefined}
<span class="overflow-label">{value}</span>
<span class="overflow-label">
{value}
{#if childTime !== 0}
/ {childTime}
{/if}
</span>
{:else}
<span class="dark-color"><Label label={placeholder} /></span>
{/if}
@@ -20,6 +20,7 @@
import { Button, eventToHTMLElement, IconAdd, Scroller, showPopup } from '@anticrm/ui'
import { TableBrowser } from '@anticrm/view-resources'
import tracker from '../../../plugin'
import IssuePresenter from '../IssuePresenter.svelte'
import TimeSpendReportPopup from './TimeSpendReportPopup.svelte'
export let issue: Issue
@@ -27,7 +28,10 @@
return true
}
const options: FindOptions<TimeSpendReport> = {
lookup: { employee: contact.class.Employee }
lookup: {
attachedTo: tracker.class.Issue,
employee: contact.class.Employee
}
}
function addReport (event: MouseEvent): void {
showPopup(
@@ -45,11 +49,23 @@
okAction={() => {}}
okLabel={presentation.string.Ok}
>
<svelte:fragment slot="header">
<IssuePresenter value={issue} disableClick />
</svelte:fragment>
<Scroller tableFade>
<TableBrowser
_class={tracker.class.TimeSpendReport}
query={{ attachedTo: issue._id }}
config={['', '$lookup.employee', 'description', 'date', 'modifiedOn', 'modifiedBy']}
query={{ attachedTo: { $in: [issue._id, ...issue.childInfo.map((it) => it.childId)] } }}
config={[
'$lookup.attachedTo',
'$lookup.attachedTo.title',
'',
'$lookup.employee',
'description',
'date',
'modifiedOn',
'modifiedBy'
]}
{options}
/>
</Scroller>
@@ -55,12 +55,15 @@
)
}
$: totalEstimation = (issues ?? [{ estimation: 0 }])
$: ids = new Set(issues?.map((it) => it._id) ?? [])
$: noParents = issues?.filter((it) => !ids.has(it.attachedTo as Ref<Issue>))
$: totalEstimation = (noParents ?? [{ estimation: 0 }])
.map((it) => it.estimation)
.reduce((it, cur) => {
return it + cur
})
$: totalReported = (issues ?? [{ reportedTime: 0 }])
$: totalReported = (noParents ?? [{ reportedTime: 0 }])
.map((it) => it.reportedTime)
.reduce((it, cur) => {
return it + cur
@@ -108,13 +111,15 @@
{@const now = Date.now()}
{#if sprint.startDate < now && now < sprint.targetDate}
<!-- Active sprint in time -->
<Label
label={tracker.string.SprintPassed}
params={{
from: getDayOfSprint(sprint.startDate, now),
to: getDayOfSprint(sprint.startDate, sprint.targetDate) - 1
}}
/>
<div class="ml-2">
<Label
label={tracker.string.SprintPassed}
params={{
from: getDayOfSprint(sprint.startDate, now),
to: getDayOfSprint(sprint.startDate, sprint.targetDate) - 1
}}
/>
</div>
{/if}
{/if}
<!-- <Label label={tracker.string.SprintDay} value={}/> -->
+4 -1
View File
@@ -224,7 +224,10 @@ export default mergeIds(trackerId, tracker, {
TimeSpendReportValue: '' as IntlString,
TimeSpendReportDescription: '' as IntlString,
TimeSpendValue: '' as IntlString,
SprintPassed: '' as IntlString
SprintPassed: '' as IntlString,
ChildEstimation: '' as IntlString,
ChildReportedTime: '' as IntlString
},
component: {
NopeComponent: '' as AnyComponent,