Avec MalioInputRichText qui émet désormais du HTML par défaut, plusieurs points d'affichage rendaient les balises brutes au lieu du texte. Ajoute un helper stripRichText() (frontend) et descriptionToPlainText() (backend) pour neutraliser ces cas. - TimeEntryList : strip avant truncate dans la liste des time entries. - ProjectGroupTab : strip dans la cellule description du tableau des groupes. - CalDavService : strip_tags + html_entity_decode avant injection dans le DESCRIPTION VEVENT/VTODO iCal (sinon Outlook/Apple Calendar affichaient les <p>...</p> à l'utilisateur). Co-Authored-By: RuFlo <ruv@ruv.net>
107 lines
4.1 KiB
Vue
107 lines
4.1 KiB
Vue
<template>
|
||
<div class="space-y-2">
|
||
<div v-if="entries.length === 0" class="rounded-lg border border-neutral-200 bg-neutral-50 py-12 text-center text-sm text-neutral-400">
|
||
{{ $t('timeEntries.noEntries') }}
|
||
</div>
|
||
|
||
<div
|
||
v-for="entry in sortedEntries"
|
||
:key="entry.id"
|
||
class="group flex items-center gap-2 sm:gap-4 rounded-lg border border-neutral-200 bg-white px-3 sm:px-4 py-3 cursor-pointer transition hover:border-neutral-300 hover:shadow-sm"
|
||
@click="emit('editEntry', entry)"
|
||
>
|
||
<!-- Color bar -->
|
||
<div
|
||
class="h-10 w-1 shrink-0 rounded-full"
|
||
:style="{ backgroundColor: entry.project?.color ?? '#94a3b8' }"
|
||
/>
|
||
|
||
<!-- Main info -->
|
||
<div class="min-w-0 flex-1">
|
||
<div class="truncate text-sm font-semibold text-neutral-900">
|
||
{{ entry.title || $t('common.untitled') }}
|
||
</div>
|
||
<div v-if="entry.tags.length" class="mt-1 flex flex-wrap gap-1">
|
||
<span
|
||
v-for="tag in entry.tags"
|
||
:key="tag.id"
|
||
class="rounded-full px-2 py-0.5 text-[10px] font-semibold text-white"
|
||
:style="{ backgroundColor: tag.color }"
|
||
>
|
||
{{ tag.label }}
|
||
</span>
|
||
</div>
|
||
<div class="mt-0.5 flex items-center gap-2 text-xs text-neutral-500">
|
||
<span v-if="entry.project">{{ entry.project.name }}</span>
|
||
<span v-if="entry.project && stripRichText(entry.description)" class="text-neutral-300">·</span>
|
||
<span v-if="stripRichText(entry.description)" class="truncate">{{ stripRichText(entry.description) }}</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Time info -->
|
||
<div class="shrink-0 text-right">
|
||
<div class="text-sm font-semibold tabular-nums text-neutral-900">
|
||
{{ formatDuration(entry) }}
|
||
</div>
|
||
<div class="text-xs tabular-nums text-neutral-400">
|
||
{{ formatTime(entry.startedAt) }} – {{ entry.stoppedAt ? formatTime(entry.stoppedAt) : '...' }}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Date -->
|
||
<div class="hidden shrink-0 text-xs text-neutral-400 sm:block">
|
||
{{ formatDate(entry.startedAt) }}
|
||
</div>
|
||
|
||
<!-- Delete action -->
|
||
<MalioButtonIcon
|
||
icon="mdi:delete-outline"
|
||
:aria-label="$t('common.delete')"
|
||
variant="ghost"
|
||
icon-size="18"
|
||
button-class="shrink-0 text-neutral-300 opacity-0 hover:bg-red-50 hover:text-red-500 group-hover:opacity-100"
|
||
@click.stop="emit('deleteEntry', entry)"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import type { TimeEntry } from '~/services/dto/time-entry'
|
||
import { stripRichText } from '~/utils/format'
|
||
|
||
const props = defineProps<{
|
||
entries: TimeEntry[]
|
||
}>()
|
||
|
||
const emit = defineEmits<{
|
||
(e: 'editEntry', entry: TimeEntry): void
|
||
(e: 'deleteEntry', entry: TimeEntry): void
|
||
}>()
|
||
|
||
const sortedEntries = computed(() => {
|
||
return [...props.entries].sort((a, b) => {
|
||
return new Date(b.startedAt).getTime() - new Date(a.startedAt).getTime()
|
||
})
|
||
})
|
||
|
||
function formatDuration(entry: TimeEntry): string {
|
||
const start = new Date(entry.startedAt).getTime()
|
||
const end = entry.stoppedAt ? new Date(entry.stoppedAt).getTime() : Date.now()
|
||
const diff = end - start
|
||
const h = Math.floor(diff / 3600000)
|
||
const m = Math.floor((diff % 3600000) / 60000)
|
||
return m > 0 ? `${h}h${String(m).padStart(2, '0')}` : `${h}h`
|
||
}
|
||
|
||
function formatTime(iso: string): string {
|
||
const d = new Date(iso)
|
||
return d.toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' })
|
||
}
|
||
|
||
function formatDate(iso: string): string {
|
||
const d = new Date(iso)
|
||
return d.toLocaleDateString('fr-FR', { weekday: 'short', day: 'numeric', month: 'short' })
|
||
}
|
||
</script>
|