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">
|
<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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user