Initial commit: Import existing Laravel project

This commit is contained in:
jeremy bayse
2026-06-15 08:12:33 +02:00
parent 7420d1b466
commit 030d76af53
143 changed files with 21885 additions and 1 deletions

View File

@@ -0,0 +1,307 @@
<script setup>
import { useForm, Head, Link } from '@inertiajs/vue3';
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue';
import InputError from '@/Components/InputError.vue';
import InputLabel from '@/Components/InputLabel.vue';
import TextInput from '@/Components/TextInput.vue';
import PrimaryButton from '@/Components/PrimaryButton.vue';
const props = defineProps({
hardware: {
type: Object,
default: () => null
},
orders: Array,
isEdit: Boolean,
});
// Initialisation du formulaire Inertia
const form = useForm({
name: props.hardware?.data?.name || '',
type: props.hardware?.data?.type || '',
brand: props.hardware?.data?.brand || '',
model: props.hardware?.data?.model || '',
serial_number: props.hardware?.data?.serial_number || '',
status: props.hardware?.data?.status || 'en_stock',
purchase_date: props.hardware?.data?.purchase_date || '',
commissioning_date: props.hardware?.data?.commissioning_date || '',
warranty_expiration_date: props.hardware?.data?.warranty_expiration_date || '',
location: props.hardware?.data?.location || '',
ip_address: props.hardware?.data?.ip_address || '',
order_id: props.hardware?.data?.order_id || '',
notes: props.hardware?.data?.notes || '',
});
// Raccourci pour définir une garantie standard (+3 ans)
const setStandardWarranty = () => {
if (form.purchase_date) {
const purchase = new Date(form.purchase_date);
purchase.setFullYear(purchase.getFullYear() + 3);
// Formater au format YYYY-MM-DD
const yyyy = purchase.getFullYear();
let mm = purchase.getMonth() + 1;
let dd = purchase.getDate();
if (mm < 10) mm = '0' + mm;
if (dd < 10) dd = '0' + dd;
form.warranty_expiration_date = `${yyyy}-${mm}-${dd}`;
} else {
alert("Veuillez d'abord saisir la date d'achat de l'équipement.");
}
};
const submit = () => {
if (props.isEdit) {
form.put(route('materiels.update', { materiel: props.hardware.data.id }));
} else {
form.post(route('materiels.store'));
}
};
</script>
<template>
<Head :title="isEdit ? 'Modifier équipement' : 'Ajouter un équipement'" />
<AuthenticatedLayout>
<template #header>
<div class="flex items-center justify-between">
<h2 class="text-xl font-bold leading-tight text-slate-800 dark:text-slate-200">
{{ isEdit ? `Modifier la fiche : ${hardware.data.name}` : 'Ajouter un équipement à l\'inventaire' }}
</h2>
<Link
:href="isEdit ? route('materiels.show', { materiel: hardware.data.id }) : route('materiels.index')"
class="inline-flex items-center px-4 py-2 text-sm font-semibold text-slate-700 bg-white border border-slate-300 rounded-lg hover:bg-slate-50 dark:bg-slate-900 dark:text-slate-300 dark:border-slate-850 dark:hover:bg-slate-800 transition-colors"
>
Annuler
</Link>
</div>
</template>
<div class="py-6">
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-xl shadow-sm overflow-hidden p-6">
<form @submit.prevent="submit" class="space-y-6">
<!-- Section : Identification -->
<div>
<h3 class="text-sm font-bold text-sky-600 dark:text-sky-400 uppercase tracking-wider border-b border-slate-100 dark:border-slate-850 pb-2 mb-4">
Identification du matériel
</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<InputLabel for="name" value="Nom d'usage de l'équipement" />
<TextInput
id="name"
type="text"
class="mt-1 block w-full"
v-model="form.name"
required
placeholder="ex: Serveur Hyper-V 01"
/>
<InputError class="mt-2" :message="form.errors.name" />
</div>
<div>
<InputLabel for="type" value="Type de matériel" />
<select
id="type"
v-model="form.type"
required
class="mt-1 block w-full border-gray-300 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-300 focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 rounded-md shadow-sm"
>
<option value="" disabled>Sélectionner un type</option>
<option value="serveur">Serveur</option>
<option value="switch">Switch</option>
<option value="routeur">Routeur</option>
<option value="onduleur">Onduleur</option>
<option value="stockage">Stockage (NAS/SAN)</option>
<option value="pare-feu">Pare-feu</option>
<option value="poste_travail">Poste de travail</option>
<option value="autre">Autre</option>
</select>
<InputError class="mt-2" :message="form.errors.type" />
</div>
<div>
<InputLabel for="brand" value="Marque / Constructeur" />
<TextInput
id="brand"
type="text"
class="mt-1 block w-full"
v-model="form.brand"
required
placeholder="ex: Dell"
/>
<InputError class="mt-2" :message="form.errors.brand" />
</div>
<div>
<InputLabel for="model" value="Modèle" />
<TextInput
id="model"
type="text"
class="mt-1 block w-full"
v-model="form.model"
required
placeholder="ex: PowerEdge R750"
/>
<InputError class="mt-2" :message="form.errors.model" />
</div>
<div>
<InputLabel for="serial_number" value="Numéro de série physique" />
<TextInput
id="serial_number"
type="text"
class="mt-1 block w-full font-mono uppercase"
v-model="form.serial_number"
required
placeholder="ex: CN-0ABC12-DEF34-..."
/>
<InputError class="mt-2" :message="form.errors.serial_number" />
</div>
<div>
<InputLabel for="status" value="Statut courant" />
<select
id="status"
v-model="form.status"
required
class="mt-1 block w-full border-gray-300 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-300 focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 rounded-md shadow-sm"
>
<option value="en_stock">En stock / Rechange</option>
<option value="en_service">En service / Actif</option>
<option value="en_panne">En panne / Maintenance</option>
<option value="au_rebut">Au rebut / Declassé</option>
</select>
<InputError class="mt-2" :message="form.errors.status" />
</div>
</div>
</div>
<!-- Section : Cycle de vie & Dates -->
<div>
<h3 class="text-sm font-bold text-sky-600 dark:text-sky-400 uppercase tracking-wider border-b border-slate-100 dark:border-slate-850 pb-2 mb-4">
Cycle de vie & Garantie
</h3>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<InputLabel for="purchase_date" value="Date d'achat" />
<TextInput
id="purchase_date"
type="date"
class="mt-1 block w-full"
v-model="form.purchase_date"
/>
<InputError class="mt-2" :message="form.errors.purchase_date" />
</div>
<div>
<InputLabel for="commissioning_date" value="Date de mise en service" />
<TextInput
id="commissioning_date"
type="date"
class="mt-1 block w-full"
v-model="form.commissioning_date"
/>
<InputError class="mt-2" :message="form.errors.commissioning_date" />
</div>
<div>
<div class="flex justify-between items-center">
<InputLabel for="warranty_expiration_date" value="Fin de garantie" />
<button
type="button"
@click="setStandardWarranty"
class="text-xxs font-bold text-sky-600 hover:text-sky-500 dark:text-sky-450 underline"
>
+3 ans de garantie
</button>
</div>
<TextInput
id="warranty_expiration_date"
type="date"
class="mt-1 block w-full"
v-model="form.warranty_expiration_date"
/>
<InputError class="mt-2" :message="form.errors.warranty_expiration_date" />
</div>
</div>
</div>
<!-- Section : Technique & Traçabilité -->
<div>
<h3 class="text-sm font-bold text-sky-600 dark:text-sky-400 uppercase tracking-wider border-b border-slate-100 dark:border-slate-850 pb-2 mb-4">
Localisation & Réseau
</h3>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<InputLabel for="location" value="Emplacement physique" />
<TextInput
id="location"
type="text"
class="mt-1 block w-full"
v-model="form.location"
required
placeholder="ex: Baie A, Salle Serveur 1"
/>
<InputError class="mt-2" :message="form.errors.location" />
</div>
<div>
<InputLabel for="ip_address" value="Adresse IP de gestion" />
<TextInput
id="ip_address"
type="text"
class="mt-1 block w-full font-mono"
v-model="form.ip_address"
placeholder="ex: 192.168.10.25"
/>
<InputError class="mt-2" :message="form.errors.ip_address" />
</div>
<div>
<InputLabel for="order_id" value="Commande d'achat d'origine" />
<select
id="order_id"
v-model="form.order_id"
class="mt-1 block w-full border-gray-300 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-300 focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 rounded-md shadow-sm"
>
<option value="">Aucune commande liée</option>
<option v-for="order in orders" :key="order.id" :value="order.id">
{{ order.number }} - {{ order.label }}
</option>
</select>
<InputError class="mt-2" :message="form.errors.order_id" />
</div>
</div>
</div>
<!-- Notes libres -->
<div>
<InputLabel for="notes" value="Notes libres & Historique des interventions" />
<textarea
id="notes"
v-model="form.notes"
rows="4"
placeholder="Historique des pannes, changements de pièces, interventions..."
class="mt-1 block w-full border-gray-300 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-300 focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 rounded-md shadow-sm"
></textarea>
<InputError class="mt-2" :message="form.errors.notes" />
</div>
<!-- Boutons d'action -->
<div class="flex items-center justify-end gap-3 pt-4 border-t border-slate-100 dark:border-slate-850">
<PrimaryButton :disabled="form.processing">
{{ isEdit ? 'Enregistrer les modifications' : 'Enregistrer le matériel' }}
</PrimaryButton>
</div>
</form>
</div>
</div>
</div>
</AuthenticatedLayout>
</template>

