Some checks failed
Auto Tag Develop / tag (push) Has been cancelled
- Parc Machines transformé en DataTable avec filtres (site, date création, recherche) - Vue d'ensemble : ajout filtre par plage de dates de création - Activity-log : correction des liens entités (routes singulier sans /edit, ajout machine/document/model_type) - ComponentItem & PieceItem : refonte complète des cartes dépliantes (design industriel raffiné) - Header compact avec tags colorés contrastés (référence, réf. auto, prix, produit, champs machine) - Panneau déplié structuré en sections avec mini-headers - Bordure gauche primary pour hiérarchie visuelle - Ajout referenceAuto dans header et infos pour composants et pièces - Suppression double encadrement ComponentHierarchy Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
243 lines
7.6 KiB
Vue
243 lines
7.6 KiB
Vue
<template>
|
|
<main class="container mx-auto px-6 py-10 space-y-8">
|
|
<header>
|
|
<h1 class="text-3xl font-semibold text-base-content">Journal d'activité</h1>
|
|
<p class="text-sm text-gray-500">
|
|
Historique des modifications sur l'ensemble des pièces, produits et composants.
|
|
</p>
|
|
</header>
|
|
|
|
<section class="card border border-base-200 bg-base-100 shadow-sm">
|
|
<div class="card-body space-y-4">
|
|
<DataTable
|
|
:columns="columns"
|
|
:rows="entries"
|
|
:loading="loading"
|
|
:pagination="paginationState"
|
|
:show-per-page="true"
|
|
:show-counter="true"
|
|
:expandable="true"
|
|
:expanded-keys="expandedIds"
|
|
:can-expand="canExpandRow"
|
|
row-key="id"
|
|
empty-message="Aucune activité enregistrée."
|
|
no-results-message="Aucune activité ne correspond à vos filtres."
|
|
@update:current-page="table.handlePageChange"
|
|
@update:per-page="table.handlePerPageChange"
|
|
@toggle-expand="toggleExpanded"
|
|
>
|
|
<template #toolbar>
|
|
<div class="flex items-center gap-2">
|
|
<label
|
|
class="text-xs font-semibold uppercase tracking-wide text-base-content/70"
|
|
for="activity-entity-type"
|
|
>
|
|
Type
|
|
</label>
|
|
<select
|
|
id="activity-entity-type"
|
|
v-model="entityTypeFilter"
|
|
class="select select-bordered select-sm"
|
|
@change="table.handleFilterChange"
|
|
>
|
|
<option value="">Tous</option>
|
|
<option value="piece">Pièce</option>
|
|
<option value="product">Produit</option>
|
|
<option value="composant">Composant</option>
|
|
<option value="machine">Machine</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="flex items-center gap-2">
|
|
<label
|
|
class="text-xs font-semibold uppercase tracking-wide text-base-content/70"
|
|
for="activity-action"
|
|
>
|
|
Action
|
|
</label>
|
|
<select
|
|
id="activity-action"
|
|
v-model="actionFilter"
|
|
class="select select-bordered select-sm"
|
|
@change="table.handleFilterChange"
|
|
>
|
|
<option value="">Toutes</option>
|
|
<option value="create">Création</option>
|
|
<option value="update">Modification</option>
|
|
<option value="delete">Suppression</option>
|
|
</select>
|
|
</div>
|
|
</template>
|
|
|
|
<template #cell-createdAt="{ row }">
|
|
<span class="whitespace-nowrap">{{ formatHistoryDate(row.createdAt) }}</span>
|
|
</template>
|
|
|
|
<template #cell-action="{ row }">
|
|
<span
|
|
class="badge badge-sm"
|
|
:class="actionBadgeClass(row.action)"
|
|
>
|
|
{{ historyActionLabel(row.action) }}
|
|
</span>
|
|
</template>
|
|
|
|
<template #cell-entityType="{ row }">
|
|
<span class="badge badge-ghost badge-sm">
|
|
{{ entityTypeLabel(row.entityType) }}
|
|
</span>
|
|
</template>
|
|
|
|
<template #cell-entity="{ row }">
|
|
<NuxtLink
|
|
v-if="row.action !== 'delete' && entityEditLink(row) !== '#'"
|
|
:to="entityEditLink(row)"
|
|
class="link link-hover link-primary"
|
|
>
|
|
{{ row.entityName || 'Sans nom' }}
|
|
</NuxtLink>
|
|
<span v-else-if="row.action === 'delete'" class="text-base-content/50 line-through">
|
|
{{ row.entityName || 'Sans nom' }}
|
|
</span>
|
|
<span v-else>
|
|
{{ row.entityName || 'Sans nom' }}
|
|
</span>
|
|
<span
|
|
v-if="row.entityRef"
|
|
class="text-xs text-base-content/50 ml-1"
|
|
>
|
|
({{ row.entityRef }})
|
|
</span>
|
|
</template>
|
|
|
|
<template #cell-author="{ row }">
|
|
{{ row.actor?.label || '—' }}
|
|
</template>
|
|
|
|
<template #row-expanded="{ row }">
|
|
<div class="space-y-1 text-sm">
|
|
<div
|
|
v-for="diffEntry in historyDiffEntries(row, globalFieldLabels)"
|
|
:key="diffEntry.field"
|
|
class="flex gap-2"
|
|
>
|
|
<span class="font-medium min-w-[10rem]">{{ diffEntry.label }} :</span>
|
|
<span class="text-error line-through">{{ diffEntry.fromLabel }}</span>
|
|
<span>→</span>
|
|
<span class="text-success">{{ diffEntry.toLabel }}</span>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</DataTable>
|
|
</div>
|
|
</section>
|
|
</main>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { computed, onMounted, reactive, type Ref } from 'vue'
|
|
import DataTable from '~/components/common/DataTable.vue'
|
|
import { useActivityLog } from '~/composables/useActivityLog'
|
|
import type { ActivityLogEntry } from '~/composables/useActivityLog'
|
|
import { useDataTable } from '~/composables/useDataTable'
|
|
import {
|
|
historyActionLabel,
|
|
formatHistoryDate,
|
|
historyDiffEntries,
|
|
} from '~/shared/utils/historyDisplayUtils'
|
|
|
|
const { entries, total, loading, loadActivityLog } = useActivityLog()
|
|
|
|
const table = useDataTable(
|
|
{ fetchData: fetchLog },
|
|
{
|
|
defaultSort: 'createdAt',
|
|
defaultDirection: 'desc',
|
|
defaultPerPage: 50,
|
|
persistToUrl: true,
|
|
extraParams: {
|
|
entityType: { default: '' },
|
|
action: { default: '' },
|
|
},
|
|
},
|
|
)
|
|
|
|
const entityTypeFilter = table.filters.entityType as Ref<string>
|
|
const actionFilter = table.filters.action as Ref<string>
|
|
|
|
const entriesOnPage = computed(() => entries.value.length)
|
|
const paginationState = table.pagination(total, entriesOnPage)
|
|
|
|
const columns = [
|
|
{ key: 'createdAt', label: 'Date' },
|
|
{ key: 'action', label: 'Action' },
|
|
{ key: 'entityType', label: 'Type' },
|
|
{ key: 'entity', label: 'Entité' },
|
|
{ key: 'author', label: 'Auteur' },
|
|
]
|
|
|
|
const expandedIds = reactive(new Set<string>())
|
|
|
|
const toggleExpanded = (id: string) => {
|
|
if (expandedIds.has(id)) expandedIds.delete(id)
|
|
else expandedIds.add(id)
|
|
}
|
|
|
|
const canExpandRow = (row: any) =>
|
|
row.diff !== null && row.diff !== undefined && Object.keys(row.diff).length > 0
|
|
|
|
function fetchLog() {
|
|
loadActivityLog({
|
|
page: table.currentPage.value,
|
|
itemsPerPage: table.itemsPerPage.value,
|
|
entityType: entityTypeFilter.value || undefined,
|
|
action: actionFilter.value || undefined,
|
|
})
|
|
}
|
|
|
|
const ENTITY_TYPE_LABELS: Record<string, string> = {
|
|
piece: 'Pièce',
|
|
product: 'Produit',
|
|
composant: 'Composant',
|
|
machine: 'Machine',
|
|
document: 'Document',
|
|
model_type: 'Modèle',
|
|
}
|
|
|
|
const entityTypeLabel = (type: string) => ENTITY_TYPE_LABELS[type] ?? type
|
|
|
|
const ENTITY_ROUTES: Record<string, string> = {
|
|
piece: '/piece',
|
|
product: '/product',
|
|
composant: '/component',
|
|
machine: '/machine',
|
|
}
|
|
|
|
const entityEditLink = (entry: ActivityLogEntry) => {
|
|
const base = ENTITY_ROUTES[entry.entityType] ?? ''
|
|
return base ? `${base}/${entry.entityId}` : '#'
|
|
}
|
|
|
|
const actionBadgeClass = (action: string) => {
|
|
if (action === 'create') return 'badge-success'
|
|
if (action === 'delete') return 'badge-error'
|
|
return 'badge-warning'
|
|
}
|
|
|
|
const globalFieldLabels: Record<string, string> = {
|
|
name: 'Nom',
|
|
reference: 'Référence',
|
|
prix: 'Prix',
|
|
supplierPrice: 'Prix fournisseur',
|
|
typePiece: 'Type de pièce',
|
|
typeProduct: 'Type de produit',
|
|
typeComposant: 'Type de composant',
|
|
product: 'Produit',
|
|
productIds: 'Produits',
|
|
constructeurIds: 'Fournisseurs',
|
|
structure: 'Structure',
|
|
}
|
|
|
|
onMounted(fetchLog)
|
|
</script>
|