feat(time-tracking): add the task-detail time-tracking section
This commit is contained in:
parent
8febfac742
commit
2ef898e89d
|
|
@ -0,0 +1,83 @@
|
|||
<template>
|
||||
<div class="task-time-tracking">
|
||||
<XButton
|
||||
v-if="entries.length > 0"
|
||||
v-tooltip="$t('timeTracking.logTime')"
|
||||
v-cy="'addTaskTimeEntry'"
|
||||
class="is-pulled-right d-print-none"
|
||||
:class="{'is-active': showForm}"
|
||||
variant="secondary"
|
||||
icon="plus"
|
||||
:shadow="false"
|
||||
@click="showForm = !showForm"
|
||||
/>
|
||||
<h3 class="title is-5">
|
||||
{{ $t('timeTracking.title') }}
|
||||
</h3>
|
||||
<TimeEntryForm
|
||||
v-if="formVisible"
|
||||
:task-id="taskId"
|
||||
:entry="editingEntry"
|
||||
:recent-entries="entries"
|
||||
@saved="onSaved"
|
||||
@cancel="editingEntry = null"
|
||||
/>
|
||||
<TimeEntryList
|
||||
class="mbs-4"
|
||||
:entries="entries"
|
||||
:card="false"
|
||||
:empty-text="$t('timeTracking.list.emptyTask')"
|
||||
hide-label-column
|
||||
@edit="editingEntry = $event"
|
||||
@delete="onDelete"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {ref, computed, watch} from 'vue'
|
||||
|
||||
import TimeEntryForm from '@/components/time-tracking/TimeEntryForm.vue'
|
||||
import TimeEntryList from '@/components/time-tracking/TimeEntryList.vue'
|
||||
|
||||
import {useTimeEntryService} from '@/services/timeEntry'
|
||||
import {useTimeTrackingStore} from '@/stores/timeTracking'
|
||||
|
||||
import type {ITimeEntry} from '@/modelTypes/ITimeEntry'
|
||||
|
||||
const props = defineProps<{
|
||||
taskId: number
|
||||
}>()
|
||||
|
||||
const timeTrackingStore = useTimeTrackingStore()
|
||||
const entries = ref<ITimeEntry[]>([])
|
||||
const editingEntry = ref<ITimeEntry | null>(null)
|
||||
const showForm = ref(false)
|
||||
|
||||
// Like related tasks: the form is implicit when empty, otherwise behind the +.
|
||||
const formVisible = computed(() => entries.value.length === 0 || showForm.value || editingEntry.value !== null)
|
||||
|
||||
async function load() {
|
||||
const {items} = await useTimeEntryService().getAll({
|
||||
filter: `task_id = ${props.taskId}`,
|
||||
perPage: 250,
|
||||
})
|
||||
entries.value = items
|
||||
}
|
||||
|
||||
async function onSaved() {
|
||||
editingEntry.value = null
|
||||
showForm.value = false
|
||||
await load()
|
||||
}
|
||||
|
||||
async function onDelete(id: number) {
|
||||
await timeTrackingStore.removeEntry(id)
|
||||
await load()
|
||||
}
|
||||
|
||||
watch(() => props.taskId, load, {immediate: true})
|
||||
// The header badge can start/stop the timer without going through this form;
|
||||
// reload so the row reflects the stop (its new end time).
|
||||
watch(() => timeTrackingStore.activeTimer, load)
|
||||
</script>
|
||||
|
|
@ -51,6 +51,7 @@ export interface ITask extends IAbstract {
|
|||
reactions: IReactionPerEntity
|
||||
comments: ITaskComment[]
|
||||
commentCount?: number
|
||||
timeEntriesCount?: number
|
||||
|
||||
createdBy: IUser
|
||||
created: Date
|
||||
|
|
|
|||
|
|
@ -366,6 +366,15 @@
|
|||
/>
|
||||
</div>
|
||||
|
||||
<!-- Time Tracking -->
|
||||
<div
|
||||
v-if="timeTrackingEnabled && activeFields.timeTracking"
|
||||
:ref="e => setFieldRef('timeTracking', e)"
|
||||
class="content time-tracking"
|
||||
>
|
||||
<TaskTimeTracking :task-id="task.id" />
|
||||
</div>
|
||||
|
||||
<!-- Related Tasks -->
|
||||
<div
|
||||
v-if="activeFields.relatedTasks"
|
||||
|
|
@ -537,6 +546,16 @@
|
|||
|
||||
<span class="action-heading">{{ $t('task.detail.dateAndTime') }}</span>
|
||||
|
||||
<XButton
|
||||
v-if="timeTrackingEnabled"
|
||||
v-cy="'taskTrackTimeAction'"
|
||||
variant="secondary"
|
||||
:icon="['far', 'clock']"
|
||||
@click="setFieldActive('timeTracking')"
|
||||
>
|
||||
{{ $t('task.detail.actions.timeTracking') }}
|
||||
</XButton>
|
||||
|
||||
<XButton
|
||||
v-shortcut="'KeyD'"
|
||||
variant="secondary"
|
||||
|
|
@ -643,11 +662,13 @@ import type {IProject} from '@/modelTypes/IProject'
|
|||
|
||||
import {PRIORITIES, type Priority} from '@/constants/priorities'
|
||||
import {PERMISSIONS} from '@/constants/permissions'
|
||||
import {PRO_FEATURE} from '@/constants/proFeatures'
|
||||
|
||||
import BaseButton from '@/components/base/BaseButton.vue'
|
||||
|
||||
// partials
|
||||
import Attachments from '@/components/tasks/partials/Attachments.vue'
|
||||
import TaskTimeTracking from '@/components/time-tracking/TaskTimeTracking.vue'
|
||||
import ChecklistSummary from '@/components/tasks/partials/ChecklistSummary.vue'
|
||||
import ColorPicker from '@/components/input/ColorPicker.vue'
|
||||
import Comments from '@/components/tasks/partials/Comments.vue'
|
||||
|
|
@ -682,6 +703,7 @@ import {useKanbanStore} from '@/stores/kanban'
|
|||
import {useProjectStore} from '@/stores/projects'
|
||||
import {useAuthStore} from '@/stores/auth'
|
||||
import {useBaseStore} from '@/stores/base'
|
||||
import {useConfigStore} from '@/stores/config'
|
||||
|
||||
import {useTitle} from '@/composables/useTitle'
|
||||
import {useTaskDetailShortcuts} from '@/composables/useTaskDetailShortcuts'
|
||||
|
|
@ -704,6 +726,8 @@ const {t} = useI18n({useScope: 'global'})
|
|||
|
||||
const projectStore = useProjectStore()
|
||||
const taskStore = useTaskStore()
|
||||
const configStore = useConfigStore()
|
||||
const timeTrackingEnabled = computed(() => configStore.isProFeatureEnabled(PRO_FEATURE.TIME_TRACKING))
|
||||
const kanbanStore = useKanbanStore()
|
||||
const authStore = useAuthStore()
|
||||
const baseStore = useBaseStore()
|
||||
|
|
@ -923,7 +947,12 @@ watch(
|
|||
}
|
||||
|
||||
try {
|
||||
const loaded = await taskService.get({id}, {expand: ['reactions', 'comments', 'is_unread', 'buckets']})
|
||||
const expand = ['reactions', 'comments', 'is_unread', 'buckets']
|
||||
if (timeTrackingEnabled.value) {
|
||||
// Only request the (server-computed) count when the feature is on.
|
||||
expand.push('time_entries_count')
|
||||
}
|
||||
const loaded = await taskService.get({id}, {expand})
|
||||
Object.assign(task.value, loaded)
|
||||
taskColor.value = task.value.hexColor
|
||||
setActiveFields()
|
||||
|
|
@ -967,6 +996,7 @@ type FieldType =
|
|||
| 'reminders'
|
||||
| 'repeatAfter'
|
||||
| 'startDate'
|
||||
| 'timeTracking'
|
||||
|
||||
const activeFields: { [type in FieldType]: boolean } = reactive({
|
||||
assignees: false,
|
||||
|
|
@ -982,6 +1012,7 @@ const activeFields: { [type in FieldType]: boolean } = reactive({
|
|||
reminders: false,
|
||||
repeatAfter: false,
|
||||
startDate: false,
|
||||
timeTracking: false,
|
||||
})
|
||||
|
||||
function setActiveFields() {
|
||||
|
|
@ -992,6 +1023,7 @@ function setActiveFields() {
|
|||
// Set all active fields based on values in the model
|
||||
activeFields.assignees = task.value.assignees.length > 0
|
||||
activeFields.attachments = task.value.attachments.length > 0
|
||||
activeFields.timeTracking = (task.value.timeEntriesCount ?? 0) > 0
|
||||
activeFields.dueDate = task.value.dueDate !== null
|
||||
activeFields.endDate = task.value.endDate !== null
|
||||
activeFields.labels = task.value.labels.length > 0
|
||||
|
|
|
|||
Loading…
Reference in New Issue