integrate tags in Timer and Entries views

This commit is contained in:
2026-02-18 10:51:39 +02:00
parent 4fc8b0e51b
commit b231df4819
2 changed files with 93 additions and 9 deletions

View File

@@ -94,6 +94,16 @@
<td class="px-4 py-3 text-[0.75rem] text-text-secondary"> <td class="px-4 py-3 text-[0.75rem] text-text-secondary">
<span v-if="entry.description" v-html="renderMarkdown(entry.description)" class="markdown-inline" /> <span v-if="entry.description" v-html="renderMarkdown(entry.description)" class="markdown-inline" />
<span v-else>-</span> <span v-else>-</span>
<div v-if="entryTags[entry.id!]?.length" class="flex flex-wrap gap-1 mt-1">
<span
v-for="tagId in entryTags[entry.id!]"
:key="tagId"
class="inline-flex items-center gap-1 px-1.5 py-0.5 rounded-full text-[0.5625rem]"
:style="{ backgroundColor: getTagColor(tagId) + '22', color: getTagColor(tagId) }"
>
{{ getTagName(tagId) }}
</span>
</div>
</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) }}
@@ -174,6 +184,12 @@
/> />
</div> </div>
<!-- Tags -->
<div>
<label class="block text-[0.6875rem] text-text-tertiary uppercase tracking-[0.08em] mb-1.5">Tags</label>
<AppTagInput v-model="editEntryTags" />
</div>
<!-- Duration --> <!-- Duration -->
<div> <div>
<label class="block text-[0.6875rem] text-text-tertiary uppercase tracking-[0.08em] mb-1.5">Duration (minutes)</label> <label class="block text-[0.6875rem] text-text-tertiary uppercase tracking-[0.08em] mb-1.5">Duration (minutes)</label>
@@ -257,14 +273,19 @@ 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 AppDiscardDialog from '../components/AppDiscardDialog.vue'
import AppTagInput from '../components/AppTagInput.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 { useTagsStore } from '../stores/tags'
import { formatDate } from '../utils/locale' import { formatDate } from '../utils/locale'
import { useFormGuard } from '../utils/formGuard' import { useFormGuard } from '../utils/formGuard'
import { renderMarkdown } from '../utils/markdown' import { renderMarkdown } from '../utils/markdown'
const entriesStore = useEntriesStore() const entriesStore = useEntriesStore()
const projectsStore = useProjectsStore() const projectsStore = useProjectsStore()
const tagsStore = useTagsStore()
const entryTags = ref<Record<number, number[]>>({})
const editEntryTags = ref<number[]>([])
const { showDiscardDialog, snapshot: snapshotForm, tryClose: tryCloseForm, confirmDiscard, cancelDiscard } = useFormGuard() const { showDiscardDialog, snapshot: snapshotForm, tryClose: tryCloseForm, confirmDiscard, cancelDiscard } = useFormGuard()
@@ -362,6 +383,28 @@ function formatDuration(seconds: number): string {
return `${minutes}m` return `${minutes}m`
} }
// Tag helper functions
function getTagName(tagId: number): string {
const tag = tagsStore.tags.find(t => t.id === tagId)
return tag?.name || ''
}
function getTagColor(tagId: number): string {
const tag = tagsStore.tags.find(t => t.id === tagId)
return tag?.color || '#6B7280'
}
async function loadEntryTags() {
const tags: Record<number, number[]> = {}
for (const entry of entriesStore.entries) {
if (entry.id) {
const entryTagList = await tagsStore.getEntryTags(entry.id)
tags[entry.id] = entryTagList.map(t => t.id!).filter(id => id != null)
}
}
entryTags.value = tags
}
// Duplicate an entry with current timestamp // Duplicate an entry with current timestamp
async function duplicateEntry(entry: TimeEntry) { async function duplicateEntry(entry: TimeEntry) {
const now = new Date() const now = new Date()
@@ -375,6 +418,7 @@ async function duplicateEntry(entry: TimeEntry) {
} }
await entriesStore.createEntry(newEntry) await entriesStore.createEntry(newEntry)
await entriesStore.fetchEntries() await entriesStore.fetchEntries()
await loadEntryTags()
} }
// Copy yesterday's entries to today // Copy yesterday's entries to today
@@ -401,6 +445,7 @@ async function copyPreviousDay() {
}) })
} }
await entriesStore.fetchEntries() await entriesStore.fetchEntries()
await loadEntryTags()
} }
// Copy last week's entries shifted forward 7 days // Copy last week's entries shifted forward 7 days
@@ -428,23 +473,26 @@ async function copyPreviousWeek() {
}) })
} }
await entriesStore.fetchEntries() await entriesStore.fetchEntries()
await loadEntryTags()
} }
// Apply filters // Apply filters
function applyFilters() { async function applyFilters() {
entriesStore.fetchEntries(startDate.value || undefined, endDate.value || undefined) await entriesStore.fetchEntries(startDate.value || undefined, endDate.value || undefined)
await loadEntryTags()
} }
// Clear filters // Clear filters
function clearFilters() { async function clearFilters() {
startDate.value = '' startDate.value = ''
endDate.value = '' endDate.value = ''
filterProject.value = null filterProject.value = null
entriesStore.fetchEntries() await entriesStore.fetchEntries()
await loadEntryTags()
} }
// Open edit dialog // Open edit dialog
function openEditDialog(entry: TimeEntry) { async function openEditDialog(entry: TimeEntry) {
editingEntry.value = entry editingEntry.value = entry
editForm.id = entry.id || 0 editForm.id = entry.id || 0
editForm.project_id = entry.project_id editForm.project_id = entry.project_id
@@ -460,6 +508,14 @@ function openEditDialog(entry: TimeEntry) {
editHour.value = dt.getHours() editHour.value = dt.getHours()
editMinute.value = dt.getMinutes() editMinute.value = dt.getMinutes()
// Load tags for this entry
if (entry.id) {
const tags = await tagsStore.getEntryTags(entry.id)
editEntryTags.value = tags.map(t => t.id!).filter(id => id != null)
} else {
editEntryTags.value = []
}
snapshotForm(getEditFormData()) snapshotForm(getEditFormData())
showEditDialog.value = true showEditDialog.value = true
} }
@@ -488,6 +544,13 @@ async function handleEdit() {
} }
await entriesStore.updateEntry(updatedEntry) await entriesStore.updateEntry(updatedEntry)
// Save tags for the edited entry
if (editForm.id) {
await tagsStore.setEntryTags(editForm.id, editEntryTags.value)
await loadEntryTags()
}
closeEditDialog() closeEditDialog()
} }
} }
@@ -516,9 +579,12 @@ async function handleDelete() {
onMounted(async () => { onMounted(async () => {
await Promise.all([ await Promise.all([
entriesStore.fetchEntries(), entriesStore.fetchEntries(),
projectsStore.fetchProjects() projectsStore.fetchProjects(),
tagsStore.fetchTags()
]) ])
await loadEntryTags()
// Set default date range to this week // Set default date range to this week
const now = new Date() const now = new Date()
const weekStart = new Date(now) const weekStart = new Date(now)

View File

@@ -104,6 +104,10 @@
</button> </button>
</div> </div>
</div> </div>
<div class="mt-3">
<label class="block text-[0.6875rem] text-text-tertiary uppercase tracking-[0.08em] mb-1.5">Tags</label>
<AppTagInput v-model="selectedTags" />
</div>
</div> </div>
<!-- Recent entries --> <!-- Recent entries -->
@@ -183,10 +187,12 @@ import { useToastStore } from '../stores/toast'
import { Timer as TimerIcon, RotateCcw, ExternalLink, Star } from 'lucide-vue-next' import { Timer as TimerIcon, RotateCcw, ExternalLink, Star } from 'lucide-vue-next'
import { invoke } from '@tauri-apps/api/core' import { invoke } from '@tauri-apps/api/core'
import AppSelect from '../components/AppSelect.vue' import AppSelect from '../components/AppSelect.vue'
import AppTagInput from '../components/AppTagInput.vue'
import IdlePromptDialog from '../components/IdlePromptDialog.vue' import IdlePromptDialog from '../components/IdlePromptDialog.vue'
import AppTrackingPromptDialog from '../components/AppTrackingPromptDialog.vue' import AppTrackingPromptDialog from '../components/AppTrackingPromptDialog.vue'
import { formatDateTime } from '../utils/locale' import { formatDateTime } from '../utils/locale'
import { useFavoritesStore, type Favorite } from '../stores/favorites' import { useFavoritesStore, type Favorite } from '../stores/favorites'
import { useTagsStore } from '../stores/tags'
const timerStore = useTimerStore() const timerStore = useTimerStore()
const projectsStore = useProjectsStore() const projectsStore = useProjectsStore()
@@ -194,12 +200,14 @@ const entriesStore = useEntriesStore()
const settingsStore = useSettingsStore() const settingsStore = useSettingsStore()
const toastStore = useToastStore() const toastStore = useToastStore()
const favoritesStore = useFavoritesStore() const favoritesStore = useFavoritesStore()
const tagsStore = useTagsStore()
const favorites = computed(() => favoritesStore.favorites) const favorites = computed(() => favoritesStore.favorites)
// Local state for inputs // Local state for inputs
const selectedProject = ref<number | null>(timerStore.selectedProjectId) const selectedProject = ref<number | null>(timerStore.selectedProjectId)
const selectedTask = ref<number | null>(timerStore.selectedTaskId) const selectedTask = ref<number | null>(timerStore.selectedTaskId)
const description = ref(timerStore.description) const description = ref(timerStore.description)
const selectedTags = ref<number[]>([])
const projectTasks = ref<Task[]>([]) const projectTasks = ref<Task[]>([])
// Split timer into parts for colon animation // Split timer into parts for colon animation
@@ -300,7 +308,7 @@ async function openMiniTimer() {
} }
// Toggle timer // Toggle timer
function toggleTimer() { async function toggleTimer() {
if (timerStore.isStopped) { if (timerStore.isStopped) {
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')
@@ -309,7 +317,16 @@ function toggleTimer() {
timerStore.start() timerStore.start()
} else { } else {
timerStore.stop() timerStore.stop()
entriesStore.fetchEntries() await entriesStore.fetchEntries()
// Save tags for the new entry if any were selected
if (selectedTags.value.length > 0) {
const entries = entriesStore.entries
if (entries.length > 0 && entries[0].id) {
await tagsStore.setEntryTags(entries[0].id, selectedTags.value)
}
}
selectedTags.value = []
} }
} }
@@ -390,7 +407,8 @@ onMounted(async () => {
projectsStore.fetchProjects(), projectsStore.fetchProjects(),
entriesStore.fetchEntries(), entriesStore.fetchEntries(),
settingsStore.fetchSettings(), settingsStore.fetchSettings(),
favoritesStore.fetchFavorites() favoritesStore.fetchFavorites(),
tagsStore.fetchTags()
]) ])
// Restore timer state // Restore timer state