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">
<span v-if="entry.description" v-html="renderMarkdown(entry.description)" class="markdown-inline" />
<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 class="px-4 py-3 text-right text-[0.75rem] font-mono text-accent-text">
{{ formatDuration(entry.duration) }}
@@ -174,6 +184,12 @@
/>
</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 -->
<div>
<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 AppDatePicker from '../components/AppDatePicker.vue'
import AppDiscardDialog from '../components/AppDiscardDialog.vue'
import AppTagInput from '../components/AppTagInput.vue'
import { useEntriesStore, type TimeEntry } from '../stores/entries'
import { useProjectsStore } from '../stores/projects'
import { useTagsStore } from '../stores/tags'
import { formatDate } from '../utils/locale'
import { useFormGuard } from '../utils/formGuard'
import { renderMarkdown } from '../utils/markdown'
const entriesStore = useEntriesStore()
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()
@@ -362,6 +383,28 @@ function formatDuration(seconds: number): string {
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
async function duplicateEntry(entry: TimeEntry) {
const now = new Date()
@@ -375,6 +418,7 @@ async function duplicateEntry(entry: TimeEntry) {
}
await entriesStore.createEntry(newEntry)
await entriesStore.fetchEntries()
await loadEntryTags()
}
// Copy yesterday's entries to today
@@ -401,6 +445,7 @@ async function copyPreviousDay() {
})
}
await entriesStore.fetchEntries()
await loadEntryTags()
}
// Copy last week's entries shifted forward 7 days
@@ -428,23 +473,26 @@ async function copyPreviousWeek() {
})
}
await entriesStore.fetchEntries()
await loadEntryTags()
}
// Apply filters
function applyFilters() {
entriesStore.fetchEntries(startDate.value || undefined, endDate.value || undefined)
async function applyFilters() {
await entriesStore.fetchEntries(startDate.value || undefined, endDate.value || undefined)
await loadEntryTags()
}
// Clear filters
function clearFilters() {
async function clearFilters() {
startDate.value = ''
endDate.value = ''
filterProject.value = null
entriesStore.fetchEntries()
await entriesStore.fetchEntries()
await loadEntryTags()
}
// Open edit dialog
function openEditDialog(entry: TimeEntry) {
async function openEditDialog(entry: TimeEntry) {
editingEntry.value = entry
editForm.id = entry.id || 0
editForm.project_id = entry.project_id
@@ -460,6 +508,14 @@ function openEditDialog(entry: TimeEntry) {
editHour.value = dt.getHours()
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())
showEditDialog.value = true
}
@@ -488,6 +544,13 @@ async function handleEdit() {
}
await entriesStore.updateEntry(updatedEntry)
// Save tags for the edited entry
if (editForm.id) {
await tagsStore.setEntryTags(editForm.id, editEntryTags.value)
await loadEntryTags()
}
closeEditDialog()
}
}
@@ -516,9 +579,12 @@ async function handleDelete() {
onMounted(async () => {
await Promise.all([
entriesStore.fetchEntries(),
projectsStore.fetchProjects()
projectsStore.fetchProjects(),
tagsStore.fetchTags()
])
await loadEntryTags()
// Set default date range to this week
const now = new Date()
const weekStart = new Date(now)

View File

@@ -104,6 +104,10 @@
</button>
</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>
<!-- Recent entries -->
@@ -183,10 +187,12 @@ import { useToastStore } from '../stores/toast'
import { Timer as TimerIcon, RotateCcw, ExternalLink, Star } from 'lucide-vue-next'
import { invoke } from '@tauri-apps/api/core'
import AppSelect from '../components/AppSelect.vue'
import AppTagInput from '../components/AppTagInput.vue'
import IdlePromptDialog from '../components/IdlePromptDialog.vue'
import AppTrackingPromptDialog from '../components/AppTrackingPromptDialog.vue'
import { formatDateTime } from '../utils/locale'
import { useFavoritesStore, type Favorite } from '../stores/favorites'
import { useTagsStore } from '../stores/tags'
const timerStore = useTimerStore()
const projectsStore = useProjectsStore()
@@ -194,12 +200,14 @@ const entriesStore = useEntriesStore()
const settingsStore = useSettingsStore()
const toastStore = useToastStore()
const favoritesStore = useFavoritesStore()
const tagsStore = useTagsStore()
const favorites = computed(() => favoritesStore.favorites)
// Local state for inputs
const selectedProject = ref<number | null>(timerStore.selectedProjectId)
const selectedTask = ref<number | null>(timerStore.selectedTaskId)
const description = ref(timerStore.description)
const selectedTags = ref<number[]>([])
const projectTasks = ref<Task[]>([])
// Split timer into parts for colon animation
@@ -300,7 +308,7 @@ async function openMiniTimer() {
}
// Toggle timer
function toggleTimer() {
async function toggleTimer() {
if (timerStore.isStopped) {
if (!selectedProject.value) {
toastStore.info('Please select a project before starting the timer')
@@ -309,7 +317,16 @@ function toggleTimer() {
timerStore.start()
} else {
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(),
entriesStore.fetchEntries(),
settingsStore.fetchSettings(),
favoritesStore.fetchFavorites()
favoritesStore.fetchFavorites(),
tagsStore.fetchTags()
])
// Restore timer state