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: #13 Co-authored-by: tristan <tristan@yuno.malio.fr> Co-committed-by: tristan <tristan@yuno.malio.fr>
255 lines
9.0 KiB
Vue
255 lines
9.0 KiB
Vue
<template>
|
|
<div class="h-full flex flex-col overflow-hidden">
|
|
<h1 class="text-4xl font-bold text-primary-500 pb-6">Journal des actions</h1>
|
|
|
|
<div class="flex items-end gap-4 pb-6 flex-wrap">
|
|
<div>
|
|
<label class="text-md font-semibold text-neutral-700">Employé</label>
|
|
<select
|
|
v-model="filters.employeeId"
|
|
class="mt-2 h-[42px] w-full rounded-md border border-neutral-300 px-3 py-2 text-md text-neutral-900 focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-secondary-500/20"
|
|
>
|
|
<option :value="undefined">Tous</option>
|
|
<option v-for="emp in employees" :key="emp.id" :value="emp.id">
|
|
{{ emp.lastName }} {{ emp.firstName }}
|
|
</option>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label class="text-md font-semibold text-neutral-700">Du</label>
|
|
<input
|
|
v-model="filters.from"
|
|
type="date"
|
|
class="mt-2 w-full rounded-md border border-neutral-300 px-3 py-2 text-md text-neutral-900 focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-secondary-500/20"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label class="text-md font-semibold text-neutral-700">Au</label>
|
|
<input
|
|
v-model="filters.to"
|
|
type="date"
|
|
class="mt-2 w-full rounded-md border border-neutral-300 px-3 py-2 text-md text-neutral-900 focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-secondary-500/20"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label class="text-md font-semibold text-neutral-700">Type</label>
|
|
<select
|
|
v-model="filters.entityType"
|
|
class="mt-2 h-[42px] w-full rounded-md border border-neutral-300 px-3 py-2 text-md text-neutral-900 focus:border-primary-500 focus:outline-none focus:ring-2 focus:ring-secondary-500/20"
|
|
>
|
|
<option :value="undefined">Tous</option>
|
|
<option value="work_hour">Heures</option>
|
|
<option value="absence">Absences</option>
|
|
<option value="employee">Employé</option>
|
|
<option value="contract_suspension">Suspension</option>
|
|
<option value="rtt_payment">Paiement RTT</option>
|
|
<option value="fractioned_days">Jours fractionnés</option>
|
|
<option value="paid_leave_days">Congés N-1 payés</option>
|
|
</select>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
class="h-[42px] rounded-lg bg-primary-500 px-4 py-2 text-md font-semibold text-white hover:bg-secondary-500"
|
|
@click="search"
|
|
>
|
|
Rechercher
|
|
</button>
|
|
</div>
|
|
|
|
<div v-if="isLoading" class="rounded-lg border border-neutral-200 bg-white p-6 text-md text-neutral-600">
|
|
Chargement...
|
|
</div>
|
|
|
|
<div v-else-if="logs.length === 0" class="rounded-lg border border-neutral-200 bg-white p-6 text-md text-neutral-600">
|
|
Aucune entrée trouvée.
|
|
</div>
|
|
|
|
<template v-else>
|
|
<div class="min-h-0 flex-1 overflow-auto rounded-md bg-white">
|
|
<div class="grid grid-cols-[140px_110px_90px_100px_150px_1fr_130px] 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>Date action</span>
|
|
<span>Utilisateur</span>
|
|
<span>Action</span>
|
|
<span>Type</span>
|
|
<span>Employé</span>
|
|
<span>Description</span>
|
|
<span>Date affectée</span>
|
|
</div>
|
|
<div class="border-x border-b border-primary-500 rounded-b-md">
|
|
<template v-for="log in logs" :key="log.id">
|
|
<div
|
|
class="grid grid-cols-[140px_110px_90px_100px_150px_1fr_130px] 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="toggleExpand(log.id)"
|
|
>
|
|
<span>{{ formatDateTime(log.createdAt) }}</span>
|
|
<span>{{ log.username }}</span>
|
|
<span>
|
|
<span class="rounded px-2 py-0.5 text-xs font-bold text-white" :class="actionClass(log.action)">
|
|
{{ actionLabel(log.action) }}
|
|
</span>
|
|
</span>
|
|
<span>{{ entityTypeLabel(log.entityType) }}</span>
|
|
<span>{{ log.employeeName ?? '-' }}</span>
|
|
<span class="truncate font-normal" :title="log.description">{{ log.description }}</span>
|
|
<span>{{ log.affectedDate ? formatDate(log.affectedDate) : '-' }}</span>
|
|
</div>
|
|
|
|
<div
|
|
v-if="expandedIds.has(log.id)"
|
|
class="border-b border-primary-500 px-6 py-4 bg-neutral-50"
|
|
>
|
|
<div v-if="log.changes" class="grid grid-cols-2 gap-6 text-sm font-mono">
|
|
<div v-if="log.changes.old">
|
|
<p class="font-bold text-red-600 mb-2">Ancien</p>
|
|
<pre class="whitespace-pre-wrap text-neutral-700">{{ JSON.stringify(log.changes.old, null, 2) }}</pre>
|
|
</div>
|
|
<div v-if="log.changes.new">
|
|
<p class="font-bold text-green-600 mb-2">Nouveau</p>
|
|
<pre class="whitespace-pre-wrap text-neutral-700">{{ JSON.stringify(log.changes.new, null, 2) }}</pre>
|
|
</div>
|
|
</div>
|
|
<p v-else class="text-md text-neutral-400">Pas de détail disponible.</p>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex items-center justify-between pt-4">
|
|
<p class="text-md text-neutral-500">
|
|
{{ total }} résultat{{ total > 1 ? 's' : '' }} — page {{ currentPage }}/{{ totalPages }}
|
|
</p>
|
|
<div class="flex gap-3">
|
|
<button
|
|
type="button"
|
|
:disabled="currentPage <= 1"
|
|
class="rounded-lg border border-primary-500 px-4 py-2 text-md font-semibold text-primary-500 hover:bg-tertiary-500 disabled:opacity-30 disabled:cursor-not-allowed"
|
|
@click="goToPage(currentPage - 1)"
|
|
>
|
|
Précédent
|
|
</button>
|
|
<button
|
|
type="button"
|
|
:disabled="currentPage >= totalPages"
|
|
class="rounded-lg bg-primary-500 px-4 py-2 text-md font-semibold text-white hover:bg-secondary-500 disabled:opacity-30 disabled:cursor-not-allowed"
|
|
@click="goToPage(currentPage + 1)"
|
|
>
|
|
Suivant
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, reactive, computed, onMounted } from 'vue'
|
|
import type { AuditLog } from '~/services/dto/audit-log'
|
|
import type { Employee } from '~/services/dto/employee'
|
|
import { fetchAuditLogs } from '~/services/audit-logs'
|
|
import { listEmployees } from '~/services/employees'
|
|
|
|
definePageMeta({
|
|
middleware: 'super-admin'
|
|
})
|
|
|
|
useHead({ title: 'Journal des actions' })
|
|
|
|
const logs = ref<AuditLog[]>([])
|
|
const employees = ref<Employee[]>([])
|
|
const isLoading = ref(false)
|
|
const expandedIds = ref(new Set<number>())
|
|
const total = ref(0)
|
|
const currentPage = ref(1)
|
|
const perPage = ref(50)
|
|
|
|
const totalPages = computed(() => Math.max(1, Math.ceil(total.value / perPage.value)))
|
|
|
|
const filters = reactive<{
|
|
employeeId?: number
|
|
from?: string
|
|
to?: string
|
|
entityType?: string
|
|
}>({})
|
|
|
|
const loadLogs = async (page = 1) => {
|
|
isLoading.value = true
|
|
try {
|
|
const result = await fetchAuditLogs({ ...filters, page })
|
|
logs.value = result.items
|
|
total.value = result.total
|
|
currentPage.value = result.page
|
|
perPage.value = result.perPage
|
|
expandedIds.value.clear()
|
|
} finally {
|
|
isLoading.value = false
|
|
}
|
|
}
|
|
|
|
const search = () => {
|
|
loadLogs(1)
|
|
}
|
|
|
|
const goToPage = (page: number) => {
|
|
if (page >= 1 && page <= totalPages.value) {
|
|
loadLogs(page)
|
|
}
|
|
}
|
|
|
|
const toggleExpand = (id: number) => {
|
|
if (expandedIds.value.has(id)) {
|
|
expandedIds.value.delete(id)
|
|
} else {
|
|
expandedIds.value.add(id)
|
|
}
|
|
}
|
|
|
|
const formatDateTime = (dt: string) => {
|
|
const d = new Date(dt)
|
|
return d.toLocaleDateString('fr-FR') + ' ' + d.toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' })
|
|
}
|
|
|
|
const formatDate = (d: string) => {
|
|
return d.split('-').reverse().join('/')
|
|
}
|
|
|
|
const actionLabel = (action: string): string => {
|
|
const map: Record<string, string> = {
|
|
create: 'Créer',
|
|
update: 'Modifier',
|
|
delete: 'Suppr.',
|
|
validate: 'Valid.',
|
|
site_validate: 'Valid. site',
|
|
}
|
|
return map[action] ?? action
|
|
}
|
|
|
|
const actionClass = (action: string): string => {
|
|
const map: Record<string, string> = {
|
|
create: 'bg-green-500',
|
|
update: 'bg-blue-500',
|
|
delete: 'bg-red-500',
|
|
validate: 'bg-purple-500',
|
|
site_validate: 'bg-indigo-500',
|
|
}
|
|
return map[action] ?? 'bg-neutral-500'
|
|
}
|
|
|
|
const entityTypeLabel = (type: string): string => {
|
|
const map: Record<string, string> = {
|
|
work_hour: 'Heures',
|
|
absence: 'Absence',
|
|
employee: 'Employé',
|
|
contract_suspension: 'Suspension',
|
|
rtt_payment: 'RTT',
|
|
fractioned_days: 'Fract.',
|
|
paid_leave_days: 'Congés payés',
|
|
}
|
|
return map[type] ?? type
|
|
}
|
|
|
|
onMounted(async () => {
|
|
employees.value = await listEmployees()
|
|
await loadLogs()
|
|
})
|
|
</script>
|