refactor(frontend) : reorganize components into subdirectories and fix imports

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-10 23:59:24 +01:00
parent d28f385918
commit 1efa0fa9ca
30 changed files with 9 additions and 9 deletions

View File

@@ -0,0 +1,123 @@
<template>
<AppDrawer v-model="isOpen" :title="isEditing ? 'Modifier un projet' : 'Ajouter un projet'">
<form @submit.prevent="handleSubmit" class="flex flex-col gap-2">
<MalioInputText
v-model="form.name"
label="Titre"
input-class="w-full"
:error="touched.name && !form.name.trim() ? 'Le titre est requis' : ''"
@blur="touched.name = true"
/>
<MalioInputTextArea
v-model="form.description"
label="Description"
:size="3"
/>
<MalioSelect
v-model="form.clientId"
:options="clientOptions"
label="Client"
empty-option-label="Aucun client"
min-width="w-full"
/>
<div class="mt-4">
<ColorPicker v-model="form.color" />
</div>
<div class="mt-6 flex justify-end">
<button
type="submit"
class="rounded-md bg-primary-500 px-6 py-2 text-sm font-semibold text-white hover:bg-secondary-500 disabled:cursor-not-allowed disabled:opacity-50"
:disabled="isSubmitting"
>
Enregistrer
</button>
</div>
</form>
</AppDrawer>
</template>
<script setup lang="ts">
import type { Project, ProjectWrite } from '~/services/dto/project'
import type { Client } from '~/services/dto/client'
import { useProjectService } from '~/services/projects'
const props = defineProps<{
modelValue: boolean
project: Project | null
clients: Client[]
}>()
const emit = defineEmits<{
(e: 'update:modelValue', value: boolean): void
(e: 'saved'): void
}>()
const isOpen = computed({
get: () => props.modelValue,
set: (v) => emit('update:modelValue', v),
})
const isEditing = computed(() => !!props.project)
const isSubmitting = ref(false)
const form = reactive({
name: '',
description: '',
color: '#222783',
clientId: null as number | null,
})
const touched = reactive({
name: false,
})
const clientOptions = computed(() =>
props.clients.map(c => ({ label: c.name, value: c.id }))
)
watch(() => props.modelValue, (open) => {
if (open) {
if (props.project) {
form.name = props.project.name ?? ''
form.description = props.project.description ?? ''
form.color = props.project.color ?? '#222783'
form.clientId = props.project.client?.id ?? null
} else {
form.name = ''
form.description = ''
form.color = '#222783'
form.clientId = null
}
touched.name = false
}
})
const { create, update } = useProjectService()
async function handleSubmit() {
touched.name = true
if (!form.name.trim()) return
isSubmitting.value = true
try {
const payload: ProjectWrite = {
name: form.name.trim(),
description: form.description.trim() || null,
color: form.color,
client: form.clientId ? `/api/clients/${form.clientId}` : null,
}
if (isEditing.value && props.project) {
await update(props.project.id, payload)
} else {
await create(payload)
}
emit('saved')
isOpen.value = false
} finally {
isSubmitting.value = false
}
}
</script>

View File

@@ -0,0 +1,101 @@
<template>
<div>
<div class="flex items-center justify-between">
<h2 class="text-lg font-bold text-neutral-900">Groupes</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 groupe
</button>
</div>
<DataTable
:columns="columns"
:items="items"
:loading="isLoading"
empty-message="Aucun groupe trouvé."
deletable
@row-click="openEdit"
@delete="(item) => handleDelete(item.id)"
>
<template #cell-color="{ item }">
<span
class="inline-block h-6 w-6 rounded-full"
:style="{ backgroundColor: item.color }"
/>
</template>
<template #cell-description="{ item }">
{{ item.description ?? '—' }}
</template>
</DataTable>
<TaskGroupDrawer
v-model="drawerOpen"
:group="selectedItem"
:project-id="projectId"
@saved="onSaved"
/>
</div>
</template>
<script setup lang="ts">
import type { TaskGroup } from '~/services/dto/task-group'
import { useTaskGroupService } from '~/services/task-groups'
const props = defineProps<{
projectId: number
}>()
const emit = defineEmits<{
(e: 'updated'): void
}>()
import type { DataTableColumn } from '~/components/ui/DataTable.vue'
const columns: DataTableColumn[] = [
{ key: 'title', label: 'Titre', primary: true },
{ key: 'color', label: 'Couleur' },
{ key: 'description', label: 'Description', class: 'max-w-xs truncate text-neutral-700' },
]
const { getByProject, remove } = useTaskGroupService()
const items = ref<TaskGroup[]>([])
const isLoading = ref(true)
const drawerOpen = ref(false)
const selectedItem = ref<TaskGroup | null>(null)
async function loadItems() {
isLoading.value = true
try {
items.value = await getByProject(props.projectId)
} finally {
isLoading.value = false
}
}
function openCreate() {
selectedItem.value = null
drawerOpen.value = true
}
function openEdit(item: TaskGroup) {
selectedItem.value = item
drawerOpen.value = true
}
async function handleDelete(id: number) {
await remove(id)
await loadItems()
emit('updated')
}
async function onSaved() {
await loadItems()
emit('updated')
}
onMounted(() => {
loadItems()
})
</script>

