Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
90843dd997 | ||
| 8a449cf81b | |||
|
|
3926946a5f | ||
| b9c3a8a84f | |||
|
|
b2f6fdf222 | ||
| 0fe82c63c5 | |||
| 849d19f124 |
@@ -3,6 +3,7 @@
|
||||
.env.local
|
||||
.env.test
|
||||
docker/
|
||||
!docker/php/config/php.ini
|
||||
deploy/docker/docker-compose.prod.yml
|
||||
deploy/docker/deploy.sh
|
||||
deploy/docker/.env.example
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
parameters:
|
||||
app.version: '0.1.92'
|
||||
app.version: '0.1.95'
|
||||
|
||||
@@ -9,6 +9,13 @@
|
||||
<h2 class="text-[32px] font-semibold text-primary-500">
|
||||
{{ title }}
|
||||
</h2>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-md p-1 text-primary-500 hover:text-secondary-500"
|
||||
@click="close"
|
||||
>
|
||||
<Icon name="mdi:close" size="24"/>
|
||||
</button>
|
||||
</div>
|
||||
<div class="min-h-0 flex-1 overflow-y-auto px-[20px] pb-4">
|
||||
<slot />
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
<template>
|
||||
<header ref="headerRef" class="border-b border-neutral-200 bg-primary-500 p-5 text-white">
|
||||
<div class="flex h-full items-center justify-end">
|
||||
<div class="flex gap-6 text-xl text-white">
|
||||
<header ref="headerRef" class="border-b border-neutral-200 bg-primary-500 px-4 py-3 text-white lg:p-5">
|
||||
<div class="flex h-full items-center justify-between lg:justify-end">
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-md p-1 text-white hover:text-neutral-200 lg:hidden"
|
||||
@click="$emit('toggleSidebar')"
|
||||
>
|
||||
<Icon name="mdi:menu" size="28"/>
|
||||
</button>
|
||||
<div class="flex gap-4 text-xl text-white lg:gap-6">
|
||||
<div v-if="isAdmin" ref="bellRoot" class="relative">
|
||||
<button type="button" class="relative self-center cursor-pointer flex items-center" @click="toggleNotifications">
|
||||
<Icon name="mdi:bell-plus" size="36"/>
|
||||
@@ -15,8 +22,8 @@
|
||||
|
||||
<div
|
||||
v-if="isNotificationsOpen"
|
||||
class="fixed right-[20px] z-30 w-[400px] rounded-md border border-neutral-200 bg-white text-neutral-800 shadow-lg"
|
||||
:style="{ top: `${navbarBottom + 20}px` }"
|
||||
class="fixed right-2 z-30 w-[calc(100vw-1rem)] max-w-[400px] rounded-md border border-neutral-200 bg-white text-neutral-800 shadow-lg lg:right-[20px]"
|
||||
:style="{ top: `${navbarBottom + 10}px` }"
|
||||
>
|
||||
<div class="px-3 pt-3 pb-6 text-xl font-semibold">
|
||||
Notifications
|
||||
@@ -66,7 +73,7 @@
|
||||
<div ref="userMenuRoot" class="relative flex gap-4">
|
||||
<button type="button" class="flex items-center gap-4 cursor-pointer" @click="toggleUserMenu">
|
||||
<Icon name="mdi:account-circle-outline" class="self-center" size="36"/>
|
||||
<p class="self-center">{{ user?.username }}</p>
|
||||
<p class="hidden self-center sm:block">{{ user?.username }}</p>
|
||||
</button>
|
||||
<div
|
||||
v-if="isUserMenuOpen"
|
||||
@@ -103,6 +110,10 @@ defineProps<{
|
||||
user?: User
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
(event: 'toggleSidebar'): void
|
||||
}>()
|
||||
|
||||
const formatTimeAgo = (dateString: string): string => {
|
||||
const date = new Date(dateString)
|
||||
const now = new Date()
|
||||
|
||||
@@ -149,13 +149,13 @@
|
||||
<tr>
|
||||
<td class="px-5 py-[10px] font-bold text-primary-500 border border-primary-500">Payé</td>
|
||||
<td class="px-4 py-[10px] text-center text-neutral-500 border border-primary-500 border-r-2">-</td>
|
||||
<td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500">{{ currentPayment ? formatMinutes(-currentPayment.paidBase25Minutes) : '0 h' }}</td>
|
||||
<td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500">{{ currentPayment ? formatMinutes(-currentPayment.paidBonus25Minutes) : '0 h' }}</td>
|
||||
<td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500 border-r-2">{{ currentPayment ? formatMinutes(-(currentPayment.paidBase25Minutes + currentPayment.paidBonus25Minutes)) : '0 h' }}</td>
|
||||
<td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500">{{ currentPayment ? formatMinutes(-currentPayment.paidBase50Minutes) : '0 h' }}</td>
|
||||
<td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500">{{ currentPayment ? formatMinutes(-currentPayment.paidBonus50Minutes) : '0 h' }}</td>
|
||||
<td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500 border-r-2">{{ currentPayment ? formatMinutes(-(currentPayment.paidBase50Minutes + currentPayment.paidBonus50Minutes)) : '0 h' }}</td>
|
||||
<td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500">{{ formatMinutes(paidTotal) }}</td>
|
||||
<td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500">{{ currentPayment ? formatMinutes(-currentPayment.paidBase25Minutes) : '0 h' }} <span class="text-neutral-400">/ {{ formatCentiemes(currentPayment ? -currentPayment.paidBase25Minutes : 0) }}</span></td>
|
||||
<td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500">{{ currentPayment ? formatMinutes(-currentPayment.paidBonus25Minutes) : '0 h' }} <span class="text-neutral-400">/ {{ formatCentiemes(currentPayment ? -currentPayment.paidBonus25Minutes : 0) }}</span></td>
|
||||
<td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500 border-r-2">{{ currentPayment ? formatMinutes(-(currentPayment.paidBase25Minutes + currentPayment.paidBonus25Minutes)) : '0 h' }} <span class="text-neutral-400">/ {{ formatCentiemes(currentPayment ? -(currentPayment.paidBase25Minutes + currentPayment.paidBonus25Minutes) : 0) }}</span></td>
|
||||
<td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500">{{ currentPayment ? formatMinutes(-currentPayment.paidBase50Minutes) : '0 h' }} <span class="text-neutral-400">/ {{ formatCentiemes(currentPayment ? -currentPayment.paidBase50Minutes : 0) }}</span></td>
|
||||
<td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500">{{ currentPayment ? formatMinutes(-currentPayment.paidBonus50Minutes) : '0 h' }} <span class="text-neutral-400">/ {{ formatCentiemes(currentPayment ? -currentPayment.paidBonus50Minutes : 0) }}</span></td>
|
||||
<td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500 border-r-2">{{ currentPayment ? formatMinutes(-(currentPayment.paidBase50Minutes + currentPayment.paidBonus50Minutes)) : '0 h' }} <span class="text-neutral-400">/ {{ formatCentiemes(currentPayment ? -(currentPayment.paidBase50Minutes + currentPayment.paidBonus50Minutes) : 0) }}</span></td>
|
||||
<td class="px-4 py-[10px] text-center font-bold text-primary-500 border border-primary-500">{{ formatMinutes(paidTotal) }} <span class="text-neutral-400">/ {{ formatCentiemes(paidTotal) }}</span></td>
|
||||
</tr>
|
||||
|
||||
<!-- Reste row -->
|
||||
@@ -187,41 +187,41 @@
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-neutral-700">Base 25% (heures)</label>
|
||||
<label class="block text-sm font-medium text-neutral-700">Base 25% (centièmes)</label>
|
||||
<input
|
||||
v-model.number="paymentForm.base25Hours"
|
||||
type="number"
|
||||
step="0.5"
|
||||
step="0.01"
|
||||
min="0"
|
||||
class="mt-2 w-full rounded-md border border-neutral-300 px-3 py-2 text-base text-neutral-900 focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-secondary-500/20"
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-neutral-700">Heures 25% (heures)</label>
|
||||
<label class="block text-sm font-medium text-neutral-700">Heures 25% (centièmes)</label>
|
||||
<input
|
||||
v-model.number="paymentForm.bonus25Hours"
|
||||
type="number"
|
||||
step="0.5"
|
||||
step="0.01"
|
||||
min="0"
|
||||
class="mt-2 w-full rounded-md border border-neutral-300 px-3 py-2 text-base text-neutral-900 focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-secondary-500/20"
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-neutral-700">Base 50% (heures)</label>
|
||||
<label class="block text-sm font-medium text-neutral-700">Base 50% (centièmes)</label>
|
||||
<input
|
||||
v-model.number="paymentForm.base50Hours"
|
||||
type="number"
|
||||
step="0.5"
|
||||
step="0.01"
|
||||
min="0"
|
||||
class="mt-2 w-full rounded-md border border-neutral-300 px-3 py-2 text-base text-neutral-900 focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-secondary-500/20"
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<label class="block text-sm font-medium text-neutral-700">Heures 50% (heures)</label>
|
||||
<label class="block text-sm font-medium text-neutral-700">Heures 50% (centièmes)</label>
|
||||
<input
|
||||
v-model.number="paymentForm.bonus50Hours"
|
||||
type="number"
|
||||
step="0.5"
|
||||
step="0.01"
|
||||
min="0"
|
||||
class="mt-2 w-full rounded-md border border-neutral-300 px-3 py-2 text-base text-neutral-900 focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-secondary-500/20"
|
||||
/>
|
||||
@@ -500,10 +500,10 @@ const paymentForm = reactive({
|
||||
const prefillFromExistingPayment = (month: number) => {
|
||||
const existing = props.summary?.monthPayments.find((p) => p.month === month) ?? null
|
||||
if (existing) {
|
||||
paymentForm.base25Hours = existing.paidBase25Minutes / 60
|
||||
paymentForm.bonus25Hours = existing.paidBonus25Minutes / 60
|
||||
paymentForm.base50Hours = existing.paidBase50Minutes / 60
|
||||
paymentForm.bonus50Hours = existing.paidBonus50Minutes / 60
|
||||
paymentForm.base25Hours = Math.round(existing.paidBase25Minutes / 60 * 100) / 100
|
||||
paymentForm.bonus25Hours = Math.round(existing.paidBonus25Minutes / 60 * 100) / 100
|
||||
paymentForm.base50Hours = Math.round(existing.paidBase50Minutes / 60 * 100) / 100
|
||||
paymentForm.bonus50Hours = Math.round(existing.paidBonus50Minutes / 60 * 100) / 100
|
||||
} else {
|
||||
paymentForm.base25Hours = 0
|
||||
paymentForm.bonus25Hours = 0
|
||||
@@ -516,6 +516,14 @@ watch(() => paymentForm.month, (newMonth) => {
|
||||
prefillFromExistingPayment(newMonth)
|
||||
})
|
||||
|
||||
watch(() => paymentForm.base25Hours, (value) => {
|
||||
paymentForm.bonus25Hours = Math.round(value * 0.25 * 100) / 100
|
||||
})
|
||||
|
||||
watch(() => paymentForm.base50Hours, (value) => {
|
||||
paymentForm.bonus50Hours = Math.round(value * 0.50 * 100) / 100
|
||||
})
|
||||
|
||||
const openPaymentDrawer = () => {
|
||||
paymentForm.month = currentMonth.value
|
||||
prefillFromExistingPayment(currentMonth.value)
|
||||
|
||||
@@ -1,6 +1,174 @@
|
||||
<template>
|
||||
<div class="bg-white overflow-hidden flex min-h-0 flex-col">
|
||||
<div class="overflow-y-auto min-h-0">
|
||||
<!-- Mobile card layout -->
|
||||
<div class="overflow-y-auto min-h-0 space-y-3 lg:hidden">
|
||||
<div
|
||||
v-for="employee in employees"
|
||||
:key="'m-' + employee.id"
|
||||
class="rounded-md border border-primary-500 bg-white p-4"
|
||||
>
|
||||
<!-- Employee name + site -->
|
||||
<div class="mb-3">
|
||||
<p class="text-md font-bold text-primary-500 truncate">
|
||||
{{ employee.firstName }} {{ employee.lastName }}
|
||||
<span class="font-normal text-neutral-600 text-sm">({{ contractLabel(employee) }})</span>
|
||||
</p>
|
||||
<p class="text-sm text-neutral-500 truncate">
|
||||
{{ employee.site?.name ?? 'Sans site' }}<span v-if="employee.currentContractNature"> — {{ contractNatureLabel(employee.currentContractNature) }}</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Absence / Holiday / Formation pills -->
|
||||
<div class="mb-3 flex flex-col gap-1">
|
||||
<p
|
||||
v-if="getRowAbsenceLabel(employee.id)"
|
||||
class="rounded-md px-2 py-1 text-xs text-white truncate"
|
||||
:style="getRowAbsenceStyle(employee.id)"
|
||||
>
|
||||
{{ getRowAbsenceLabel(employee.id) }}
|
||||
</p>
|
||||
<p
|
||||
v-else
|
||||
class="text-xs text-neutral-400"
|
||||
>
|
||||
Aucune absence
|
||||
</p>
|
||||
<p
|
||||
v-if="isHoliday"
|
||||
class="rounded-md px-2 py-1 text-xs text-sky-900 inline-flex items-center gap-1"
|
||||
style="background-color: #b3e5fc"
|
||||
>
|
||||
<Icon name="mdi:calendar-star" size="14" class="shrink-0"/>
|
||||
<span class="truncate">{{ holidayLabel || 'Férié' }}</span>
|
||||
</p>
|
||||
<p
|
||||
v-if="hasRowFormation(employee.id)"
|
||||
class="rounded-md px-2 py-1 text-xs text-white bg-indigo-500 inline-flex items-center gap-1"
|
||||
>
|
||||
<Icon name="mdi:school-outline" size="14" class="shrink-0"/>
|
||||
<span class="truncate">{{ getRowFormationLabel(employee.id) }}</span>
|
||||
</p>
|
||||
<button
|
||||
v-if="!hasRowFormation(employee.id)"
|
||||
type="button"
|
||||
class="self-start text-xs font-semibold underline"
|
||||
:class="isRowLocked(employee.id) || !hasContractAtSelectedDate(employee.id) ? 'text-neutral-400 cursor-not-allowed' : 'text-primary-500 cursor-pointer'"
|
||||
:disabled="isRowLocked(employee.id) || !hasContractAtSelectedDate(employee.id)"
|
||||
@click="onAbsenceClick(employee.id)"
|
||||
>
|
||||
Modifier
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Time inputs (TIME tracking) -->
|
||||
<div v-if="isTimeTracking(employee)" class="space-y-2">
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<div>
|
||||
<label class="text-xs text-neutral-500">Début matin</label>
|
||||
<TimeSelect
|
||||
v-model="rows[employee.id].morningFrom"
|
||||
:disabled="!hasContractAtSelectedDate(employee.id) || isRowLocked(employee.id) || (!isAdmin && isHalfLockedByAbsence(employee.id, 'AM'))"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-xs text-neutral-500">Fin matin</label>
|
||||
<TimeSelect
|
||||
v-model="rows[employee.id].morningTo"
|
||||
:disabled="!hasContractAtSelectedDate(employee.id) || isRowLocked(employee.id) || (!isAdmin && isHalfLockedByAbsence(employee.id, 'AM'))"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<div>
|
||||
<label class="text-xs text-neutral-500">Début après-midi</label>
|
||||
<TimeSelect
|
||||
v-model="rows[employee.id].afternoonFrom"
|
||||
:disabled="!hasContractAtSelectedDate(employee.id) || isRowLocked(employee.id) || (!isAdmin && isHalfLockedByAbsence(employee.id, 'PM'))"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-xs text-neutral-500">Fin après-midi</label>
|
||||
<TimeSelect
|
||||
v-model="rows[employee.id].afternoonTo"
|
||||
:disabled="!hasContractAtSelectedDate(employee.id) || isRowLocked(employee.id) || (!isAdmin && isHalfLockedByAbsence(employee.id, 'PM'))"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<div>
|
||||
<label class="text-xs text-neutral-500">Début soir</label>
|
||||
<TimeSelect
|
||||
v-model="rows[employee.id].eveningFrom"
|
||||
:disabled="!hasContractAtSelectedDate(employee.id) || isRowLocked(employee.id) || (!isAdmin && isEveningLockedByAbsence(employee.id))"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-xs text-neutral-500">Fin soir</label>
|
||||
<TimeSelect
|
||||
v-model="rows[employee.id].eveningTo"
|
||||
:disabled="!hasContractAtSelectedDate(employee.id) || isRowLocked(employee.id) || (!isAdmin && isEveningLockedByAbsence(employee.id))"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-4 pt-1 text-sm font-semibold text-primary-500">
|
||||
<span>Jour : {{ formatMinutes(getRowMetrics(employee.id).dayMinutes) }}</span>
|
||||
<span>Nuit : {{ formatMinutes(getRowMetrics(employee.id).nightMinutes) }}</span>
|
||||
<span>Total : {{ formatMinutes(getRowMetrics(employee.id).totalMinutes) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Presence tracking -->
|
||||
<div v-else-if="isPresenceTracking(employee)" class="space-y-2">
|
||||
<div class="flex items-center gap-4">
|
||||
<label class="flex items-center gap-2 text-sm">
|
||||
<input
|
||||
v-model="rows[employee.id].isPresentMorning"
|
||||
type="checkbox"
|
||||
class="h-4 w-4 cursor-pointer"
|
||||
:disabled="!hasContractAtSelectedDate(employee.id) || isRowLocked(employee.id) || (!isAdmin && isHalfLockedByAbsence(employee.id, 'AM'))"
|
||||
/>
|
||||
Matin
|
||||
</label>
|
||||
<label class="flex items-center gap-2 text-sm">
|
||||
<input
|
||||
v-model="rows[employee.id].isPresentAfternoon"
|
||||
type="checkbox"
|
||||
class="h-4 w-4 cursor-pointer"
|
||||
:disabled="!hasContractAtSelectedDate(employee.id) || isRowLocked(employee.id) || (!isAdmin && isHalfLockedByAbsence(employee.id, 'PM'))"
|
||||
/>
|
||||
Après-midi
|
||||
</label>
|
||||
</div>
|
||||
<p class="text-sm font-semibold text-primary-500">Total : {{ getPresenceDayValue(employee.id) }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Validation status (non-admin) -->
|
||||
<div v-if="!isAdmin" class="mt-3 flex gap-4 text-xs border-t border-neutral-200 pt-2">
|
||||
<span v-if="!isSiteManager" class="flex items-center gap-1">
|
||||
<Icon name="mdi:check-circle-outline" size="14" :class="rows[employee.id]?.isSiteValid ? 'text-green-600' : 'text-neutral-400'"/>
|
||||
Validation site : <span :class="rows[employee.id]?.isSiteValid ? 'font-semibold text-green-600' : 'text-neutral-500'">{{ rows[employee.id]?.isSiteValid ? 'Validé' : 'En attente' }}</span>
|
||||
</span>
|
||||
<span class="flex items-center gap-1">
|
||||
<Icon name="mdi:check-circle-outline" size="14" :class="rows[employee.id]?.isValid ? 'text-green-600' : 'text-neutral-400'"/>
|
||||
Validation RH : <span :class="rows[employee.id]?.isValid ? 'font-semibold text-green-600' : 'text-neutral-500'">{{ rows[employee.id]?.isValid ? 'Validé' : 'En attente' }}</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Validation checkbox (admin) -->
|
||||
<div v-if="isAdmin" class="mt-3 flex items-center gap-2 text-sm">
|
||||
<input
|
||||
:checked="rows[employee.id]?.isValid ?? false"
|
||||
type="checkbox"
|
||||
class="h-4 w-4 cursor-pointer"
|
||||
@change="onToggleValidation(employee.id, ($event.target as HTMLInputElement).checked)"
|
||||
/>
|
||||
<span class="text-neutral-700 font-semibold">Valider</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Desktop table layout -->
|
||||
<div class="overflow-y-auto min-h-0 hidden lg:block">
|
||||
<div
|
||||
class="grid w-full min-w-0 gap-1 border border-black bg-tertiary-500 px-4 py-3 text-sm font-semibold text-black rounded-t-md sticky top-0 z-10"
|
||||
:style="{ gridTemplateColumns: dayGridCols }"
|
||||
|
||||
@@ -1,17 +1,68 @@
|
||||
<template>
|
||||
<div class="py-6 flex flex-col gap-3">
|
||||
<div class="flex gap-4">
|
||||
<div class="py-4 flex flex-col gap-3 lg:py-6">
|
||||
<!-- Desktop: filters row -->
|
||||
<div class="hidden lg:flex lg:gap-4">
|
||||
<SiteFilterSelector v-if="sites.length > 0 && isAdmin" v-model="selectedSiteIds" :sites="sites" />
|
||||
<div v-if="isAdmin" class="w-80 max-w-full">
|
||||
<div v-if="isAdmin" class="w-80">
|
||||
<EmployeeNameFilterInput v-model="employeeFilter" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between items-center gap-4">
|
||||
<div class="flex gap-4 flex-wrap">
|
||||
<!-- Mobile: search + filter button -->
|
||||
<div v-if="isAdmin" class="flex gap-2 lg:hidden">
|
||||
<div class="flex-1 min-w-0">
|
||||
<EmployeeNameFilterInput v-model="employeeFilter" />
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="flex h-10 w-10 shrink-0 items-center justify-center rounded-md border border-primary-500 bg-white text-primary-500"
|
||||
@click="filtersDrawerOpen = true"
|
||||
>
|
||||
<Icon name="mdi:filter-variant" size="22"/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Mobile filters drawer -->
|
||||
<AppDrawer v-model="filtersDrawerOpen" title="Filtres">
|
||||
<div class="space-y-6">
|
||||
<div v-if="sites.length > 0 && isAdmin">
|
||||
<label class="text-md font-semibold text-neutral-700">Sites</label>
|
||||
<div class="mt-2">
|
||||
<SiteFilterSelector v-model="selectedSiteIds" :sites="sites" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="isAdmin">
|
||||
<label class="text-md font-semibold text-neutral-700">Vue</label>
|
||||
<div class="mt-2 inline-flex h-10 w-full overflow-hidden rounded-md border border-primary-500 bg-white">
|
||||
<button
|
||||
type="button"
|
||||
class="flex-1 inline-flex items-center justify-center gap-2 px-4 py-2 text-sm font-semibold"
|
||||
:class="viewModeButtonClass('day')"
|
||||
@click="viewMode = 'day'; filtersDrawerOpen = false"
|
||||
>
|
||||
<Icon name="mdi:calendar-clock" />
|
||||
Jour
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="flex-1 inline-flex items-center justify-center gap-2 border-l border-primary-500 px-4 py-2 text-sm font-semibold"
|
||||
:class="viewModeButtonClass('week')"
|
||||
@click="viewMode = 'week'; filtersDrawerOpen = false"
|
||||
>
|
||||
<Icon name="mdi:calendar-week" />
|
||||
Semaine
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AppDrawer>
|
||||
|
||||
<!-- Date navigation -->
|
||||
<div class="flex flex-col gap-3 lg:flex-row lg:justify-between lg:items-center lg:gap-4">
|
||||
<div class="flex flex-col gap-3 lg:flex-row lg:flex-wrap lg:gap-4">
|
||||
<div
|
||||
v-if="viewMode === 'day'"
|
||||
class="inline-flex h-10 w-[320px] overflow-hidden rounded-md border border-primary-500 bg-white"
|
||||
class="inline-flex h-10 w-full overflow-hidden rounded-md border border-primary-500 bg-white lg:w-[320px]"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
@@ -41,7 +92,7 @@
|
||||
|
||||
<div
|
||||
v-else
|
||||
class="inline-flex h-10 w-[320px] overflow-hidden rounded-md border border-primary-500 bg-white"
|
||||
class="inline-flex h-10 w-full overflow-hidden rounded-md border border-primary-500 bg-white lg:w-[320px]"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
@@ -70,7 +121,7 @@
|
||||
</div>
|
||||
|
||||
<PeriodStepperPicker
|
||||
width-class="w-[320px]"
|
||||
width-class="w-full lg:w-[320px]"
|
||||
:label="formattedSelectedDate"
|
||||
:picker-type="viewMode === 'week' ? 'week' : 'date'"
|
||||
:picker-value="pickerValue"
|
||||
@@ -82,7 +133,8 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="isAdmin" class="inline-flex h-10 overflow-hidden rounded-md border border-primary-500 bg-white">
|
||||
<!-- Desktop: view mode toggle -->
|
||||
<div v-if="isAdmin" class="hidden lg:inline-flex h-10 overflow-hidden rounded-md border border-primary-500 bg-white">
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center gap-2 px-4 py-2 text-sm font-semibold active:scale-[0.98] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500/40"
|
||||
@@ -106,7 +158,7 @@
|
||||
|
||||
<div
|
||||
v-if="isAdmin && viewMode === 'week' && absenceTypes.length > 0"
|
||||
class="flex flex-wrap items-center gap-6"
|
||||
class="hidden lg:flex flex-wrap items-center gap-6"
|
||||
>
|
||||
<p class="font-bold">Légende :</p>
|
||||
<div v-for="type in absenceTypes" :key="type.id" class="flex items-center gap-2">
|
||||
@@ -123,6 +175,7 @@ import type { AbsenceType } from '~/services/dto/absence-type'
|
||||
import EmployeeNameFilterInput from '~/components/EmployeeNameFilterInput.vue'
|
||||
import SiteFilterSelector from '~/components/SiteFilterSelector.vue'
|
||||
import PeriodStepperPicker from '~/components/PeriodStepperPicker.vue'
|
||||
import AppDrawer from '~/components/AppDrawer.vue'
|
||||
import { weekInputValueToYmd, ymdToWeekInputValue } from '~/utils/date'
|
||||
|
||||
const selectedDate = defineModel<string>('selectedDate', { required: true })
|
||||
@@ -150,6 +203,8 @@ const emit = defineEmits<{
|
||||
(e: 'shift-date', value: number): void
|
||||
}>()
|
||||
|
||||
const filtersDrawerOpen = ref(false)
|
||||
|
||||
const pickerValue = computed(() => {
|
||||
if (viewMode.value === 'week') return ymdToWeekInputValue(selectedDate.value)
|
||||
return selectedDate.value
|
||||
|
||||
@@ -1,7 +1,71 @@
|
||||
<template>
|
||||
<div class="bg-white overflow-hidden flex min-h-0 flex-col">
|
||||
<div v-if="isWeekLoading" class="p-6 text-md text-neutral-600">Chargement de la semaine...</div>
|
||||
<div v-else class="overflow-y-auto min-h-0">
|
||||
|
||||
<!-- Mobile cards -->
|
||||
<div v-else class="overflow-y-auto min-h-0 space-y-3 lg:hidden">
|
||||
<div
|
||||
v-for="row in weeklySummary?.rows ?? []"
|
||||
:key="'m-' + row.employeeId"
|
||||
class="rounded-md border border-primary-500 bg-white p-4"
|
||||
>
|
||||
<div class="mb-3">
|
||||
<p class="text-md font-bold text-primary-500 truncate">
|
||||
{{ row.firstName }} {{ row.lastName }}
|
||||
<span class="font-normal text-neutral-600 text-sm">({{ row.contractName ?? '-' }})</span>
|
||||
</p>
|
||||
<p class="text-xs text-neutral-500 truncate">
|
||||
{{ row.siteName ?? 'Sans site' }}<span v-if="row.contractNature"> — {{ contractNatureLabel(row.contractNature) }}</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Daily breakdown -->
|
||||
<div class="mb-3 space-y-1">
|
||||
<div
|
||||
v-for="(daily, i) in row.daily"
|
||||
:key="daily.date"
|
||||
class="flex items-center justify-between rounded-md px-2 py-1 text-xs"
|
||||
:class="daily.hasAbsence ? 'text-white' : 'text-primary-500'"
|
||||
:style="getDailyCellStyle(daily)"
|
||||
>
|
||||
<span class="font-semibold">{{ weekDayHeaders[i]?.label ?? '' }}</span>
|
||||
<span v-if="row.trackingMode === 'PRESENCE'">{{ daily.present ?? 0 }}</span>
|
||||
<span v-else>J {{ formatMinutes(daily.dayMinutes) }} / N {{ formatMinutes(daily.nightMinutes) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Weekly totals -->
|
||||
<div class="border-t border-neutral-200 pt-2 grid grid-cols-2 gap-x-4 gap-y-1 text-xs">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-neutral-500">Total sem.</span>
|
||||
<span class="font-bold text-primary-500">{{ row.trackingMode === 'PRESENCE' ? (row.weeklyPresenceCount ?? 0) : formatMinutes(row.weeklyTotalMinutes) }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-neutral-500">H. supp.</span>
|
||||
<span class="font-bold text-primary-500">{{ row.trackingMode === 'PRESENCE' ? '-' : formatMinutes(row.weeklyOvertimeTotalMinutes ?? 0) }}</span>
|
||||
</div>
|
||||
<div v-if="row.trackingMode !== 'PRESENCE' && !isInterimContract(row.contractType)" class="flex justify-between">
|
||||
<span class="text-neutral-500">+25%</span>
|
||||
<span class="font-bold text-primary-500">{{ formatMinutes(row.weeklyOvertime25Minutes ?? 0) }}</span>
|
||||
</div>
|
||||
<div v-if="row.trackingMode !== 'PRESENCE' && !isInterimContract(row.contractType)" class="flex justify-between">
|
||||
<span class="text-neutral-500">+50%</span>
|
||||
<span class="font-bold text-primary-500">{{ formatMinutes(row.weeklyOvertime50Minutes ?? 0) }}</span>
|
||||
</div>
|
||||
<div v-if="row.trackingMode !== 'PRESENCE' && !isInterimContract(row.contractType)" class="flex justify-between">
|
||||
<span class="text-neutral-500">Récup.</span>
|
||||
<span class="font-bold text-primary-500">{{ formatMinutes(row.weeklyRecoveryMinutes ?? 0) }}</span>
|
||||
</div>
|
||||
<div v-if="(row.weeklyNightBasketCount ?? 0) > 0" class="flex justify-between">
|
||||
<span class="text-neutral-500">Panier nuit</span>
|
||||
<span class="font-bold text-primary-500">{{ row.weeklyNightBasketCount }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Desktop table -->
|
||||
<div v-if="!isWeekLoading" class="overflow-y-auto min-h-0 hidden lg:block">
|
||||
<div
|
||||
class="grid w-full min-w-0 gap-1 border border-black bg-tertiary-500 px-4 py-3 text-sm font-semibold text-black rounded-t-md sticky top-0 z-10"
|
||||
:style="{ gridTemplateColumns: weekGridCols }"
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<button
|
||||
type="button"
|
||||
tabindex="-1"
|
||||
class="inline-flex h-8 w-8 items-center justify-center rounded text-neutral-600 hover:bg-tertiary-500 disabled:cursor-not-allowed disabled:bg-neutral-200 disabled:text-neutral-500"
|
||||
class="hidden lg:inline-flex h-8 w-8 items-center justify-center rounded text-neutral-600 hover:bg-tertiary-500 disabled:cursor-not-allowed disabled:bg-neutral-200 disabled:text-neutral-500"
|
||||
:disabled="props.disabled"
|
||||
@mousedown.prevent
|
||||
@click="toggleOpen"
|
||||
@@ -149,8 +149,11 @@ const toggleOpen = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const isMobile = () => window.innerWidth < 1024
|
||||
|
||||
const openMenu = () => {
|
||||
if (props.disabled) return
|
||||
if (isMobile()) return
|
||||
if (!isOpen.value) {
|
||||
isOpen.value = true
|
||||
nextTick(updateMenuPosition)
|
||||
@@ -165,8 +168,28 @@ const closeMenu = () => {
|
||||
isOpen.value = false
|
||||
}
|
||||
|
||||
const snapToNearest15 = (time: string): string => {
|
||||
const [h, m] = time.split(':').map(Number)
|
||||
const snapped = Math.round(m / 15) * 15
|
||||
if (snapped === 60) {
|
||||
const newH = h + 1
|
||||
if (newH > 23) return '23:45'
|
||||
return `${String(newH).padStart(2, '0')}:00`
|
||||
}
|
||||
return `${String(h).padStart(2, '0')}:${String(snapped).padStart(2, '0')}`
|
||||
}
|
||||
|
||||
const commitInput = () => {
|
||||
const normalized = normalizeTypedTime(inputValue.value)
|
||||
let value = inputValue.value
|
||||
if (isMobile()) {
|
||||
value = clampTime(value)
|
||||
const normalized = normalizeTypedTime(value)
|
||||
if (normalized !== null && normalized !== '') {
|
||||
value = snapToNearest15(normalized)
|
||||
}
|
||||
inputValue.value = value
|
||||
}
|
||||
const normalized = normalizeTypedTime(value)
|
||||
if (normalized === null || (normalized !== '' && !timeSlots.value.includes(normalized))) {
|
||||
emit('update:modelValue', '')
|
||||
inputValue.value = ''
|
||||
@@ -184,13 +207,26 @@ const onInput = (event: Event) => {
|
||||
if (masked !== inputValue.value) {
|
||||
inputValue.value = masked
|
||||
}
|
||||
openMenu()
|
||||
if (!isMobile()) {
|
||||
openMenu()
|
||||
}
|
||||
}
|
||||
|
||||
const clampTime = (value: string): string => {
|
||||
const normalized = normalizeTypedTime(value)
|
||||
if (normalized === null || normalized === '') return value
|
||||
const [h, m] = normalized.split(':').map(Number)
|
||||
if (h > 23 || (h === 23 && m > 45)) return '23:45'
|
||||
return normalized
|
||||
}
|
||||
|
||||
const onInputBlur = () => {
|
||||
// Laisse le temps au click menu de passer avant fermeture.
|
||||
setTimeout(() => {
|
||||
if (menu.value?.contains(document.activeElement)) return
|
||||
if (isMobile()) {
|
||||
inputValue.value = clampTime(inputValue.value)
|
||||
}
|
||||
commitInput()
|
||||
}, 50)
|
||||
}
|
||||
|
||||
@@ -1,11 +1,40 @@
|
||||
<template>
|
||||
<div class="h-screen overflow-hidden">
|
||||
<div class="flex h-full">
|
||||
<aside class="flex h-full w-64 flex-shrink-0 flex-col border-r border-neutral-200 bg-tertiary-500">
|
||||
<div class="h-[75px]">
|
||||
<!-- Mobile overlay -->
|
||||
<Transition
|
||||
enter-active-class="transition-opacity duration-300"
|
||||
enter-from-class="opacity-0"
|
||||
enter-to-class="opacity-100"
|
||||
leave-active-class="transition-opacity duration-300"
|
||||
leave-from-class="opacity-100"
|
||||
leave-to-class="opacity-0"
|
||||
>
|
||||
<div
|
||||
v-if="sidebarOpen"
|
||||
class="fixed inset-0 z-40 bg-black/50 lg:hidden"
|
||||
@click="sidebarOpen = false"
|
||||
/>
|
||||
</Transition>
|
||||
|
||||
<!-- Sidebar -->
|
||||
<aside
|
||||
:class="[
|
||||
'fixed inset-y-0 left-0 z-50 flex w-64 flex-col border-r border-neutral-200 bg-tertiary-500 transition-transform duration-300 lg:static lg:translate-x-0 lg:flex-shrink-0',
|
||||
sidebarOpen ? 'translate-x-0' : '-translate-x-full'
|
||||
]"
|
||||
>
|
||||
<div class="flex h-[75px] items-center justify-between">
|
||||
<img src="/malio.png" alt="Logo" class="w-auto"/>
|
||||
<button
|
||||
type="button"
|
||||
class="mr-3 rounded-md p-1 text-primary-500 hover:text-secondary-500 lg:hidden"
|
||||
@click="sidebarOpen = false"
|
||||
>
|
||||
<Icon name="mdi:close" size="24"/>
|
||||
</button>
|
||||
</div>
|
||||
<nav class="flex-1 px-4 pb-6">
|
||||
<nav class="flex-1 overflow-y-auto px-4 pb-6">
|
||||
<template v-if="isAdmin">
|
||||
<NuxtLink
|
||||
to="/calendar"
|
||||
@@ -13,6 +42,7 @@
|
||||
:class="route.path.startsWith('/calendar')
|
||||
? 'bg-tertiary-500 text-primary-500 font-bold'
|
||||
: ''"
|
||||
@click="closeSidebarOnMobile"
|
||||
>
|
||||
<Icon name="mdi:calendar-blank" size="24"/>
|
||||
<p>Calendrier</p>
|
||||
@@ -26,6 +56,7 @@
|
||||
route.path.startsWith('/hours') ? 'bg-tertiary-500 text-primary-500 font-bold' : '',
|
||||
!isAdmin ? 'border-t border-secondary-500 pt-3' : ''
|
||||
]"
|
||||
@click="closeSidebarOnMobile"
|
||||
>
|
||||
<Icon name="mdi:clock-time-four-outline" size="24"/>
|
||||
<p>Heures</p>
|
||||
@@ -38,6 +69,7 @@
|
||||
route.path.startsWith('/driver-hours') ? 'bg-tertiary-500 text-primary-500 font-bold' : '',
|
||||
!isAdmin && isDriver ? 'border-t border-secondary-500 pt-3' : ''
|
||||
]"
|
||||
@click="closeSidebarOnMobile"
|
||||
>
|
||||
<Icon name="mdi:truck-outline" size="24"/>
|
||||
<p>Heures Conducteurs</p>
|
||||
@@ -49,6 +81,7 @@
|
||||
:class="route.path.startsWith('/employees')
|
||||
? 'bg-tertiary-500 text-primary-500 font-bold'
|
||||
: ''"
|
||||
@click="closeSidebarOnMobile"
|
||||
>
|
||||
<Icon name="mdi:account-group-outline" size="24"/>
|
||||
<p>Employés</p>
|
||||
@@ -60,6 +93,7 @@
|
||||
:class="route.path.startsWith('/leave-recap')
|
||||
? 'bg-tertiary-500 text-primary-500 font-bold'
|
||||
: ''"
|
||||
@click="closeSidebarOnMobile"
|
||||
>
|
||||
<Icon name="mdi:beach" size="24"/>
|
||||
<p>Récap. congés</p>
|
||||
@@ -70,6 +104,7 @@
|
||||
:class="route.path.startsWith('/sites')
|
||||
? 'bg-tertiary-500 text-primary-500 font-bold'
|
||||
: ''"
|
||||
@click="closeSidebarOnMobile"
|
||||
>
|
||||
<Icon name="mdi:business" size="24"/>
|
||||
<p>Sites</p>
|
||||
@@ -80,6 +115,7 @@
|
||||
:class="route.path.startsWith('/absence-types')
|
||||
? 'bg-tertiary-500 text-primary-500 font-bold'
|
||||
: ''"
|
||||
@click="closeSidebarOnMobile"
|
||||
>
|
||||
<Icon name="mdi:umbrella-beach-outline" size="24"/>
|
||||
<p>Types de statut</p>
|
||||
@@ -90,6 +126,7 @@
|
||||
:class="route.path.startsWith('/users')
|
||||
? 'bg-tertiary-500 text-primary-500 font-bold'
|
||||
: ''"
|
||||
@click="closeSidebarOnMobile"
|
||||
>
|
||||
<Icon name="mdi:account-outline" size="24"/>
|
||||
<p>Utilisateurs</p>
|
||||
@@ -100,6 +137,7 @@
|
||||
to="/leave-recap"
|
||||
class="flex items-center gap-2 py-3 text-md text-black hover:bg-tertiary-500 hover:text-primary-500 pt-3"
|
||||
:class="route.path.startsWith('/leave-recap') ? 'bg-tertiary-500 text-primary-500 font-bold' : ''"
|
||||
@click="closeSidebarOnMobile"
|
||||
>
|
||||
<Icon name="mdi:beach" size="24"/>
|
||||
<p>Récap. congés</p>
|
||||
@@ -111,6 +149,7 @@
|
||||
:class="route.path.startsWith('/audit-logs')
|
||||
? 'bg-tertiary-500 text-primary-500 font-bold'
|
||||
: ''"
|
||||
@click="closeSidebarOnMobile"
|
||||
>
|
||||
<Icon name="mdi:clipboard-text-clock-outline" size="24"/>
|
||||
<p>Journal</p>
|
||||
@@ -121,6 +160,7 @@
|
||||
:class="route.path.startsWith('/documentation')
|
||||
? 'bg-tertiary-500 text-primary-500 font-bold'
|
||||
: ''"
|
||||
@click="closeSidebarOnMobile"
|
||||
>
|
||||
<Icon name="mdi:book-open-page-variant-outline" size="24"/>
|
||||
<p>Documentation</p>
|
||||
@@ -132,9 +172,9 @@
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<div class="h-full flex-1 overflow-hidden flex flex-col">
|
||||
<AppTopNav :user="auth.user" />
|
||||
<main class="flex-1 overflow-y-auto px-8 py-12">
|
||||
<div class="h-full flex-1 overflow-hidden flex flex-col min-w-0">
|
||||
<AppTopNav :user="auth.user" @toggle-sidebar="sidebarOpen = !sidebarOpen" />
|
||||
<main class="flex-1 overflow-y-auto px-4 py-6 lg:px-8 lg:py-12">
|
||||
<slot/>
|
||||
</main>
|
||||
</div>
|
||||
@@ -150,4 +190,9 @@ const isSuperAdmin = computed(() => auth.user?.roles?.includes('ROLE_SUPER_ADMIN
|
||||
const isDriver = computed(() => auth.user?.isDriver ?? false)
|
||||
const hasLeaveRecapAccess = computed(() => auth.user?.hasLeaveRecapAccess ?? false)
|
||||
const route = useRoute()
|
||||
const sidebarOpen = ref(false)
|
||||
|
||||
const closeSidebarOnMobile = () => {
|
||||
sidebarOpen.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="h-full overflow-hidden flex flex-col">
|
||||
<div class="flex flex-wrap items-center justify-between gap-4">
|
||||
<h1 class="text-4xl font-bold text-primary-500">Heures</h1>
|
||||
<h1 class="text-2xl font-bold text-primary-500 lg:text-4xl">Heures</h1>
|
||||
</div>
|
||||
|
||||
<HoursToolbar
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="h-full flex flex-col overflow-hidden">
|
||||
<div class="flex flex-wrap items-center justify-between gap-4 pb-8">
|
||||
<h1 class="text-4xl font-bold text-primary-500">Récap. congés</h1>
|
||||
<h1 class="text-2xl font-bold text-primary-500 lg:text-4xl">Récap. congés</h1>
|
||||
<span
|
||||
v-if="cutoffLabel"
|
||||
class="inline-flex items-center gap-2 rounded-full bg-tertiary-500 px-4 py-1 text-sm font-semibold text-primary-500"
|
||||
@@ -25,7 +25,8 @@
|
||||
Aucun employé à afficher.
|
||||
</div>
|
||||
|
||||
<div v-else class="min-h-0 overflow-auto rounded-md bg-white">
|
||||
<!-- Desktop table -->
|
||||
<div v-else class="min-h-0 overflow-auto rounded-md bg-white hidden lg:block">
|
||||
<div
|
||||
:class="`grid ${gridColsClass} 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`"
|
||||
>
|
||||
@@ -64,6 +65,47 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mobile cards -->
|
||||
<div v-if="!isLoading && rows.length > 0" class="min-h-0 overflow-auto space-y-3 lg:hidden">
|
||||
<div
|
||||
v-for="row in rows"
|
||||
:key="'m-' + row.employeeId"
|
||||
class="rounded-md border border-primary-500 bg-white p-4"
|
||||
>
|
||||
<div class="mb-3 flex items-center justify-between gap-2">
|
||||
<p class="text-md font-bold text-primary-500 truncate">
|
||||
{{ row.lastName }} {{ row.firstName }}
|
||||
</p>
|
||||
<span
|
||||
v-if="showSiteColumn && row.siteName"
|
||||
class="inline-block shrink-0 rounded-full px-3 py-1 text-sm"
|
||||
:style="{ backgroundColor: row.siteColor || '#ffd7d7', color: '#1a1a1a' }"
|
||||
>
|
||||
{{ row.siteName }}
|
||||
</span>
|
||||
</div>
|
||||
<p v-if="row.contractName" class="mb-3 text-sm text-neutral-600">{{ row.contractName }}</p>
|
||||
<div class="grid grid-cols-2 gap-x-4 gap-y-2 text-sm">
|
||||
<div class="flex justify-between">
|
||||
<span class="text-neutral-500">CP N-1</span>
|
||||
<span class="font-bold text-primary-500 tabular-nums">{{ formatNumber(row.cpN1Remaining) }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-neutral-500">Samedis</span>
|
||||
<span class="font-bold text-primary-500 tabular-nums">{{ row.acquiredSaturdays }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-neutral-500">CP N</span>
|
||||
<span class="font-bold text-primary-500 tabular-nums">{{ formatNumber(row.cpN) }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span class="text-neutral-500">RTT</span>
|
||||
<span class="font-bold text-primary-500 tabular-nums">{{ row.rtt }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<template>
|
||||
<div class="h-full flex flex-col overflow-hidden">
|
||||
<div class="flex items-center justify-between pb-6">
|
||||
<h1 class="text-4xl font-bold text-primary-500">Utilisateurs</h1>
|
||||
<h1 class="text-2xl font-bold text-primary-500 lg:text-4xl">Utilisateurs</h1>
|
||||
<button
|
||||
type="button"
|
||||
class="rounded-lg bg-primary-500 px-4 py-2 text-md font-semibold text-white hover:bg-secondary-500"
|
||||
class="rounded-lg bg-primary-500 px-3 py-2 text-sm font-semibold text-white hover:bg-secondary-500 lg:px-4 lg:text-md"
|
||||
@click="openCreate"
|
||||
>
|
||||
+ Ajouter un utilisateur
|
||||
+ Ajouter
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -18,7 +18,8 @@
|
||||
Aucun utilisateur pour le moment.
|
||||
</div>
|
||||
|
||||
<div v-else class="min-h-0 overflow-auto rounded-md bg-white">
|
||||
<!-- 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>
|
||||
@@ -56,6 +57,42 @@
|
||||
</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>
|
||||
|
||||
<AppDrawer
|
||||
v-model="isDrawerOpen"
|
||||
:title="editingUser ? 'Modifier un utilisateur' : 'Ajouter un utilisateur'"
|
||||
|
||||
Reference in New Issue
Block a user