feat: integrate tags in Timer and Entries views

Timer shows tag selector below description, saves tags on stop.
Entries table displays tag chips per row with color coding.
Tags loaded from store on mount.
This commit is contained in:
Your Name
2026-02-18 10:51:39 +02:00
parent 8eb2d135c8
commit 5e608a98e6
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