View File

@@ -0,0 +1,144 @@
<template>
<div>
<div class="flex items-center justify-between">
<h2 class="text-lg font-bold text-neutral-900">Statuts</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 statut
</button>
</div>
<DataTable
:columns="columns"
:items="items"
:loading="isLoading"
empty-message="Aucun statut trouvé."
deletable
@row-click="openEdit"
@delete="requestDelete"
>
<template #cell-color="{ item }">
<span
class="inline-block h-6 w-6 rounded-full"
:style="{ backgroundColor: item.color }"
/>
</template>
</DataTable>
<TaskStatusDrawer
v-model="drawerOpen"
:item="selectedItem"
:project-id="projectId"
@saved="onSaved"
/>
<ConfirmDeleteStatusModal
v-model="confirmModalOpen"
:status-label="statusToDelete?.label ?? ''"
:task-count="affectedTaskCount"
:available-statuses="reassignTargets"
@confirm="onConfirmDelete"
/>
</div>
</template>
<script setup lang="ts">
import type { TaskStatus } from '~/services/dto/task-status'
import type { Task } from '~/services/dto/task'
import { useTaskStatusService } from '~/services/task-statuses'
import { useTaskService } from '~/services/tasks'
const props = defineProps<{
projectId: number
}>()
import type { DataTableColumn } from '~/components/ui/DataTable.vue'
const columns: DataTableColumn[] = [
{ key: 'label', label: 'Libellé', primary: true },
{ key: 'color', label: 'Couleur' },
{ key: 'position', label: 'Position', class: 'text-neutral-700' },
]
const statusService = useTaskStatusService()
const taskService = useTaskService()
const items = ref<TaskStatus[]>([])
const tasks = ref<Task[]>([])
const isLoading = ref(true)
const drawerOpen = ref(false)
const selectedItem = ref<TaskStatus | null>(null)
const confirmModalOpen = ref(false)
const statusToDelete = ref<TaskStatus | null>(null)
const affectedTaskCount = computed(() => {
if (!statusToDelete.value) return 0
return tasks.value.filter(t => t.status?.id === statusToDelete.value!.id).length
})
const reassignTargets = computed(() => {
if (!statusToDelete.value) return items.value
return items.value.filter(s => s.id !== statusToDelete.value!.id)
})
async function loadItems() {
isLoading.value = true
try {
const [statuses, projectTasks] = await Promise.all([
statusService.getByProject(props.projectId),
taskService.getByProject(props.projectId),
])
items.value = statuses
tasks.value = projectTasks
} finally {
isLoading.value = false
}
}
function openCreate() {
selectedItem.value = null
drawerOpen.value = true
}
function openEdit(item: TaskStatus) {
selectedItem.value = item
drawerOpen.value = true
}
async function requestDelete(item: TaskStatus) {
statusToDelete.value = item
const count = tasks.value.filter(t => t.status?.id === item.id).length
if (count === 0) {
await statusService.remove(item.id)
await loadItems()
} else {
confirmModalOpen.value = true
}
}
async function onConfirmDelete(targetStatusId: number | null) {
if (!statusToDelete.value) return
const affectedTasks = tasks.value.filter(t => t.status?.id === statusToDelete.value!.id)
const statusIri = targetStatusId ? `/api/task_statuses/${targetStatusId}` : null
await Promise.all(
affectedTasks.map(t => taskService.update(t.id, { status: statusIri }))
)
await statusService.remove(statusToDelete.value.id)
confirmModalOpen.value = false
statusToDelete.value = null
await loadItems()
}
async function onSaved() {
await loadItems()
}
onMounted(() => {
loadItems()
})
</script>