Some checks failed
Auto Tag Develop / tag (push) Has been cancelled
| Numéro du ticket | Titre du ticket | |------------------|-----------------| | | | ## Description de la PR ## Modification du .env ## Check list - [ ] Pas de régression - [ ] TU/TI/TF rédigée - [ ] TU/TI/TF OK - [ ] CHANGELOG modifié Reviewed-on: #17 Co-authored-by: tristan <tristan@yuno.malio.fr> Co-committed-by: tristan <tristan@yuno.malio.fr>
480 lines
15 KiB
Vue
480 lines
15 KiB
Vue
<template>
|
|
<div class="h-full flex flex-col overflow-hidden">
|
|
<div class="flex items-center justify-between pb-6">
|
|
<h1 class="text-2xl font-bold text-primary-500 lg:text-4xl">Utilisateurs</h1>
|
|
<MalioButton
|
|
label="Ajouter"
|
|
icon-name="mdi:plus"
|
|
icon-position="left"
|
|
@click="openCreate"
|
|
/>
|
|
</div>
|
|
|
|
<div
|
|
v-if="!isLoading && users.length === 0"
|
|
class="rounded-lg border border-neutral-200 bg-white p-6 text-md text-neutral-600"
|
|
>
|
|
Aucun utilisateur pour le moment.
|
|
</div>
|
|
|
|
<!-- Desktop table -->
|
|
<div v-else class="min-h-0 overflow-auto rounded-md bg-white hidden lg:block">
|
|
<div class="grid grid-cols-5 gap-4 border border-black bg-tertiary-500 px-6 py-3 text-[20px] font-semibold text-black rounded-t-md sticky top-0 z-10">
|
|
<span class="text-left">Utilisateur</span>
|
|
<span class="text-left">Employé</span>
|
|
<span class="text-left">Accès</span>
|
|
<span class="text-left">Sites</span>
|
|
<span class="text-left">Statut</span>
|
|
</div>
|
|
<div v-if="isLoading" class="px-6 py-4 text-md text-neutral-500">
|
|
Chargement...
|
|
</div>
|
|
<div v-else class="border-x border-b border-primary-500 rounded-b-md">
|
|
<div
|
|
v-for="user in users"
|
|
:key="user.id"
|
|
class="grid grid-cols-5 items-center gap-4 border-b border-primary-500 px-6 py-3 text-md font-bold text-primary-500 last:border-b-0 cursor-pointer hover:bg-tertiary-500"
|
|
@click="openEdit(user)"
|
|
>
|
|
<span>{{ user.username }}</span>
|
|
<span>
|
|
{{ user.employee ? `${user.employee.firstName} ${user.employee.lastName}` : '-' }}
|
|
</span>
|
|
<span>{{ getAccessLabel(user) }}</span>
|
|
<span>{{ getSiteLabels(user) }}</span>
|
|
<span>
|
|
<span
|
|
v-if="user.isLocked"
|
|
class="inline-block rounded-full bg-red-100 px-3 py-1 text-sm font-semibold text-red-700"
|
|
>Verrouillé</span>
|
|
<span
|
|
v-else
|
|
class="inline-block rounded-full bg-green-100 px-3 py-1 text-sm font-semibold text-green-700"
|
|
>Actif</span>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Mobile cards -->
|
|
<div v-if="isLoading" class="px-6 py-4 text-md text-neutral-500 lg:hidden">
|
|
Chargement...
|
|
</div>
|
|
<div v-else-if="users.length > 0" class="min-h-0 overflow-auto space-y-3 lg:hidden">
|
|
<div
|
|
v-for="user in users"
|
|
:key="'m-' + user.id"
|
|
class="rounded-md border border-primary-500 bg-white p-4 cursor-pointer active:bg-tertiary-500"
|
|
@click="openEdit(user)"
|
|
>
|
|
<div class="flex items-center justify-between gap-2 mb-2">
|
|
<p class="text-md font-bold text-primary-500 truncate">{{ user.username }}</p>
|
|
<span
|
|
v-if="user.isLocked"
|
|
class="shrink-0 inline-block rounded-full bg-red-100 px-3 py-1 text-xs font-semibold text-red-700"
|
|
>Verrouillé</span>
|
|
<span
|
|
v-else
|
|
class="shrink-0 inline-block rounded-full bg-green-100 px-3 py-1 text-xs font-semibold text-green-700"
|
|
>Actif</span>
|
|
</div>
|
|
<div class="space-y-1 text-sm">
|
|
<p v-if="user.employee" class="text-neutral-600">
|
|
{{ user.employee.firstName }} {{ user.employee.lastName }}
|
|
</p>
|
|
<p class="text-neutral-500">
|
|
Accès : <span class="font-semibold text-primary-500">{{ getAccessLabel(user) }}</span>
|
|
</p>
|
|
<p v-if="getSiteLabels(user) !== '-'" class="text-neutral-500 truncate">
|
|
Sites : <span class="font-semibold text-primary-500">{{ getSiteLabels(user) }}</span>
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<MalioDrawer
|
|
v-model="isDrawerOpen"
|
|
:title="editingUser ? 'Modifier un utilisateur' : 'Ajouter un utilisateur'"
|
|
>
|
|
<form class="space-y-4" @submit.prevent="handleSubmit">
|
|
<MalioInputText
|
|
v-model="form.username"
|
|
:label="editingUser ? `Nom d'utilisateur` : `Nom d'utilisateur *`"
|
|
group-class="mt-2"
|
|
:error="showUsernameError ? `Le nom d'utilisateur est obligatoire.` : ''"
|
|
/>
|
|
|
|
<div>
|
|
<MalioInputPassword
|
|
v-model="form.password"
|
|
:label="editingUser ? 'Mot de passe' : 'Mot de passe *'"
|
|
:hint="editingUser ? 'Laisse vide pour ne pas changer le mot de passe.' : ''"
|
|
:error="!editingUser && showPasswordError ? 'Le mot de passe est obligatoire.' : ''"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<p class="text-md font-semibold text-neutral-700">Accès</p>
|
|
<div class="mt-2 flex flex-wrap gap-2">
|
|
<button
|
|
type="button"
|
|
class="rounded-full border px-3 py-1 text-sm font-semibold"
|
|
:class="form.accessMode === 'admin' ? 'border-primary-500 bg-tertiary-500 text-primary-500' : 'border-neutral-200 text-neutral-700'"
|
|
@click="selectAccessMode('admin')"
|
|
>
|
|
Admin
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="rounded-full border px-3 py-1 text-sm font-semibold"
|
|
:class="form.accessMode === 'self' ? 'border-primary-500 bg-tertiary-500 text-primary-500' : 'border-neutral-200 text-neutral-700'"
|
|
@click="selectAccessMode('self')"
|
|
>
|
|
Accès personnel
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="rounded-full border px-3 py-1 text-sm font-semibold"
|
|
:class="form.accessMode === 'sites' ? 'border-primary-500 bg-tertiary-500 text-primary-500' : 'border-neutral-200 text-neutral-700'"
|
|
@click="selectAccessMode('sites')"
|
|
>
|
|
Sites
|
|
</button>
|
|
</div>
|
|
<p class="mt-2 text-sm text-neutral-500">
|
|
{{
|
|
form.accessMode === 'admin'
|
|
? 'Donne accès à tout.'
|
|
: form.accessMode === 'self'
|
|
? "Donne accès uniquement à ses propres données."
|
|
: 'Donne accès aux employés des sites sélectionnés.'
|
|
}}
|
|
</p>
|
|
</div>
|
|
|
|
<div v-if="form.accessMode === 'self'">
|
|
<MalioSelect
|
|
:model-value="form.employeeId === '' ? null : form.employeeId"
|
|
:options="employeeOptions"
|
|
label="Employé lié"
|
|
empty-option-label="Aucun"
|
|
min-width=""
|
|
:error="showSelfEmployeeError ? 'Sélectionne un employé.' : ''"
|
|
@update:model-value="onEmployeeChange"
|
|
/>
|
|
</div>
|
|
|
|
<div v-if="form.accessMode === 'sites'">
|
|
<p class="text-md font-semibold text-neutral-700">Sites autorisés</p>
|
|
<div class="mt-2 grid gap-2 sm:grid-cols-2">
|
|
<div
|
|
v-for="site in sites"
|
|
:key="site.id"
|
|
class="flex h-10 items-center rounded-md border border-neutral-200 px-3"
|
|
>
|
|
<MalioCheckbox
|
|
:model-value="form.siteIds.includes(site.id)"
|
|
:label="site.name"
|
|
group-class="flex items-center"
|
|
@update:model-value="toggleSite(site.id)"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<p v-if="showSitesError" class="mt-1 text-sm text-red-600">
|
|
Sélectionne au moins un site.
|
|
</p>
|
|
</div>
|
|
|
|
<div>
|
|
<MalioCheckbox
|
|
v-model="form.isLocked"
|
|
label="Verrouiller le compte"
|
|
hint="Un compte verrouillé ne peut plus se connecter."
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<MalioCheckbox
|
|
v-model="form.hasLeaveRecapAccess"
|
|
label="Accès à l'écran Récap. congés"
|
|
hint="Affiche l'onglet dans la sidebar et donne accès au tableau récap."
|
|
/>
|
|
</div>
|
|
|
|
<div class="flex justify-center pt-2">
|
|
<MalioButton
|
|
type="submit"
|
|
:label="editingUser ? 'Modifier' : 'Valider'"
|
|
button-class="w-[200px]"
|
|
:disabled="isSubmitting || !isFormValid"
|
|
/>
|
|
</div>
|
|
</form>
|
|
</MalioDrawer>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import type { Employee } from '~/services/dto/employee'
|
|
import type { Site } from '~/services/dto/site'
|
|
import type { User } from '~/services/dto/user'
|
|
import type { UserSiteRole } from '~/services/user-site-roles'
|
|
import { listEmployees } from '~/services/employees'
|
|
import { listSites } from '~/services/sites'
|
|
import { createUser, listUsers, updateUser } from '~/services/users'
|
|
import { createUserSiteRole, deleteUserSiteRole, listUserSiteRoles } from '~/services/user-site-roles'
|
|
|
|
definePageMeta({ middleware: ['admin'] })
|
|
useHead({
|
|
title: 'Utilisateurs'
|
|
})
|
|
|
|
const users = ref<User[]>([])
|
|
const employees = ref<Employee[]>([])
|
|
const sites = ref<Site[]>([])
|
|
const userSiteRoles = ref<UserSiteRole[]>([])
|
|
const isLoading = ref(false)
|
|
const isDrawerOpen = ref(false)
|
|
const isSubmitting = ref(false)
|
|
const editingUser = ref<User | null>(null)
|
|
|
|
const form = reactive({
|
|
username: '',
|
|
password: '',
|
|
accessMode: 'admin' as 'admin' | 'self' | 'sites',
|
|
employeeId: '' as number | '',
|
|
siteIds: [] as number[],
|
|
isLocked: false,
|
|
hasLeaveRecapAccess: false
|
|
})
|
|
|
|
const validationTouched = reactive({
|
|
username: false,
|
|
password: false,
|
|
sites: false,
|
|
selfEmployee: false
|
|
})
|
|
|
|
const isUsernameValid = computed(() => form.username.trim() !== '')
|
|
const isPasswordValid = computed(() =>
|
|
editingUser.value ? true : form.password.trim() !== ''
|
|
)
|
|
const isFormValid = computed(() => isUsernameValid.value && isPasswordValid.value)
|
|
const isSitesValid = computed(() => form.siteIds.length > 0)
|
|
const isSelfEmployeeValid = computed(() => form.employeeId !== '')
|
|
|
|
const showUsernameError = computed(
|
|
() => validationTouched.username && !isUsernameValid.value
|
|
)
|
|
const showPasswordError = computed(
|
|
() => validationTouched.password && !isPasswordValid.value
|
|
)
|
|
const showSitesError = computed(
|
|
() => validationTouched.sites && form.accessMode === 'sites' && !isSitesValid.value
|
|
)
|
|
const showSelfEmployeeError = computed(
|
|
() =>
|
|
validationTouched.selfEmployee &&
|
|
form.accessMode === 'self' &&
|
|
!isSelfEmployeeValid.value
|
|
)
|
|
|
|
const userAccessById = computed(() => {
|
|
const rolesByUser = new Map<number, UserSiteRole[]>()
|
|
for (const role of userSiteRoles.value) {
|
|
const userId = role.user?.id
|
|
if (!userId) continue
|
|
const list = rolesByUser.get(userId) ?? []
|
|
list.push(role)
|
|
rolesByUser.set(userId, list)
|
|
}
|
|
|
|
return rolesByUser
|
|
})
|
|
|
|
const getAccessLabel = (user: User) => {
|
|
if (user.roles.includes('ROLE_ADMIN')) return 'Admin'
|
|
if (user.roles.includes('ROLE_SELF')) return 'Self'
|
|
const siteRoles = userAccessById.value.get(user.id) ?? []
|
|
return siteRoles.length > 0 ? 'Sites' : 'Aucun'
|
|
}
|
|
|
|
const getSiteLabels = (user: User) => {
|
|
const siteRoles = userAccessById.value.get(user.id) ?? []
|
|
if (siteRoles.length === 0) return '-'
|
|
const names = siteRoles
|
|
.map((role) => role.site?.name)
|
|
.filter((name): name is string => Boolean(name))
|
|
return names.length > 0 ? names.join(', ') : 'Sites sélectionnés'
|
|
}
|
|
|
|
const employeeOptions = computed(() =>
|
|
employees.value.map((e) => ({ label: `${e.firstName} ${e.lastName}`, value: e.id }))
|
|
)
|
|
|
|
const onEmployeeChange = (value: string | number | null) => {
|
|
form.employeeId = value === null ? '' : Number(value)
|
|
}
|
|
|
|
const loadData = async () => {
|
|
isLoading.value = true
|
|
try {
|
|
const [usersData, employeesData, sitesData, userSiteRolesData] = await Promise.all([
|
|
listUsers(),
|
|
listEmployees(),
|
|
listSites(),
|
|
listUserSiteRoles()
|
|
])
|
|
users.value = usersData
|
|
employees.value = employeesData
|
|
sites.value = sitesData
|
|
userSiteRoles.value = userSiteRolesData
|
|
} finally {
|
|
isLoading.value = false
|
|
}
|
|
}
|
|
|
|
onMounted(loadData)
|
|
|
|
const resetForm = () => {
|
|
form.username = ''
|
|
form.password = ''
|
|
form.employeeId = ''
|
|
form.accessMode = 'admin'
|
|
form.siteIds = []
|
|
form.isLocked = false
|
|
form.hasLeaveRecapAccess = false
|
|
editingUser.value = null
|
|
validationTouched.username = false
|
|
validationTouched.password = false
|
|
validationTouched.sites = false
|
|
validationTouched.selfEmployee = false
|
|
}
|
|
|
|
const openCreate = () => {
|
|
resetForm()
|
|
isDrawerOpen.value = true
|
|
}
|
|
|
|
const openEdit = (user: User) => {
|
|
resetForm()
|
|
editingUser.value = user
|
|
form.username = user.username
|
|
form.password = ''
|
|
|
|
if (user.roles.includes('ROLE_ADMIN')) {
|
|
selectAccessMode('admin')
|
|
} else if (user.roles.includes('ROLE_SELF')) {
|
|
selectAccessMode('self')
|
|
} else {
|
|
selectAccessMode('sites')
|
|
}
|
|
|
|
form.employeeId = user.employee?.id ?? ''
|
|
form.isLocked = user.isLocked
|
|
form.hasLeaveRecapAccess = user.hasLeaveRecapAccess ?? false
|
|
|
|
const siteRoles = userAccessById.value.get(user.id) ?? []
|
|
form.siteIds = siteRoles.map((role) => role.site?.id).filter((id): id is number => typeof id === 'number')
|
|
isDrawerOpen.value = true
|
|
}
|
|
|
|
const closeDrawer = () => {
|
|
isDrawerOpen.value = false
|
|
resetForm()
|
|
}
|
|
|
|
const selectAccessMode = (mode: 'admin' | 'self' | 'sites') => {
|
|
form.accessMode = mode
|
|
if (mode !== 'sites') {
|
|
form.siteIds = []
|
|
}
|
|
}
|
|
|
|
const toggleSite = (siteId: number) => {
|
|
if (form.siteIds.includes(siteId)) {
|
|
form.siteIds = form.siteIds.filter((existing) => existing !== siteId)
|
|
} else {
|
|
form.siteIds = [...form.siteIds, siteId]
|
|
}
|
|
}
|
|
|
|
const handleSubmit = async () => {
|
|
if (isSubmitting.value) return
|
|
validationTouched.username = true
|
|
validationTouched.password = true
|
|
validationTouched.sites = true
|
|
validationTouched.selfEmployee = true
|
|
if (!isFormValid.value) return
|
|
if (form.accessMode === 'sites' && !isSitesValid.value) return
|
|
if (form.accessMode === 'self' && !isSelfEmployeeValid.value) return
|
|
|
|
isSubmitting.value = true
|
|
try {
|
|
const roles =
|
|
form.accessMode === 'admin'
|
|
? ['ROLE_ADMIN']
|
|
: form.accessMode === 'self'
|
|
? ['ROLE_SELF']
|
|
: []
|
|
|
|
const employeeId =
|
|
form.accessMode === 'self' ? (form.employeeId === '' ? null : Number(form.employeeId)) : null
|
|
|
|
if (editingUser.value) {
|
|
await updateUser(editingUser.value.id, {
|
|
username: form.username,
|
|
plainPassword: form.password.trim() ? form.password : undefined,
|
|
roles,
|
|
employeeId,
|
|
isLocked: form.isLocked,
|
|
hasLeaveRecapAccess: form.hasLeaveRecapAccess
|
|
})
|
|
|
|
const existingSiteRoles = userAccessById.value.get(editingUser.value.id) ?? []
|
|
if (existingSiteRoles.length > 0) {
|
|
await Promise.all(existingSiteRoles.map((role) => deleteUserSiteRole(role.id)))
|
|
}
|
|
|
|
if (form.accessMode === 'sites' && form.siteIds.length > 0) {
|
|
await Promise.all(
|
|
form.siteIds.map((siteId) =>
|
|
createUserSiteRole({
|
|
userId: editingUser.value!.id,
|
|
siteId,
|
|
role: 'SITE_ACCESS'
|
|
})
|
|
)
|
|
)
|
|
}
|
|
} else {
|
|
const created = await createUser({
|
|
username: form.username,
|
|
plainPassword: form.password,
|
|
roles,
|
|
employeeId,
|
|
isLocked: form.isLocked,
|
|
hasLeaveRecapAccess: form.hasLeaveRecapAccess
|
|
})
|
|
|
|
if (form.accessMode === 'sites' && form.siteIds.length > 0) {
|
|
await Promise.all(
|
|
form.siteIds.map((siteId) =>
|
|
createUserSiteRole({
|
|
userId: created.id,
|
|
siteId,
|
|
role: 'SITE_ACCESS'
|
|
})
|
|
)
|
|
)
|
|
}
|
|
}
|
|
|
|
closeDrawer()
|
|
await loadData()
|
|
} finally {
|
|
isSubmitting.value = false
|
|
}
|
|
}
|
|
</script>
|