integrate tags in Timer and Entries views
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user