mirror of
https://github.com/hcengineering/platform.git
synced 2026-09-22 09:35:00 +02:00
Subissue estimations (#2254)
Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
@@ -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}
|
||||
|
||||
+81
-15
@@ -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>
|
||||
|
||||
-3
@@ -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
|
||||
|
||||
+13
-2
@@ -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={}/> -->
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user