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>
|
||||
<div>
|
||||
<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
|
||||
class="rounded-md bg-primary-500 px-4 py-2 text-sm font-semibold text-white hover:bg-secondary-500"
|
||||
@click="openCreate"
|
||||
>
|
||||
+ Ajouter un type
|
||||
+ Ajouter un tag
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
:columns="columns"
|
||||
:items="items"
|
||||
:loading="isLoading"
|
||||
empty-message="Aucun type trouvé."
|
||||
empty-message="Aucun tag trouvé."
|
||||
deletable
|
||||
@row-click="openEdit"
|
||||
@delete="(item) => handleDelete(item.id)"
|
||||
@@ -27,7 +27,7 @@
|
||||
</template>
|
||||
</DataTable>
|
||||
|
||||
<TaskTypeDrawer
|
||||
<TaskTagDrawer
|
||||
v-model="drawerOpen"
|
||||
:item="selectedItem"
|
||||
@saved="onSaved"
|
||||
@@ -36,8 +36,8 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { TaskType } from '~/services/dto/task-type'
|
||||
import { useTaskTypeService } from '~/services/task-types'
|
||||
import type { TaskTag } from '~/services/dto/task-tag'
|
||||
import { useTaskTagService } from '~/services/task-tags'
|
||||
|
||||
import type { DataTableColumn } from '~/components/ui/DataTable.vue'
|
||||
|
||||
@@ -46,11 +46,11 @@ const columns: DataTableColumn[] = [
|
||||
{ key: 'color', label: 'Couleur' },
|
||||
]
|
||||
|
||||
const { getAll, remove } = useTaskTypeService()
|
||||
const items = ref<TaskType[]>([])
|
||||
const { getAll, remove } = useTaskTagService()
|
||||
const items = ref<TaskTag[]>([])
|
||||
const isLoading = ref(true)
|
||||
const drawerOpen = ref(false)
|
||||
const selectedItem = ref<TaskType | null>(null)
|
||||
const selectedItem = ref<TaskTag | null>(null)
|
||||
|
||||
async function loadItems() {
|
||||
isLoading.value = true
|
||||
@@ -66,7 +66,7 @@ function openCreate() {
|
||||
drawerOpen.value = true
|
||||
}
|
||||
|
||||
function openEdit(item: TaskType) {
|
||||
function openEdit(item: TaskTag) {
|
||||
selectedItem.value = item
|
||||
drawerOpen.value = true
|
||||
}
|
||||
@@ -7,7 +7,10 @@
|
||||
@click="emit('click')"
|
||||
>
|
||||
<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
|
||||
class="shrink-0 transition-colors"
|
||||
:class="isTimerOnTask ? 'text-[#F18619] hover:text-[#d97314]' : 'text-neutral-400 hover:text-primary-500'"
|
||||
@@ -26,12 +29,12 @@
|
||||
{{ task.priority.label }}
|
||||
</span>
|
||||
<span
|
||||
v-for="type in task.types"
|
||||
:key="type.id"
|
||||
v-for="tag in task.tags"
|
||||
:key="tag.id"
|
||||
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
|
||||
v-if="task.assignee"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<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">
|
||||
<MalioInputText
|
||||
v-model="form.label"
|
||||
@@ -26,12 +26,12 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { TaskType, TaskTypeWrite } from '~/services/dto/task-type'
|
||||
import { useTaskTypeService } from '~/services/task-types'
|
||||
import type { TaskTag, TaskTagWrite } from '~/services/dto/task-tag'
|
||||
import { useTaskTagService } from '~/services/task-tags'
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: boolean
|
||||
item: TaskType | null
|
||||
item: TaskTag | null
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -69,7 +69,7 @@ watch(() => props.modelValue, (open) => {
|
||||
}
|
||||
})
|
||||
|
||||
const { create, update } = useTaskTypeService()
|
||||
const { create, update } = useTaskTagService()
|
||||
|
||||
async function handleSubmit() {
|
||||
touched.label = true
|
||||
@@ -77,7 +77,7 @@ async function handleSubmit() {
|
||||
|
||||
isSubmitting.value = true
|
||||
try {
|
||||
const payload: TaskTypeWrite = {
|
||||
const payload: TaskTagWrite = {
|
||||
label: form.label.trim(),
|
||||
color: form.color,
|
||||
}
|
||||
@@ -25,14 +25,14 @@
|
||||
<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.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
|
||||
v-for="type in entry.types"
|
||||
:key="type.id"
|
||||
v-for="tag in entry.tags"
|
||||
:key="tag.id"
|
||||
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 }" />
|
||||
{{ type.label }}
|
||||
<span class="inline-block h-1.5 w-1.5 shrink-0 rounded-full" :style="{ backgroundColor: tag.color }" />
|
||||
{{ tag.label }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -73,25 +73,25 @@
|
||||
/>
|
||||
|
||||
<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">
|
||||
<label
|
||||
v-for="type in types"
|
||||
:key="type.id"
|
||||
v-for="tag in tags"
|
||||
:key="tag.id"
|
||||
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'
|
||||
: '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
|
||||
type="checkbox"
|
||||
class="hidden"
|
||||
:value="type.id"
|
||||
:checked="form.typeIds.includes(type.id)"
|
||||
@change="toggleType(type.id)"
|
||||
:value="tag.id"
|
||||
:checked="form.tagIds.includes(tag.id)"
|
||||
@change="toggleTag(tag.id)"
|
||||
/>
|
||||
{{ type.label }}
|
||||
{{ tag.label }}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
@@ -120,7 +120,7 @@
|
||||
import type { TimeEntry } from '~/services/dto/time-entry'
|
||||
import type { UserData } from '~/services/dto/user-data'
|
||||
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'
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -129,7 +129,7 @@ const props = defineProps<{
|
||||
prefillStartedAt?: string | null
|
||||
users: UserData[]
|
||||
projects: Project[]
|
||||
types: TaskType[]
|
||||
tags: TaskTag[]
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -154,7 +154,7 @@ const form = reactive({
|
||||
endTime: '',
|
||||
userId: authStore.user?.id ?? null as number | null,
|
||||
projectId: null as number | null,
|
||||
typeIds: [] as number[],
|
||||
tagIds: [] as number[],
|
||||
})
|
||||
|
||||
const userOptions = computed(() =>
|
||||
@@ -176,12 +176,12 @@ const durationLabel = computed(() => {
|
||||
return m > 0 ? `${h}h${String(m).padStart(2, '0')}` : `${h}h`
|
||||
})
|
||||
|
||||
function toggleType(id: number) {
|
||||
const idx = form.typeIds.indexOf(id)
|
||||
function toggleTag(id: number) {
|
||||
const idx = form.tagIds.indexOf(id)
|
||||
if (idx >= 0) {
|
||||
form.typeIds.splice(idx, 1)
|
||||
form.tagIds.splice(idx, 1)
|
||||
} 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.userId = entry.user?.id ?? authStore.user?.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 {
|
||||
form.title = ''
|
||||
form.description = ''
|
||||
@@ -221,7 +221,7 @@ function populateForm(entry: TimeEntry | null | undefined) {
|
||||
form.endTime = ''
|
||||
form.userId = authStore.user?.id ?? null
|
||||
form.projectId = null
|
||||
form.typeIds = []
|
||||
form.tagIds = []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -251,7 +251,7 @@ async function onSubmit() {
|
||||
stoppedAt: form.endTime ? toISO(form.date, form.endTime) : null,
|
||||
user: `/api/users/${form.userId}`,
|
||||
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) {
|
||||
|
||||
@@ -23,12 +23,12 @@
|
||||
{{ entry.title || 'Sans titre' }}
|
||||
</span>
|
||||
<span
|
||||
v-for="type in entry.types"
|
||||
:key="type.id"
|
||||
v-for="tag in entry.tags"
|
||||
:key="tag.id"
|
||||
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>
|
||||
</div>
|
||||
<div class="mt-0.5 flex items-center gap-2 text-xs text-neutral-500">
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<AdminStatusTab v-if="activeTab === 'statuses'" />
|
||||
<AdminEffortTab v-if="activeTab === 'efforts'" />
|
||||
<AdminPriorityTab v-if="activeTab === 'priorities'" />
|
||||
<AdminTypeTab v-if="activeTab === 'types'" />
|
||||
<AdminTagTab v-if="activeTab === 'tags'" />
|
||||
<AdminUserTab v-if="activeTab === 'users'" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -37,7 +37,7 @@ const tabs = [
|
||||
{ key: 'statuses', label: 'Statuts' },
|
||||
{ key: 'efforts', label: 'Efforts' },
|
||||
{ key: 'priorities', label: 'Priorités' },
|
||||
{ key: 'types', label: 'Types' },
|
||||
{ key: 'tags', label: 'Tags' },
|
||||
{ key: 'users', label: 'Utilisateurs' },
|
||||
] as const
|
||||
|
||||
|
||||
@@ -55,10 +55,10 @@
|
||||
/>
|
||||
|
||||
<MalioSelect
|
||||
v-model="selectedTypeId"
|
||||
:options="typeOptions"
|
||||
v-model="selectedTagId"
|
||||
:options="tagOptions"
|
||||
empty-option-label="Tous"
|
||||
label="Type"
|
||||
label="Tag"
|
||||
min-width="!w-40"
|
||||
text-field="text-sm"
|
||||
text-value="text-sm"
|
||||
@@ -93,7 +93,7 @@
|
||||
:prefill-started-at="prefillStartedAt"
|
||||
:users="users"
|
||||
:projects="projects"
|
||||
:types="types"
|
||||
:tags="tags"
|
||||
@saved="loadEntries"
|
||||
/>
|
||||
|
||||
@@ -115,7 +115,7 @@
|
||||
import type { TimeEntry } from '~/services/dto/time-entry'
|
||||
import type { UserData } from '~/services/dto/user-data'
|
||||
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 { extractHydraMembers } from '~/utils/api'
|
||||
|
||||
@@ -127,13 +127,13 @@ const timeEntryService = useTimeEntryService()
|
||||
const viewMode = ref<'week' | 'day' | 'list'>('week')
|
||||
const startDate = ref(getMonday(new Date()))
|
||||
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 entries = ref<TimeEntry[]>([])
|
||||
const users = ref<UserData[]>([])
|
||||
const projects = ref<Project[]>([])
|
||||
const types = ref<TaskType[]>([])
|
||||
const tags = ref<TaskTag[]>([])
|
||||
|
||||
const drawerOpen = ref(false)
|
||||
const editingEntry = ref<TimeEntry | null>(null)
|
||||
@@ -164,8 +164,8 @@ 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 tagOptions = computed(() =>
|
||||
tags.value.map(t => ({ label: t.label, value: t.id }))
|
||||
)
|
||||
|
||||
let pageHeaderResizeObserver: ResizeObserver | null = null
|
||||
@@ -179,8 +179,8 @@ const filteredEntries = computed(() => {
|
||||
if (selectedProjectId.value) {
|
||||
result = result.filter((e) => e.project?.id === selectedProjectId.value)
|
||||
}
|
||||
if (selectedTypeId.value) {
|
||||
result = result.filter((e) => e.types.some((t) => t.id === selectedTypeId.value))
|
||||
if (selectedTagId.value) {
|
||||
result = result.filter((e) => e.tags.some((t) => t.id === selectedTagId.value))
|
||||
}
|
||||
return result
|
||||
})
|
||||
@@ -269,7 +269,7 @@ async function onPaste() {
|
||||
stoppedAt: clipboard.value.stoppedAt ?? undefined,
|
||||
user: `/api/users/${selectedUserId.value}`,
|
||||
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()
|
||||
}
|
||||
@@ -314,12 +314,12 @@ async function loadReferenceData() {
|
||||
const [usersData, projectsData, typesData] = await Promise.all([
|
||||
api.get<any>('/users'),
|
||||
api.get<any>('/projects'),
|
||||
api.get<any>('/task_types'),
|
||||
api.get<any>('/task_tags'),
|
||||
])
|
||||
|
||||
users.value = extractHydraMembers(usersData)
|
||||
projects.value = extractHydraMembers(projectsData)
|
||||
types.value = extractHydraMembers(typesData)
|
||||
tags.value = extractHydraMembers(typesData)
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
export type TaskType = {
|
||||
export type TaskTag = {
|
||||
id: number
|
||||
'@id'?: string
|
||||
label: string
|
||||
color: string
|
||||
}
|
||||
|
||||
export type TaskTypeWrite = {
|
||||
export type TaskTagWrite = {
|
||||
label: string
|
||||
color: string
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { UserData } from './user-data'
|
||||
import type { Project } from './project'
|
||||
import type { Task } from './task'
|
||||
import type { TaskType } from './task-type'
|
||||
import type { TaskTag } from './task-tag'
|
||||
|
||||
export type TimeEntry = {
|
||||
id: number
|
||||
@@ -13,7 +13,7 @@ export type TimeEntry = {
|
||||
user: UserData
|
||||
project: Project | null
|
||||
task: Task | null
|
||||
types: TaskType[]
|
||||
tags: TaskTag[]
|
||||
}
|
||||
|
||||
export type TimeEntryWrite = {
|
||||
@@ -24,5 +24,5 @@ export type TimeEntryWrite = {
|
||||
user: string
|
||||
project?: 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)))
|
||||
: null,
|
||||
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()
|
||||
}
|
||||
|
||||
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']],
|
||||
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'])]
|
||||
#[ORM\Entity(repositoryClass: TimeEntryRepository::class)]
|
||||
#[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'])]
|
||||
private ?Task $task = null;
|
||||
|
||||
/** @var Collection<int, TaskType> */
|
||||
#[ORM\ManyToMany(targetEntity: TaskType::class)]
|
||||
#[ORM\JoinTable(name: 'time_entry_task_type')]
|
||||
/** @var Collection<int, TaskTag> */
|
||||
#[ORM\ManyToMany(targetEntity: TaskTag::class)]
|
||||
#[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'])]
|
||||
private Collection $types;
|
||||
private Collection $tags;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->types = new ArrayCollection();
|
||||
$this->tags = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
@@ -184,24 +188,24 @@ class TimeEntry
|
||||
return $this;
|
||||
}
|
||||
|
||||
/** @return Collection<int, TaskType> */
|
||||
public function getTypes(): Collection
|
||||
/** @return Collection<int, TaskTag> */
|
||||
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)) {
|
||||
$this->types->add($type);
|
||||
if (!$this->tags->contains($tag)) {
|
||||
$this->tags->add($tag);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeType(TaskType $type): static
|
||||
public function removeTag(TaskTag $tag): static
|
||||
{
|
||||
$this->types->removeElement($type);
|
||||
$this->tags->removeElement($tag);
|
||||
|
||||
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