feat: edit mode
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
import { useTaskStore } from '@/modules/task/stores/useTask.store'
|
||||
import { formatDiffInMinutes } from '@/shared/format-date'
|
||||
import { toISODate } from '@/shared/types/date'
|
||||
import { computed, onUnmounted, ref } from 'vue'
|
||||
import { computed, nextTick, onUnmounted, ref } from 'vue'
|
||||
import { useTaskRecordStore } from '../stores/useTaskRecordStore'
|
||||
import { is10PercentOffThanEstimation } from '@/modules/record/services/compare-with-estimation'
|
||||
|
||||
@@ -10,6 +10,7 @@ const props = defineProps<{
|
||||
taskId: string
|
||||
stepId: string
|
||||
stepNumber: number
|
||||
isLastCompletedStep?: boolean
|
||||
}>()
|
||||
|
||||
const taskStore = useTaskStore()
|
||||
@@ -67,6 +68,39 @@ const isOffEstimation = computed(() => {
|
||||
duration: duration.value
|
||||
})
|
||||
})
|
||||
|
||||
// Duration editing
|
||||
const isEditing = ref(false)
|
||||
const editedDuration = ref(0)
|
||||
const durationInputRef = ref<HTMLInputElement | null>(null)
|
||||
|
||||
const isEditable = computed(
|
||||
() => props.isLastCompletedStep && record.value && !record.value.end
|
||||
)
|
||||
|
||||
function startEditing() {
|
||||
editedDuration.value = duration.value ?? 0
|
||||
isEditing.value = true
|
||||
nextTick(() => {
|
||||
durationInputRef.value?.focus()
|
||||
durationInputRef.value?.select()
|
||||
})
|
||||
}
|
||||
|
||||
function confirmEdit() {
|
||||
if (editedDuration.value >= 1) {
|
||||
recordStore.updateStepDuration({
|
||||
taskId: props.taskId,
|
||||
stepId: props.stepId,
|
||||
newDurationMinutes: editedDuration.value
|
||||
})
|
||||
}
|
||||
isEditing.value = false
|
||||
}
|
||||
|
||||
function cancelEdit() {
|
||||
isEditing.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -96,7 +130,28 @@ const isOffEstimation = computed(() => {
|
||||
{{ step.title }}
|
||||
</td>
|
||||
<td class="estimation minutes">{{ step.estimation }} min</td>
|
||||
<td class="minutes" v-if="stepRecord">{{ duration }} min</td>
|
||||
<td class="minutes" v-if="stepRecord">
|
||||
<template v-if="isEditing">
|
||||
<input
|
||||
ref="durationInputRef"
|
||||
type="number"
|
||||
min="1"
|
||||
v-model.number="editedDuration"
|
||||
@blur="confirmEdit"
|
||||
@keydown.enter="confirmEdit"
|
||||
@keydown.escape="cancelEdit"
|
||||
class="input is-small duration-input"
|
||||
/>
|
||||
min
|
||||
</template>
|
||||
<span
|
||||
v-else
|
||||
:class="{ 'is-editable': isEditable }"
|
||||
@click="isEditable && startEditing()"
|
||||
>
|
||||
{{ duration }} min
|
||||
</span>
|
||||
</td>
|
||||
<td v-else></td>
|
||||
</tr>
|
||||
</template>
|
||||
@@ -145,6 +200,19 @@ $blob-color: $link;
|
||||
.minutes {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.is-editable {
|
||||
cursor: pointer;
|
||||
text-decoration: underline dotted;
|
||||
&:hover {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
.duration-input {
|
||||
width: 60px;
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
|
||||
@@ -32,6 +32,16 @@ useAppTitle(task.value?.title ?? '')
|
||||
|
||||
const record = computed(() => recordStore.getTaskRecord(props.taskId))
|
||||
const recordNotes = computed(() => recordStore.getRecordNotes(props.taskId))
|
||||
|
||||
const lastCompletedStepId = computed(() => {
|
||||
if (!record.value || !task.value || !record.value.currentStepId) return null
|
||||
|
||||
const currentIndex = task.value.steps.findIndex(
|
||||
(s) => s.id === record.value?.currentStepId
|
||||
)
|
||||
|
||||
return currentIndex > 0 ? task.value.steps[currentIndex - 1].id : null
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -65,6 +75,7 @@ const recordNotes = computed(() => recordStore.getRecordNotes(props.taskId))
|
||||
:key="step.id"
|
||||
:step-id="step.id"
|
||||
:step-number="key + 1"
|
||||
:is-last-completed-step="step.id === lastCompletedStepId"
|
||||
/>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { Recordable } from '../interfaces/recordable'
|
||||
import type { TimeRange } from '../interfaces/time-range'
|
||||
import { TaskRecord } from '../models/task-record'
|
||||
import { addBreakTimeToStepRecords } from '../services/breaktime-service'
|
||||
import { isTimeSpeedUp } from '@/shared/format-date'
|
||||
|
||||
export interface TaskRecordStoreState {
|
||||
records: { [recordId: string]: Recordable }
|
||||
@@ -221,6 +222,32 @@ export const useTaskRecordStore = defineStore('task-record-store', {
|
||||
start: toISODate(new Date(latestStartDate))
|
||||
})
|
||||
}
|
||||
},
|
||||
updateStepDuration(params: {
|
||||
taskId: string
|
||||
stepId: string
|
||||
newDurationMinutes: number
|
||||
}) {
|
||||
const record = this.records[params.taskId]
|
||||
if (!record) return
|
||||
|
||||
const stepRecord = record.stepRecords[params.stepId]
|
||||
if (!stepRecord?.end) return // Only completed steps
|
||||
|
||||
// Calculate new end time (in dev mode, units are seconds for faster testing)
|
||||
const unitMultiplier = isTimeSpeedUp() ? 1 : 60
|
||||
const startMs = new Date(stepRecord.start).getTime()
|
||||
const newEndMs =
|
||||
startMs + params.newDurationMinutes * unitMultiplier * 1000
|
||||
const newEnd = toISODate(new Date(newEndMs))
|
||||
|
||||
// Adjust current step's start time for continuity
|
||||
const currentStepId = record.currentStepId
|
||||
if (currentStepId && record.stepRecords[currentStepId]) {
|
||||
record.stepRecords[currentStepId].start = newEnd
|
||||
}
|
||||
|
||||
stepRecord.end = newEnd
|
||||
}
|
||||
},
|
||||
getters: {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ISODate } from './types/date'
|
||||
|
||||
const isTimeSpeedUp = () => process.env.NODE_ENV === 'development'
|
||||
export const isTimeSpeedUp = () => process.env.NODE_ENV === 'development'
|
||||
|
||||
export const formatDate = (date: Date | string) =>
|
||||
new Date(date).toLocaleString()
|
||||
|
||||
Reference in New Issue
Block a user