refactor : rename TaskType to TaskTag across the stack

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Matthieu
2026-03-13 08:20:21 +01:00
parent dbae1f7536
commit 56275a9ebe
16 changed files with 216 additions and 117 deletions

View File

@@ -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
} }

View File

@@ -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"

View File

@@ -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,
} }

View File

@@ -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>

View File

@@ -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) {

View File

@@ -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">

View File

@@ -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

View File

@@ -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 () => {

View File

@@ -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
} }

View File

@@ -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[]
} }

View 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 }
}

View File

@@ -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 }
}

View File

@@ -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
View 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;
}
}

View File

@@ -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;
} }

View 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);
}
}