feat: edit mode
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
import { useTaskStore } from '@/modules/task/stores/useTask.store'
|
import { useTaskStore } from '@/modules/task/stores/useTask.store'
|
||||||
import { formatDiffInMinutes } from '@/shared/format-date'
|
import { formatDiffInMinutes } from '@/shared/format-date'
|
||||||
import { toISODate } from '@/shared/types/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 { useTaskRecordStore } from '../stores/useTaskRecordStore'
|
||||||
import { is10PercentOffThanEstimation } from '@/modules/record/services/compare-with-estimation'
|
import { is10PercentOffThanEstimation } from '@/modules/record/services/compare-with-estimation'
|
||||||
|
|
||||||
@@ -10,6 +10,7 @@ const props = defineProps<{
|
|||||||
taskId: string
|
taskId: string
|
||||||
stepId: string
|
stepId: string
|
||||||
stepNumber: number
|
stepNumber: number
|
||||||
|
isLastCompletedStep?: boolean
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const taskStore = useTaskStore()
|
const taskStore = useTaskStore()
|
||||||
@@ -67,6 +68,39 @@ const isOffEstimation = computed(() => {
|
|||||||
duration: duration.value
|
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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -96,7 +130,28 @@ const isOffEstimation = computed(() => {
|
|||||||
{{ step.title }}
|
{{ step.title }}
|
||||||
</td>
|
</td>
|
||||||
<td class="estimation minutes">{{ step.estimation }} min</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>
|
<td v-else></td>
|
||||||
</tr>
|
</tr>
|
||||||
</template>
|
</template>
|
||||||
@@ -145,6 +200,19 @@ $blob-color: $link;
|
|||||||
.minutes {
|
.minutes {
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.is-editable {
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: underline dotted;
|
||||||
|
&:hover {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.duration-input {
|
||||||
|
width: 60px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes pulse {
|
@keyframes pulse {
|
||||||
|
|||||||
@@ -32,6 +32,16 @@ useAppTitle(task.value?.title ?? '')
|
|||||||
|
|
||||||
const record = computed(() => recordStore.getTaskRecord(props.taskId))
|
const record = computed(() => recordStore.getTaskRecord(props.taskId))
|
||||||
const recordNotes = computed(() => recordStore.getRecordNotes(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>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -65,6 +75,7 @@ const recordNotes = computed(() => recordStore.getRecordNotes(props.taskId))
|
|||||||
:key="step.id"
|
:key="step.id"
|
||||||
:step-id="step.id"
|
:step-id="step.id"
|
||||||
:step-number="key + 1"
|
:step-number="key + 1"
|
||||||
|
:is-last-completed-step="step.id === lastCompletedStepId"
|
||||||
/>
|
/>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import type { Recordable } from '../interfaces/recordable'
|
|||||||
import type { TimeRange } from '../interfaces/time-range'
|
import type { TimeRange } from '../interfaces/time-range'
|
||||||
import { TaskRecord } from '../models/task-record'
|
import { TaskRecord } from '../models/task-record'
|
||||||
import { addBreakTimeToStepRecords } from '../services/breaktime-service'
|
import { addBreakTimeToStepRecords } from '../services/breaktime-service'
|
||||||
|
import { isTimeSpeedUp } from '@/shared/format-date'
|
||||||
|
|
||||||
export interface TaskRecordStoreState {
|
export interface TaskRecordStoreState {
|
||||||
records: { [recordId: string]: Recordable }
|
records: { [recordId: string]: Recordable }
|
||||||
@@ -221,6 +222,32 @@ export const useTaskRecordStore = defineStore('task-record-store', {
|
|||||||
start: toISODate(new Date(latestStartDate))
|
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: {
|
getters: {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { ISODate } from './types/date'
|
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) =>
|
export const formatDate = (date: Date | string) =>
|
||||||
new Date(date).toLocaleString()
|
new Date(date).toLocaleString()
|
||||||
|
|||||||
Reference in New Issue
Block a user