229 lines
7.8 KiB
Vue
229 lines
7.8 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 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 mb-6">
|
|
<div class="card-body">
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div class="form-control">
|
|
<label class="label">
|
|
<span class="label-text">Sites</span>
|
|
</label>
|
|
<div class="flex flex-wrap gap-3">
|
|
<label
|
|
v-for="site in sites"
|
|
:key="site.id"
|
|
class="flex items-center gap-2 cursor-pointer"
|
|
>
|
|
<input
|
|
type="checkbox"
|
|
class="checkbox checkbox-sm"
|
|
:checked="selectedSites.has(site.id)"
|
|
@change="selectedSites.has(site.id) ? selectedSites.delete(site.id) : selectedSites.add(site.id)"
|
|
>
|
|
<span class="text-sm">{{ site.name }}</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
<div class="form-control">
|
|
<label class="label">
|
|
<span class="label-text">Recherche</span>
|
|
</label>
|
|
<input
|
|
v-model="searchQuery"
|
|
type="text"
|
|
placeholder="Rechercher par nom ou référence..."
|
|
class="input input-bordered"
|
|
>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="loading" class="flex justify-center items-center py-12">
|
|
<span class="loading loading-spinner loading-lg" />
|
|
</div>
|
|
|
|
<div v-else-if="filteredMachines.length === 0" class="text-center py-12">
|
|
<div class="max-w-md mx-auto">
|
|
<IconLucideFactory class="w-16 h-16 mx-auto text-gray-400 mb-4" aria-hidden="true" />
|
|
<h3 class="text-lg font-medium text-gray-900 mb-2">
|
|
Aucune machine trouvée
|
|
</h3>
|
|
<p class="text-gray-500 mb-4">
|
|
Commencez par ajouter votre première machine.
|
|
</p>
|
|
<NuxtLink to="/machines/new" class="btn btn-primary">
|
|
Ajouter une machine
|
|
</NuxtLink>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-else class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
<div
|
|
v-for="machine in filteredMachines"
|
|
:key="machine.id"
|
|
class="card site-card shadow-md hover:shadow-xl transition-shadow cursor-pointer overflow-hidden"
|
|
:style="{
|
|
borderTop: machine.site?.color ? `4px solid ${machine.site.color}` : '4px solid transparent',
|
|
background: machine.site?.color ? `linear-gradient(160deg, ${machine.site.color}30 0%, ${machine.site.color}08 40%, var(--color-base-100) 100%)` : undefined,
|
|
}"
|
|
@click="viewMachineDetails(machine)"
|
|
>
|
|
<div class="card-body flex flex-col">
|
|
<div class="flex items-center justify-between mb-2">
|
|
<h3 class="card-title text-lg">
|
|
{{ machine.name }}
|
|
</h3>
|
|
</div>
|
|
|
|
<div class="space-y-2 text-sm">
|
|
<div class="flex items-center gap-2">
|
|
<IconLucideMapPin class="w-4 h-4" :style="{ color: machine.site?.color || '#3b82f6' }" aria-hidden="true" />
|
|
<span
|
|
class="font-bold text-sm px-2.5 py-1 rounded-lg text-base-content"
|
|
:style="machine.site?.color ? { backgroundColor: machine.site.color + '30', border: `1px solid ${machine.site.color}40` } : {}"
|
|
>{{ machine.site?.name || 'Site inconnu' }}</span>
|
|
</div>
|
|
|
|
<div v-if="machine.reference" class="flex items-center gap-2">
|
|
<IconLucideTag class="w-4 h-4 text-orange-500" aria-hidden="true" />
|
|
<span class="text-gray-600">{{ machine.reference }}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mt-auto pt-3 flex items-center justify-end gap-2">
|
|
<button v-if="canEdit" class="btn btn-ghost btn-sm" @click.stop="editMachine(machine)">
|
|
Modifier
|
|
</button>
|
|
<button v-if="canEdit" class="btn btn-ghost btn-sm text-error" @click.stop="confirmDeleteMachine(machine)">
|
|
Supprimer
|
|
</button>
|
|
<NuxtLink :to="`/machine/${machine.id}`" class="btn btn-primary btn-sm">
|
|
Détails
|
|
</NuxtLink>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, reactive, computed, onMounted, watch } from '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 IconLucidePlus from '~icons/lucide/plus'
|
|
import IconLucideFactory from '~icons/lucide/factory'
|
|
import IconLucideMapPin from '~icons/lucide/map-pin'
|
|
import IconLucideTag from '~icons/lucide/tag'
|
|
|
|
const { canEdit } = usePermissions()
|
|
const { machines, loading, loadMachines, deleteMachine } = useMachines()
|
|
const { sites, loadSites } = useSites()
|
|
const toast = useToast()
|
|
|
|
const urlState = useUrlState({
|
|
q: { default: '', debounce: 300 },
|
|
sites: { default: '' },
|
|
})
|
|
|
|
const searchQuery = urlState.q
|
|
const selectedSites = reactive(new Set())
|
|
|
|
// Sync URL → selectedSites on load and back/forward
|
|
watch(urlState.sites, (val) => {
|
|
selectedSites.clear()
|
|
if (val) {
|
|
for (const id of String(val).split(',')) {
|
|
if (id) selectedSites.add(id)
|
|
}
|
|
}
|
|
}, { immediate: true })
|
|
|
|
// Sync selectedSites → URL
|
|
watch(() => [...selectedSites], (ids) => {
|
|
urlState.sites.value = ids.join(',')
|
|
})
|
|
|
|
// Enrichir les machines avec les objets site complets
|
|
const enrichedMachines = computed(() => {
|
|
return machines.value.map((machine) => {
|
|
const site = sites.value.find(s => s.id === machine.siteId)
|
|
return {
|
|
...machine,
|
|
site: site || null,
|
|
}
|
|
})
|
|
})
|
|
|
|
const filteredMachines = computed(() => {
|
|
let filtered = enrichedMachines.value
|
|
|
|
if (selectedSites.size > 0) {
|
|
filtered = filtered.filter(machine => selectedSites.has(machine.siteId))
|
|
}
|
|
|
|
if (searchQuery.value.trim()) {
|
|
const term = searchQuery.value.trim().toLowerCase()
|
|
filtered = filtered.filter(machine =>
|
|
machine.name?.toLowerCase().includes(term)
|
|
|| machine.reference?.toLowerCase().includes(term),
|
|
)
|
|
}
|
|
|
|
filtered = [...filtered].sort((a, b) =>
|
|
(a.name || '').localeCompare(b.name || '', 'fr')
|
|
)
|
|
|
|
return filtered
|
|
})
|
|
|
|
const viewMachineDetails = (machine) => {
|
|
navigateTo(`/machine/${machine.id}`)
|
|
}
|
|
|
|
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>
|