View File

@@ -0,0 +1,368 @@
<script setup>
import { ref, computed, watch } from 'vue';
import { Head, Link, router } from '@inertiajs/vue3';
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue';
import MetricCard from '@/Components/MetricCard.vue';
const props = defineProps({
hardwares: Object,
metrics: Object,
filters: Object,
});
// État des filtres
const search = ref(props.filters.search || '');
const status = ref(props.filters.status || '');
const type = ref(props.filters.type || '');
// Recherche réactive avec debouncing
let timeout;
const handleSearch = () => {
clearTimeout(timeout);
timeout = setTimeout(() => {
runFilters();
}, 300);
};
const runFilters = () => {
router.get(route('materiels.index'), {
search: search.value,
status: status.value,
type: type.value,
}, {
preserveState: true,
replace: true,
});
};
const resetFilters = () => {
search.value = '';
status.value = '';
type.value = '';
runFilters();
};
// Exporter en CSV
const exportCsv = () => {
window.location.href = route('materiels.index', {
search: search.value,
status: status.value,
type: type.value,
export: 1,
});
};
// Alerte de garanties expirées ou arrivant à expiration sous 30 jours
const expiringOrExpiredHardwares = computed(() => {
return props.hardwares.data.filter(hw => {
if (!hw.warranty_expiration_date) return false;
// Si le matériel a une garantie de 0 jours restants ou moins de 30 jours restants
return hw.warranty_remaining_days <= 30 || !hw.is_under_warranty;
});
});
// Formatage français pour le type
const getTypeLabel = (type) => {
switch (type) {
case 'serveur': return 'Serveur';
case 'switch': return 'Switch';
case 'routeur': return 'Routeur';
case 'onduleur': return 'Onduleur';
case 'stockage': return 'Stockage (NAS/SAN)';
case 'pare-feu': return 'Pare-feu';
case 'poste_travail': return 'Poste de travail';
case 'autre': return 'Autre';
default: return type;
}
};
// Stylisation des badges de statut
const getStatusClasses = (status) => {
switch (status) {
case 'en_stock':
return 'bg-blue-100 text-blue-800 border-blue-200 dark:bg-blue-950/40 dark:text-blue-300 dark:border-blue-900';
case 'en_service':
return 'bg-emerald-100 text-emerald-800 border-emerald-200 dark:bg-emerald-950/40 dark:text-emerald-300 dark:border-emerald-900';
case 'en_panne':
return 'bg-rose-100 text-rose-800 border-rose-200 dark:bg-rose-950/40 dark:text-rose-300 dark:border-rose-900';
case 'au_rebut':
return 'bg-slate-100 text-slate-800 border-slate-200 dark:bg-slate-900/60 dark:text-slate-400 dark:border-slate-800';
default:
return 'bg-gray-100 text-gray-800 border-gray-200 dark:bg-gray-800 dark:text-gray-300';
}
};
const getStatusLabel = (status) => {
switch (status) {
case 'en_stock': return 'En stock';
case 'en_service': return 'En service';
case 'en_panne': return 'En panne';
case 'au_rebut': return 'Au rebut';
default: return status;
}
};
// Stylisation de la garantie
const getWarrantyClasses = (hw) => {
if (!hw.warranty_expiration_date) {
return 'text-slate-400 dark:text-slate-500';
}
if (hw.is_under_warranty) {
if (hw.warranty_remaining_days <= 90) {
return 'text-amber-600 dark:text-amber-400 font-semibold';
}
return 'text-emerald-600 dark:text-emerald-400';
}
return 'text-rose-600 dark:text-rose-450 font-bold';
};
</script>
<template>
<Head title="Inventaire Matériel Infrastructure" />
<AuthenticatedLayout>
<template #header>
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<h2 class="text-xl font-bold leading-tight text-slate-800 dark:text-slate-200">
Inventaire Matériel Infrastructure
</h2>
<div class="flex gap-2">
<button
@click="exportCsv"
class="inline-flex items-center px-4 py-2 text-sm font-semibold text-slate-700 bg-white border border-slate-300 rounded-lg hover:bg-slate-50 dark:bg-slate-900 dark:text-slate-300 dark:border-slate-850 dark:hover:bg-slate-800 transition-colors shadow-sm"
>
<svg class="w-4 h-4 mr-1.5" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5M16.5 12 12 16.5m0 0L7.5 12m4.5 4.5V3" />
</svg>
Exporter
</button>
<Link
:href="route('materiels.create')"
class="inline-flex items-center px-4 py-2 text-sm font-semibold text-white bg-sky-600 rounded-lg hover:bg-sky-500 dark:bg-sky-500 dark:hover:bg-sky-400 transition-colors shadow-sm"
>
<svg class="w-4 h-4 mr-1.5" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
</svg>
Ajouter un matériel
</Link>
</div>
</div>
</template>
<div class="py-6">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 space-y-6">
<!-- Notifications flash -->
<div v-if="$page.props.flash?.success" class="p-4 bg-emerald-50 border border-emerald-200 text-emerald-800 rounded-lg dark:bg-emerald-950/40 dark:border-emerald-900 dark:text-emerald-300 flex items-center shadow-sm">
<svg class="w-5 h-5 mr-3 shrink-0" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75 11.25 15 15 9.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
</svg>
<span>{{ $page.props.flash?.success }}</span>
</div>
<!-- KPIs de l'inventaire -->
<div class="grid grid-cols-2 lg:grid-cols-5 gap-4">
<MetricCard title="Total Équipements" :value="metrics.total" color="text-slate-800 dark:text-white" />
<MetricCard title="En service" :value="metrics.en_service" color="text-emerald-600 dark:text-emerald-400" />
<MetricCard title="En stock (Rechange)" :value="metrics.en_stock" color="text-blue-600 dark:text-blue-400" />
<MetricCard title="En panne" :value="metrics.en_panne" color="text-rose-600 dark:text-rose-450" />
<MetricCard title="Sous garantie active" :value="metrics.under_warranty" color="text-sky-600 dark:text-sky-400" />
</div>
<!-- Alertes Garantie -->
<div v-if="expiringOrExpiredHardwares.length > 0" class="p-4 bg-amber-50 border border-amber-200 text-amber-900 rounded-lg dark:bg-amber-950/25 dark:border-amber-900/60 dark:text-amber-300 shadow-sm space-y-2">
<div class="flex items-center">
<svg class="w-5 h-5 mr-2 shrink-0 text-amber-500" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z" />
</svg>
<span class="font-bold">Alerte cycle de vie : Matériel(s) hors garantie ou expirant bientôt (sous 30j)</span>
</div>
<ul class="list-disc pl-5 text-xs space-y-1">
<li v-for="hw in expiringOrExpiredHardwares.slice(0, 5)" :key="hw.id">
<Link :href="route('materiels.show', hw.id)" class="underline hover:text-amber-800 dark:hover:text-amber-100 font-semibold">
{{ hw.name }} ({{ hw.brand }} {{ hw.model }})
</Link>
- {{ hw.warranty_status_label }}
</li>
<li v-if="expiringOrExpiredHardwares.length > 5">
Et {{ expiringOrExpiredHardwares.length - 5 }} autre(s) équipement(s)...
</li>
</ul>
</div>
<!-- Filtres et recherche -->
<div class="bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-xl p-4 shadow-sm flex flex-col md:flex-row md:items-center justify-between gap-4">
<!-- Recherche textuelle -->
<div class="flex-1 relative">
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none text-slate-400">
<svg class="h-4.5 w-4.5" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.602 10.602Z" />
</svg>
</div>
<input
v-model="search"
@input="handleSearch"
type="text"
placeholder="Rechercher par nom, marque, modèle, n° série, IP, emplacement..."
class="block w-full pl-10 pr-3 py-2 text-sm bg-slate-50 border border-slate-300 rounded-lg text-slate-900 placeholder-slate-400 focus:outline-none focus:ring-1 focus:ring-sky-500 focus:border-sky-500 dark:bg-slate-950 dark:border-slate-850 dark:text-slate-200"
/>
</div>
<!-- Sélections filtres -->
<div class="flex flex-wrap items-center gap-2">
<!-- Filtrer par type -->
<select
v-model="type"
@change="runFilters"
class="bg-slate-50 border border-slate-300 rounded-lg text-sm px-3 py-2 text-slate-700 dark:bg-slate-950 dark:border-slate-850 dark:text-slate-300"
>
<option value="">Tous les types</option>
<option value="serveur">Serveur</option>
<option value="switch">Switch</option>
<option value="routeur">Routeur</option>
<option value="onduleur">Onduleur</option>
<option value="stockage">Stockage</option>
<option value="pare-feu">Pare-feu</option>
<option value="poste_travail">Poste de travail</option>
<option value="autre">Autre</option>
</select>
<!-- Filtrer par statut -->
<select
v-model="status"
@change="runFilters"
class="bg-slate-50 border border-slate-300 rounded-lg text-sm px-3 py-2 text-slate-700 dark:bg-slate-950 dark:border-slate-850 dark:text-slate-300"
>
<option value="">Tous les statuts</option>
<option value="en_stock">En stock</option>
<option value="en_service">En service</option>
<option value="en_panne">En panne</option>
<option value="au_rebut">Au rebut</option>
</select>
<!-- Réinitialiser -->
<button
v-if="search || status || type"
@click="resetFilters"
class="text-xs font-semibold text-rose-600 hover:text-rose-500 dark:text-rose-450 dark:hover:text-rose-400 px-2.5 py-2"
>
Réinitialiser
</button>
</div>
</div>
<!-- Liste du matériel -->
<div class="bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-xl shadow-sm overflow-hidden">
<div class="overflow-x-auto">
<table class="w-full text-left border-collapse">
<thead>
<tr class="bg-slate-50 border-b border-slate-100 dark:bg-slate-950/40 dark:border-slate-850 text-xxs font-semibold uppercase tracking-wider text-slate-400">
<th class="px-6 py-4">Nom de l'équipement</th>
<th class="px-6 py-4">Marque / Modèle</th>
<th class="px-6 py-4">Numéro de série</th>
<th class="px-6 py-4">Emplacement / IP</th>
<th class="px-6 py-4">Garantie</th>
<th class="px-6 py-4">Statut</th>
<th class="px-6 py-4 text-right">Actions</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-100 dark:divide-slate-850 text-sm">
<tr
v-for="hw in hardwares.data"
:key="hw.id"
class="hover:bg-slate-50/50 dark:hover:bg-slate-950/20 transition-colors"
>
<td class="px-6 py-4">
<div class="font-bold text-slate-800 dark:text-slate-200">
<Link :href="route('materiels.show', hw.id)" class="hover:text-sky-600 dark:hover:text-sky-400">
{{ hw.name }}
</Link>
</div>
<span class="inline-block mt-0.5 text-xxs font-semibold text-slate-400 uppercase">
{{ getTypeLabel(hw.type) }}
</span>
</td>
<td class="px-6 py-4">
<div class="text-slate-800 dark:text-slate-200 font-medium">{{ hw.brand }}</div>
<div class="text-xs text-slate-450">{{ hw.model }}</div>
</td>
<td class="px-6 py-4 font-mono text-xs text-slate-600 dark:text-slate-350 font-semibold">
{{ hw.serial_number }}
</td>
<td class="px-6 py-4">
<div class="text-slate-800 dark:text-slate-200 font-medium">{{ hw.location }}</div>
<div v-if="hw.ip_address" class="text-xs font-mono text-sky-600 dark:text-sky-400 mt-0.5">{{ hw.ip_address }}</div>
<div v-else class="text-xxs text-slate-400 italic">Pas d'IP de gestion</div>
</td>
<td class="px-6 py-4 text-xs">
<span :class="getWarrantyClasses(hw)">
{{ hw.warranty_status_label }}
</span>
<span class="block text-xxs text-slate-405 mt-0.5" v-if="hw.warranty_expiration_date">
Fin : {{ hw.warranty_expiration_date_formatted }}
</span>
</td>
<td class="px-6 py-4">
<span
:class="`inline-flex items-center px-2 py-0.5 rounded text-xs font-semibold border ${getStatusClasses(hw.status)}`"
>
{{ getStatusLabel(hw.status) }}
</span>
</td>
<td class="px-6 py-4 text-right">
<div class="flex items-center justify-end gap-3">
<Link
:href="route('materiels.show', hw.id)"
class="text-slate-500 hover:text-sky-600 dark:text-slate-400 dark:hover:text-sky-400"
title="Voir les détails"
>
<svg class="w-4.5 h-4.5" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M2.036 12.322a1.012 1.012 0 0 1 0-.639C3.423 7.51 7.36 4.5 12 4.5c4.638 0 8.573 3.007 9.963 7.178.07.207.07.431 0 .639C20.577 16.49 16.64 19.5 12 19.5c-4.638 0-8.573-3.007-9.963-7.178Z" />
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
</svg>
</Link>
<Link
:href="route('materiels.edit', hw.id)"
class="text-slate-500 hover:text-amber-500 dark:text-slate-400 dark:hover:text-amber-400"
title="Modifier la fiche"
>
<svg class="w-4.5 h-4.5" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L6.83 18.75a4.48 4.48 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10" />
</svg>
</Link>
</div>
</td>
</tr>
<tr v-if="hardwares.data.length === 0">
<td colspan="7" class="px-6 py-8 text-center text-slate-500 dark:text-slate-400">
Aucun équipement de matériel trouvé dans l'inventaire.
</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- Liens de pagination -->
<div v-if="hardwares.meta && hardwares.meta.last_page > 1" class="flex justify-center mt-6">
<nav class="flex items-center space-x-1">
<Link
v-for="(link, index) in hardwares.meta.links"
:key="index"
:href="link.url || '#'"
class="px-3.5 py-2 rounded-lg text-sm font-semibold transition-colors"
:class="[
link.active
? 'bg-sky-600 text-white dark:bg-sky-500'
: 'bg-white text-slate-700 hover:bg-slate-50 border border-slate-350 dark:bg-slate-900 dark:text-slate-300 dark:border-slate-850 dark:hover:bg-slate-800',
!link.url && 'opacity-50 cursor-not-allowed pointer-events-none'
]"
v-html="link.label"
/>
</nav>
</div>
</div>
</div>
</AuthenticatedLayout>
</template>

