refactor(time-tracking) : use MalioSelect for filters and drawer, improve calendar cards
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -18,27 +18,29 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="px-1.5 py-0.5 h-full overflow-hidden">
|
<div class="px-1.5 py-0.5 h-full overflow-hidden">
|
||||||
<!-- Full display: title + project + types + duration -->
|
<!-- Full display: title + project + type dot + duration -->
|
||||||
<template v-if="sizeLevel >= 3">
|
<template v-if="sizeLevel >= 3">
|
||||||
<div class="font-semibold truncate">{{ entry.title || 'Sans titre' }}</div>
|
<div class="flex items-center gap-1">
|
||||||
|
<div class="font-semibold truncate">{{ entry.title || 'Sans titre' }}</div>
|
||||||
|
<span class="ml-auto shrink-0 text-[10px] tabular-nums opacity-80">{{ duration }}</span>
|
||||||
|
</div>
|
||||||
<div v-if="entry.project" class="truncate text-[10px] opacity-80">{{ entry.project.name }}</div>
|
<div v-if="entry.project" class="truncate text-[10px] opacity-80">{{ entry.project.name }}</div>
|
||||||
<div class="mt-0.5 flex items-center gap-1">
|
<div v-if="entry.types.length" class="mt-0.5 flex items-center gap-1 overflow-hidden">
|
||||||
<span
|
<span
|
||||||
v-for="type in entry.types"
|
v-for="type in entry.types"
|
||||||
:key="type.id"
|
:key="type.id"
|
||||||
class="rounded-full px-1.5 py-0.5 text-[9px] font-semibold"
|
class="inline-flex items-center gap-0.5 truncate text-[9px] opacity-90"
|
||||||
:style="{ backgroundColor: type.color }"
|
|
||||||
>
|
>
|
||||||
|
<span class="inline-block h-1.5 w-1.5 shrink-0 rounded-full" :style="{ backgroundColor: type.color }" />
|
||||||
{{ type.label }}
|
{{ type.label }}
|
||||||
</span>
|
</span>
|
||||||
<span class="ml-auto text-[10px] opacity-80">{{ duration }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- Medium: title + duration -->
|
<!-- Medium: title + duration -->
|
||||||
<template v-else-if="sizeLevel === 2">
|
<template v-else-if="sizeLevel === 2">
|
||||||
<div class="font-semibold truncate">{{ entry.title || 'Sans titre' }}</div>
|
<div class="font-semibold truncate">{{ entry.title || 'Sans titre' }}</div>
|
||||||
<div class="text-[10px] opacity-80">{{ duration }}</div>
|
<div class="text-[10px] tabular-nums opacity-80">{{ duration }}</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- Small: title only -->
|
<!-- Small: title only -->
|
||||||
@@ -88,13 +90,11 @@ const resizeTopDeltaMinutes = ref(0)
|
|||||||
const resizeBottomDeltaMinutes = ref(0)
|
const resizeBottomDeltaMinutes = ref(0)
|
||||||
|
|
||||||
const duration = computed(() => {
|
const duration = computed(() => {
|
||||||
const startMs = startDate.value.getTime() + resizeTopDeltaMinutes.value * 60000
|
const mins = Math.floor((endDate.value.getTime() + resizeBottomDeltaMinutes.value * 60000
|
||||||
const endMs = endDate.value.getTime() + resizeBottomDeltaMinutes.value * 60000
|
- startDate.value.getTime() - resizeTopDeltaMinutes.value * 60000) / 60000)
|
||||||
const diff = endMs - startMs
|
const h = Math.floor(mins / 60)
|
||||||
const h = Math.floor(diff / 3600000)
|
const m = mins % 60
|
||||||
const m = Math.floor((diff % 3600000) / 60000)
|
return m > 0 ? `${h}h${String(m).padStart(2, '0')}` : `${h}h`
|
||||||
const s = Math.floor((diff % 60000) / 1000)
|
|
||||||
return [h, m, s].map((v) => String(v).padStart(2, '0')).join(' : ')
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const heightPx = computed(() => {
|
const heightPx = computed(() => {
|
||||||
|
|||||||
@@ -57,37 +57,28 @@
|
|||||||
{{ durationLabel }}
|
{{ durationLabel }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<MalioSelect
|
||||||
<label class="mb-1 block text-sm font-semibold text-neutral-700">Utilisateur</label>
|
v-model="form.userId"
|
||||||
<select
|
:options="userOptions"
|
||||||
v-model="form.userId"
|
label="Utilisateur"
|
||||||
class="w-full rounded-md border border-neutral-300 px-3 py-2 text-sm focus:border-primary-500 focus:outline-none"
|
min-width="w-full"
|
||||||
>
|
/>
|
||||||
<option v-for="u in users" :key="u.id" :value="u.id">{{ u.username }}</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<MalioSelect
|
||||||
<label class="mb-1 block text-sm font-semibold text-neutral-700">Projet</label>
|
v-model="form.projectId"
|
||||||
<select
|
:options="projectOptions"
|
||||||
v-model="form.projectId"
|
label="Projet"
|
||||||
class="w-full rounded-md border border-neutral-300 px-3 py-2 text-sm focus:border-primary-500 focus:outline-none"
|
empty-option-label="— Aucun —"
|
||||||
>
|
min-width="w-full"
|
||||||
<option :value="null">— Aucun —</option>
|
/>
|
||||||
<option v-for="p in projects" :key="p.id" :value="p.id">{{ p.name }}</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<MalioSelect
|
||||||
<label class="mb-1 block text-sm font-semibold text-neutral-700">Type</label>
|
v-model="form.typeId"
|
||||||
<select
|
:options="typeOptions"
|
||||||
v-model="form.typeId"
|
label="Type"
|
||||||
class="w-full rounded-md border border-neutral-300 px-3 py-2 text-sm focus:border-primary-500 focus:outline-none"
|
empty-option-label="— Aucun —"
|
||||||
>
|
min-width="w-full"
|
||||||
<option :value="null">— Aucun —</option>
|
/>
|
||||||
<option v-for="t in types" :key="t.id" :value="t.id">{{ t.label }}</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex items-center" :class="isEditing ? 'justify-between' : 'justify-end'">
|
<div class="flex items-center" :class="isEditing ? 'justify-between' : 'justify-end'">
|
||||||
<button
|
<button
|
||||||
@@ -150,6 +141,18 @@ const form = reactive({
|
|||||||
typeId: null as number | null,
|
typeId: null as number | null,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const userOptions = computed(() =>
|
||||||
|
props.users.map(u => ({ label: u.username, value: u.id }))
|
||||||
|
)
|
||||||
|
|
||||||
|
const projectOptions = computed(() =>
|
||||||
|
props.projects.map(p => ({ label: p.name, value: p.id }))
|
||||||
|
)
|
||||||
|
|
||||||
|
const typeOptions = computed(() =>
|
||||||
|
props.types.map(t => ({ label: t.label, value: t.id }))
|
||||||
|
)
|
||||||
|
|
||||||
const durationLabel = computed(() => {
|
const durationLabel = computed(() => {
|
||||||
if (!form.startTime || !form.endTime) return ''
|
if (!form.startTime || !form.endTime) return ''
|
||||||
const [sh, sm] = form.startTime.split(':').map(Number) as [number, number]
|
const [sh, sm] = form.startTime.split(':').map(Number) as [number, number]
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-4 flex items-center gap-4">
|
<div class="relative z-30 mt-4 flex items-center gap-4">
|
||||||
<h2 class="text-lg font-bold text-orange-500">
|
<h2 class="text-lg font-bold text-orange-500">
|
||||||
{{ currentMonthLabel }}
|
{{ currentMonthLabel }}
|
||||||
</h2>
|
</h2>
|
||||||
@@ -33,29 +33,35 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<select
|
<MalioSelect
|
||||||
v-model="selectedUserId"
|
v-model="selectedUserId"
|
||||||
class="rounded-md border border-neutral-200 px-3 py-1.5 text-sm"
|
:options="userOptions"
|
||||||
@change="loadEntries"
|
min-width="!w-40"
|
||||||
>
|
text-field="text-sm"
|
||||||
<option v-for="u in users" :key="u.id" :value="u.id">{{ u.username }}</option>
|
text-value="text-sm"
|
||||||
</select>
|
label="User"
|
||||||
|
empty-option-label="User"
|
||||||
|
/>
|
||||||
|
|
||||||
<select
|
<MalioSelect
|
||||||
v-model="selectedProjectId"
|
v-model="selectedProjectId"
|
||||||
class="rounded-md border border-neutral-200 px-3 py-1.5 text-sm"
|
:options="projectOptions"
|
||||||
>
|
empty-option-label="Tous"
|
||||||
<option :value="null">Projet</option>
|
label="Projet"
|
||||||
<option v-for="p in projects" :key="p.id" :value="p.id">{{ p.name }}</option>
|
min-width="!w-40"
|
||||||
</select>
|
text-field="text-sm"
|
||||||
|
text-value="text-sm"
|
||||||
|
/>
|
||||||
|
|
||||||
<select
|
<MalioSelect
|
||||||
v-model="selectedTypeId"
|
v-model="selectedTypeId"
|
||||||
class="rounded-md border border-neutral-200 px-3 py-1.5 text-sm"
|
:options="typeOptions"
|
||||||
>
|
empty-option-label="Tous"
|
||||||
<option :value="null">Type</option>
|
label="Type"
|
||||||
<option v-for="t in types" :key="t.id" :value="t.id">{{ t.label }}</option>
|
min-width="!w-40"
|
||||||
</select>
|
text-field="text-sm"
|
||||||
|
text-value="text-sm"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
@@ -145,6 +151,18 @@ const currentMonthLabel = computed(() => {
|
|||||||
return `${months[d.getMonth()]} ${d.getFullYear()}`
|
return `${months[d.getMonth()]} ${d.getFullYear()}`
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const userOptions = computed(() =>
|
||||||
|
users.value.map(u => ({ label: u.username, value: u.id }))
|
||||||
|
)
|
||||||
|
|
||||||
|
const projectOptions = computed(() =>
|
||||||
|
projects.value.map(p => ({ label: p.name, value: p.id }))
|
||||||
|
)
|
||||||
|
|
||||||
|
const typeOptions = computed(() =>
|
||||||
|
types.value.map(t => ({ label: t.label, value: t.id }))
|
||||||
|
)
|
||||||
|
|
||||||
const filteredEntries = computed(() => {
|
const filteredEntries = computed(() => {
|
||||||
let result = entries.value
|
let result = entries.value
|
||||||
if (selectedProjectId.value) {
|
if (selectedProjectId.value) {
|
||||||
@@ -284,4 +302,8 @@ watch(viewMode, () => {
|
|||||||
startDate.value = viewMode.value === 'day' ? startDate.value : getMonday(startDate.value)
|
startDate.value = viewMode.value === 'day' ? startDate.value : getMonday(startDate.value)
|
||||||
loadEntries()
|
loadEntries()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
watch(selectedUserId, () => {
|
||||||
|
loadEntries()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Reference in New Issue
Block a user