refacto(tables) : composant DataTable global + migration de toutes les tables

- Nouveau composant DataTable réutilisable avec tri par en-têtes, pagination, filtres colonnes
- Nouveau composable useDataTable (sort/page/search/perPage/columnFilters + persistance URL)
- Migration des 9 tables : constructeurs, comments, admin, pieces-catalog, component-catalog, product-catalog, documents, activity-log, ManagementView (catégories)
- Filtres "Type de" server-side (ipartial) pour pièces, composants, produits

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Matthieu
2026-03-04 16:05:00 +01:00
parent 89dc2e93b8
commit 6f1bac381d
16 changed files with 1945 additions and 1943 deletions

View File

@@ -9,82 +9,66 @@
</button>
</div>
<div v-if="loading" class="flex justify-center py-12">
<span class="loading loading-spinner loading-lg" />
</div>
<DataTable
:columns="columns"
:rows="sortedProfiles"
:loading="isLoading"
:sort="sortState"
:show-counter="false"
table-class="table-zebra"
empty-message="Aucun profil."
@sort="handleSort"
>
<template #cell-name="{ row }">
<span class="font-medium">{{ row.firstName }} {{ row.lastName }}</span>
</template>
<div v-else-if="profiles.length" class="overflow-x-auto">
<table class="table table-zebra w-full">
<thead>
<tr>
<th>Nom</th>
<th>Email</th>
<th>Role</th>
<th>Mot de passe</th>
<th>Statut</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr v-for="profile in profiles" :key="profile.id">
<td class="font-medium">
{{ profile.firstName }} {{ profile.lastName }}
</td>
<td class="text-sm text-base-content/70">
{{ profile.email || '-' }}
</td>
<td>
<select
class="select select-bordered select-xs"
:value="primaryRole(profile)"
@change="handleRoleChange(profile.id, $event.target.value)"
>
<option value="ROLE_ADMIN">
Admin
</option>
<option value="ROLE_GESTIONNAIRE">
Gestionnaire
</option>
<option value="ROLE_VIEWER">
Viewer
</option>
</select>
</td>
<td>
<span v-if="profile.hasPassword" class="badge badge-success badge-sm">Oui</span>
<span v-else class="badge badge-ghost badge-sm">Non</span>
<button
class="btn btn-ghost btn-xs ml-1"
@click="openPasswordDialog(profile.id)"
>
{{ profile.hasPassword ? 'Changer' : 'Definir' }}
</button>
</td>
<td>
<span
class="badge badge-sm"
:class="profile.isActive ? 'badge-success' : 'badge-error'"
>
{{ profile.isActive ? 'Actif' : 'Inactif' }}
</span>
</td>
<td>
<button
v-if="profile.isActive"
class="btn btn-ghost btn-xs text-error"
@click="handleDeactivate(profile.id)"
>
Desactiver
</button>
</td>
</tr>
</tbody>
</table>
</div>
<template #cell-email="{ row }">
<span class="text-sm text-base-content/70">{{ row.email || '-' }}</span>
</template>
<div v-else class="text-center py-12 text-base-content/60">
Aucun profil.
</div>
<template #cell-role="{ row }">
<select
class="select select-bordered select-xs"
:value="primaryRole(row)"
@change="handleRoleChange(row.id, $event.target.value)"
>
<option value="ROLE_ADMIN">Admin</option>
<option value="ROLE_GESTIONNAIRE">Gestionnaire</option>
<option value="ROLE_VIEWER">Viewer</option>
</select>
</template>
<template #cell-password="{ row }">
<span v-if="row.hasPassword" class="badge badge-success badge-sm">Oui</span>
<span v-else class="badge badge-ghost badge-sm">Non</span>
<button
class="btn btn-ghost btn-xs ml-1"
@click="openPasswordDialog(row.id)"
>
{{ row.hasPassword ? 'Changer' : 'Definir' }}
</button>
</template>
<template #cell-status="{ row }">
<span
class="badge badge-sm"
:class="row.isActive ? 'badge-success' : 'badge-error'"
>
{{ row.isActive ? 'Actif' : 'Inactif' }}
</span>
</template>
<template #cell-actions="{ row }">
<button
v-if="row.isActive"
class="btn btn-ghost btn-xs text-error"
@click="handleDeactivate(row.id)"
>
Desactiver
</button>
</template>
</DataTable>
<!-- Create Profile Dialog -->
<dialog ref="createDialog" class="modal" :open="showCreateDialog || undefined">
@@ -112,15 +96,9 @@
<div class="form-control mb-3">
<label class="label"><span class="label-text">Role</span></label>
<select v-model="createForm.role" class="select select-bordered">
<option value="ROLE_ADMIN">
Admin
</option>
<option value="ROLE_GESTIONNAIRE">
Gestionnaire
</option>
<option value="ROLE_VIEWER">
Viewer
</option>
<option value="ROLE_ADMIN">Admin</option>
<option value="ROLE_GESTIONNAIRE">Gestionnaire</option>
<option value="ROLE_VIEWER">Viewer</option>
</select>
</div>
<div class="modal-action">
@@ -173,11 +151,55 @@
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { ref, computed, onMounted } from 'vue'
import DataTable from '~/components/common/DataTable.vue'
import { useAdminProfiles } from '#imports'
const { profiles, loading, fetchAll, createProfile, updateRole, setPassword, deactivateProfile } = useAdminProfiles()
const loaded = ref(false)
const isLoading = computed(() => loading.value || !loaded.value)
const columns = [
{ key: 'name', label: 'Nom', sortable: true },
{ key: 'email', label: 'Email', sortable: true },
{ key: 'role', label: 'Role', sortable: true },
{ key: 'password', label: 'Mot de passe' },
{ key: 'status', label: 'Statut', sortable: true },
{ key: 'actions', label: 'Actions' },
]
const sortState = ref({ field: 'name', direction: 'asc' })
const handleSort = (sort) => {
sortState.value = sort
}
const sortedProfiles = computed(() => {
const { field, direction } = sortState.value
const dir = direction === 'desc' ? -1 : 1
return [...profiles.value].sort((a, b) => {
let valA, valB
if (field === 'name') {
valA = `${a.firstName} ${a.lastName}`.toLowerCase()
valB = `${b.firstName} ${b.lastName}`.toLowerCase()
}
else if (field === 'role') {
valA = primaryRole(a)
valB = primaryRole(b)
}
else if (field === 'status') {
valA = a.isActive ? '1' : '0'
valB = b.isActive ? '1' : '0'
}
else {
valA = (a[field] || '').toLowerCase()
valB = (b[field] || '').toLowerCase()
}
return dir * valA.localeCompare(valB)
})
})
const showCreateDialog = ref(false)
const showPasswordDialog = ref(false)
const creating = ref(false)
@@ -209,7 +231,8 @@ const handleCreate = async () => {
await createProfile(data)
showCreateDialog.value = false
createForm.value = { firstName: '', lastName: '', email: '', password: '', role: 'ROLE_VIEWER' }
} finally {
}
finally {
creating.value = false
}
}
@@ -230,7 +253,8 @@ const handleSetPassword = async () => {
try {
await setPassword(passwordProfileId.value, newPassword.value)
showPasswordDialog.value = false
} finally {
}
finally {
settingPassword.value = false
}
}
@@ -239,7 +263,8 @@ const handleDeactivate = async (profileId) => {
await deactivateProfile(profileId)
}
onMounted(() => {
fetchAll()
onMounted(async () => {
await fetchAll()
loaded.value = true
})
</script>