feat: add duplicate, copy previous day/week, and repeat entry
This commit is contained in:
@@ -41,6 +41,20 @@
|
|||||||
>
|
>
|
||||||
Clear
|
Clear
|
||||||
</button>
|
</button>
|
||||||
|
<div class="ml-auto flex gap-2">
|
||||||
|
<button
|
||||||
|
@click="copyPreviousDay"
|
||||||
|
class="text-text-secondary text-xs hover:text-text-primary transition-colors border border-border-subtle rounded-lg px-3 py-1.5"
|
||||||
|
>
|
||||||
|
Copy Yesterday
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
@click="copyPreviousWeek"
|
||||||
|
class="text-text-secondary text-xs hover:text-text-primary transition-colors border border-border-subtle rounded-lg px-3 py-1.5"
|
||||||
|
>
|
||||||
|
Copy Last Week
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Entries Table -->
|
<!-- Entries Table -->
|
||||||
@@ -78,13 +92,21 @@
|
|||||||
{{ getTaskName(entry.task_id) || '-' }}
|
{{ getTaskName(entry.task_id) || '-' }}
|
||||||
</td>
|
</td>
|
||||||
<td class="px-4 py-3 text-[0.75rem] text-text-secondary">
|
<td class="px-4 py-3 text-[0.75rem] text-text-secondary">
|
||||||
{{ entry.description || '-' }}
|
<span v-if="entry.description" v-html="renderMarkdown(entry.description)" class="markdown-inline" />
|
||||||
|
<span v-else>-</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-4 py-3 text-right text-[0.75rem] font-mono text-accent-text">
|
<td class="px-4 py-3 text-right text-[0.75rem] font-mono text-accent-text">
|
||||||
{{ formatDuration(entry.duration) }}
|
{{ formatDuration(entry.duration) }}
|
||||||
</td>
|
</td>
|
||||||
<td class="px-4 py-3">
|
<td class="px-4 py-3">
|
||||||
<div class="flex items-center justify-end gap-1 opacity-0 group-hover:opacity-100 transition-opacity duration-100">
|
<div class="flex items-center justify-end gap-1 opacity-0 group-hover:opacity-100 transition-opacity duration-100">
|
||||||
|
<button
|
||||||
|
@click="duplicateEntry(entry)"
|
||||||
|
class="p-1.5 text-text-tertiary hover:text-text-secondary transition-colors duration-150"
|
||||||
|
title="Duplicate"
|
||||||
|
>
|
||||||
|
<Copy class="h-3.5 w-3.5" :stroke-width="2" />
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
@click="openEditDialog(entry)"
|
@click="openEditDialog(entry)"
|
||||||
class="p-1.5 text-text-tertiary hover:text-text-secondary transition-colors duration-150"
|
class="p-1.5 text-text-tertiary hover:text-text-secondary transition-colors duration-150"
|
||||||
@@ -123,7 +145,7 @@
|
|||||||
<div
|
<div
|
||||||
v-if="showEditDialog"
|
v-if="showEditDialog"
|
||||||
class="fixed inset-0 bg-black/70 backdrop-blur-[4px] flex items-center justify-center p-4 z-50"
|
class="fixed inset-0 bg-black/70 backdrop-blur-[4px] flex items-center justify-center p-4 z-50"
|
||||||
@click.self="closeEditDialog"
|
@click.self="tryCloseEditDialog"
|
||||||
>
|
>
|
||||||
<div class="bg-bg-surface border border-border-subtle rounded-lg shadow-[0_1px_3px_rgba(0,0,0,0.3)] w-full max-w-md p-6 animate-modal-enter max-h-[calc(100vh-2rem)] overflow-y-auto">
|
<div class="bg-bg-surface border border-border-subtle rounded-lg shadow-[0_1px_3px_rgba(0,0,0,0.3)] w-full max-w-md p-6 animate-modal-enter max-h-[calc(100vh-2rem)] overflow-y-auto">
|
||||||
<h2 class="text-[1.125rem] font-semibold font-[family-name:var(--font-heading)] text-text-primary mb-4">Edit Entry</h2>
|
<h2 class="text-[1.125rem] font-semibold font-[family-name:var(--font-heading)] text-text-primary mb-4">Edit Entry</h2>
|
||||||
@@ -222,22 +244,38 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<AppDiscardDialog :show="showDiscardDialog" @cancel="cancelDiscard" @discard="confirmDiscard" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, reactive, computed, onMounted } from 'vue'
|
import { ref, reactive, computed, onMounted } from 'vue'
|
||||||
import { List as ListIcon } from 'lucide-vue-next'
|
import { List as ListIcon, Copy } from 'lucide-vue-next'
|
||||||
|
import { invoke } from '@tauri-apps/api/core'
|
||||||
import AppNumberInput from '../components/AppNumberInput.vue'
|
import AppNumberInput from '../components/AppNumberInput.vue'
|
||||||
import AppSelect from '../components/AppSelect.vue'
|
import AppSelect from '../components/AppSelect.vue'
|
||||||
import AppDatePicker from '../components/AppDatePicker.vue'
|
import AppDatePicker from '../components/AppDatePicker.vue'
|
||||||
|
import AppDiscardDialog from '../components/AppDiscardDialog.vue'
|
||||||
import { useEntriesStore, type TimeEntry } from '../stores/entries'
|
import { useEntriesStore, type TimeEntry } from '../stores/entries'
|
||||||
import { useProjectsStore } from '../stores/projects'
|
import { useProjectsStore } from '../stores/projects'
|
||||||
import { formatDate } from '../utils/locale'
|
import { formatDate } from '../utils/locale'
|
||||||
|
import { useFormGuard } from '../utils/formGuard'
|
||||||
|
import { renderMarkdown } from '../utils/markdown'
|
||||||
|
|
||||||
const entriesStore = useEntriesStore()
|
const entriesStore = useEntriesStore()
|
||||||
const projectsStore = useProjectsStore()
|
const projectsStore = useProjectsStore()
|
||||||
|
|
||||||
|
const { showDiscardDialog, snapshot: snapshotForm, tryClose: tryCloseForm, confirmDiscard, cancelDiscard } = useFormGuard()
|
||||||
|
|
||||||
|
function getEditFormData() {
|
||||||
|
return { project_id: editForm.project_id, description: editForm.description, duration: editForm.duration, date: editDate.value, hour: editHour.value, minute: editMinute.value }
|
||||||
|
}
|
||||||
|
|
||||||
|
function tryCloseEditDialog() {
|
||||||
|
tryCloseForm(getEditFormData(), closeEditDialog)
|
||||||
|
}
|
||||||
|
|
||||||
// Filter state
|
// Filter state
|
||||||
const startDate = ref('')
|
const startDate = ref('')
|
||||||
const endDate = ref('')
|
const endDate = ref('')
|
||||||
@@ -324,6 +362,74 @@ function formatDuration(seconds: number): string {
|
|||||||
return `${minutes}m`
|
return `${minutes}m`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Duplicate an entry with current timestamp
|
||||||
|
async function duplicateEntry(entry: TimeEntry) {
|
||||||
|
const now = new Date()
|
||||||
|
const newEntry: TimeEntry = {
|
||||||
|
project_id: entry.project_id,
|
||||||
|
task_id: entry.task_id,
|
||||||
|
description: entry.description,
|
||||||
|
start_time: now.toISOString(),
|
||||||
|
end_time: new Date(now.getTime() + entry.duration * 1000).toISOString(),
|
||||||
|
duration: entry.duration,
|
||||||
|
}
|
||||||
|
await entriesStore.createEntry(newEntry)
|
||||||
|
await entriesStore.fetchEntries()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy yesterday's entries to today
|
||||||
|
async function copyPreviousDay() {
|
||||||
|
const yesterday = new Date()
|
||||||
|
yesterday.setDate(yesterday.getDate() - 1)
|
||||||
|
const yStr = yesterday.toISOString().split('T')[0]
|
||||||
|
const prevEntries = await invoke<TimeEntry[]>('get_time_entries', {
|
||||||
|
startDate: yStr, endDate: yStr
|
||||||
|
})
|
||||||
|
const now = new Date()
|
||||||
|
const todayStr = now.toISOString().split('T')[0]
|
||||||
|
for (const e of prevEntries) {
|
||||||
|
const startHour = new Date(e.start_time).getHours()
|
||||||
|
const startMin = new Date(e.start_time).getMinutes()
|
||||||
|
const newStart = new Date(`${todayStr}T${String(startHour).padStart(2,'0')}:${String(startMin).padStart(2,'0')}:00`)
|
||||||
|
await entriesStore.createEntry({
|
||||||
|
project_id: e.project_id,
|
||||||
|
task_id: e.task_id,
|
||||||
|
description: e.description,
|
||||||
|
start_time: newStart.toISOString(),
|
||||||
|
end_time: new Date(newStart.getTime() + e.duration * 1000).toISOString(),
|
||||||
|
duration: e.duration,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
await entriesStore.fetchEntries()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy last week's entries shifted forward 7 days
|
||||||
|
async function copyPreviousWeek() {
|
||||||
|
const now = new Date()
|
||||||
|
const prevWeekStart = new Date(now)
|
||||||
|
prevWeekStart.setDate(now.getDate() - now.getDay() + 1 - 7)
|
||||||
|
const prevWeekEnd = new Date(prevWeekStart)
|
||||||
|
prevWeekEnd.setDate(prevWeekStart.getDate() + 6)
|
||||||
|
const prevEntries = await invoke<TimeEntry[]>('get_time_entries', {
|
||||||
|
startDate: prevWeekStart.toISOString().split('T')[0],
|
||||||
|
endDate: prevWeekEnd.toISOString().split('T')[0],
|
||||||
|
})
|
||||||
|
for (const e of prevEntries) {
|
||||||
|
const entryDate = new Date(e.start_time)
|
||||||
|
const newDate = new Date(entryDate)
|
||||||
|
newDate.setDate(entryDate.getDate() + 7)
|
||||||
|
await entriesStore.createEntry({
|
||||||
|
project_id: e.project_id,
|
||||||
|
task_id: e.task_id,
|
||||||
|
description: e.description,
|
||||||
|
start_time: newDate.toISOString(),
|
||||||
|
end_time: new Date(newDate.getTime() + e.duration * 1000).toISOString(),
|
||||||
|
duration: e.duration,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
await entriesStore.fetchEntries()
|
||||||
|
}
|
||||||
|
|
||||||
// Apply filters
|
// Apply filters
|
||||||
function applyFilters() {
|
function applyFilters() {
|
||||||
entriesStore.fetchEntries(startDate.value || undefined, endDate.value || undefined)
|
entriesStore.fetchEntries(startDate.value || undefined, endDate.value || undefined)
|
||||||
@@ -354,6 +460,7 @@ function openEditDialog(entry: TimeEntry) {
|
|||||||
editHour.value = dt.getHours()
|
editHour.value = dt.getHours()
|
||||||
editMinute.value = dt.getMinutes()
|
editMinute.value = dt.getMinutes()
|
||||||
|
|
||||||
|
snapshotForm(getEditFormData())
|
||||||
showEditDialog.value = true
|
showEditDialog.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<div class="p-6">
|
<div class="p-6">
|
||||||
<!-- Hero timer display -->
|
<!-- Hero timer display -->
|
||||||
<div class="text-center pt-4 pb-8">
|
<div class="text-center pt-4 pb-8">
|
||||||
<p class="text-[3.5rem] font-medium font-[family-name:var(--font-heading)] tracking-tighter mb-6">
|
<p class="text-[3.5rem] font-medium font-[family-name:var(--font-heading)] tracking-tighter mb-2">
|
||||||
<span class="text-text-primary">{{ timerParts.hours }}</span>
|
<span class="text-text-primary">{{ timerParts.hours }}</span>
|
||||||
<span :class="timerStore.isRunning ? 'text-accent-text animate-pulse-colon' : 'text-text-primary'">:</span>
|
<span :class="timerStore.isRunning ? 'text-accent-text animate-pulse-colon' : 'text-text-primary'">:</span>
|
||||||
<span class="text-text-primary">{{ timerParts.minutes }}</span>
|
<span class="text-text-primary">{{ timerParts.minutes }}</span>
|
||||||
@@ -10,14 +10,22 @@
|
|||||||
<span class="text-text-primary">{{ timerParts.seconds }}</span>
|
<span class="text-text-primary">{{ timerParts.seconds }}</span>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<!-- Paused indicator -->
|
||||||
|
<p
|
||||||
|
v-if="timerStore.isPaused"
|
||||||
|
class="text-[0.75rem] font-medium mb-4"
|
||||||
|
:class="timerStore.timerState === 'PAUSED_IDLE' ? 'text-status-warning' : 'text-status-info'"
|
||||||
|
>
|
||||||
|
{{ timerStore.timerState === 'PAUSED_IDLE' ? 'Paused (idle)' : 'Paused (app not visible)' }}
|
||||||
|
</p>
|
||||||
|
<div v-else class="mb-4" />
|
||||||
|
|
||||||
<button
|
<button
|
||||||
@click="toggleTimer"
|
@click="toggleTimer"
|
||||||
class="px-10 py-3 text-sm font-medium rounded-lg transition-colors duration-150"
|
class="px-10 py-3 text-sm font-medium rounded-lg transition-colors duration-150"
|
||||||
:class="timerStore.isRunning
|
:class="buttonClass"
|
||||||
? 'bg-status-error text-white hover:bg-status-error/80'
|
|
||||||
: 'bg-accent text-bg-base hover:bg-accent-hover'"
|
|
||||||
>
|
>
|
||||||
{{ timerStore.isRunning ? 'Stop' : 'Start' }}
|
{{ buttonLabel }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -33,7 +41,7 @@
|
|||||||
value-key="id"
|
value-key="id"
|
||||||
placeholder="Select project"
|
placeholder="Select project"
|
||||||
:placeholder-value="null"
|
:placeholder-value="null"
|
||||||
:disabled="timerStore.isRunning"
|
:disabled="!timerStore.isStopped"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@@ -45,7 +53,7 @@
|
|||||||
value-key="id"
|
value-key="id"
|
||||||
placeholder="Select task"
|
placeholder="Select task"
|
||||||
:placeholder-value="null"
|
:placeholder-value="null"
|
||||||
:disabled="timerStore.isRunning || !selectedProject"
|
:disabled="!timerStore.isStopped || !selectedProject"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -54,7 +62,7 @@
|
|||||||
<input
|
<input
|
||||||
v-model="description"
|
v-model="description"
|
||||||
type="text"
|
type="text"
|
||||||
:disabled="timerStore.isRunning"
|
:disabled="!timerStore.isStopped"
|
||||||
class="w-full px-3 py-2 bg-bg-inset border border-border-subtle rounded-lg text-[0.8125rem] text-text-primary placeholder-text-tertiary focus:outline-none focus:border-border-visible disabled:opacity-40 disabled:cursor-not-allowed"
|
class="w-full px-3 py-2 bg-bg-inset border border-border-subtle rounded-lg text-[0.8125rem] text-text-primary placeholder-text-tertiary focus:outline-none focus:border-border-visible disabled:opacity-40 disabled:cursor-not-allowed"
|
||||||
placeholder="What are you working on?"
|
placeholder="What are you working on?"
|
||||||
/>
|
/>
|
||||||
@@ -85,10 +93,20 @@
|
|||||||
<p class="text-[0.6875rem] text-text-tertiary">{{ entry.description || 'No description' }}</p>
|
<p class="text-[0.6875rem] text-text-tertiary">{{ entry.description || 'No description' }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
<div class="text-right">
|
<div class="text-right">
|
||||||
<p class="text-[0.75rem] font-mono text-text-primary">{{ formatDuration(entry.duration) }}</p>
|
<p class="text-[0.75rem] font-mono text-text-primary">{{ formatDuration(entry.duration) }}</p>
|
||||||
<p class="text-[0.6875rem] text-text-tertiary">{{ formatDateTime(entry.start_time) }}</p>
|
<p class="text-[0.6875rem] text-text-tertiary">{{ formatDateTime(entry.start_time) }}</p>
|
||||||
</div>
|
</div>
|
||||||
|
<button
|
||||||
|
v-if="timerStore.isStopped"
|
||||||
|
@click="repeatEntry(entry)"
|
||||||
|
class="p-1 text-text-tertiary hover:text-text-secondary transition-colors"
|
||||||
|
title="Repeat"
|
||||||
|
>
|
||||||
|
<RotateCcw class="w-3 h-3" :stroke-width="2" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -99,6 +117,22 @@
|
|||||||
<p class="text-xs text-text-tertiary mt-1">Select a project and hit Start to begin tracking</p>
|
<p class="text-xs text-text-tertiary mt-1">Select a project and hit Start to begin tracking</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Idle Prompt Dialog -->
|
||||||
|
<IdlePromptDialog
|
||||||
|
:show="timerStore.showIdlePrompt"
|
||||||
|
:idle-seconds="timerStore.idleDurationSeconds"
|
||||||
|
@continue-keep="onIdleContinueKeep"
|
||||||
|
@continue-subtract="onIdleContinueSubtract"
|
||||||
|
@stop-timer="onIdleStop"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- App Tracking Prompt Dialog -->
|
||||||
|
<AppTrackingPromptDialog
|
||||||
|
:show="timerStore.showAppPrompt"
|
||||||
|
@continue-timer="onAppContinue"
|
||||||
|
@stop-timer="onAppStop"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -107,14 +141,18 @@ import { ref, computed, watch, onMounted } from 'vue'
|
|||||||
import { useTimerStore } from '../stores/timer'
|
import { useTimerStore } from '../stores/timer'
|
||||||
import { useProjectsStore, type Task } from '../stores/projects'
|
import { useProjectsStore, type Task } from '../stores/projects'
|
||||||
import { useEntriesStore } from '../stores/entries'
|
import { useEntriesStore } from '../stores/entries'
|
||||||
|
import { useSettingsStore } from '../stores/settings'
|
||||||
import { useToastStore } from '../stores/toast'
|
import { useToastStore } from '../stores/toast'
|
||||||
import { Timer as TimerIcon } from 'lucide-vue-next'
|
import { Timer as TimerIcon, RotateCcw } from 'lucide-vue-next'
|
||||||
import AppSelect from '../components/AppSelect.vue'
|
import AppSelect from '../components/AppSelect.vue'
|
||||||
|
import IdlePromptDialog from '../components/IdlePromptDialog.vue'
|
||||||
|
import AppTrackingPromptDialog from '../components/AppTrackingPromptDialog.vue'
|
||||||
import { formatDateTime } from '../utils/locale'
|
import { formatDateTime } from '../utils/locale'
|
||||||
|
|
||||||
const timerStore = useTimerStore()
|
const timerStore = useTimerStore()
|
||||||
const projectsStore = useProjectsStore()
|
const projectsStore = useProjectsStore()
|
||||||
const entriesStore = useEntriesStore()
|
const entriesStore = useEntriesStore()
|
||||||
|
const settingsStore = useSettingsStore()
|
||||||
const toastStore = useToastStore()
|
const toastStore = useToastStore()
|
||||||
|
|
||||||
// Local state for inputs
|
// Local state for inputs
|
||||||
@@ -144,6 +182,18 @@ const recentEntries = computed(() => {
|
|||||||
return entriesStore.entries.slice(0, 5)
|
return entriesStore.entries.slice(0, 5)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Button appearance
|
||||||
|
const buttonLabel = computed(() => {
|
||||||
|
if (timerStore.isStopped) return 'Start'
|
||||||
|
if (timerStore.isPaused) return 'Stop'
|
||||||
|
return 'Stop'
|
||||||
|
})
|
||||||
|
|
||||||
|
const buttonClass = computed(() => {
|
||||||
|
if (timerStore.isStopped) return 'bg-accent text-bg-base hover:bg-accent-hover'
|
||||||
|
return 'bg-status-error text-white hover:bg-status-error/80'
|
||||||
|
})
|
||||||
|
|
||||||
// Watch project selection and fetch tasks
|
// Watch project selection and fetch tasks
|
||||||
watch(selectedProject, async (newProjectId) => {
|
watch(selectedProject, async (newProjectId) => {
|
||||||
timerStore.setProject(newProjectId)
|
timerStore.setProject(newProjectId)
|
||||||
@@ -165,20 +215,89 @@ watch(description, (newDesc) => {
|
|||||||
timerStore.setDescription(newDesc)
|
timerStore.setDescription(newDesc)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Bring window to front when prompts appear
|
||||||
|
async function bringToFront() {
|
||||||
|
try {
|
||||||
|
const { getCurrentWindow } = await import('@tauri-apps/api/window')
|
||||||
|
const win = getCurrentWindow()
|
||||||
|
await win.show()
|
||||||
|
await win.setFocus()
|
||||||
|
} catch (e) {
|
||||||
|
// Ignore if not in Tauri context
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(() => timerStore.showIdlePrompt, (show) => {
|
||||||
|
if (show) bringToFront()
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(() => timerStore.showAppPrompt, (show) => {
|
||||||
|
if (show) bringToFront()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Send OS notification when entering PAUSED_APP in "notify" mode
|
||||||
|
watch(() => timerStore.timerState, async (newState, oldState) => {
|
||||||
|
if (newState === 'PAUSED_APP' && oldState === 'RUNNING') {
|
||||||
|
const mode = settingsStore.settings.app_tracking_mode || 'auto'
|
||||||
|
if (mode === 'notify') {
|
||||||
|
try {
|
||||||
|
const { sendNotification } = await import('@tauri-apps/plugin-notification')
|
||||||
|
sendNotification({
|
||||||
|
title: 'ZeroClock',
|
||||||
|
body: 'Tracked app is no longer visible. Timer paused.',
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
// Ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// Toggle timer
|
// Toggle timer
|
||||||
function toggleTimer() {
|
function toggleTimer() {
|
||||||
if (timerStore.isRunning) {
|
if (timerStore.isStopped) {
|
||||||
timerStore.stop()
|
|
||||||
entriesStore.fetchEntries()
|
|
||||||
} else {
|
|
||||||
if (!selectedProject.value) {
|
if (!selectedProject.value) {
|
||||||
toastStore.info('Please select a project before starting the timer')
|
toastStore.info('Please select a project before starting the timer')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
timerStore.start()
|
timerStore.start()
|
||||||
|
} else {
|
||||||
|
timerStore.stop()
|
||||||
|
entriesStore.fetchEntries()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Idle prompt handlers
|
||||||
|
function onIdleContinueKeep() {
|
||||||
|
timerStore.handleIdleContinueKeep()
|
||||||
|
}
|
||||||
|
|
||||||
|
function onIdleContinueSubtract() {
|
||||||
|
timerStore.handleIdleContinueSubtract()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onIdleStop() {
|
||||||
|
await timerStore.handleIdleStop()
|
||||||
|
entriesStore.fetchEntries()
|
||||||
|
}
|
||||||
|
|
||||||
|
// App prompt handlers
|
||||||
|
function onAppContinue() {
|
||||||
|
timerStore.handleAppContinue()
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onAppStop() {
|
||||||
|
await timerStore.handleAppStop()
|
||||||
|
entriesStore.fetchEntries()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Repeat an entry (fill in project/task/description)
|
||||||
|
function repeatEntry(entry: { project_id: number; task_id?: number; description?: string }) {
|
||||||
|
selectedProject.value = entry.project_id
|
||||||
|
selectedTask.value = entry.task_id || null
|
||||||
|
description.value = entry.description || ''
|
||||||
|
}
|
||||||
|
|
||||||
// Get project name by ID
|
// Get project name by ID
|
||||||
function getProjectName(projectId: number): string {
|
function getProjectName(projectId: number): string {
|
||||||
const project = projectsStore.projects.find(p => p.id === projectId)
|
const project = projectsStore.projects.find(p => p.id === projectId)
|
||||||
@@ -205,7 +324,8 @@ function formatDuration(seconds: number): string {
|
|||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
projectsStore.fetchProjects(),
|
projectsStore.fetchProjects(),
|
||||||
entriesStore.fetchEntries()
|
entriesStore.fetchEntries(),
|
||||||
|
settingsStore.fetchSettings()
|
||||||
])
|
])
|
||||||
|
|
||||||
// Restore timer state
|
// Restore timer state
|
||||||
|
|||||||
Reference in New Issue
Block a user