View File

@@ -0,0 +1,318 @@
<script setup>
import { computed } from 'vue';
import { Head, Link, router } from '@inertiajs/vue3';
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue';
const props = defineProps({
hardware: Object,
});
// Supprimer le matériel de l'inventaire
const deleteHardware = () => {
if (confirm("Êtes-vous sûr de vouloir supprimer définitivement cet équipement de l'inventaire ? Cette action est irréversible.")) {
router.delete(route('materiels.destroy', { materiel: props.hardware.data.id }));
}
};
// Formater le type en français
const getTypeLabel = (type) => {
switch (type) {
case 'serveur': return 'Serveur';
case 'switch': return 'Switch';
case 'routeur': return 'Routeur';
case 'onduleur': return 'Onduleur';
case 'stockage': return 'Stockage (NAS/SAN)';
case 'pare-feu': return 'Pare-feu';
case 'poste_travail': return 'Poste de travail';
case 'autre': return 'Autre';
default: return type;
}
};
// Stylisation des badges de statut
const getStatusClasses = (status) => {
switch (status) {
case 'en_stock':
return 'bg-blue-100 text-blue-800 border-blue-200 dark:bg-blue-950/40 dark:text-blue-300 dark:border-blue-900';
case 'en_service':
return 'bg-emerald-100 text-emerald-800 border-emerald-200 dark:bg-emerald-950/40 dark:text-emerald-300 dark:border-emerald-900';
case 'en_panne':
return 'bg-rose-100 text-rose-800 border-rose-200 dark:bg-rose-950/40 dark:text-rose-300 dark:border-rose-900';
case 'au_rebut':
return 'bg-slate-100 text-slate-800 border-slate-200 dark:bg-slate-900/60 dark:text-slate-400 dark:border-slate-850';
default:
return 'bg-gray-100 text-gray-800 border-gray-200 dark:bg-gray-800 dark:text-gray-300';
}
};
const getStatusLabel = (status) => {
switch (status) {
case 'en_stock': return 'En stock / Rechange';
case 'en_service': return 'En service';
case 'en_panne': return 'En panne';
case 'au_rebut': return 'Au rebut';
default: return status;
}
};
// Calcul du pourcentage de garantie écoulé
const warrantyPercentage = computed(() => {
const hw = props.hardware.data;
if (!hw.purchase_date || !hw.warranty_expiration_date) {
return null;
}
const start = new Date(hw.purchase_date).getTime();
const end = new Date(hw.warranty_expiration_date).getTime();
const today = new Date().getTime();
if (today >= end) {
return 100; // Garantie entièrement expirée
}
if (today <= start) {
return 0; // Pas encore commencé
}
const total = end - start;
const elapsed = today - start;
return Math.round((elapsed / total) * 100);
});
// Calcul de l'âge de l'équipement
const ageLabel = computed(() => {
const hw = props.hardware.data;
if (!hw.purchase_date) return null;
const purchase = new Date(hw.purchase_date);
const today = new Date();
let diffYear = today.getFullYear() - purchase.getFullYear();
let diffMonth = today.getMonth() - purchase.getMonth();
if (diffMonth < 0) {
diffYear--;
diffMonth += 12;
}
if (diffYear === 0) {
return `${diffMonth} mois`;
}
return `${diffYear} an(s) et ${diffMonth} mois`;
});
</script>
<template>
<Head :title="`Équipement ${hardware.data.name}`" />
<AuthenticatedLayout>
<template #header>
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div class="flex items-center gap-3">
<h2 class="text-xl font-bold leading-tight text-slate-800 dark:text-slate-200">
{{ hardware.data.name }}
</h2>
<span
:class="`inline-flex items-center px-2 py-0.5 rounded text-xs font-semibold border ${getStatusClasses(hardware.data.status)}`"
>
{{ getStatusLabel(hardware.data.status) }}
</span>
</div>
<div class="flex gap-2">
<Link
:href="route('materiels.index')"
class="inline-flex items-center px-4 py-2 text-sm font-semibold text-slate-700 bg-white border border-slate-300 rounded-lg hover:bg-slate-50 dark:bg-slate-900 dark:text-slate-300 dark:border-slate-850 dark:hover:bg-slate-800 transition-colors shadow-sm"
>
Retour à l'inventaire
</Link>
<Link
:href="route('materiels.edit', { materiel: hardware.data.id })"
class="inline-flex items-center px-4 py-2 text-sm font-semibold text-white bg-sky-600 rounded-lg hover:bg-sky-500 dark:bg-sky-500 dark:hover:bg-sky-400 transition-colors shadow-sm"
>
<svg class="w-4 h-4 mr-1.5" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L6.83 18.75a4.48 4.48 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10" />
</svg>
Éditer
</Link>
<button
@click="deleteHardware"
class="inline-flex items-center px-4 py-2 text-sm font-semibold text-white bg-rose-600 rounded-lg hover:bg-rose-500 dark:bg-rose-650 dark:hover:bg-rose-550 transition-colors shadow-sm"
>
<svg class="w-4 h-4 mr-1.5" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" />
</svg>
Supprimer
</button>
</div>
</div>
</template>
<div class="py-6">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 space-y-6">
<!-- Grille principale -->
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<!-- Fiche Technique (Col 1 & 2) -->
<div class="lg:col-span-2 space-y-6">
<div class="bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-xl shadow-sm overflow-hidden">
<div class="border-b border-slate-100 dark:border-slate-850 px-6 py-4">
<h3 class="text-sm font-bold text-slate-850 dark:text-slate-200 uppercase tracking-wider">
Spécifications de l'équipement
</h3>
</div>
<dl class="grid grid-cols-1 md:grid-cols-2 divide-y divide-slate-100 dark:divide-slate-850 md:divide-y-0 text-sm">
<div class="px-6 py-4 space-y-1">
<dt class="text-xs font-semibold uppercase tracking-wider text-slate-400">Nom de l'Asset</dt>
<dd class="text-base font-bold text-slate-900 dark:text-white">{{ hardware.data.name }}</dd>
</div>
<div class="px-6 py-4 space-y-1">
<dt class="text-xs font-semibold uppercase tracking-wider text-slate-400">Catégorie</dt>
<dd class="text-slate-850 dark:text-slate-200 font-semibold">{{ getTypeLabel(hardware.data.type) }}</dd>
</div>
<div class="px-6 py-4 space-y-1 md:border-t border-slate-100 dark:border-slate-850">
<dt class="text-xs font-semibold uppercase tracking-wider text-slate-400">Constructeur / Marque</dt>
<dd class="text-slate-850 dark:text-slate-200 font-semibold">{{ hardware.data.brand }}</dd>
</div>
<div class="px-6 py-4 space-y-1 md:border-t border-slate-100 dark:border-slate-850">
<dt class="text-xs font-semibold uppercase tracking-wider text-slate-400">Modèle</dt>
<dd class="text-slate-850 dark:text-slate-200 font-medium">{{ hardware.data.model }}</dd>
</div>
<div class="px-6 py-4 space-y-1 border-t border-slate-100 dark:border-slate-850">
<dt class="text-xs font-semibold uppercase tracking-wider text-slate-400">Numéro de série physique</dt>
<dd class="text-slate-850 dark:text-slate-200 font-mono font-bold uppercase">{{ hardware.data.serial_number }}</dd>
</div>
<div class="px-6 py-4 space-y-1 border-t border-slate-100 dark:border-slate-850">
<dt class="text-xs font-semibold uppercase tracking-wider text-slate-400">IP de gestion réseau</dt>
<dd class="text-sky-600 dark:text-sky-400 font-mono font-semibold">
{{ hardware.data.ip_address || 'Non configurée' }}
</dd>
</div>
<div class="px-6 py-4 space-y-1 border-t border-slate-100 dark:border-slate-850">
<dt class="text-xs font-semibold uppercase tracking-wider text-slate-400">Emplacement géographique / Rack</dt>
<dd class="text-slate-850 dark:text-slate-200 font-semibold">{{ hardware.data.location }}</dd>
</div>
<div class="px-6 py-4 space-y-1 border-t border-slate-100 dark:border-slate-850 bg-slate-50/50 dark:bg-slate-950/20">
<dt class="text-xs font-semibold uppercase tracking-wider text-slate-400">Âge du matériel</dt>
<dd class="text-slate-800 dark:text-slate-200 font-medium">
{{ ageLabel || 'Date d\'achat non spécifiée' }}
</dd>
</div>
</dl>
</div>
<!-- Notes libres & Historique d'interventions -->
<div class="bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-xl shadow-sm overflow-hidden">
<div class="border-b border-slate-100 dark:border-slate-850 px-6 py-4">
<h3 class="text-sm font-bold text-slate-850 dark:text-slate-200 uppercase tracking-wider">
Notes & Historique d'interventions
</h3>
</div>
<div class="p-6 text-sm text-slate-700 dark:text-slate-300 leading-relaxed whitespace-pre-line">
{{ hardware.data.notes || 'Aucun commentaire ou rapport d\'intervention sur cet équipement.' }}
</div>
</div>
</div>
<!-- Cycle de vie & Traçabilité financière (Col 3) -->
<div class="space-y-6">
<!-- Statut Garantie & Cycle -->
<div class="bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-xl shadow-sm overflow-hidden">
<div class="border-b border-slate-100 dark:border-slate-850 px-6 py-4">
<h3 class="text-sm font-bold text-slate-850 dark:text-slate-200 uppercase tracking-wider">
Garantie & Cycle de vie
</h3>
</div>
<div class="p-6 space-y-6">
<!-- Indicateur de Garantie -->
<div class="space-y-2">
<div class="flex justify-between items-center text-xs">
<span class="font-semibold text-slate-400 uppercase">État de la Garantie</span>
<span :class="`font-bold ${hardware.data.is_under_warranty ? 'text-emerald-600 dark:text-emerald-400' : 'text-rose-600 dark:text-rose-450'}`">
{{ hardware.data.warranty_status_label }}
</span>
</div>
<!-- Barre de progression de la garantie -->
<div v-if="warrantyPercentage !== null" class="w-full bg-slate-100 dark:bg-slate-800 h-2.5 rounded-full overflow-hidden">
<div
class="h-full rounded-full transition-all duration-500"
:class="[
hardware.data.is_under_warranty
? (warrantyPercentage >= 80 ? 'bg-amber-500' : 'bg-emerald-500')
: 'bg-rose-500'
]"
:style="`width: ${warrantyPercentage}%`"
></div>
</div>
<div v-if="warrantyPercentage !== null" class="flex justify-between text-xxs text-slate-400">
<span>Achat : {{ hardware.data.purchase_date_formatted }}</span>
<span>{{ warrantyPercentage }}% écoulé</span>
<span>Fin : {{ hardware.data.warranty_expiration_date_formatted }}</span>
</div>
<div v-else class="text-xs text-slate-450 italic">
Dates de garantie non spécifiées.
</div>
</div>
<div class="border-t border-slate-100 dark:border-slate-850 pt-4 space-y-3 text-xs">
<div class="flex justify-between">
<span class="text-slate-400">Date d'achat :</span>
<span class="font-bold text-slate-800 dark:text-slate-200">{{ hardware.data.purchase_date_formatted }}</span>
</div>
<div class="flex justify-between">
<span class="text-slate-400">Mise en service :</span>
<span class="font-bold text-slate-800 dark:text-slate-200">{{ hardware.data.commissioning_date_formatted }}</span>
</div>
<div class="flex justify-between">
<span class="text-slate-400">Date fin garantie :</span>
<span class="font-bold text-slate-800 dark:text-slate-200">{{ hardware.data.warranty_expiration_date_formatted }}</span>
</div>
</div>
</div>
</div>
<!-- Traçabilité Commande -->
<div class="bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-xl shadow-sm overflow-hidden">
<div class="border-b border-slate-100 dark:border-slate-850 px-6 py-4">
<h3 class="text-sm font-bold text-slate-850 dark:text-slate-200 uppercase tracking-wider">
Traçabilité Achat
</h3>
</div>
<div class="p-6">
<div v-if="hardware.data.order" class="space-y-3 text-sm">
<p class="text-xs text-slate-500 leading-relaxed">
Cet équipement fait partie de la commande suivante :
</p>
<div class="p-3 border border-slate-200 dark:border-slate-800 rounded-xl bg-slate-50/50 dark:bg-slate-950/20">
<Link
:href="route('commandes.show', { commande: hardware.data.order.id })"
class="block font-bold text-sky-600 dark:text-sky-400 hover:underline"
>
{{ hardware.data.order.number }}
</Link>
<span class="block text-xs text-slate-700 dark:text-slate-300 font-medium mt-1">
{{ hardware.data.order.label }}
</span>
</div>
</div>
<div v-else class="text-center py-4 text-xs text-slate-450 italic">
Aucune commande liée dans le système.
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</AuthenticatedLayout>
</template>