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>
235 lines
7.7 KiB
Vue
235 lines
7.7 KiB
Vue
<template>
|
|
<main class="container mx-auto px-6 py-8">
|
|
<div class="my-8">
|
|
<div class="flex justify-between items-center mb-6">
|
|
<h2 class="text-2xl font-bold">
|
|
Parc Machines
|
|
</h2>
|
|
<NuxtLink v-if="canEdit" to="/machines/new" class="btn btn-primary">
|
|
<IconLucidePlus class="w-5 h-5 mr-2" aria-hidden="true" />
|
|
Ajouter une machine
|
|
</NuxtLink>
|
|
</div>
|
|
|
|
<div class="card bg-base-100 shadow-sm">
|
|
<div class="card-body space-y-4">
|
|
<DataTable
|
|
:columns="columns"
|
|
:rows="filteredMachines"
|
|
:loading="loading"
|
|
:sort="currentSort"
|
|
:show-counter="true"
|
|
empty-message="Aucune machine trouvée."
|
|
no-results-message="Aucune machine ne correspond à vos filtres."
|
|
@sort="handleSort"
|
|
>
|
|
<template #toolbar>
|
|
<label class="w-full sm:w-72">
|
|
<span class="text-xs font-semibold uppercase tracking-wide text-base-content/70">Recherche</span>
|
|
<input
|
|
v-model="searchQuery"
|
|
type="search"
|
|
class="input input-bordered input-sm w-full mt-1"
|
|
placeholder="Nom ou référence..."
|
|
>
|
|
</label>
|
|
|
|
<div class="flex flex-col">
|
|
<span class="text-xs font-semibold uppercase tracking-wide text-base-content/70">Site</span>
|
|
<select v-model="selectedSiteId" class="select select-bordered select-sm mt-1">
|
|
<option value="">Tous les sites</option>
|
|
<option v-for="site in sites" :key="site.id" :value="site.id">
|
|
{{ site.name }}
|
|
</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="flex flex-col">
|
|
<span class="text-xs font-semibold uppercase tracking-wide text-base-content/70">Date de création</span>
|
|
<div class="flex items-center gap-2 mt-1">
|
|
<input
|
|
v-model="dateFrom"
|
|
type="date"
|
|
class="input input-bordered input-sm"
|
|
>
|
|
<span class="text-xs text-base-content/50">à</span>
|
|
<input
|
|
v-model="dateTo"
|
|
type="date"
|
|
class="input input-bordered input-sm"
|
|
>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<template #cell-site="{ row }">
|
|
<span
|
|
v-if="row.site"
|
|
class="badge badge-sm font-bold"
|
|
:style="row.site.color ? { backgroundColor: row.site.color + '30', color: row.site.color, borderColor: row.site.color + '50' } : {}"
|
|
:class="!row.site.color ? 'badge-ghost' : ''"
|
|
>
|
|
{{ row.site.name }}
|
|
</span>
|
|
<span v-else class="text-base-content/30">—</span>
|
|
</template>
|
|
|
|
<template #cell-createdAt="{ row }">
|
|
<span class="whitespace-nowrap">{{ formatDate(row.createdAt) }}</span>
|
|
</template>
|
|
|
|
<template #cell-actions="{ row }">
|
|
<div class="flex items-center justify-end gap-2">
|
|
<button v-if="canEdit" class="btn btn-ghost btn-xs" @click="editMachine(row)">
|
|
Modifier
|
|
</button>
|
|
<button v-if="canEdit" class="btn btn-ghost btn-xs text-error" @click="confirmDeleteMachine(row)">
|
|
Supprimer
|
|
</button>
|
|
<NuxtLink :to="`/machine/${row.id}`" class="btn btn-primary btn-xs">
|
|
Détails
|
|
</NuxtLink>
|
|
</div>
|
|
</template>
|
|
</DataTable>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, computed, onMounted } from 'vue'
|
|
import DataTable from '~/components/common/DataTable.vue'
|
|
import { useMachines } from '~/composables/useMachines'
|
|
import { useSites } from '~/composables/useSites'
|
|
import { useToast } from '~/composables/useToast'
|
|
import { humanizeError } from '~/shared/utils/errorMessages'
|
|
import { useUrlState } from '~/composables/useUrlState'
|
|
import { usePersistedValue } from '~/composables/usePersistedValue'
|
|
import { formatFrenchDate } from '~/utils/date'
|
|
import IconLucidePlus from '~icons/lucide/plus'
|
|
|
|
const { canEdit } = usePermissions()
|
|
const { machines, loading, loadMachines, deleteMachine } = useMachines()
|
|
const { sites, loadSites } = useSites()
|
|
const toast = useToast()
|
|
|
|
const urlState = useUrlState({
|
|
q: { default: '', debounce: 300 },
|
|
site: { default: '' },
|
|
from: { default: '' },
|
|
to: { default: '' },
|
|
})
|
|
|
|
const searchQuery = urlState.q
|
|
const selectedSiteId = urlState.site
|
|
const dateFrom = urlState.from
|
|
const dateTo = urlState.to
|
|
|
|
const sortKey = usePersistedValue('machines-sort', 'name')
|
|
const sortDir = ref('asc')
|
|
|
|
const currentSort = computed(() => ({
|
|
field: sortKey.value,
|
|
direction: sortDir.value,
|
|
}))
|
|
|
|
const handleSort = (sort) => {
|
|
sortKey.value = sort.field
|
|
sortDir.value = sort.direction
|
|
}
|
|
|
|
const columns = [
|
|
{ key: 'name', label: 'Nom', sortable: true },
|
|
{ key: 'reference', label: 'Référence', sortable: true },
|
|
{ key: 'site', label: 'Site', sortable: true, sortKey: 'siteName' },
|
|
{ key: 'createdAt', label: 'Date de création', sortable: true },
|
|
{ key: 'actions', label: 'Actions', align: 'right' },
|
|
]
|
|
|
|
const formatDate = formatFrenchDate
|
|
|
|
const enrichedMachines = computed(() => {
|
|
return machines.value.map((machine) => {
|
|
const site = sites.value.find(s => s.id === machine.siteId)
|
|
return {
|
|
...machine,
|
|
site: site || null,
|
|
siteName: site?.name || '',
|
|
}
|
|
})
|
|
})
|
|
|
|
const filteredMachines = computed(() => {
|
|
let filtered = enrichedMachines.value
|
|
|
|
if (selectedSiteId.value) {
|
|
filtered = filtered.filter(m => m.siteId === selectedSiteId.value)
|
|
}
|
|
|
|
if (searchQuery.value.trim()) {
|
|
const term = searchQuery.value.trim().toLowerCase()
|
|
filtered = filtered.filter(m =>
|
|
m.name?.toLowerCase().includes(term)
|
|
|| m.reference?.toLowerCase().includes(term),
|
|
)
|
|
}
|
|
|
|
if (dateFrom.value) {
|
|
const from = new Date(dateFrom.value)
|
|
from.setHours(0, 0, 0, 0)
|
|
filtered = filtered.filter(m => m.createdAt && new Date(m.createdAt) >= from)
|
|
}
|
|
|
|
if (dateTo.value) {
|
|
const to = new Date(dateTo.value)
|
|
to.setHours(23, 59, 59, 999)
|
|
filtered = filtered.filter(m => m.createdAt && new Date(m.createdAt) <= to)
|
|
}
|
|
|
|
const key = sortKey.value
|
|
const dir = sortDir.value === 'desc' ? -1 : 1
|
|
filtered = [...filtered].sort((a, b) => {
|
|
if (key === 'createdAt') {
|
|
return dir * (new Date(a[key] || 0).getTime() - new Date(b[key] || 0).getTime())
|
|
}
|
|
const valA = (key === 'siteName' ? a.siteName : a[key]) || ''
|
|
const valB = (key === 'siteName' ? b.siteName : b[key]) || ''
|
|
return dir * String(valA).localeCompare(String(valB), 'fr')
|
|
})
|
|
|
|
return filtered
|
|
})
|
|
|
|
const editMachine = (machine) => {
|
|
navigateTo(`/machine/${machine.id}?edit=true`)
|
|
}
|
|
|
|
const { confirm: confirmDialog } = useConfirm()
|
|
|
|
const confirmDeleteMachine = async (machine) => {
|
|
const { showError, showSuccess } = toast
|
|
|
|
if (await confirmDialog({ message: `Êtes-vous sûr de vouloir supprimer la machine "${machine.name}" ? Cette action est irréversible.` })) {
|
|
try {
|
|
const result = await deleteMachine(machine.id)
|
|
if (result.success) {
|
|
showSuccess(`Machine "${machine.name}" supprimée avec succès`)
|
|
} else {
|
|
showError(`Impossible de supprimer la machine : ${result.error}`)
|
|
}
|
|
} catch (error) {
|
|
showError(`Impossible de supprimer la machine : ${humanizeError(error.message)}`)
|
|
}
|
|
}
|
|
}
|
|
|
|
onMounted(async () => {
|
|
await Promise.all([
|
|
loadMachines(),
|
|
loadSites(),
|
|
])
|
|
})
|
|
</script>
|