Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ec35a1b2aa | ||
|
|
0113c08a60 | ||
|
|
c176511d97 | ||
|
|
64de971872 |
@@ -1,2 +1,2 @@
|
|||||||
parameters:
|
parameters:
|
||||||
app.version: '0.3.0'
|
app.version: '0.3.1'
|
||||||
|
|||||||
@@ -73,6 +73,7 @@
|
|||||||
v-model="editForm.description"
|
v-model="editForm.description"
|
||||||
rows="5"
|
rows="5"
|
||||||
class="w-full rounded-lg border border-neutral-300 px-3 py-2 text-sm focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500"
|
class="w-full rounded-lg border border-neutral-300 px-3 py-2 text-sm focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500"
|
||||||
|
style="resize: vertical; min-height: 140px; max-height: 500px"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,12 @@
|
|||||||
<div class="flex items-start justify-between gap-2">
|
<div class="flex items-start justify-between gap-2">
|
||||||
<div class="min-w-0">
|
<div class="min-w-0">
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
<span v-if="task.project && task.number" class="text-xs font-medium text-neutral-400">{{ task.project.code }}{{ task.number }}</span>
|
<span v-if="task.project && task.number" class="text-xs font-semibold" :style="{ color: task.project.color }">{{ task.project.code }}{{ task.number }}</span>
|
||||||
|
<Icon
|
||||||
|
v-if="task.priority?.label === 'Haute'"
|
||||||
|
name="mdi:flag-variant"
|
||||||
|
class="h-3.5 w-3.5 text-red-600"
|
||||||
|
/>
|
||||||
<Icon
|
<Icon
|
||||||
v-if="task.clientTicket"
|
v-if="task.clientTicket"
|
||||||
name="heroicons:user-circle"
|
name="heroicons:user-circle"
|
||||||
|
|||||||
@@ -156,7 +156,12 @@
|
|||||||
<MalioInputTextArea
|
<MalioInputTextArea
|
||||||
v-model="form.description"
|
v-model="form.description"
|
||||||
label="Description"
|
label="Description"
|
||||||
:size="3"
|
:size="5"
|
||||||
|
resize="vertical"
|
||||||
|
:min-resize-height="140"
|
||||||
|
:max-resize-height="500"
|
||||||
|
min-resize-width="100%"
|
||||||
|
max-resize-width="100%"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -29,15 +29,21 @@
|
|||||||
|
|
||||||
<!-- Bottom: tags left, duration right -->
|
<!-- Bottom: tags left, duration right -->
|
||||||
<div v-if="sizeLevel >= 3" class="flex items-end justify-between gap-1 min-w-0">
|
<div v-if="sizeLevel >= 3" class="flex items-end justify-between gap-1 min-w-0">
|
||||||
<div v-if="entry.tags.length" class="flex items-center gap-1 overflow-hidden min-w-0">
|
<div v-if="showTags && entry.tags.length" class="flex flex-wrap items-center gap-0.5 overflow-hidden min-w-0">
|
||||||
<span
|
<span
|
||||||
v-for="tag in entry.tags"
|
v-for="tag in visibleTags"
|
||||||
:key="tag.id"
|
:key="tag.id"
|
||||||
class="inline-flex shrink-0 items-center gap-0.5 rounded-full px-1.5 py-0.5 text-[9px] font-bold text-white"
|
class="inline-flex items-center rounded-full px-1.5 py-0.5 text-[9px] font-bold text-white truncate max-w-[5rem]"
|
||||||
:style="{ backgroundColor: tag.color }"
|
:style="{ backgroundColor: tag.color }"
|
||||||
>
|
>
|
||||||
{{ tag.label }}
|
{{ tag.label }}
|
||||||
</span>
|
</span>
|
||||||
|
<span
|
||||||
|
v-if="hiddenTagCount > 0"
|
||||||
|
class="inline-flex items-center rounded-full bg-black/20 px-1 py-0.5 text-[9px] font-bold text-white"
|
||||||
|
>
|
||||||
|
+{{ hiddenTagCount }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="shrink-0 text-[10px] tabular-nums font-bold" style="color: #0A2168">{{ duration }}</span>
|
<span class="shrink-0 text-[10px] tabular-nums font-bold" style="color: #0A2168">{{ duration }}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -111,6 +117,17 @@ const sizeLevel = computed(() => {
|
|||||||
return 0
|
return 0
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const showTags = computed(() => (props.totalColumns ?? 1) <= 2)
|
||||||
|
|
||||||
|
const maxVisibleTags = computed(() => {
|
||||||
|
const total = props.totalColumns ?? 1
|
||||||
|
if (total >= 2) return 1
|
||||||
|
return 2
|
||||||
|
})
|
||||||
|
|
||||||
|
const visibleTags = computed(() => props.entry.tags.slice(0, maxVisibleTags.value))
|
||||||
|
const hiddenTagCount = computed(() => Math.max(0, props.entry.tags.length - maxVisibleTags.value))
|
||||||
|
|
||||||
const hasProject = computed(() => !!props.entry.project)
|
const hasProject = computed(() => !!props.entry.project)
|
||||||
|
|
||||||
const blockStyle = computed(() => {
|
const blockStyle = computed(() => {
|
||||||
|
|||||||
@@ -105,12 +105,22 @@
|
|||||||
>
|
>
|
||||||
Supprimer
|
Supprimer
|
||||||
</button>
|
</button>
|
||||||
<button
|
<div class="flex gap-2">
|
||||||
type="submit"
|
<button
|
||||||
class="rounded-md bg-primary-500 px-4 py-2 text-sm font-semibold text-white hover:bg-primary-600 transition"
|
v-if="isEditing"
|
||||||
>
|
type="button"
|
||||||
Enregistrer
|
class="rounded-md bg-blue-500 px-4 py-2 text-sm font-semibold text-white hover:bg-blue-600 transition"
|
||||||
</button>
|
@click="onDuplicate"
|
||||||
|
>
|
||||||
|
Dupliquer
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="rounded-md bg-primary-500 px-4 py-2 text-sm font-semibold text-white hover:bg-primary-600 transition"
|
||||||
|
>
|
||||||
|
Enregistrer
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</AppDrawer>
|
</AppDrawer>
|
||||||
@@ -231,6 +241,26 @@ watch([() => props.modelValue, () => props.entry] as const, ([open, entry]) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
async function onDuplicate() {
|
||||||
|
if (!form.date || !form.startTime || !form.endTime) return
|
||||||
|
|
||||||
|
const { create } = useTimeEntryService()
|
||||||
|
|
||||||
|
const payload: Record<string, unknown> = {
|
||||||
|
title: form.title || null,
|
||||||
|
description: form.description || null,
|
||||||
|
startedAt: toISO(form.date, form.startTime),
|
||||||
|
stoppedAt: form.endTime ? toISO(form.date, form.endTime) : null,
|
||||||
|
user: `/api/users/${form.userId}`,
|
||||||
|
project: form.projectId ? `/api/projects/${form.projectId}` : null,
|
||||||
|
tags: form.tagIds.map(id => `/api/task_tags/${id}`),
|
||||||
|
}
|
||||||
|
|
||||||
|
await create(payload as TimeEntryWrite)
|
||||||
|
emit('saved')
|
||||||
|
isOpen.value = false
|
||||||
|
}
|
||||||
|
|
||||||
async function onDelete() {
|
async function onDelete() {
|
||||||
if (!props.entry) return
|
if (!props.entry) return
|
||||||
const { remove } = useTimeEntryService()
|
const { remove } = useTimeEntryService()
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
<div
|
<div
|
||||||
v-for="entry in sortedEntries"
|
v-for="entry in sortedEntries"
|
||||||
:key="entry.id"
|
:key="entry.id"
|
||||||
class="group flex items-center gap-4 rounded-lg border border-neutral-200 bg-white px-4 py-3 cursor-pointer transition hover:border-neutral-300 hover:shadow-sm"
|
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)"
|
@click="emit('editEntry', entry)"
|
||||||
>
|
>
|
||||||
<!-- Color bar -->
|
<!-- Color bar -->
|
||||||
@@ -18,14 +18,14 @@
|
|||||||
|
|
||||||
<!-- Main info -->
|
<!-- Main info -->
|
||||||
<div class="min-w-0 flex-1">
|
<div class="min-w-0 flex-1">
|
||||||
<div class="flex items-center gap-2">
|
<div class="truncate text-sm font-semibold text-neutral-900">
|
||||||
<span class="truncate text-sm font-semibold text-neutral-900">
|
{{ entry.title || $t('common.untitled') }}
|
||||||
{{ entry.title || $t('common.untitled') }}
|
</div>
|
||||||
</span>
|
<div v-if="entry.tags.length" class="mt-1 flex flex-wrap gap-1">
|
||||||
<span
|
<span
|
||||||
v-for="tag in entry.tags"
|
v-for="tag in entry.tags"
|
||||||
:key="tag.id"
|
:key="tag.id"
|
||||||
class="shrink-0 rounded-full px-2 py-0.5 text-[10px] font-semibold text-white"
|
class="rounded-full px-2 py-0.5 text-[10px] font-semibold text-white"
|
||||||
:style="{ backgroundColor: tag.color }"
|
:style="{ backgroundColor: tag.color }"
|
||||||
>
|
>
|
||||||
{{ tag.label }}
|
{{ tag.label }}
|
||||||
|
|||||||
@@ -201,14 +201,11 @@ function getScrollParent(): HTMLElement | null {
|
|||||||
// Scroll to current hour on mount
|
// Scroll to current hour on mount
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
if (!calendarEl.value) return
|
if (!gridBodyEl.value) return
|
||||||
const scrollParent = getScrollParent()
|
|
||||||
if (!scrollParent) return
|
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
const currentMinutes = now.getHours() * 60 + now.getMinutes()
|
const currentMinutes = now.getHours() * 60 + now.getMinutes()
|
||||||
const calendarTop = calendarEl.value.offsetTop
|
const scrollTarget = (currentMinutes / 60) * hourHeight - gridBodyEl.value.clientHeight / 3
|
||||||
const scrollTarget = calendarTop + (currentMinutes / 60) * hourHeight - scrollParent.clientHeight / 3
|
gridBodyEl.value.scrollTop = Math.max(0, scrollTarget)
|
||||||
scrollParent.scrollTop = Math.max(0, scrollTarget)
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,17 @@
|
|||||||
>
|
>
|
||||||
<Icon name="mdi:menu" size="24" />
|
<Icon name="mdi:menu" size="24" />
|
||||||
</button>
|
</button>
|
||||||
|
<div class="hidden items-center gap-2 lg:flex">
|
||||||
|
<h1 class="text-lg font-bold tracking-tight">{{ appTitle }}</h1>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="rounded-md p-1 text-white/60 transition-colors hover:bg-primary-600 hover:text-white"
|
||||||
|
:title="appTitle === 'NeauTime' ? 'Switch to Lesstime' : 'Switch to NeauTime'"
|
||||||
|
@click="toggleTitle"
|
||||||
|
>
|
||||||
|
<Icon name="mdi:swap-horizontal" size="18" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<div class="ml-auto flex items-center gap-4 text-xl text-white sm:gap-8">
|
<div class="ml-auto flex items-center gap-4 text-xl text-white sm:gap-8">
|
||||||
<NotificationBell />
|
<NotificationBell />
|
||||||
<div class="group relative flex gap-2 sm:gap-4">
|
<div class="group relative flex gap-2 sm:gap-4">
|
||||||
@@ -45,6 +56,13 @@ defineProps<{
|
|||||||
const auth = useAuthStore()
|
const auth = useAuthStore()
|
||||||
const ui = useUiStore()
|
const ui = useUiStore()
|
||||||
|
|
||||||
|
const appTitle = ref(localStorage.getItem('appTitle') || 'NeauTime')
|
||||||
|
|
||||||
|
function toggleTitle() {
|
||||||
|
appTitle.value = appTitle.value === 'NeauTime' ? 'Lesstime' : 'NeauTime'
|
||||||
|
localStorage.setItem('appTitle', appTitle.value)
|
||||||
|
}
|
||||||
|
|
||||||
async function handleLogout() {
|
async function handleLogout() {
|
||||||
await auth.logout()
|
await auth.logout()
|
||||||
await navigateTo('/login')
|
await navigateTo('/login')
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="date-filter">
|
<div class="date-filter inline-flex h-8 items-center [&>.dp__main]:!inline-flex [&>.dp__main]:!items-center">
|
||||||
<VueDatePicker
|
<VueDatePicker
|
||||||
ref="datepicker"
|
ref="datepicker"
|
||||||
v-model="internalValue"
|
v-model="internalValue"
|
||||||
@@ -14,29 +14,11 @@
|
|||||||
@update:model-value="onUpdate"
|
@update:model-value="onUpdate"
|
||||||
>
|
>
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
<div class="flex items-center gap-1">
|
<button
|
||||||
<div class="relative cursor-pointer">
|
class="relative flex h-8 w-8 items-center justify-center rounded-full text-orange-500 transition hover:bg-orange-50"
|
||||||
<input
|
>
|
||||||
:value="displayValue"
|
<Icon name="mdi:calendar-blank" size="20" />
|
||||||
class="w-full cursor-pointer rounded-md border border-neutral-300 bg-white px-3 py-[7px] pr-8 text-sm text-neutral-700 outline-none transition placeholder:text-neutral-400 focus:border-primary-500"
|
</button>
|
||||||
:placeholder="t('common.dateFilter')"
|
|
||||||
readonly
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
v-if="internalValue"
|
|
||||||
class="absolute right-2 top-1/2 -translate-y-1/2 text-neutral-400 hover:text-neutral-600"
|
|
||||||
@click.stop="onClear"
|
|
||||||
>
|
|
||||||
<Icon name="mdi:close-circle" size="16" />
|
|
||||||
</button>
|
|
||||||
<Icon
|
|
||||||
v-else
|
|
||||||
name="mdi:calendar"
|
|
||||||
size="16"
|
|
||||||
class="pointer-events-none absolute right-2 top-1/2 -translate-y-1/2 text-neutral-400"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template #action-buttons>
|
<template #action-buttons>
|
||||||
|
|||||||
@@ -436,10 +436,16 @@ onMounted(() => {
|
|||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
v-if="task.project && task.number"
|
v-if="task.project && task.number"
|
||||||
class="text-sm font-medium text-primary-500"
|
class="text-sm font-semibold"
|
||||||
|
:style="{ color: task.project.color }"
|
||||||
>
|
>
|
||||||
{{ task.project.code }}-{{ task.number }}
|
{{ task.project.code }}-{{ task.number }}
|
||||||
</span>
|
</span>
|
||||||
|
<Icon
|
||||||
|
v-if="task.priority?.label === 'Haute'"
|
||||||
|
name="mdi:flag-variant"
|
||||||
|
class="h-4 w-4 text-red-600"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -41,6 +41,11 @@
|
|||||||
v-model="form.description"
|
v-model="form.description"
|
||||||
:label="$t('clientTicket.description')"
|
:label="$t('clientTicket.description')"
|
||||||
:size="5"
|
:size="5"
|
||||||
|
resize="vertical"
|
||||||
|
:min-resize-height="140"
|
||||||
|
:max-resize-height="500"
|
||||||
|
min-resize-width="100%"
|
||||||
|
max-resize-width="100%"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -13,34 +13,33 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="relative z-30 mt-4 flex flex-wrap items-center gap-3 sm:gap-4">
|
<div class="relative z-30 mt-4 flex flex-wrap items-center gap-3 sm:gap-4">
|
||||||
<h2 class="shrink-0 whitespace-nowrap text-lg font-bold text-orange-500">
|
<div class="flex shrink-0 items-center gap-1 h-8">
|
||||||
{{ currentMonthLabel }}
|
<button class="flex h-8 w-8 items-center justify-center rounded-full text-neutral-400 hover:text-neutral-700 transition" @click="navigatePrev">
|
||||||
</h2>
|
|
||||||
|
|
||||||
<div class="flex shrink-0 items-center gap-3">
|
|
||||||
<button class="rounded-full p-1 text-neutral-400 hover:text-neutral-700 transition" @click="navigatePrev">
|
|
||||||
<Icon name="mdi:chevron-left" size="20" />
|
<Icon name="mdi:chevron-left" size="20" />
|
||||||
</button>
|
</button>
|
||||||
|
<DateFilter v-model="selectedDateFilter" :picker-mode="viewMode === 'day' ? 'day' : 'week'" />
|
||||||
<div class="flex items-center rounded-full bg-neutral-100 p-1">
|
<h2 class="shrink-0 whitespace-nowrap text-lg font-bold leading-8 text-orange-500">
|
||||||
<button
|
{{ currentMonthLabel }}
|
||||||
v-for="mode in (['week', 'day', 'list'] as const)"
|
</h2>
|
||||||
:key="mode"
|
<button class="flex h-8 w-8 items-center justify-center rounded-full text-neutral-400 hover:text-neutral-700 transition" @click="navigateNext">
|
||||||
class="rounded-full px-4 py-1.5 text-sm font-semibold transition-all"
|
|
||||||
:class="viewMode === mode
|
|
||||||
? 'bg-primary-500 text-white shadow-sm'
|
|
||||||
: 'text-neutral-500 hover:text-neutral-700'"
|
|
||||||
@click="viewMode = mode"
|
|
||||||
>
|
|
||||||
{{ mode === 'week' ? 'Semaine' : mode === 'day' ? 'Jour' : 'Liste' }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button class="rounded-full p-1 text-neutral-400 hover:text-neutral-700 transition" @click="navigateNext">
|
|
||||||
<Icon name="mdi:chevron-right" size="20" />
|
<Icon name="mdi:chevron-right" size="20" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center rounded-full bg-neutral-100 p-1">
|
||||||
|
<button
|
||||||
|
v-for="mode in (['week', 'day', 'list'] as const)"
|
||||||
|
:key="mode"
|
||||||
|
class="rounded-full px-4 py-1.5 text-sm font-semibold transition-all"
|
||||||
|
:class="viewMode === mode
|
||||||
|
? 'bg-primary-500 text-white shadow-sm'
|
||||||
|
: 'text-neutral-500 hover:text-neutral-700'"
|
||||||
|
@click="viewMode = mode"
|
||||||
|
>
|
||||||
|
{{ mode === 'week' ? 'Semaine' : mode === 'day' ? 'Jour' : 'Liste' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="[&>div]:!mt-0">
|
<div class="[&>div]:!mt-0">
|
||||||
<MalioSelect
|
<MalioSelect
|
||||||
v-model="selectedUserId"
|
v-model="selectedUserId"
|
||||||
@@ -76,8 +75,6 @@
|
|||||||
text-value="text-sm"
|
text-value="text-sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DateFilter v-model="selectedDateFilter" :picker-mode="viewMode === 'day' ? 'day' : 'week'" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user