refactor : rename TaskType to TaskTag across the stack
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,12 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<h2 class="text-lg font-bold text-neutral-900">Types</h2>
|
<h2 class="text-lg font-bold text-neutral-900">Tags</h2>
|
||||||
<button
|
<button
|
||||||
class="rounded-md bg-primary-500 px-4 py-2 text-sm font-semibold text-white hover:bg-secondary-500"
|
class="rounded-md bg-primary-500 px-4 py-2 text-sm font-semibold text-white hover:bg-secondary-500"
|
||||||
@click="openCreate"
|
@click="openCreate"
|
||||||
>
|
>
|
||||||
+ Ajouter un type
|
+ Ajouter un tag
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
:columns="columns"
|
:columns="columns"
|
||||||
:items="items"
|
:items="items"
|
||||||
:loading="isLoading"
|
:loading="isLoading"
|
||||||
empty-message="Aucun type trouvé."
|
empty-message="Aucun tag trouvé."
|
||||||
deletable
|
deletable
|
||||||
@row-click="openEdit"
|
@row-click="openEdit"
|
||||||
@delete="(item) => handleDelete(item.id)"
|
@delete="(item) => handleDelete(item.id)"
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</DataTable>
|
</DataTable>
|
||||||
|
|
||||||
<TaskTypeDrawer
|
<TaskTagDrawer
|
||||||
v-model="drawerOpen"
|
v-model="drawerOpen"
|
||||||
:item="selectedItem"
|
:item="selectedItem"
|
||||||
@saved="onSaved"
|
@saved="onSaved"
|
||||||
@@ -36,8 +36,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { TaskType } from '~/services/dto/task-type'
|
import type { TaskTag } from '~/services/dto/task-tag'
|
||||||
import { useTaskTypeService } from '~/services/task-types'
|
import { useTaskTagService } from '~/services/task-tags'
|
||||||
|
|
||||||
import type { DataTableColumn } from '~/components/ui/DataTable.vue'
|
import type { DataTableColumn } from '~/components/ui/DataTable.vue'
|
||||||
|
|
||||||
@@ -46,11 +46,11 @@ const columns: DataTableColumn[] = [
|
|||||||
{ key: 'color', label: 'Couleur' },
|
{ key: 'color', label: 'Couleur' },
|
||||||
]
|
]
|
||||||
|
|
||||||
const { getAll, remove } = useTaskTypeService()
|
const { getAll, remove } = useTaskTagService()
|
||||||
const items = ref<TaskType[]>([])
|
const items = ref<TaskTag[]>([])
|
||||||
const isLoading = ref(true)
|
const isLoading = ref(true)
|
||||||
const drawerOpen = ref(false)
|
const drawerOpen = ref(false)
|
||||||
const selectedItem = ref<TaskType | null>(null)
|
const selectedItem = ref<TaskTag | null>(null)
|
||||||
|
|
||||||
async function loadItems() {
|
async function loadItems() {
|
||||||
isLoading.value = true
|
isLoading.value = true
|
||||||
@@ -66,7 +66,7 @@ function openCreate() {
|
|||||||
drawerOpen.value = true
|
drawerOpen.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
function openEdit(item: TaskType) {
|
function openEdit(item: TaskTag) {
|
||||||
selectedItem.value = item
|
selectedItem.value = item
|
||||||
drawerOpen.value = true
|
drawerOpen.value = true
|
||||||
}
|
}
|
||||||
@@ -7,7 +7,10 @@
|
|||||||
@click="emit('click')"
|
@click="emit('click')"
|
||||||
>
|
>
|
||||||
<div class="flex items-start justify-between gap-2">
|
<div class="flex items-start justify-between gap-2">
|
||||||
<h4 class="text-sm font-semibold text-neutral-900">{{ task.title }}</h4>
|
<div class="min-w-0">
|
||||||
|
<span v-if="task.project && task.number" class="text-xs font-medium text-neutral-400">{{ task.project.code }}{{ task.number }}</span>
|
||||||
|
<h4 class="text-sm font-semibold text-neutral-900">{{ task.title }}</h4>
|
||||||
|
</div>
|
||||||
<button
|
<button
|
||||||
class="shrink-0 transition-colors"
|
class="shrink-0 transition-colors"
|
||||||
:class="isTimerOnTask ? 'text-[#F18619] hover:text-[#d97314]' : 'text-neutral-400 hover:text-primary-500'"
|
:class="isTimerOnTask ? 'text-[#F18619] hover:text-[#d97314]' : 'text-neutral-400 hover:text-primary-500'"
|
||||||
@@ -26,12 +29,12 @@
|
|||||||
{{ task.priority.label }}
|
{{ task.priority.label }}
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
v-for="type in task.types"
|
v-for="tag in task.tags"
|
||||||
:key="type.id"
|
:key="tag.id"
|
||||||
class="rounded-full px-2 py-0.5 text-xs font-semibold text-white"
|
class="rounded-full px-2 py-0.5 text-xs font-semibold text-white"
|
||||||
:style="{ backgroundColor: type.color }"
|
:style="{ backgroundColor: tag.color }"
|
||||||
>
|
>
|
||||||
{{ type.label }}
|
{{ tag.label }}
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
v-if="task.assignee"
|
v-if="task.assignee"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<AppDrawer v-model="isOpen" :title="isEditing ? 'Modifier un type' : 'Ajouter un type'">
|
<AppDrawer v-model="isOpen" :title="isEditing ? 'Modifier un tag' : 'Ajouter un tag'">
|
||||||
<form @submit.prevent="handleSubmit" class="flex flex-col gap-2">
|
<form @submit.prevent="handleSubmit" class="flex flex-col gap-2">
|
||||||
<MalioInputText
|
<MalioInputText
|
||||||
v-model="form.label"
|
v-model="form.label"
|
||||||
@@ -26,12 +26,12 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { TaskType, TaskTypeWrite } from '~/services/dto/task-type'
|
import type { TaskTag, TaskTagWrite } from '~/services/dto/task-tag'
|
||||||
import { useTaskTypeService } from '~/services/task-types'
|
import { useTaskTagService } from '~/services/task-tags'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
modelValue: boolean
|
modelValue: boolean
|
||||||
item: TaskType | null
|
item: TaskTag | null
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@@ -69,7 +69,7 @@ watch(() => props.modelValue, (open) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const { create, update } = useTaskTypeService()
|
const { create, update } = useTaskTagService()
|
||||||
|
|
||||||
async function handleSubmit() {
|
async function handleSubmit() {
|
||||||
touched.label = true
|
touched.label = true
|
||||||
@@ -77,7 +77,7 @@ async function handleSubmit() {
|
|||||||
|
|
||||||
isSubmitting.value = true
|
isSubmitting.value = true
|
||||||
try {
|
try {
|
||||||
const payload: TaskTypeWrite = {
|
const payload: TaskTagWrite = {
|
||||||
label: form.label.trim(),
|
label: form.label.trim(),
|
||||||
color: form.color,
|
color: form.color,
|
||||||
}
|
}
|
||||||
@@ -25,14 +25,14 @@
|
|||||||
<span class="ml-auto shrink-0 text-[10px] tabular-nums opacity-80">{{ duration }}</span>
|
<span class="ml-auto shrink-0 text-[10px] tabular-nums opacity-80">{{ duration }}</span>
|
||||||
</div>
|
</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 v-if="entry.types.length" class="mt-0.5 flex items-center gap-1 overflow-hidden">
|
<div v-if="entry.tags.length" class="mt-0.5 flex items-center gap-1 overflow-hidden">
|
||||||
<span
|
<span
|
||||||
v-for="type in entry.types"
|
v-for="tag in entry.tags"
|
||||||
:key="type.id"
|
:key="tag.id"
|
||||||
class="inline-flex items-center gap-0.5 truncate text-[9px] opacity-90"
|
class="inline-flex items-center gap-0.5 truncate text-[9px] opacity-90"
|
||||||
>
|
>
|
||||||
<span class="inline-block h-1.5 w-1.5 shrink-0 rounded-full" :style="{ backgroundColor: type.color }" />
|
<span class="inline-block h-1.5 w-1.5 shrink-0 rounded-full" :style="{ backgroundColor: tag.color }" />
|
||||||
{{ type.label }}
|
{{ tag.label }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -73,25 +73,25 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<p class="mb-2 text-sm font-semibold text-neutral-700">Types</p>
|
<p class="mb-2 text-sm font-semibold text-neutral-700">Tags</p>
|
||||||
<div class="flex flex-wrap gap-2">
|
<div class="flex flex-wrap gap-2">
|
||||||
<label
|
<label
|
||||||
v-for="type in types"
|
v-for="tag in tags"
|
||||||
:key="type.id"
|
:key="tag.id"
|
||||||
class="cursor-pointer rounded-full px-3 py-1 text-xs font-semibold transition"
|
class="cursor-pointer rounded-full px-3 py-1 text-xs font-semibold transition"
|
||||||
:class="form.typeIds.includes(type.id)
|
:class="form.tagIds.includes(tag.id)
|
||||||
? 'text-white'
|
? 'text-white'
|
||||||
: 'bg-neutral-100 text-neutral-600 hover:bg-neutral-200'"
|
: 'bg-neutral-100 text-neutral-600 hover:bg-neutral-200'"
|
||||||
:style="form.typeIds.includes(type.id) ? { backgroundColor: type.color } : {}"
|
:style="form.tagIds.includes(tag.id) ? { backgroundColor: tag.color } : {}"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
class="hidden"
|
class="hidden"
|
||||||
:value="type.id"
|
:value="tag.id"
|
||||||
:checked="form.typeIds.includes(type.id)"
|
:checked="form.tagIds.includes(tag.id)"
|
||||||
@change="toggleType(type.id)"
|
@change="toggleTag(tag.id)"
|
||||||
/>
|
/>
|
||||||
{{ type.label }}
|
{{ tag.label }}
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -120,7 +120,7 @@
|
|||||||
import type { TimeEntry } from '~/services/dto/time-entry'
|
import type { TimeEntry } from '~/services/dto/time-entry'
|
||||||
import type { UserData } from '~/services/dto/user-data'
|
import type { UserData } from '~/services/dto/user-data'
|
||||||
import type { Project } from '~/services/dto/project'
|
import type { Project } from '~/services/dto/project'
|
||||||
import type { TaskType } from '~/services/dto/task-type'
|
import type { TaskTag } from '~/services/dto/task-tag'
|
||||||
import { useTimeEntryService } from '~/services/time-entries'
|
import { useTimeEntryService } from '~/services/time-entries'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
@@ -129,7 +129,7 @@ const props = defineProps<{
|
|||||||
prefillStartedAt?: string | null
|
prefillStartedAt?: string | null
|
||||||
users: UserData[]
|
users: UserData[]
|
||||||
projects: Project[]
|
projects: Project[]
|
||||||
types: TaskType[]
|
tags: TaskTag[]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@@ -154,7 +154,7 @@ const form = reactive({
|
|||||||
endTime: '',
|
endTime: '',
|
||||||
userId: authStore.user?.id ?? null as number | null,
|
userId: authStore.user?.id ?? null as number | null,
|
||||||
projectId: null as number | null,
|
projectId: null as number | null,
|
||||||
typeIds: [] as number[],
|
tagIds: [] as number[],
|
||||||
})
|
})
|
||||||
|
|
||||||
const userOptions = computed(() =>
|
const userOptions = computed(() =>
|
||||||
@@ -176,12 +176,12 @@ const durationLabel = computed(() => {
|
|||||||
return m > 0 ? `${h}h${String(m).padStart(2, '0')}` : `${h}h`
|
return m > 0 ? `${h}h${String(m).padStart(2, '0')}` : `${h}h`
|
||||||
})
|
})
|
||||||
|
|
||||||
function toggleType(id: number) {
|
function toggleTag(id: number) {
|
||||||
const idx = form.typeIds.indexOf(id)
|
const idx = form.tagIds.indexOf(id)
|
||||||
if (idx >= 0) {
|
if (idx >= 0) {
|
||||||
form.typeIds.splice(idx, 1)
|
form.tagIds.splice(idx, 1)
|
||||||
} else {
|
} else {
|
||||||
form.typeIds.push(id)
|
form.tagIds.push(id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,7 +212,7 @@ function populateForm(entry: TimeEntry | null | undefined) {
|
|||||||
form.endTime = entry.stoppedAt ? toLocalTime(entry.stoppedAt) : ''
|
form.endTime = entry.stoppedAt ? toLocalTime(entry.stoppedAt) : ''
|
||||||
form.userId = entry.user?.id ?? authStore.user?.id ?? null
|
form.userId = entry.user?.id ?? authStore.user?.id ?? null
|
||||||
form.projectId = entry.project?.id ?? null
|
form.projectId = entry.project?.id ?? null
|
||||||
form.typeIds = entry.types?.map(t => t.id) ?? []
|
form.tagIds = entry.tags?.map(t => t.id) ?? []
|
||||||
} else {
|
} else {
|
||||||
form.title = ''
|
form.title = ''
|
||||||
form.description = ''
|
form.description = ''
|
||||||
@@ -221,7 +221,7 @@ function populateForm(entry: TimeEntry | null | undefined) {
|
|||||||
form.endTime = ''
|
form.endTime = ''
|
||||||
form.userId = authStore.user?.id ?? null
|
form.userId = authStore.user?.id ?? null
|
||||||
form.projectId = null
|
form.projectId = null
|
||||||
form.typeIds = []
|
form.tagIds = []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -251,7 +251,7 @@ async function onSubmit() {
|
|||||||
stoppedAt: form.endTime ? toISO(form.date, form.endTime) : null,
|
stoppedAt: form.endTime ? toISO(form.date, form.endTime) : null,
|
||||||
user: `/api/users/${form.userId}`,
|
user: `/api/users/${form.userId}`,
|
||||||
project: form.projectId ? `/api/projects/${form.projectId}` : null,
|
project: form.projectId ? `/api/projects/${form.projectId}` : null,
|
||||||
types: form.typeIds.map(id => `/api/task_types/${id}`),
|
tags: form.tagIds.map(id => `/api/task_tags/${id}`),
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isEditing.value && props.entry) {
|
if (isEditing.value && props.entry) {
|
||||||
|
|||||||
@@ -23,12 +23,12 @@
|
|||||||
{{ entry.title || 'Sans titre' }}
|
{{ entry.title || 'Sans titre' }}
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
v-for="type in entry.types"
|
v-for="tag in entry.tags"
|
||||||
:key="type.id"
|
:key="tag.id"
|
||||||
class="shrink-0 rounded-full px-2 py-0.5 text-[10px] font-semibold text-white"
|
class="shrink-0 rounded-full px-2 py-0.5 text-[10px] font-semibold text-white"
|
||||||
:style="{ backgroundColor: type.color }"
|
:style="{ backgroundColor: tag.color }"
|
||||||
>
|
>
|
||||||
{{ type.label }}
|
{{ tag.label }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-0.5 flex items-center gap-2 text-xs text-neutral-500">
|
<div class="mt-0.5 flex items-center gap-2 text-xs text-neutral-500">
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
<AdminStatusTab v-if="activeTab === 'statuses'" />
|
<AdminStatusTab v-if="activeTab === 'statuses'" />
|
||||||
<AdminEffortTab v-if="activeTab === 'efforts'" />
|
<AdminEffortTab v-if="activeTab === 'efforts'" />
|
||||||
<AdminPriorityTab v-if="activeTab === 'priorities'" />
|
<AdminPriorityTab v-if="activeTab === 'priorities'" />
|
||||||
<AdminTypeTab v-if="activeTab === 'types'" />
|
<AdminTagTab v-if="activeTab === 'tags'" />
|
||||||
<AdminUserTab v-if="activeTab === 'users'" />
|
<AdminUserTab v-if="activeTab === 'users'" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -37,7 +37,7 @@ const tabs = [
|
|||||||
{ key: 'statuses', label: 'Statuts' },
|
{ key: 'statuses', label: 'Statuts' },
|
||||||
{ key: 'efforts', label: 'Efforts' },
|
{ key: 'efforts', label: 'Efforts' },
|
||||||
{ key: 'priorities', label: 'Priorités' },
|
{ key: 'priorities', label: 'Priorités' },
|
||||||
{ key: 'types', label: 'Types' },
|
{ key: 'tags', label: 'Tags' },
|
||||||
{ key: 'users', label: 'Utilisateurs' },
|
{ key: 'users', label: 'Utilisateurs' },
|
||||||
] as const
|
] as const
|
||||||
|
|
||||||
|
|||||||
@@ -55,10 +55,10 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<MalioSelect
|
<MalioSelect
|
||||||
v-model="selectedTypeId"
|
v-model="selectedTagId"
|
||||||
:options="typeOptions"
|
:options="tagOptions"
|
||||||
empty-option-label="Tous"
|
empty-option-label="Tous"
|
||||||
label="Type"
|
label="Tag"
|
||||||
min-width="!w-40"
|
min-width="!w-40"
|
||||||
text-field="text-sm"
|
text-field="text-sm"
|
||||||
text-value="text-sm"
|
text-value="text-sm"
|
||||||
@@ -93,7 +93,7 @@
|
|||||||
:prefill-started-at="prefillStartedAt"
|
:prefill-started-at="prefillStartedAt"
|
||||||
:users="users"
|
:users="users"
|
||||||
:projects="projects"
|
:projects="projects"
|
||||||
:types="types"
|
:tags="tags"
|
||||||
@saved="loadEntries"
|
@saved="loadEntries"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -115,7 +115,7 @@
|
|||||||
import type { TimeEntry } from '~/services/dto/time-entry'
|
import type { TimeEntry } from '~/services/dto/time-entry'
|
||||||
import type { UserData } from '~/services/dto/user-data'
|
import type { UserData } from '~/services/dto/user-data'
|
||||||
import type { Project } from '~/services/dto/project'
|
import type { Project } from '~/services/dto/project'
|
||||||
import type { TaskType } from '~/services/dto/task-type'
|
import type { TaskTag } from '~/services/dto/task-tag'
|
||||||
import { useTimeEntryService } from '~/services/time-entries'
|
import { useTimeEntryService } from '~/services/time-entries'
|
||||||
import { extractHydraMembers } from '~/utils/api'
|
import { extractHydraMembers } from '~/utils/api'
|
||||||
|
|
||||||
@@ -127,13 +127,13 @@ const timeEntryService = useTimeEntryService()
|
|||||||
const viewMode = ref<'week' | 'day' | 'list'>('week')
|
const viewMode = ref<'week' | 'day' | 'list'>('week')
|
||||||
const startDate = ref(getMonday(new Date()))
|
const startDate = ref(getMonday(new Date()))
|
||||||
const selectedUserId = ref<number | null>(authStore.user?.id ?? null)
|
const selectedUserId = ref<number | null>(authStore.user?.id ?? null)
|
||||||
const selectedTypeId = ref<number | null>(null)
|
const selectedTagId = ref<number | null>(null)
|
||||||
const selectedProjectId = ref<number | null>(null)
|
const selectedProjectId = ref<number | null>(null)
|
||||||
|
|
||||||
const entries = ref<TimeEntry[]>([])
|
const entries = ref<TimeEntry[]>([])
|
||||||
const users = ref<UserData[]>([])
|
const users = ref<UserData[]>([])
|
||||||
const projects = ref<Project[]>([])
|
const projects = ref<Project[]>([])
|
||||||
const types = ref<TaskType[]>([])
|
const tags = ref<TaskTag[]>([])
|
||||||
|
|
||||||
const drawerOpen = ref(false)
|
const drawerOpen = ref(false)
|
||||||
const editingEntry = ref<TimeEntry | null>(null)
|
const editingEntry = ref<TimeEntry | null>(null)
|
||||||
@@ -164,8 +164,8 @@ const projectOptions = computed(() =>
|
|||||||
projects.value.map(p => ({ label: p.name, value: p.id }))
|
projects.value.map(p => ({ label: p.name, value: p.id }))
|
||||||
)
|
)
|
||||||
|
|
||||||
const typeOptions = computed(() =>
|
const tagOptions = computed(() =>
|
||||||
types.value.map(t => ({ label: t.label, value: t.id }))
|
tags.value.map(t => ({ label: t.label, value: t.id }))
|
||||||
)
|
)
|
||||||
|
|
||||||
let pageHeaderResizeObserver: ResizeObserver | null = null
|
let pageHeaderResizeObserver: ResizeObserver | null = null
|
||||||
@@ -179,8 +179,8 @@ const filteredEntries = computed(() => {
|
|||||||
if (selectedProjectId.value) {
|
if (selectedProjectId.value) {
|
||||||
result = result.filter((e) => e.project?.id === selectedProjectId.value)
|
result = result.filter((e) => e.project?.id === selectedProjectId.value)
|
||||||
}
|
}
|
||||||
if (selectedTypeId.value) {
|
if (selectedTagId.value) {
|
||||||
result = result.filter((e) => e.types.some((t) => t.id === selectedTypeId.value))
|
result = result.filter((e) => e.tags.some((t) => t.id === selectedTagId.value))
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
})
|
})
|
||||||
@@ -269,7 +269,7 @@ async function onPaste() {
|
|||||||
stoppedAt: clipboard.value.stoppedAt ?? undefined,
|
stoppedAt: clipboard.value.stoppedAt ?? undefined,
|
||||||
user: `/api/users/${selectedUserId.value}`,
|
user: `/api/users/${selectedUserId.value}`,
|
||||||
project: clipboard.value.project ? `/api/projects/${clipboard.value.project.id}` : null,
|
project: clipboard.value.project ? `/api/projects/${clipboard.value.project.id}` : null,
|
||||||
types: clipboard.value.types.map((t) => `/api/task_types/${t.id}`),
|
tags: clipboard.value.tags.map((t) => `/api/task_tags/${t.id}`),
|
||||||
})
|
})
|
||||||
await loadEntries()
|
await loadEntries()
|
||||||
}
|
}
|
||||||
@@ -314,12 +314,12 @@ async function loadReferenceData() {
|
|||||||
const [usersData, projectsData, typesData] = await Promise.all([
|
const [usersData, projectsData, typesData] = await Promise.all([
|
||||||
api.get<any>('/users'),
|
api.get<any>('/users'),
|
||||||
api.get<any>('/projects'),
|
api.get<any>('/projects'),
|
||||||
api.get<any>('/task_types'),
|
api.get<any>('/task_tags'),
|
||||||
])
|
])
|
||||||
|
|
||||||
users.value = extractHydraMembers(usersData)
|
users.value = extractHydraMembers(usersData)
|
||||||
projects.value = extractHydraMembers(projectsData)
|
projects.value = extractHydraMembers(projectsData)
|
||||||
types.value = extractHydraMembers(typesData)
|
tags.value = extractHydraMembers(typesData)
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
export type TaskType = {
|
export type TaskTag = {
|
||||||
id: number
|
id: number
|
||||||
'@id'?: string
|
'@id'?: string
|
||||||
label: string
|
label: string
|
||||||
color: string
|
color: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TaskTypeWrite = {
|
export type TaskTagWrite = {
|
||||||
label: string
|
label: string
|
||||||
color: string
|
color: string
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { UserData } from './user-data'
|
import type { UserData } from './user-data'
|
||||||
import type { Project } from './project'
|
import type { Project } from './project'
|
||||||
import type { Task } from './task'
|
import type { Task } from './task'
|
||||||
import type { TaskType } from './task-type'
|
import type { TaskTag } from './task-tag'
|
||||||
|
|
||||||
export type TimeEntry = {
|
export type TimeEntry = {
|
||||||
id: number
|
id: number
|
||||||
@@ -13,7 +13,7 @@ export type TimeEntry = {
|
|||||||
user: UserData
|
user: UserData
|
||||||
project: Project | null
|
project: Project | null
|
||||||
task: Task | null
|
task: Task | null
|
||||||
types: TaskType[]
|
tags: TaskTag[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TimeEntryWrite = {
|
export type TimeEntryWrite = {
|
||||||
@@ -24,5 +24,5 @@ export type TimeEntryWrite = {
|
|||||||
user: string
|
user: string
|
||||||
project?: string | null
|
project?: string | null
|
||||||
task?: string | null
|
task?: string | null
|
||||||
types?: string[]
|
tags?: string[]
|
||||||
}
|
}
|
||||||
|
|||||||
32
frontend/services/task-tags.ts
Normal file
32
frontend/services/task-tags.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import type { TaskTag, TaskTagWrite } from './dto/task-tag'
|
||||||
|
import type { HydraCollection } from '~/utils/api'
|
||||||
|
import { extractHydraMembers } from '~/utils/api'
|
||||||
|
|
||||||
|
export function useTaskTagService() {
|
||||||
|
const api = useApi()
|
||||||
|
|
||||||
|
async function getAll(): Promise<TaskTag[]> {
|
||||||
|
const data = await api.get<HydraCollection<TaskTag>>('/task_tags')
|
||||||
|
return extractHydraMembers(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function create(payload: TaskTagWrite): Promise<TaskTag> {
|
||||||
|
return api.post<TaskTag>('/task_tags', payload as Record<string, unknown>, {
|
||||||
|
toastSuccessKey: 'taskTags.created',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function update(id: number, payload: Partial<TaskTagWrite>): Promise<TaskTag> {
|
||||||
|
return api.patch<TaskTag>(`/task_tags/${id}`, payload as Record<string, unknown>, {
|
||||||
|
toastSuccessKey: 'taskTags.updated',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function remove(id: number): Promise<void> {
|
||||||
|
await api.delete(`/task_tags/${id}`, {}, {
|
||||||
|
toastSuccessKey: 'taskTags.deleted',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return { getAll, create, update, remove }
|
||||||
|
}
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
import type { TaskType, TaskTypeWrite } from './dto/task-type'
|
|
||||||
import type { HydraCollection } from '~/utils/api'
|
|
||||||
import { extractHydraMembers } from '~/utils/api'
|
|
||||||
|
|
||||||
export function useTaskTypeService() {
|
|
||||||
const api = useApi()
|
|
||||||
|
|
||||||
async function getAll(): Promise<TaskType[]> {
|
|
||||||
const data = await api.get<HydraCollection<TaskType>>('/task_types')
|
|
||||||
return extractHydraMembers(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function create(payload: TaskTypeWrite): Promise<TaskType> {
|
|
||||||
return api.post<TaskType>('/task_types', payload as Record<string, unknown>, {
|
|
||||||
toastSuccessKey: 'taskTypes.created',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async function update(id: number, payload: Partial<TaskTypeWrite>): Promise<TaskType> {
|
|
||||||
return api.patch<TaskType>(`/task_types/${id}`, payload as Record<string, unknown>, {
|
|
||||||
toastSuccessKey: 'taskTypes.updated',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async function remove(id: number): Promise<void> {
|
|
||||||
await api.delete(`/task_types/${id}`, {}, {
|
|
||||||
toastSuccessKey: 'taskTypes.deleted',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return { getAll, create, update, remove }
|
|
||||||
}
|
|
||||||
@@ -83,7 +83,7 @@ export const useTimerStore = defineStore('timer', () => {
|
|||||||
? (typeof task.project === 'string' ? task.project : (task.project['@id'] ?? (task.project.id ? `/api/projects/${task.project.id}` : null)))
|
? (typeof task.project === 'string' ? task.project : (task.project['@id'] ?? (task.project.id ? `/api/projects/${task.project.id}` : null)))
|
||||||
: null,
|
: null,
|
||||||
task: typeof task === 'string' ? task : (task['@id'] ?? `/api/tasks/${task.id}`),
|
task: typeof task === 'string' ? task : (task['@id'] ?? `/api/tasks/${task.id}`),
|
||||||
types: task.types?.map((t) => typeof t === 'string' ? t : (t['@id'] ?? `/api/task_types/${t.id}`)) ?? [],
|
tags: task.tags?.map((t) => typeof t === 'string' ? t : (t['@id'] ?? `/api/task_tags/${t.id}`)) ?? [],
|
||||||
})
|
})
|
||||||
startTicking()
|
startTicking()
|
||||||
}
|
}
|
||||||
|
|||||||
75
src/Entity/TaskTag.php
Normal file
75
src/Entity/TaskTag.php
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Entity;
|
||||||
|
|
||||||
|
use ApiPlatform\Metadata\ApiResource;
|
||||||
|
use ApiPlatform\Metadata\Delete;
|
||||||
|
use ApiPlatform\Metadata\Get;
|
||||||
|
use ApiPlatform\Metadata\GetCollection;
|
||||||
|
use ApiPlatform\Metadata\Patch;
|
||||||
|
use ApiPlatform\Metadata\Post;
|
||||||
|
use App\Repository\TaskTagRepository;
|
||||||
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
use Symfony\Component\Serializer\Attribute\Groups;
|
||||||
|
|
||||||
|
#[ApiResource(
|
||||||
|
operations: [
|
||||||
|
new GetCollection(),
|
||||||
|
new Get(),
|
||||||
|
new Post(security: "is_granted('ROLE_ADMIN')"),
|
||||||
|
new Patch(security: "is_granted('ROLE_ADMIN')"),
|
||||||
|
new Delete(security: "is_granted('ROLE_ADMIN')"),
|
||||||
|
],
|
||||||
|
normalizationContext: ['groups' => ['task_tag:read']],
|
||||||
|
denormalizationContext: ['groups' => ['task_tag:write']],
|
||||||
|
order: ['label' => 'ASC'],
|
||||||
|
)]
|
||||||
|
#[ORM\Entity(repositoryClass: TaskTagRepository::class)]
|
||||||
|
#[ORM\Table(name: 'task_type')]
|
||||||
|
class TaskTag
|
||||||
|
{
|
||||||
|
#[ORM\Id]
|
||||||
|
#[ORM\GeneratedValue]
|
||||||
|
#[ORM\Column]
|
||||||
|
#[Groups(['task_tag:read', 'task:read', 'time_entry:read'])]
|
||||||
|
private ?int $id = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 255)]
|
||||||
|
#[Groups(['task_tag:read', 'task_tag:write', 'task:read', 'time_entry:read'])]
|
||||||
|
private ?string $label = null;
|
||||||
|
|
||||||
|
#[ORM\Column(length: 7)]
|
||||||
|
#[Groups(['task_tag:read', 'task_tag:write', 'task:read', 'time_entry:read'])]
|
||||||
|
private ?string $color = '#222783';
|
||||||
|
|
||||||
|
public function getId(): ?int
|
||||||
|
{
|
||||||
|
return $this->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getLabel(): ?string
|
||||||
|
{
|
||||||
|
return $this->label;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setLabel(string $label): static
|
||||||
|
{
|
||||||
|
$this->label = $label;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getColor(): ?string
|
||||||
|
{
|
||||||
|
return $this->color;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setColor(string $color): static
|
||||||
|
{
|
||||||
|
$this->color = $color;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -41,7 +41,7 @@ use Symfony\Component\Serializer\Attribute\Groups;
|
|||||||
denormalizationContext: ['groups' => ['time_entry:write']],
|
denormalizationContext: ['groups' => ['time_entry:write']],
|
||||||
order: ['startedAt' => 'DESC'],
|
order: ['startedAt' => 'DESC'],
|
||||||
)]
|
)]
|
||||||
#[ApiFilter(SearchFilter::class, properties: ['user' => 'exact', 'project' => 'exact', 'types' => 'exact'])]
|
#[ApiFilter(SearchFilter::class, properties: ['user' => 'exact', 'project' => 'exact', 'tags' => 'exact'])]
|
||||||
#[ApiFilter(DateFilter::class, properties: ['startedAt'])]
|
#[ApiFilter(DateFilter::class, properties: ['startedAt'])]
|
||||||
#[ORM\Entity(repositoryClass: TimeEntryRepository::class)]
|
#[ORM\Entity(repositoryClass: TimeEntryRepository::class)]
|
||||||
#[ORM\UniqueConstraint(name: 'uniq_active_timer', columns: ['user_id'], options: ['where' => '(stopped_at IS NULL)'])]
|
#[ORM\UniqueConstraint(name: 'uniq_active_timer', columns: ['user_id'], options: ['where' => '(stopped_at IS NULL)'])]
|
||||||
@@ -84,15 +84,19 @@ class TimeEntry
|
|||||||
#[Groups(['time_entry:read', 'time_entry:write'])]
|
#[Groups(['time_entry:read', 'time_entry:write'])]
|
||||||
private ?Task $task = null;
|
private ?Task $task = null;
|
||||||
|
|
||||||
/** @var Collection<int, TaskType> */
|
/** @var Collection<int, TaskTag> */
|
||||||
#[ORM\ManyToMany(targetEntity: TaskType::class)]
|
#[ORM\ManyToMany(targetEntity: TaskTag::class)]
|
||||||
#[ORM\JoinTable(name: 'time_entry_task_type')]
|
#[ORM\JoinTable(
|
||||||
|
name: 'time_entry_task_type',
|
||||||
|
joinColumns: [new ORM\JoinColumn(name: 'time_entry_id', referencedColumnName: 'id')],
|
||||||
|
inverseJoinColumns: [new ORM\JoinColumn(name: 'task_type_id', referencedColumnName: 'id')],
|
||||||
|
)]
|
||||||
#[Groups(['time_entry:read', 'time_entry:write'])]
|
#[Groups(['time_entry:read', 'time_entry:write'])]
|
||||||
private Collection $types;
|
private Collection $tags;
|
||||||
|
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
$this->types = new ArrayCollection();
|
$this->tags = new ArrayCollection();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getId(): ?int
|
public function getId(): ?int
|
||||||
@@ -184,24 +188,24 @@ class TimeEntry
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @return Collection<int, TaskType> */
|
/** @return Collection<int, TaskTag> */
|
||||||
public function getTypes(): Collection
|
public function getTags(): Collection
|
||||||
{
|
{
|
||||||
return $this->types;
|
return $this->tags;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function addType(TaskType $type): static
|
public function addTag(TaskTag $tag): static
|
||||||
{
|
{
|
||||||
if (!$this->types->contains($type)) {
|
if (!$this->tags->contains($tag)) {
|
||||||
$this->types->add($type);
|
$this->tags->add($tag);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function removeType(TaskType $type): static
|
public function removeTag(TaskTag $tag): static
|
||||||
{
|
{
|
||||||
$this->types->removeElement($type);
|
$this->tags->removeElement($tag);
|
||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|||||||
17
src/Repository/TaskTagRepository.php
Normal file
17
src/Repository/TaskTagRepository.php
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Repository;
|
||||||
|
|
||||||
|
use App\Entity\TaskTag;
|
||||||
|
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||||
|
use Doctrine\Persistence\ManagerRegistry;
|
||||||
|
|
||||||
|
class TaskTagRepository extends ServiceEntityRepository
|
||||||
|
{
|
||||||
|
public function __construct(ManagerRegistry $registry)
|
||||||
|
{
|
||||||
|
parent::__construct($registry, TaskTag::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user