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,55 @@
<script setup>
import GuestLayout from '@/Layouts/GuestLayout.vue';
import InputError from '@/Components/InputError.vue';
import InputLabel from '@/Components/InputLabel.vue';
import PrimaryButton from '@/Components/PrimaryButton.vue';
import TextInput from '@/Components/TextInput.vue';
import { Head, useForm } from '@inertiajs/vue3';
const form = useForm({
password: '',
});
const submit = () => {
form.post(route('password.confirm'), {
onFinish: () => form.reset(),
});
};
</script>
<template>
<GuestLayout>
<Head title="Confirm Password" />
<div class="mb-4 text-sm text-gray-600 dark:text-gray-400">
This is a secure area of the application. Please confirm your
password before continuing.
</div>
<form @submit.prevent="submit">
<div>
<InputLabel for="password" value="Password" />
<TextInput
id="password"
type="password"
class="mt-1 block w-full"
v-model="form.password"
required
autocomplete="current-password"
autofocus
/>
<InputError class="mt-2" :message="form.errors.password" />
</div>
<div class="mt-4 flex justify-end">
<PrimaryButton
class="ms-4"
:class="{ 'opacity-25': form.processing }"
:disabled="form.processing"
>
Confirm
</PrimaryButton>
</div>
</form>
</GuestLayout>
</template>

View File

@@ -0,0 +1,68 @@
<script setup>
import GuestLayout from '@/Layouts/GuestLayout.vue';
import InputError from '@/Components/InputError.vue';
import InputLabel from '@/Components/InputLabel.vue';
import PrimaryButton from '@/Components/PrimaryButton.vue';
import TextInput from '@/Components/TextInput.vue';
import { Head, useForm } from '@inertiajs/vue3';
defineProps({
status: {
type: String,
},
});
const form = useForm({
email: '',
});
const submit = () => {
form.post(route('password.email'));
};
</script>
<template>
<GuestLayout>
<Head title="Forgot Password" />
<div class="mb-4 text-sm text-gray-600 dark:text-gray-400">
Forgot your password? No problem. Just let us know your email
address and we will email you a password reset link that will allow
you to choose a new one.
</div>
<div
v-if="status"
class="mb-4 text-sm font-medium text-green-600 dark:text-green-400"
>
{{ status }}
</div>
<form @submit.prevent="submit">
<div>
<InputLabel for="email" value="Email" />
<TextInput
id="email"
type="email"
class="mt-1 block w-full"
v-model="form.email"
required
autofocus
autocomplete="username"
/>
<InputError class="mt-2" :message="form.errors.email" />
</div>
<div class="mt-4 flex items-center justify-end">
<PrimaryButton
:class="{ 'opacity-25': form.processing }"
:disabled="form.processing"
>
Email Password Reset Link
</PrimaryButton>
</div>
</form>
</GuestLayout>
</template>

View File

@@ -0,0 +1,100 @@
<script setup>
import Checkbox from '@/Components/Checkbox.vue';
import GuestLayout from '@/Layouts/GuestLayout.vue';
import InputError from '@/Components/InputError.vue';
import InputLabel from '@/Components/InputLabel.vue';
import PrimaryButton from '@/Components/PrimaryButton.vue';
import TextInput from '@/Components/TextInput.vue';
import { Head, Link, useForm } from '@inertiajs/vue3';
defineProps({
canResetPassword: {
type: Boolean,
},
status: {
type: String,
},
});
const form = useForm({
email: '',
password: '',
remember: false,
});
const submit = () => {
form.post(route('login'), {
onFinish: () => form.reset('password'),
});
};
</script>
<template>
<GuestLayout>
<Head title="Log in" />
<div v-if="status" class="mb-4 text-sm font-medium text-green-600">
{{ status }}
</div>
<form @submit.prevent="submit">
<div>
<InputLabel for="email" value="Email" />
<TextInput
id="email"
type="email"
class="mt-1 block w-full"
v-model="form.email"
required
autofocus
autocomplete="username"
/>
<InputError class="mt-2" :message="form.errors.email" />
</div>
<div class="mt-4">
<InputLabel for="password" value="Password" />
<TextInput
id="password"
type="password"
class="mt-1 block w-full"
v-model="form.password"
required
autocomplete="current-password"
/>
<InputError class="mt-2" :message="form.errors.password" />
</div>
<div class="mt-4 block">
<label class="flex items-center">
<Checkbox name="remember" v-model:checked="form.remember" />
<span class="ms-2 text-sm text-gray-600 dark:text-gray-400"
>Remember me</span
>
</label>
</div>
<div class="mt-4 flex items-center justify-end">
<Link
v-if="canResetPassword"
:href="route('password.request')"
class="rounded-md text-sm text-gray-600 underline hover:text-gray-900 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:text-gray-400 dark:hover:text-gray-100 dark:focus:ring-offset-gray-800"
>
Forgot your password?
</Link>
<PrimaryButton
class="ms-4"
:class="{ 'opacity-25': form.processing }"
:disabled="form.processing"
>
Log in
</PrimaryButton>
</div>
</form>
</GuestLayout>
</template>

View File

@@ -0,0 +1,113 @@
<script setup>
import GuestLayout from '@/Layouts/GuestLayout.vue';
import InputError from '@/Components/InputError.vue';
import InputLabel from '@/Components/InputLabel.vue';
import PrimaryButton from '@/Components/PrimaryButton.vue';
import TextInput from '@/Components/TextInput.vue';
import { Head, Link, useForm } from '@inertiajs/vue3';
const form = useForm({
name: '',
email: '',
password: '',
password_confirmation: '',
});
const submit = () => {
form.post(route('register'), {
onFinish: () => form.reset('password', 'password_confirmation'),
});
};
</script>
<template>
<GuestLayout>
<Head title="Register" />
<form @submit.prevent="submit">
<div>
<InputLabel for="name" value="Name" />
<TextInput
id="name"
type="text"
class="mt-1 block w-full"
v-model="form.name"
required
autofocus
autocomplete="name"
/>
<InputError class="mt-2" :message="form.errors.name" />
</div>
<div class="mt-4">
<InputLabel for="email" value="Email" />
<TextInput
id="email"
type="email"
class="mt-1 block w-full"
v-model="form.email"
required
autocomplete="username"
/>
<InputError class="mt-2" :message="form.errors.email" />
</div>
<div class="mt-4">
<InputLabel for="password" value="Password" />
<TextInput
id="password"
type="password"
class="mt-1 block w-full"
v-model="form.password"
required
autocomplete="new-password"
/>
<InputError class="mt-2" :message="form.errors.password" />
</div>
<div class="mt-4">
<InputLabel
for="password_confirmation"
value="Confirm Password"
/>
<TextInput
id="password_confirmation"
type="password"
class="mt-1 block w-full"
v-model="form.password_confirmation"
required
autocomplete="new-password"
/>
<InputError
class="mt-2"
:message="form.errors.password_confirmation"
/>
</div>
<div class="mt-4 flex items-center justify-end">
<Link
:href="route('login')"
class="rounded-md text-sm text-gray-600 underline hover:text-gray-900 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:text-gray-400 dark:hover:text-gray-100 dark:focus:ring-offset-gray-800"
>
Already registered?
</Link>
<PrimaryButton
class="ms-4"
:class="{ 'opacity-25': form.processing }"
:disabled="form.processing"
>
Register
</PrimaryButton>
</div>
</form>
</GuestLayout>
</template>

View File

@@ -0,0 +1,101 @@
<script setup>
import GuestLayout from '@/Layouts/GuestLayout.vue';
import InputError from '@/Components/InputError.vue';
import InputLabel from '@/Components/InputLabel.vue';
import PrimaryButton from '@/Components/PrimaryButton.vue';
import TextInput from '@/Components/TextInput.vue';
import { Head, useForm } from '@inertiajs/vue3';
const props = defineProps({
email: {
type: String,
required: true,
},
token: {
type: String,
required: true,
},
});
const form = useForm({
token: props.token,
email: props.email,
password: '',
password_confirmation: '',
});
const submit = () => {
form.post(route('password.store'), {
onFinish: () => form.reset('password', 'password_confirmation'),
});
};
</script>
<template>
<GuestLayout>
<Head title="Reset Password" />
<form @submit.prevent="submit">
<div>
<InputLabel for="email" value="Email" />
<TextInput
id="email"
type="email"
class="mt-1 block w-full"
v-model="form.email"
required
autofocus
autocomplete="username"
/>
<InputError class="mt-2" :message="form.errors.email" />
</div>
<div class="mt-4">
<InputLabel for="password" value="Password" />
<TextInput
id="password"
type="password"
class="mt-1 block w-full"
v-model="form.password"
required
autocomplete="new-password"
/>
<InputError class="mt-2" :message="form.errors.password" />
</div>
<div class="mt-4">
<InputLabel
for="password_confirmation"
value="Confirm Password"
/>
<TextInput
id="password_confirmation"
type="password"
class="mt-1 block w-full"
v-model="form.password_confirmation"
required
autocomplete="new-password"
/>
<InputError
class="mt-2"
:message="form.errors.password_confirmation"
/>
</div>
<div class="mt-4 flex items-center justify-end">
<PrimaryButton
:class="{ 'opacity-25': form.processing }"
:disabled="form.processing"
>
Reset Password
</PrimaryButton>
</div>
</form>
</GuestLayout>
</template>

View File

@@ -0,0 +1,61 @@
<script setup>
import { computed } from 'vue';
import GuestLayout from '@/Layouts/GuestLayout.vue';
import PrimaryButton from '@/Components/PrimaryButton.vue';
import { Head, Link, useForm } from '@inertiajs/vue3';
const props = defineProps({
status: {
type: String,
},
});
const form = useForm({});
const submit = () => {
form.post(route('verification.send'));
};
const verificationLinkSent = computed(
() => props.status === 'verification-link-sent',
);
</script>
<template>
<GuestLayout>
<Head title="Email Verification" />
<div class="mb-4 text-sm text-gray-600 dark:text-gray-400">
Thanks for signing up! Before getting started, could you verify your
email address by clicking on the link we just emailed to you? If you
didn't receive the email, we will gladly send you another.
</div>
<div
class="mb-4 text-sm font-medium text-green-600 dark:text-green-400"
v-if="verificationLinkSent"
>
A new verification link has been sent to the email address you
provided during registration.
</div>
<form @submit.prevent="submit">
<div class="mt-4 flex items-center justify-between">
<PrimaryButton
:class="{ 'opacity-25': form.processing }"
:disabled="form.processing"
>
Resend Verification Email
</PrimaryButton>
<Link
:href="route('logout')"
method="post"
as="button"
class="rounded-md text-sm text-gray-600 underline hover:text-gray-900 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:text-gray-400 dark:hover:text-gray-100 dark:focus:ring-offset-gray-800"
>Log Out</Link
>
</div>
</form>
</GuestLayout>
</template>

View File

@@ -0,0 +1,390 @@
<script setup>
import { computed } from 'vue';
import { Head, Link, useForm } from '@inertiajs/vue3';
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue';
import InputLabel from '@/Components/InputLabel.vue';
import TextInput from '@/Components/TextInput.vue';
import InputError from '@/Components/InputError.vue';
import PrimaryButton from '@/Components/PrimaryButton.vue';
const props = defineProps({
order: {
type: Object,
default: null,
},
isEdit: {
type: Boolean,
default: false,
},
});
// Calcul de la date par défaut (+30 jours) au format YYYY-MM-DD
const getDefaultDeadline = () => {
const date = new Date();
date.setDate(date.getDate() + 30);
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
};
// Initialisation du formulaire Inertia
const form = useForm({
label: props.order?.data?.label || props.order?.label || '',
type: props.order?.data?.type || props.order?.type || 'Matériel réseau / serveur',
supplier: props.order?.data?.supplier || props.order?.supplier || '',
quote_number: props.order?.data?.quote_number || props.order?.quote_number || '',
amount_ht: props.order?.data?.amount_ht !== undefined && props.order?.data?.amount_ht !== null
? String(props.order.data.amount_ht)
: (props.order?.amount_ht !== undefined && props.order?.amount_ht !== null
? String(props.order.amount_ht)
: ''),
requested_by: props.order?.data?.requested_by || props.order?.requested_by || 'Jérémy',
prescriber: props.order?.data?.prescriber || props.order?.prescriber || '',
delivery_deadline: props.order?.data?.delivery_deadline || props.order?.delivery_deadline || getDefaultDeadline(),
notes: props.order?.data?.notes || props.order?.notes || '',
exclude_vat: props.order?.data?.exclude_vat || props.order?.exclude_vat || false,
quote_file: null,
delivery_note_file: null,
invoice_file: null,
_method: props.isEdit ? 'put' : 'post', // Pour supporter l'upload de fichier en PHP/Laravel lors de la mise à jour
});
// Calcul automatique du montant TTC (HT + 20% TVA) en temps réel
const amountTtc = computed(() => {
const ht = parseFloat(form.amount_ht);
if (isNaN(ht) || ht < 0) return 0;
if (form.exclude_vat) return ht.toFixed(2);
return (ht * 1.20).toFixed(2);
});
// Récupérer le fichier en cours d'édition si présent
const getExistingFile = (type) => {
const attachments = props.order?.data?.attachments || props.order?.attachments;
if (!attachments) return null;
return attachments.find(a => a.file_type === type);
};
// Soumission du formulaire
const submit = () => {
if (props.isEdit) {
const orderId = props.order?.data?.id || props.order?.id;
form.post(route('commandes.update', { commande: orderId }), {
forceFormData: true,
});
} else {
form.post(route('commandes.store'));
}
};
const orderTypes = [
'Matériel réseau / serveur',
'Licences logicielles',
'Consommables / câblage',
'Prestations / services',
];
const demandeurs = ['Jérémy', 'Sylvain', 'Kévin'];
</script>
<template>
<Head :title="isEdit ? `Modifier la Commande ${order?.data?.number || order?.number}` : 'Nouvelle Commande'" />
<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 Commande ${order?.data?.number || order?.number}` : 'Nouvelle Demande de Commande' }}
</h2>
<Link
:href="isEdit ? route('commandes.show', { commande: order?.data?.id || order?.id }) : route('commandes.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"
>
Retour
</Link>
</div>
</template>
<div class="py-6">
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
<!-- Formulaire -->
<div class="bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-xl shadow-sm overflow-hidden">
<form @submit.prevent="submit" class="p-6 space-y-6">
<!-- Détails Article -->
<div class="border-b border-slate-100 dark:border-slate-850 pb-5">
<h3 class="text-md font-bold text-slate-800 dark:text-slate-200 mb-1">
Informations sur l'article
</h3>
<p class="text-xs text-slate-500 dark:text-slate-400">
Saisissez la référence ou le libellé principal du matériel ou service demandé.
</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<!-- Libellé article -->
<div class="md:col-span-3">
<InputLabel for="label" value="Libellé / Référence article" class="font-bold" />
<TextInput
id="label"
type="text"
class="mt-1 block w-full"
v-model="form.label"
required
placeholder="Ex: Serveur NAS Synology 8 baies ou 5x Licences Office 365"
/>
<InputError class="mt-2" :message="form.errors.label" />
</div>
<!-- Type de commande -->
<div>
<InputLabel for="type" value="Type de commande" class="font-bold" />
<select
id="type"
v-model="form.type"
class="mt-1 block w-full text-sm rounded-lg border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:bg-slate-950 dark:border-slate-800 dark:text-slate-200"
required
>
<option v-for="type in orderTypes" :key="type" :value="type">
{{ type }}
</option>
</select>
<InputError class="mt-2" :message="form.errors.type" />
</div>
<!-- Demandeur -->
<div>
<InputLabel for="requested_by" value="Demandeur" class="font-bold" />
<select
id="requested_by"
v-model="form.requested_by"
class="mt-1 block w-full text-sm rounded-lg border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:bg-slate-950 dark:border-slate-800 dark:text-slate-200"
required
>
<option v-for="d in demandeurs" :key="d" :value="d">
{{ d }}
</option>
</select>
<InputError class="mt-2" :message="form.errors.requested_by" />
</div>
<!-- Prescripteur -->
<div>
<InputLabel for="prescriber" value="Prescripteur / Service à l'origine" class="font-bold" />
<TextInput
id="prescriber"
type="text"
class="mt-1 block w-full"
v-model="form.prescriber"
required
placeholder="Ex: Service RH, Urbanisme..."
/>
<InputError class="mt-2" :message="form.errors.prescriber" />
</div>
</div>
<!-- Fournisseur & Devis -->
<div class="border-b border-slate-100 dark:border-slate-850 pb-5 pt-4">
<h3 class="text-md font-bold text-slate-800 dark:text-slate-200 mb-1">
Fournisseur & Devis
</h3>
<p class="text-xs text-slate-500 dark:text-slate-400">
Références d'acquisition et détails financiers.
</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- Fournisseur -->
<div>
<InputLabel for="supplier" value="Fournisseur" class="font-bold" />
<TextInput
id="supplier"
type="text"
class="mt-1 block w-full"
v-model="form.supplier"
required
placeholder="Ex: LDLC Pro, Dell France..."
/>
<InputError class="mt-2" :message="form.errors.supplier" />
</div>
<!-- Numéro de devis -->
<div>
<InputLabel for="quote_number" value="Numéro de devis" class="font-bold" />
<TextInput
id="quote_number"
type="text"
class="mt-1 block w-full"
v-model="form.quote_number"
required
placeholder="Ex: DEV-2026-99182"
/>
<InputError class="mt-2" :message="form.errors.quote_number" />
</div>
<!-- Montant HT -->
<div>
<InputLabel for="amount_ht" value="Montant HT ()" class="font-bold" />
<TextInput
id="amount_ht"
type="number"
step="0.01"
min="0"
class="mt-1 block w-full"
v-model="form.amount_ht"
required
placeholder="0.00"
/>
<InputError class="mt-2" :message="form.errors.amount_ht" />
<div class="mt-3 flex items-center">
<input
id="exclude_vat"
type="checkbox"
v-model="form.exclude_vat"
class="rounded border-gray-300 text-sky-600 shadow-sm focus:ring-sky-500 dark:border-gray-700 dark:bg-gray-900"
/>
<label for="exclude_vat" class="ml-2 text-xs font-semibold text-slate-600 dark:text-slate-400 cursor-pointer select-none">
Exonérer de TVA / Non soumis à la TVA
</label>
</div>
</div>
<!-- Montant TTC (calculé) -->
<div>
<InputLabel for="amount_ttc" :value="form.exclude_vat ? 'Montant TTC (€) - Sans TVA' : 'Montant TTC (€) - TVA 20 % (Calculé)'" class="font-bold text-slate-400" />
<input
id="amount_ttc"
type="text"
readonly
class="mt-1 block w-full text-sm rounded-lg border-slate-300 bg-slate-50 text-slate-500 shadow-sm focus:border-slate-300 focus:ring-0 dark:bg-slate-950 dark:border-slate-850 dark:text-slate-400 cursor-not-allowed font-semibold"
:value="new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(amountTtc)"
/>
</div>
<!-- Date souhaitée de livraison -->
<div>
<InputLabel for="delivery_deadline" value="Date souhaitée de livraison" class="font-bold" />
<TextInput
id="delivery_deadline"
type="date"
class="mt-1 block w-full"
v-model="form.delivery_deadline"
required
/>
<InputError class="mt-2" :message="form.errors.delivery_deadline" />
</div>
</div>
<!-- Pièces jointes -->
<div class="border-b border-slate-100 dark:border-slate-850 pb-5 pt-4">
<h3 class="text-md font-bold text-slate-800 dark:text-slate-200 mb-1">
Pièces jointes (PDF, Images, Word, Excel)
</h3>
<p class="text-xs text-slate-500 dark:text-slate-400">
Joignez les fichiers officiels pour le cycle de vie de la commande.
</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<!-- Devis -->
<div class="space-y-2">
<InputLabel for="quote_file" value="Devis initial (PDF, Image...)" class="font-bold" />
<input
id="quote_file"
type="file"
@input="form.quote_file = $event.target.files[0]"
class="block w-full text-xs text-slate-500 file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-xs file:font-semibold file:bg-sky-50 file:text-sky-700 hover:file:bg-sky-100 dark:file:bg-slate-800 dark:file:text-slate-300"
/>
<div v-if="getExistingFile('quote')" class="text-xs mt-1 text-slate-500">
Fichier existant :
<a :href="getExistingFile('quote').url" target="_blank" class="text-sky-600 dark:text-sky-400 hover:underline inline-flex items-center">
{{ getExistingFile('quote').file_name }}
</a>
</div>
<InputError class="mt-2" :message="form.errors.quote_file" />
</div>
<!-- Bon de livraison -->
<div class="space-y-2">
<InputLabel for="delivery_note_file" value="Bon de livraison" class="font-bold" />
<input
id="delivery_note_file"
type="file"
@input="form.delivery_note_file = $event.target.files[0]"
class="block w-full text-xs text-slate-500 file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-xs file:font-semibold file:bg-sky-50 file:text-sky-700 hover:file:bg-sky-100 dark:file:bg-slate-800 dark:file:text-slate-300"
/>
<div v-if="getExistingFile('delivery_note')" class="text-xs mt-1 text-slate-500">
Fichier existant :
<a :href="getExistingFile('delivery_note').url" target="_blank" class="text-sky-600 dark:text-sky-400 hover:underline inline-flex items-center">
{{ getExistingFile('delivery_note').file_name }}
</a>
</div>
<InputError class="mt-2" :message="form.errors.delivery_note_file" />
</div>
<!-- Facture -->
<div class="space-y-2">
<InputLabel for="invoice_file" value="Facture d'achat" class="font-bold" />
<input
id="invoice_file"
type="file"
@input="form.invoice_file = $event.target.files[0]"
class="block w-full text-xs text-slate-500 file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-xs file:font-semibold file:bg-sky-50 file:text-sky-700 hover:file:bg-sky-100 dark:file:bg-slate-800 dark:file:text-slate-300"
/>
<div v-if="getExistingFile('invoice')" class="text-xs mt-1 text-slate-500">
Fichier existant :
<a :href="getExistingFile('invoice').url" target="_blank" class="text-sky-600 dark:text-sky-400 hover:underline inline-flex items-center">
{{ getExistingFile('invoice').file_name }}
</a>
</div>
<InputError class="mt-2" :message="form.errors.invoice_file" />
</div>
</div>
<!-- Notes libres -->
<div class="border-b border-slate-100 dark:border-slate-850 pb-5 pt-4">
<h3 class="text-md font-bold text-slate-800 dark:text-slate-200 mb-1">
Notes & Commentaires
</h3>
</div>
<div>
<InputLabel for="notes" value="Notes libres" class="font-bold" />
<textarea
id="notes"
rows="4"
class="mt-1 block w-full text-sm rounded-lg border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:bg-slate-950 dark:border-slate-800 dark:text-slate-200"
v-model="form.notes"
placeholder="Commentaires libres sur la commande, le suivi fournisseur, ou l'utilisation du matériel..."
></textarea>
<InputError class="mt-2" :message="form.errors.notes" />
</div>
<!-- Barre de boutons -->
<div class="flex items-center justify-end gap-3 pt-6 border-t border-slate-100 dark:border-slate-850">
<Link
:href="isEdit ? route('commandes.show', { commande: order?.data?.id || order?.id }) : route('commandes.index')"
class="inline-flex items-center px-4 py-2 border border-slate-300 text-sm font-semibold rounded-lg text-slate-700 bg-white 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>
<PrimaryButton
type="submit"
:disabled="form.processing"
class="inline-flex items-center px-5 py-2.5 text-sm font-semibold text-white bg-sky-600 hover:bg-sky-500 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-sky-500 transition-colors"
>
<svg v-if="form.processing" class="animate-spin -ml-1 mr-3 h-4 w-4 text-white" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
{{ isEdit ? 'Mettre à jour' : 'Créer la demande' }}
</PrimaryButton>
</div>
</form>
</div>
</div>
</div>
</AuthenticatedLayout>
</template>

View File

@@ -0,0 +1,368 @@
<script setup>
import { ref, reactive } from 'vue';
import { Head, Link, router } from '@inertiajs/vue3';
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue';
import StatusBadge from '@/Components/StatusBadge.vue';
const props = defineProps({
orders: Object,
filters: Object,
});
// État des filtres réactifs
const form = reactive({
search: props.filters?.search || '',
status: props.filters?.status || '',
requested_by: props.filters?.requested_by || '',
type: props.filters?.type || '',
date_start: props.filters?.date_start || '',
date_end: props.filters?.date_end || '',
});
// Appliquer les filtres
const applyFilters = () => {
router.get(route('commandes.index'), form, {
preserveState: true,
replace: true,
});
};
// Réinitialiser les filtres
const resetFilters = () => {
form.search = '';
form.status = '';
form.requested_by = '';
form.type = '';
form.date_start = '';
form.date_end = '';
applyFilters();
};
// Exporter en CSV
const exportCsv = () => {
// Génère l'URL d'export avec les filtres courants
const params = new URLSearchParams(form).toString();
window.location.href = `${route('commandes.index')}?export=1&${params}`;
};
// Types de commandes pour le filtre
const orderTypes = [
'Matériel réseau / serveur',
'Licences logicielles',
'Consommables / câblage',
'Prestations / services',
];
// Demandeur pour le filtre
const demandeurs = ['Jérémy', 'Sylvain', 'Kévin'];
</script>
<template>
<Head title="Suivi des Commandes" />
<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">
Suivi des Commandes
</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 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-sky-500 dark:bg-slate-900 dark:text-slate-300 dark:border-slate-755 dark:hover:bg-slate-800 transition-colors"
>
<svg class="w-4 h-4 mr-2" 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 en CSV
</button>
<Link
:href="route('commandes.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 shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-sky-500 dark:bg-sky-500 dark:hover:bg-sky-400 transition-colors"
>
<svg class="w-4 h-4 mr-2" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
</svg>
Créer une demande
</Link>
</div>
</div>
</template>
<div class="py-6">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<!-- Session Flash Messages -->
<div v-if="$page.props.flash?.success" class="mb-6 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>
<!-- Filtres -->
<div class="bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-xl p-5 shadow-sm mb-6">
<h3 class="text-sm font-bold text-slate-800 dark:text-slate-200 mb-4 flex items-center uppercase tracking-wider">
<svg class="w-4 h-4 mr-2 text-slate-400" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 3c2.755 0 5.455.232 8.083.678.533.09.917.556.917 1.096v1.044a2.25 2.25 0 0 1-.659 1.591l-5.432 5.432a2.25 2.25 0 0 0-.659 1.591v2.927a2.25 2.25 0 0 1-1.244 2.013L9.75 21v-6.562a2.25 2.25 0 0 0-.659-1.591L3.659 7.409A2.25 2.25 0 0 1 3 5.818V4.774c0-.54.384-1.006.917-1.096A48.32 48.32 0 0 1 12 3Z" />
</svg>
Filtres de recherche
</h3>
<div class="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-4">
<!-- Recherche textuelle -->
<div class="md:col-span-2 lg:col-span-1">
<label class="block text-xs font-semibold text-slate-500 dark:text-slate-400 mb-1">Recherche</label>
<input
type="text"
v-model="form.search"
placeholder="Numéro, libellé, fournisseur..."
@keydown.enter="applyFilters"
class="w-full text-sm rounded-lg border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:bg-slate-950 dark:border-slate-800 dark:text-slate-200"
/>
</div>
<!-- Statut -->
<div>
<label class="block text-xs font-semibold text-slate-500 dark:text-slate-400 mb-1">Statut</label>
<select
v-model="form.status"
@change="applyFilters"
class="w-full text-sm rounded-lg border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:bg-slate-950 dark:border-slate-800 dark:text-slate-200"
>
<option value="">Tous les statuts</option>
<option value="draft">Brouillon</option>
<option value="validated">Validée</option>
<option value="ordered">Commandée</option>
<option value="delivered">Livrée</option>
<option value="closed">Clôturée</option>
</select>
</div>
<!-- Demandeur -->
<div>
<label class="block text-xs font-semibold text-slate-500 dark:text-slate-400 mb-1">Demandeur</label>
<select
v-model="form.requested_by"
@change="applyFilters"
class="w-full text-sm rounded-lg border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:bg-slate-950 dark:border-slate-800 dark:text-slate-200"
>
<option value="">Tous les demandeurs</option>
<option v-for="d in demandeurs" :key="d" :value="d">{{ d }}</option>
</select>
</div>
<!-- Type -->
<div>
<label class="block text-xs font-semibold text-slate-500 dark:text-slate-400 mb-1">Type de commande</label>
<select
v-model="form.type"
@change="applyFilters"
class="w-full text-sm rounded-lg border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:bg-slate-950 dark:border-slate-800 dark:text-slate-200"
>
<option value="">Tous les types</option>
<option v-for="t in orderTypes" :key="t" :value="t">{{ t }}</option>
</select>
</div>
<!-- Période début -->
<div>
<label class="block text-xs font-semibold text-slate-500 dark:text-slate-400 mb-1">Livraison souhaitée du</label>
<input
type="date"
v-model="form.date_start"
@change="applyFilters"
class="w-full text-sm rounded-lg border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:bg-slate-950 dark:border-slate-800 dark:text-slate-200"
/>
</div>
<!-- Période fin -->
<div>
<label class="block text-xs font-semibold text-slate-500 dark:text-slate-400 mb-1">Au</label>
<input
type="date"
v-model="form.date_end"
@change="applyFilters"
class="w-full text-sm rounded-lg border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:bg-slate-950 dark:border-slate-800 dark:text-slate-200"
/>
</div>
<!-- Actions filtres -->
<div class="flex items-end gap-2 md:col-span-3 lg:col-span-2">
<button
@click="applyFilters"
class="inline-flex items-center justify-center px-4 py-2 border border-transparent text-sm font-semibold rounded-lg text-white bg-slate-800 hover:bg-slate-700 dark:bg-slate-800 dark:hover:bg-slate-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-slate-500 transition-colors w-full sm:w-auto"
>
Filtrer
</button>
<button
@click="resetFilters"
class="inline-flex items-center justify-center px-4 py-2 border border-slate-300 text-sm font-semibold rounded-lg text-slate-700 bg-white hover:bg-slate-50 dark:bg-slate-900 dark:text-slate-300 dark:border-slate-800 dark:hover:bg-slate-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-slate-500 transition-colors w-full sm:w-auto"
>
Réinitialiser
</button>
</div>
</div>
</div>
<!-- Tableau des commandes -->
<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="min-w-full divide-y divide-slate-200 dark:divide-slate-800 text-left">
<thead class="bg-slate-50 dark:bg-slate-950">
<tr>
<th scope="col" class="px-6 py-4 text-xs font-bold uppercase tracking-wider text-slate-500 dark:text-slate-400">Numéro</th>
<th scope="col" class="px-6 py-4 text-xs font-bold uppercase tracking-wider text-slate-500 dark:text-slate-400">Libellé / Article</th>
<th scope="col" class="px-6 py-4 text-xs font-bold uppercase tracking-wider text-slate-500 dark:text-slate-400">Type</th>
<th scope="col" class="px-6 py-4 text-xs font-bold uppercase tracking-wider text-slate-500 dark:text-slate-400">Fournisseur</th>
<th scope="col" class="px-6 py-4 text-xs font-bold uppercase tracking-wider text-slate-500 dark:text-slate-400">Demandeur / Service</th>
<th scope="col" class="px-6 py-4 text-xs font-bold uppercase tracking-wider text-slate-500 dark:text-slate-400">Date souhaitée</th>
<th scope="col" class="px-6 py-4 text-xs font-bold uppercase tracking-wider text-slate-500 dark:text-slate-400 text-right">Montant TTC</th>
<th scope="col" class="px-6 py-4 text-xs font-bold uppercase tracking-wider text-slate-500 dark:text-slate-400">Statut</th>
<th scope="col" class="px-6 py-4 text-xs font-bold uppercase tracking-wider text-slate-500 dark:text-slate-400 text-right">Actions</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-200 dark:divide-slate-850">
<tr
v-for="order in orders?.data || []"
:key="order.id"
class="hover:bg-slate-50/50 dark:hover:bg-slate-900/40 transition-colors"
>
<!-- Numéro -->
<td class="px-6 py-4 whitespace-nowrap text-sm font-semibold text-sky-600 dark:text-sky-400">
<Link :href="route('commandes.show', { commande: order.id })" class="hover:underline">
{{ order.number }}
</Link>
</td>
<!-- Libellé -->
<td class="px-6 py-4 text-sm text-slate-900 dark:text-slate-200 max-w-xs truncate">
<span class="font-medium">{{ order.label }}</span>
</td>
<!-- Type -->
<td class="px-6 py-4 whitespace-nowrap text-sm text-slate-500 dark:text-slate-400">
{{ order.type }}
</td>
<!-- Fournisseur -->
<td class="px-6 py-4 whitespace-nowrap text-sm text-slate-900 dark:text-slate-300 font-medium">
{{ order.supplier }}
</td>
<!-- Demandeur / Service -->
<td class="px-6 py-4 whitespace-nowrap text-sm text-slate-900 dark:text-slate-350">
<div class="font-medium text-slate-800 dark:text-slate-200">{{ order.requested_by }}</div>
<div class="text-xs text-slate-450 dark:text-slate-400 font-semibold">{{ order.prescriber }}</div>
</td>
<!-- Date -->
<td class="px-6 py-4 whitespace-nowrap text-sm text-slate-500 dark:text-slate-400">
<div class="flex items-center gap-1.5">
<span>{{ order.delivery_deadline_formatted }}</span>
<!-- Alerte de retard -->
<span
v-if="order.is_overdue"
class="inline-flex items-center px-1.5 py-0.5 rounded text-xxs font-bold bg-rose-100 text-rose-800 border border-rose-200 dark:bg-rose-950/40 dark:text-rose-300 dark:border-rose-900"
title="Date souhaitée de livraison dépassée"
>
RETARD
</span>
</div>
</td>
<!-- Montant TTC -->
<td class="px-6 py-4 whitespace-nowrap text-sm text-slate-900 dark:text-slate-100 text-right font-bold">
{{ new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(order.amount_ttc) }}
</td>
<!-- Statut -->
<td class="px-6 py-4 whitespace-nowrap text-sm">
<StatusBadge :status="order.status" />
</td>
<!-- Actions -->
<td class="px-6 py-4 whitespace-nowrap text-sm font-semibold text-right space-x-3">
<Link
:href="route('commandes.show', { commande: order.id })"
class="text-slate-600 hover:text-slate-900 dark:text-slate-400 dark:hover:text-slate-200"
>
Voir
</Link>
<Link
v-if="order.can.update"
:href="route('commandes.edit', { commande: order.id })"
class="text-sky-600 hover:text-sky-900 dark:text-sky-400 dark:hover:text-sky-300"
>
Éditer
</Link>
</td>
</tr>
<tr v-if="!orders?.data || orders.data.length === 0">
<td colspan="9" class="px-6 py-8 text-center text-sm text-slate-500 dark:text-slate-400">
Aucune commande ne correspond aux critères de recherche.
</td>
</tr>
</tbody>
</table>
</div>
<!-- Pagination -->
<div
v-if="orders.meta && orders.meta.links && orders.meta.links.length > 3"
class="bg-slate-50 dark:bg-slate-950 border-t border-slate-200 dark:border-slate-800 px-6 py-4 flex items-center justify-between"
>
<div class="flex-1 flex justify-between sm:hidden">
<Link
:href="orders.links.prev || '#'"
:disabled="!orders.links.prev"
class="relative inline-flex items-center px-4 py-2 border border-slate-300 text-sm font-semibold rounded-md text-slate-700 bg-white hover:bg-slate-50 disabled:opacity-50"
>
Précédent
</Link>
<Link
:href="orders.links.next || '#'"
:disabled="!orders.links.next"
class="relative inline-flex items-center px-4 py-2 border border-slate-300 text-sm font-semibold rounded-md text-slate-700 bg-white hover:bg-slate-50 disabled:opacity-50"
>
Suivant
</Link>
</div>
<div class="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
<div>
<p class="text-sm text-slate-700 dark:text-slate-400">
Affichage de la page
<span class="font-bold">{{ orders.meta.current_page }}</span>
sur
<span class="font-bold">{{ orders.meta.last_page }}</span>
</p>
</div>
<div>
<nav class="relative z-0 inline-flex rounded-md shadow-sm -space-x-px" aria-label="Pagination">
<template v-for="(link, key) in orders.meta.links" :key="key">
<div
v-if="link.url === null"
class="relative inline-flex items-center px-3 py-2 border border-slate-300 dark:border-slate-800 text-xs font-semibold text-slate-400 bg-white dark:bg-slate-900 rounded-md cursor-default select-none"
v-html="link.label"
/>
<Link
v-else
:href="link.url"
class="relative inline-flex items-center px-3 py-2 border text-xs font-semibold transition-colors"
:class="[
link.active
? 'z-10 bg-sky-50 border-sky-500 text-sky-600 dark:bg-sky-950/40 dark:border-sky-500 dark:text-sky-300'
: 'bg-white border-slate-300 text-slate-500 hover:bg-slate-50 dark:bg-slate-900 dark:border-slate-800 dark:text-slate-400 dark:hover:bg-slate-800'
]"
v-html="link.label"
/>
</template>
</nav>
</div>
</div>
</div>
</div>
</div>
</div>
</AuthenticatedLayout>
</template>

View File

@@ -0,0 +1,371 @@
<script setup>
import { Head, Link, router } from '@inertiajs/vue3';
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue';
import StatusBadge from '@/Components/StatusBadge.vue';
const props = defineProps({
order: Object,
});
// Lancer la transition de statut
const transitionTo = (status) => {
router.post(route('commandes.transition', { commande: props.order.data.id }), {
new_status: status,
}, {
preserveScroll: true,
});
};
// Supprimer la commande
const deleteOrder = () => {
if (confirm("Êtes-vous sûr de vouloir supprimer définitivement cette commande ainsi que ses pièces jointes ? Cette action est irréversible.")) {
router.delete(route('commandes.destroy', { commande: props.order.data.id }));
}
};
// Label français des statuts pour l'historique
const getStatusLabel = (status) => {
switch (status) {
case 'draft': return 'Brouillon';
case 'validated': return 'Validée';
case 'ordered': return 'Commandée';
case 'delivered': return 'Livrée';
case 'closed': return 'Clôturée';
default: return status || 'Création';
}
};
// Obtenir la couleur du point de la timeline
const getTimelineColor = (status) => {
switch (status) {
case 'draft': return 'bg-slate-400 dark:bg-slate-500';
case 'validated': return 'bg-sky-500 dark:bg-sky-400';
case 'ordered': return 'bg-amber-500 dark:bg-amber-400';
case 'delivered': return 'bg-emerald-500 dark:bg-emerald-400';
case 'closed': return 'bg-purple-500 dark:bg-purple-400';
default: return 'bg-gray-400';
}
};
</script>
<template>
<Head :title="`Commande ${order.data.number}`" />
<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">
Commande {{ order.data.number }}
</h2>
<StatusBadge :status="order.data.status" />
<span
v-if="order.data.is_overdue"
class="inline-flex items-center px-2 py-0.5 rounded text-xs font-bold bg-rose-100 text-rose-800 border border-rose-200 dark:bg-rose-950/40 dark:text-rose-300 dark:border-rose-900"
>
En Retard de Livraison
</span>
</div>
<div class="flex gap-2">
<Link
:href="route('commandes.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"
>
Retour à la liste
</Link>
<Link
v-if="order.data.can.update"
:href="route('commandes.edit', { commande: order.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
v-if="order.data.can.delete"
@click="deleteOrder"
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">
<!-- 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>
<div v-if="$page.props.errors.error" class="p-4 bg-rose-50 border border-rose-200 text-rose-800 rounded-lg dark:bg-rose-950/40 dark:border-rose-900 dark:text-rose-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="M12 9v3.75m9-.75a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9 3.75h.008v.008H12v-.008Z" />
</svg>
<span>{{ $page.props.errors.error }}</span>
</div>
<!-- Section des transitions contextuelles de statut -->
<div class="bg-gradient-to-r from-sky-50 to-indigo-50 border border-sky-100 dark:from-slate-900 dark:to-slate-900 dark:border-slate-800 rounded-xl p-6 shadow-sm flex flex-col md:flex-row md:items-center justify-between gap-4">
<div>
<h3 class="text-sm font-bold text-slate-850 dark:text-slate-200">
Actions sur le cycle de vie de la commande
</h3>
<p class="text-xs text-slate-500 dark:text-slate-400 mt-0.5">
Faites avancer cette demande à l'étape suivante en fonction de votre rôle.
</p>
</div>
<div class="flex flex-wrap gap-2">
<!-- Transition Draft -> Validated -->
<button
v-if="order.data.can_transition_to.validated"
@click="transitionTo('validated')"
class="inline-flex items-center px-4 py-2 text-sm font-semibold text-white bg-sky-600 rounded-lg hover:bg-sky-500 transition-colors shadow-sm"
>
<svg class="w-4 h-4 mr-2" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5" />
</svg>
Valider la commande (Émettre BC)
</button>
<!-- Transition Validated -> Ordered -->
<button
v-if="order.data.can_transition_to.ordered"
@click="transitionTo('ordered')"
class="inline-flex items-center px-4 py-2 text-sm font-semibold text-white bg-amber-600 rounded-lg hover:bg-amber-500 transition-colors shadow-sm"
>
<svg class="w-4 h-4 mr-2" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 12 3.269 3.125A59.769 59.769 0 0 1 21.485 12 59.768 59.768 0 0 1 3.27 20.875L5.999 12Zm0 0h7.5" />
</svg>
Marquer comme Commandée
</button>
<!-- Transition Ordered -> Delivered -->
<button
v-if="order.data.can_transition_to.delivered"
@click="transitionTo('delivered')"
class="inline-flex items-center px-4 py-2 text-sm font-semibold text-white bg-emerald-600 rounded-lg hover:bg-emerald-500 transition-colors shadow-sm"
>
<svg class="w-4 h-4 mr-2" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 18.75a1.5 1.5 0 0 1-3 0m3 0a1.5 1.5 0 0 0-3 0m3 0h6m-9 0H3.375a1.125 1.125 0 0 1-1.125-1.125V14.25m17.25 4.5a1.5 1.5 0 0 1-3 0m3 0a1.5 1.5 0 0 0-3 0m3 0h1.125c.621 0 1.129-.504 1.129-1.125V11.25M3 14.25h16.5M5.25 14.25V4.5A2.25 2.25 0 0 1 7.5 2.25h6A2.25 2.25 0 0 1 15.75 4.5V14.25m-3-12h.008v.008H12.75V2.25Z" />
</svg>
Marquer comme Livrée (Réceptionnée)
</button>
<!-- Transition Delivered -> Closed -->
<button
v-if="order.data.can_transition_to.closed"
@click="transitionTo('closed')"
class="inline-flex items-center px-4 py-2 text-sm font-semibold text-white bg-purple-600 rounded-lg hover:bg-purple-500 transition-colors shadow-sm"
>
<svg class="w-4 h-4 mr-2" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="m20.25 7.5-.625 10.632a2.25 2.25 0 0 1-2.247 2.118H6.622a2.25 2.25 0 0 1-2.247-2.118L3.75 7.5M10 11.25h4M3.375 7.5h17.25c.621 0 1.125-.504 1.125-1.125v-1.5c0-.621-.504-1.125-1.125-1.125H3.375c-.621 0-1.125.504-1.125 1.125v1.5c0 .621.504 1.125 1.125 1.125Z" />
</svg>
Clôturer & Archiver la commande
</button>
<!-- Aucune transition possible -->
<span
v-if="!order.data.can_transition_to.validated && !order.data.can_transition_to.ordered && !order.data.can_transition_to.delivered && !order.data.can_transition_to.closed"
class="text-xs font-semibold text-slate-500 bg-slate-100 dark:bg-slate-800 dark:text-slate-400 px-3 py-2 rounded-lg"
>
{{ order.data.status === 'closed' ? 'Cette commande est archivée.' : 'En attente d\'une action par un profil autorisé.' }}
</span>
</div>
</div>
<!-- Grille Principale (Fiche + Pièces Jointes / Historique) -->
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<!-- Fiche Détails (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-800 dark:text-slate-200 uppercase tracking-wider">
Détails de la demande
</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">Libellé / Article</dt>
<dd class="text-base font-bold text-slate-900 dark:text-white">{{ order.data.label }}</dd>
</div>
<div class="px-6 py-4 space-y-1">
<dt class="text-xs font-semibold uppercase tracking-wider text-slate-400">Type de commande</dt>
<dd class="text-slate-800 dark:text-slate-200 font-medium">{{ order.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">Fournisseur</dt>
<dd class="text-slate-800 dark:text-slate-200 font-medium">{{ order.data.supplier }}</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">Numéro de Devis</dt>
<dd class="text-slate-800 dark:text-slate-200 font-mono">{{ order.data.quote_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">Demandeur</dt>
<dd class="text-slate-800 dark:text-slate-200 font-medium">{{ order.data.requested_by }}</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">Prescripteur / Service à l'origine</dt>
<dd class="text-slate-800 dark:text-slate-200 font-semibold">{{ order.data.prescriber || 'Non spécifié' }}</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">Livraison souhaitée</dt>
<dd class="text-slate-800 dark:text-slate-200 font-medium">{{ order.data.delivery_deadline_formatted }}</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">Montant HT</dt>
<dd class="text-slate-800 dark:text-slate-200 font-semibold">
{{ new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(order.data.amount_ht) }}
</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">
{{ order.data.exclude_vat ? 'Montant TTC (Sans TVA)' : 'Montant TTC (TVA 20%)' }}
</dt>
<dd class="text-lg font-bold text-sky-600 dark:text-sky-400">
{{ new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(order.data.amount_ttc) }}
</dd>
</div>
</dl>
</div>
<!-- Notes libres -->
<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-800 dark:text-slate-200 uppercase tracking-wider">
Notes & Commentaires
</h3>
</div>
<div class="p-6 text-sm text-slate-700 dark:text-slate-300 leading-relaxed whitespace-pre-line">
{{ order.data.notes || 'Aucun commentaire sur cette commande.' }}
</div>
</div>
<!-- Pièces Jointes -->
<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-800 dark:text-slate-200 uppercase tracking-wider">
Documents joints
</h3>
</div>
<div class="p-6">
<div v-if="order.data.attachments && order.data.attachments.length > 0" class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div
v-for="file in order.data.attachments"
:key="file.id"
class="flex items-start p-3 border border-slate-200 dark:border-slate-800 hover:border-slate-300 dark:hover:border-slate-700 rounded-xl bg-slate-50/50 dark:bg-slate-950/20 hover:bg-slate-50 dark:hover:bg-slate-950/55 transition-colors"
>
<div class="p-2 bg-sky-50 dark:bg-slate-800 text-sky-600 dark:text-sky-400 rounded-lg mr-3">
<!-- Icône générique de fichier -->
<svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m2.25 0H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z" />
</svg>
</div>
<div class="flex-1 min-w-0">
<span class="block text-xxs font-semibold uppercase tracking-wider text-slate-400">
{{
file.file_type === 'quote' ? 'Devis' :
file.file_type === 'delivery_note' ? 'Bon de livraison' :
file.file_type === 'invoice' ? 'Facture' : file.file_type
}}
</span>
<a
:href="file.url"
target="_blank"
class="block text-sm font-bold text-slate-800 dark:text-slate-200 truncate hover:text-sky-600 dark:hover:text-sky-400 hover:underline mt-0.5"
>
{{ file.file_name }}
</a>
<span class="block text-xxs text-slate-400 mt-0.5">Ajouté le {{ file.created_at }}</span>
</div>
</div>
</div>
<div v-else class="text-center py-6 text-sm text-slate-500 dark:text-slate-400">
Aucune pièce jointe n'a été téléversée pour le moment.
</div>
</div>
</div>
</div>
<!-- Journal d'historique (Col 3) -->
<div class="bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-xl shadow-sm overflow-hidden h-fit">
<div class="border-b border-slate-100 dark:border-slate-850 px-6 py-4">
<h3 class="text-sm font-bold text-slate-800 dark:text-slate-200 uppercase tracking-wider">
Historique des statuts
</h3>
</div>
<div class="p-6">
<ul v-if="order.data.status_logs && order.data.status_logs.length > 0" class="relative border-l border-slate-200 dark:border-slate-800 space-y-6 ml-2 pl-4">
<li
v-for="log in order.data.status_logs"
:key="log.id"
class="relative"
>
<!-- Point coloré -->
<div
:class="`absolute -left-6.5 top-1.5 h-3.5 w-3.5 rounded-full border-2 border-white dark:border-slate-900 ${getTimelineColor(log.new_status)} shadow-sm`"
></div>
<div class="space-y-1">
<div class="flex items-center justify-between gap-2">
<span class="text-sm font-bold text-slate-800 dark:text-slate-200">
{{ getStatusLabel(log.new_status) }}
</span>
<span class="text-xxs font-medium text-slate-400">
{{ log.changed_at }}
</span>
</div>
<p class="text-xs text-slate-500 dark:text-slate-400">
Auteur : <span class="font-semibold text-slate-600 dark:text-slate-300">{{ log.user.name }}</span>
<span class="text-slate-400 text-xxs font-normal">({{ log.user.role === 'chef_service' ? 'Chef de service' : 'Admin réseau' }})</span>
</p>
<p v-if="log.old_status" class="text-xxs text-slate-400">
Transition depuis : {{ getStatusLabel(log.old_status) }}
</p>
<p v-else class="text-xxs text-slate-400 font-semibold italic text-sky-600 dark:text-sky-400">
Création initiale de la demande
</p>
</div>
</li>
</ul>
<div v-else class="text-center py-6 text-sm text-slate-500 dark:text-slate-400">
Aucun journal de statut disponible.
</div>
</div>
</div>
</div>
</div>
</div>
</AuthenticatedLayout>
</template>
<style scoped>
/* Ajustement de la position du point de timeline sur la bordure gauche */
.-left-6\.5 {
left: -23px;
}
</style>

View File

@@ -0,0 +1,305 @@
<script setup>
import { Head, Link } from '@inertiajs/vue3';
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue';
import MetricCard from '@/Components/MetricCard.vue';
import StatusBadge from '@/Components/StatusBadge.vue';
const props = defineProps({
metrics: Object,
pending_deliveries: Object,
overdue_orders: Object,
});
// Formatage monétaire en euros
const formatCurrency = (value) => {
return new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(value);
};
</script>
<template>
<Head title="Tableau de bord" />
<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">
Tableau de bord de l'Infrastructure
</h2>
<span class="text-xs text-slate-500 dark:text-slate-400 font-medium">
Aujourd'hui : {{ new Date().toLocaleDateString('fr-FR', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }) }}
</span>
</div>
</template>
<div class="py-6">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 space-y-6">
<!-- Alertes de retard critiques -->
<div
v-if="overdue_orders.data && overdue_orders.data.length > 0"
class="bg-rose-50 border border-rose-200 dark:bg-rose-950/20 dark:border-rose-900 rounded-xl p-5 shadow-sm space-y-3"
>
<div class="flex items-center text-rose-800 dark:text-rose-350">
<svg class="w-5 h-5 mr-2.5 shrink-0" 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>
<h3 class="text-sm font-bold uppercase tracking-wider">
Alertes de livraison : {{ overdue_orders.data.length }} commande(s) en retard
</h3>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
<div
v-for="order in overdue_orders.data"
:key="order.id"
class="bg-white dark:bg-slate-900 border border-rose-200/60 dark:border-rose-900/40 rounded-lg p-3 hover:shadow-sm transition-shadow flex justify-between items-center"
>
<div class="min-w-0">
<Link
:href="route('commandes.show', { commande: order.id })"
class="text-xs font-bold text-rose-700 dark:text-rose-400 hover:underline block truncate"
>
{{ order.number }} - {{ order.label }}
</Link>
<span class="text-xxs text-slate-400 block mt-0.5">
Date limite : {{ order.delivery_deadline_formatted }} (par {{ order.requested_by }} - Service : {{ order.prescriber || 'Non spécifié' }})
</span>
</div>
<span class="text-xs font-bold text-rose-600 dark:text-rose-400 ml-2">
{{ formatCurrency(order.amount_ttc) }}
</span>
</div>
</div>
</div>
<!-- Grille de KPIs principaux -->
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
<!-- Montant Total Engagé -->
<MetricCard
title="Montant engagé HT"
:value="formatCurrency(metrics.total_engaged_ht)"
subtitle="Commandes validées + transmises"
>
<template #icon>
<svg class="h-6 w-6 text-sky-600 dark:text-sky-400" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 18.75a60.07 60.07 0 0 1 15.797 2.101c.727.198 1.453-.342 1.453-1.096V18.75M3.75 4.5v.75m-.75-3h1.5m-1.5 0v-1.5m0 1.5H1.5m1.5 0c-.828 0-1.5.672-1.5 1.5v12a1.5 1.5 0 0 0 1.5 1.5h15a1.5 1.5 0 0 0 1.5-1.5v-12c0-.828-.672-1.5-1.5-1.5h-15Z" />
</svg>
</template>
</MetricCard>
<!-- Montant Total TTC des commandes de l'année en cours -->
<MetricCard
title="Montant total TTC"
:value="formatCurrency(metrics.total_ttc)"
:subtitle="`Année en cours (${new Date().getFullYear()})`"
>
<template #icon>
<svg class="h-6 w-6 text-emerald-600 dark:text-emerald-400" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6v12m-3-2.818.879.659c1.171.879 3.07.879 4.242 0 1.172-.879 1.172-2.303 0-3.182C13.536 12.219 12.768 12 12 12c-.725 0-1.45-.22-2.003-.659-1.106-.879-1.106-2.303 0-3.182s2.9-.879 4.006 0l.415.33M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
</svg>
</template>
</MetricCard>
<!-- Alertes retards -->
<MetricCard
title="Commandes en retard"
:value="metrics.overdue_count"
:subtitle="metrics.overdue_count > 0 ? 'Livraison souhaitée dépassée' : 'Aucun retard constaté'"
:class="{ 'border-rose-300 bg-rose-50/10 dark:border-rose-900/30': metrics.overdue_count > 0 }"
>
<template #icon>
<svg
class="h-6 w-6"
:class="metrics.overdue_count > 0 ? 'text-rose-600 dark:text-rose-455 animate-pulse' : 'text-slate-400'"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
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>
</template>
</MetricCard>
<!-- Total Commandes actives -->
<MetricCard
title="Commandes actives"
:value="metrics.counts.validated + metrics.counts.ordered + metrics.counts.draft"
subtitle="Brouillons, validées et en cours"
>
<template #icon>
<svg class="h-6 w-6 text-indigo-650 dark:text-indigo-400" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m2.25 0H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z" />
</svg>
</template>
</MetricCard>
</div>
<!-- Grille d'état des pipelines par statut -->
<div class="bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-xl p-5 shadow-sm">
<h3 class="text-sm font-bold text-slate-800 dark:text-slate-200 uppercase tracking-wider mb-4">
Pipeline du cycle de vie des demandes
</h3>
<div class="grid grid-cols-2 sm:grid-cols-5 gap-4">
<!-- Brouillon -->
<Link
:href="route('commandes.index', { status: 'draft' })"
class="bg-slate-50 dark:bg-slate-950/40 border border-slate-100 dark:border-slate-850 rounded-xl p-4 text-center hover:scale-[1.02] hover:shadow-md transition-all duration-200 block hover:border-slate-350 dark:hover:border-slate-750 cursor-pointer group"
>
<span class="text-xs font-semibold text-slate-500 uppercase tracking-wider group-hover:text-slate-755 dark:group-hover:text-slate-350 transition-colors">Brouillons</span>
<div class="mt-2 text-2xl font-bold text-slate-800 dark:text-slate-200">{{ metrics.counts.draft }}</div>
</Link>
<!-- Validée -->
<Link
:href="route('commandes.index', { status: 'validated' })"
class="bg-sky-50/40 dark:bg-slate-950/40 border border-sky-100/50 dark:border-slate-850 rounded-xl p-4 text-center hover:scale-[1.02] hover:shadow-md transition-all duration-200 block hover:border-sky-300 dark:hover:border-sky-900 cursor-pointer group"
>
<span class="text-xs font-semibold text-sky-600 dark:text-sky-400 uppercase tracking-wider group-hover:text-sky-755 dark:group-hover:text-sky-300 transition-colors">Validées (BC)</span>
<div class="mt-2 text-2xl font-bold text-sky-700 dark:text-sky-300">{{ metrics.counts.validated }}</div>
</Link>
<!-- Commandée -->
<Link
:href="route('commandes.index', { status: 'ordered' })"
class="bg-amber-50/40 dark:bg-slate-950/40 border border-amber-100/50 dark:border-slate-850 rounded-xl p-4 text-center hover:scale-[1.02] hover:shadow-md transition-all duration-200 block hover:border-amber-300 dark:hover:border-amber-900 cursor-pointer group"
>
<span class="text-xs font-semibold text-amber-600 dark:text-amber-400 uppercase tracking-wider group-hover:text-amber-755 dark:group-hover:text-amber-300 transition-colors">Transmises</span>
<div class="mt-2 text-2xl font-bold text-amber-700 dark:text-amber-300">{{ metrics.counts.ordered }}</div>
</Link>
<!-- Livrée -->
<Link
:href="route('commandes.index', { status: 'delivered' })"
class="bg-emerald-50/40 dark:bg-slate-950/40 border border-emerald-100/50 dark:border-slate-850 rounded-xl p-4 text-center hover:scale-[1.02] hover:shadow-md transition-all duration-200 block hover:border-emerald-300 dark:hover:border-emerald-900 cursor-pointer group"
>
<span class="text-xs font-semibold text-emerald-600 dark:text-emerald-400 uppercase tracking-wider group-hover:text-emerald-755 dark:group-hover:text-emerald-300 transition-colors">Livrées</span>
<div class="mt-2 text-2xl font-bold text-emerald-700 dark:text-emerald-300">{{ metrics.counts.delivered }}</div>
</Link>
<!-- Clôturée -->
<Link
:href="route('commandes.index', { status: 'closed' })"
class="bg-purple-50/40 dark:bg-slate-950/40 border border-purple-100/50 dark:border-slate-850 rounded-xl p-4 text-center col-span-2 sm:col-span-1 hover:scale-[1.02] hover:shadow-md transition-all duration-200 block hover:border-purple-300 dark:hover:border-purple-900 cursor-pointer group"
>
<span class="text-xs font-semibold text-purple-600 dark:text-purple-400 uppercase tracking-wider group-hover:text-purple-755 dark:group-hover:text-purple-300 transition-colors">Clôturées</span>
<div class="mt-2 text-2xl font-bold text-purple-700 dark:text-purple-300">{{ metrics.counts.closed }}</div>
</Link>
</div>
</div>
<!-- Section principale : Tableau des commandes en attente de livraison -->
<div class="bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-xl shadow-sm overflow-hidden">
<div class="px-6 py-5 border-b border-slate-100 dark:border-slate-850 flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div>
<h3 class="text-base font-bold text-slate-800 dark:text-slate-200">
Commandes en attente de livraison
</h3>
<p class="text-xs text-slate-500 dark:text-slate-400 mt-0.5">
Commandes validées ou transmises au fournisseur en attente de réception physique ou logique.
</p>
</div>
<Link
:href="route('commandes.index', { status: ['validated', 'ordered'] })"
class="text-xs font-bold text-sky-600 hover:text-sky-500 dark:text-sky-455 dark:hover:text-sky-350 inline-flex items-center"
>
Voir toutes les commandes actives
<svg class="w-3.5 h-3.5 ml-1" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="m8.25 4.5 7.5 7.5-7.5 7.5" />
</svg>
</Link>
</div>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-slate-200 dark:divide-slate-800 text-left">
<thead class="bg-slate-50 dark:bg-slate-950">
<tr>
<th scope="col" class="px-6 py-4 text-xs font-bold uppercase tracking-wider text-slate-500 dark:text-slate-400">Numéro</th>
<th scope="col" class="px-6 py-4 text-xs font-bold uppercase tracking-wider text-slate-500 dark:text-slate-400">Libellé</th>
<th scope="col" class="px-6 py-4 text-xs font-bold uppercase tracking-wider text-slate-500 dark:text-slate-400">Fournisseur</th>
<th scope="col" class="px-6 py-4 text-xs font-bold uppercase tracking-wider text-slate-500 dark:text-slate-400">Demandeur / Service</th>
<th scope="col" class="px-6 py-4 text-xs font-bold uppercase tracking-wider text-slate-500 dark:text-slate-400">Date de livraison souhaitée</th>
<th scope="col" class="px-6 py-4 text-xs font-bold uppercase tracking-wider text-slate-500 dark:text-slate-400 text-right">Montant TTC</th>
<th scope="col" class="px-6 py-4 text-xs font-bold uppercase tracking-wider text-slate-500 dark:text-slate-400">Statut</th>
<th scope="col" class="px-6 py-4 text-xs font-bold uppercase tracking-wider text-slate-500 dark:text-slate-400 text-right">Action</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-200 dark:divide-slate-850">
<tr
v-for="order in pending_deliveries.data"
:key="order.id"
class="hover:bg-slate-50/50 dark:hover:bg-slate-900/40 transition-colors"
>
<!-- Numéro -->
<td class="px-6 py-4 whitespace-nowrap text-sm font-semibold text-sky-600 dark:text-sky-400">
<Link :href="route('commandes.show', { commande: order.id })" class="hover:underline">
{{ order.number }}
</Link>
</td>
<!-- Libellé -->
<td class="px-6 py-4 text-sm text-slate-900 dark:text-slate-200 max-w-xs truncate font-semibold">
{{ order.label }}
</td>
<!-- Fournisseur -->
<td class="px-6 py-4 whitespace-nowrap text-sm text-slate-900 dark:text-slate-350 font-medium">
{{ order.supplier }}
</td>
<!-- Demandeur / Service -->
<td class="px-6 py-4 whitespace-nowrap text-sm text-slate-900 dark:text-slate-350">
<div class="font-medium text-slate-800 dark:text-slate-200">{{ order.requested_by }}</div>
<div class="text-xs text-slate-450 dark:text-slate-400 font-semibold">{{ order.prescriber }}</div>
</td>
<!-- Date -->
<td class="px-6 py-4 whitespace-nowrap text-sm text-slate-500 dark:text-slate-400">
<div class="flex items-center gap-1.5">
<span>{{ order.delivery_deadline_formatted }}</span>
<!-- Retard -->
<span
v-if="order.is_overdue"
class="inline-flex items-center px-1.5 py-0.5 rounded text-xxs font-bold bg-rose-100 text-rose-800 border border-rose-200 dark:bg-rose-950/40 dark:text-rose-300 dark:border-rose-900 animate-pulse"
>
RETARD
</span>
</div>
</td>
<!-- Montant -->
<td class="px-6 py-4 whitespace-nowrap text-sm text-slate-900 dark:text-slate-100 text-right font-bold">
{{ formatCurrency(order.amount_ttc) }}
</td>
<!-- Statut -->
<td class="px-6 py-4 whitespace-nowrap text-sm">
<StatusBadge :status="order.status" />
</td>
<!-- Action -->
<td class="px-6 py-4 whitespace-nowrap text-sm font-semibold text-right">
<Link
:href="route('commandes.show', { commande: order.id })"
class="text-slate-600 hover:text-slate-900 dark:text-slate-400 dark:hover:text-slate-200"
>
Consulter
</Link>
</td>
</tr>
<tr v-if="pending_deliveries.data.length === 0">
<td colspan="8" class="px-6 py-8 text-center text-sm text-slate-500 dark:text-slate-400">
Aucune commande active en attente de livraison pour le moment.
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</AuthenticatedLayout>
</template>

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>

View File

@@ -0,0 +1,56 @@
<script setup>
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue';
import DeleteUserForm from './Partials/DeleteUserForm.vue';
import UpdatePasswordForm from './Partials/UpdatePasswordForm.vue';
import UpdateProfileInformationForm from './Partials/UpdateProfileInformationForm.vue';
import { Head } from '@inertiajs/vue3';
defineProps({
mustVerifyEmail: {
type: Boolean,
},
status: {
type: String,
},
});
</script>
<template>
<Head title="Profile" />
<AuthenticatedLayout>
<template #header>
<h2
class="text-xl font-semibold leading-tight text-gray-800 dark:text-gray-200"
>
Profile
</h2>
</template>
<div class="py-12">
<div class="mx-auto max-w-7xl space-y-6 sm:px-6 lg:px-8">
<div
class="bg-white p-4 shadow sm:rounded-lg sm:p-8 dark:bg-gray-800"
>
<UpdateProfileInformationForm
:must-verify-email="mustVerifyEmail"
:status="status"
class="max-w-xl"
/>
</div>
<div
class="bg-white p-4 shadow sm:rounded-lg sm:p-8 dark:bg-gray-800"
>
<UpdatePasswordForm class="max-w-xl" />
</div>
<div
class="bg-white p-4 shadow sm:rounded-lg sm:p-8 dark:bg-gray-800"
>
<DeleteUserForm class="max-w-xl" />
</div>
</div>
</div>
</AuthenticatedLayout>
</template>

View File

@@ -0,0 +1,108 @@
<script setup>
import DangerButton from '@/Components/DangerButton.vue';
import InputError from '@/Components/InputError.vue';
import InputLabel from '@/Components/InputLabel.vue';
import Modal from '@/Components/Modal.vue';
import SecondaryButton from '@/Components/SecondaryButton.vue';
import TextInput from '@/Components/TextInput.vue';
import { useForm } from '@inertiajs/vue3';
import { nextTick, ref } from 'vue';
const confirmingUserDeletion = ref(false);
const passwordInput = ref(null);
const form = useForm({
password: '',
});
const confirmUserDeletion = () => {
confirmingUserDeletion.value = true;
nextTick(() => passwordInput.value.focus());
};
const deleteUser = () => {
form.delete(route('profile.destroy'), {
preserveScroll: true,
onSuccess: () => closeModal(),
onError: () => passwordInput.value.focus(),
onFinish: () => form.reset(),
});
};
const closeModal = () => {
confirmingUserDeletion.value = false;
form.clearErrors();
form.reset();
};
</script>
<template>
<section class="space-y-6">
<header>
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">
Delete Account
</h2>
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
Once your account is deleted, all of its resources and data will
be permanently deleted. Before deleting your account, please
download any data or information that you wish to retain.
</p>
</header>
<DangerButton @click="confirmUserDeletion">Delete Account</DangerButton>
<Modal :show="confirmingUserDeletion" @close="closeModal">
<div class="p-6">
<h2
class="text-lg font-medium text-gray-900 dark:text-gray-100"
>
Are you sure you want to delete your account?
</h2>
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
Once your account is deleted, all of its resources and data
will be permanently deleted. Please enter your password to
confirm you would like to permanently delete your account.
</p>
<div class="mt-6">
<InputLabel
for="password"
value="Password"
class="sr-only"
/>
<TextInput
id="password"
ref="passwordInput"
v-model="form.password"
type="password"
class="mt-1 block w-3/4"
placeholder="Password"
@keyup.enter="deleteUser"
/>
<InputError :message="form.errors.password" class="mt-2" />
</div>
<div class="mt-6 flex justify-end">
<SecondaryButton @click="closeModal">
Cancel
</SecondaryButton>
<DangerButton
class="ms-3"
:class="{ 'opacity-25': form.processing }"
:disabled="form.processing"
@click="deleteUser"
>
Delete Account
</DangerButton>
</div>
</div>
</Modal>
</section>
</template>

View File

@@ -0,0 +1,122 @@
<script setup>
import InputError from '@/Components/InputError.vue';
import InputLabel from '@/Components/InputLabel.vue';
import PrimaryButton from '@/Components/PrimaryButton.vue';
import TextInput from '@/Components/TextInput.vue';
import { useForm } from '@inertiajs/vue3';
import { ref } from 'vue';
const passwordInput = ref(null);
const currentPasswordInput = ref(null);
const form = useForm({
current_password: '',
password: '',
password_confirmation: '',
});
const updatePassword = () => {
form.put(route('password.update'), {
preserveScroll: true,
onSuccess: () => form.reset(),
onError: () => {
if (form.errors.password) {
form.reset('password', 'password_confirmation');
passwordInput.value.focus();
}
if (form.errors.current_password) {
form.reset('current_password');
currentPasswordInput.value.focus();
}
},
});
};
</script>
<template>
<section>
<header>
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">
Update Password
</h2>
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
Ensure your account is using a long, random password to stay
secure.
</p>
</header>
<form @submit.prevent="updatePassword" class="mt-6 space-y-6">
<div>
<InputLabel for="current_password" value="Current Password" />
<TextInput
id="current_password"
ref="currentPasswordInput"
v-model="form.current_password"
type="password"
class="mt-1 block w-full"
autocomplete="current-password"
/>
<InputError
:message="form.errors.current_password"
class="mt-2"
/>
</div>
<div>
<InputLabel for="password" value="New Password" />
<TextInput
id="password"
ref="passwordInput"
v-model="form.password"
type="password"
class="mt-1 block w-full"
autocomplete="new-password"
/>
<InputError :message="form.errors.password" class="mt-2" />
</div>
<div>
<InputLabel
for="password_confirmation"
value="Confirm Password"
/>
<TextInput
id="password_confirmation"
v-model="form.password_confirmation"
type="password"
class="mt-1 block w-full"
autocomplete="new-password"
/>
<InputError
:message="form.errors.password_confirmation"
class="mt-2"
/>
</div>
<div class="flex items-center gap-4">
<PrimaryButton :disabled="form.processing">Save</PrimaryButton>
<Transition
enter-active-class="transition ease-in-out"
enter-from-class="opacity-0"
leave-active-class="transition ease-in-out"
leave-to-class="opacity-0"
>
<p
v-if="form.recentlySuccessful"
class="text-sm text-gray-600 dark:text-gray-400"
>
Saved.
</p>
</Transition>
</div>
</form>
</section>
</template>

View File

@@ -0,0 +1,112 @@
<script setup>
import InputError from '@/Components/InputError.vue';
import InputLabel from '@/Components/InputLabel.vue';
import PrimaryButton from '@/Components/PrimaryButton.vue';
import TextInput from '@/Components/TextInput.vue';
import { Link, useForm, usePage } from '@inertiajs/vue3';
defineProps({
mustVerifyEmail: {
type: Boolean,
},
status: {
type: String,
},
});
const user = usePage().props.auth.user;
const form = useForm({
name: user.name,
email: user.email,
});
</script>
<template>
<section>
<header>
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">
Profile Information
</h2>
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
Update your account's profile information and email address.
</p>
</header>
<form
@submit.prevent="form.patch(route('profile.update'))"
class="mt-6 space-y-6"
>
<div>
<InputLabel for="name" value="Name" />
<TextInput
id="name"
type="text"
class="mt-1 block w-full"
v-model="form.name"
required
autofocus
autocomplete="name"
/>
<InputError class="mt-2" :message="form.errors.name" />
</div>
<div>
<InputLabel for="email" value="Email" />
<TextInput
id="email"
type="email"
class="mt-1 block w-full"
v-model="form.email"
required
autocomplete="username"
/>
<InputError class="mt-2" :message="form.errors.email" />
</div>
<div v-if="mustVerifyEmail && user.email_verified_at === null">
<p class="mt-2 text-sm text-gray-800 dark:text-gray-200">
Your email address is unverified.
<Link
:href="route('verification.send')"
method="post"
as="button"
class="rounded-md text-sm text-gray-600 underline hover:text-gray-900 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:text-gray-400 dark:hover:text-gray-100 dark:focus:ring-offset-gray-800"
>
Click here to re-send the verification email.
</Link>
</p>
<div
v-show="status === 'verification-link-sent'"
class="mt-2 text-sm font-medium text-green-600 dark:text-green-400"
>
A new verification link has been sent to your email address.
</div>
</div>
<div class="flex items-center gap-4">
<PrimaryButton :disabled="form.processing">Save</PrimaryButton>
<Transition
enter-active-class="transition ease-in-out"
enter-from-class="opacity-0"
leave-active-class="transition ease-in-out"
leave-to-class="opacity-0"
>
<p
v-if="form.recentlySuccessful"
class="text-sm text-gray-600 dark:text-gray-400"
>
Saved.
</p>
</Transition>
</div>
</form>
</section>
</template>

View File

@@ -0,0 +1,386 @@
<script setup>
import { Head, Link } from '@inertiajs/vue3';
defineProps({
canLogin: {
type: Boolean,
},
canRegister: {
type: Boolean,
},
laravelVersion: {
type: String,
required: true,
},
phpVersion: {
type: String,
required: true,
},
});
function handleImageError() {
document.getElementById('screenshot-container')?.classList.add('!hidden');
document.getElementById('docs-card')?.classList.add('!row-span-1');
document.getElementById('docs-card-content')?.classList.add('!flex-row');
document.getElementById('background')?.classList.add('!hidden');
}
</script>
<template>
<Head title="Welcome" />
<div class="bg-gray-50 text-black/50 dark:bg-black dark:text-white/50">
<img
id="background"
class="absolute -left-20 top-0 max-w-[877px]"
src="https://laravel.com/assets/img/welcome/background.svg"
/>
<div
class="relative flex min-h-screen flex-col items-center justify-center selection:bg-[#FF2D20] selection:text-white"
>
<div class="relative w-full max-w-2xl px-6 lg:max-w-7xl">
<header
class="grid grid-cols-2 items-center gap-2 py-10 lg:grid-cols-3"
>
<div class="flex lg:col-start-2 lg:justify-center">
<svg
class="h-12 w-auto text-white lg:h-16 lg:text-[#FF2D20]"
viewBox="0 0 62 65"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M61.8548 14.6253C61.8778 14.7102 61.8895 14.7978 61.8897 14.8858V28.5615C61.8898 28.737 61.8434 28.9095 61.7554 29.0614C61.6675 29.2132 61.5409 29.3392 61.3887 29.4265L49.9104 36.0351V49.1337C49.9104 49.4902 49.7209 49.8192 49.4118 49.9987L25.4519 63.7916C25.3971 63.8227 25.3372 63.8427 25.2774 63.8639C25.255 63.8714 25.2338 63.8851 25.2101 63.8913C25.0426 63.9354 24.8666 63.9354 24.6991 63.8913C24.6716 63.8838 24.6467 63.8689 24.6205 63.8589C24.5657 63.8389 24.5084 63.8215 24.456 63.7916L0.501061 49.9987C0.348882 49.9113 0.222437 49.7853 0.134469 49.6334C0.0465019 49.4816 0.000120578 49.3092 0 49.1337L0 8.10652C0 8.01678 0.0124642 7.92953 0.0348998 7.84477C0.0423783 7.8161 0.0598282 7.78993 0.0697995 7.76126C0.0884958 7.70891 0.105946 7.65531 0.133367 7.6067C0.152063 7.5743 0.179485 7.54812 0.20192 7.51821C0.230588 7.47832 0.256763 7.43719 0.290416 7.40229C0.319084 7.37362 0.356476 7.35243 0.388883 7.32751C0.425029 7.29759 0.457436 7.26518 0.498568 7.2415L12.4779 0.345059C12.6296 0.257786 12.8015 0.211853 12.9765 0.211853C13.1515 0.211853 13.3234 0.257786 13.475 0.345059L25.4531 7.2415H25.4556C25.4955 7.26643 25.5292 7.29759 25.5653 7.32626C25.5977 7.35119 25.6339 7.37362 25.6625 7.40104C25.6974 7.43719 25.7224 7.47832 25.7523 7.51821C25.7735 7.54812 25.8021 7.5743 25.8196 7.6067C25.8483 7.65656 25.8645 7.70891 25.8844 7.76126C25.8944 7.78993 25.9118 7.8161 25.9193 7.84602C25.9423 7.93096 25.954 8.01853 25.9542 8.10652V33.7317L35.9355 27.9844V14.8846C35.9355 14.7973 35.948 14.7088 35.9704 14.6253C35.9792 14.5954 35.9954 14.5692 36.0053 14.5405C36.0253 14.4882 36.0427 14.4346 36.0702 14.386C36.0888 14.3536 36.1163 14.3274 36.1375 14.2975C36.1674 14.2576 36.1923 14.2165 36.2272 14.1816C36.2559 14.1529 36.292 14.1317 36.3244 14.1068C36.3618 14.0769 36.3942 14.0445 36.4341 14.0208L48.4147 7.12434C48.5663 7.03694 48.7383 6.99094 48.9133 6.99094C49.0883 6.99094 49.2602 7.03694 49.4118 7.12434L61.3899 14.0208C61.4323 14.0457 61.4647 14.0769 61.5021 14.1055C61.5333 14.1305 61.5694 14.1529 61.5981 14.1803C61.633 14.2165 61.6579 14.2576 61.6878 14.2975C61.7103 14.3274 61.7377 14.3536 61.7551 14.386C61.7838 14.4346 61.8 14.4882 61.8199 14.5405C61.8312 14.5692 61.8474 14.5954 61.8548 14.6253ZM59.893 27.9844V16.6121L55.7013 19.0252L49.9104 22.3593V33.7317L59.8942 27.9844H59.893ZM47.9149 48.5566V37.1768L42.2187 40.4299L25.953 49.7133V61.2003L47.9149 48.5566ZM1.99677 9.83281V48.5566L23.9562 61.199V49.7145L12.4841 43.2219L12.4804 43.2194L12.4754 43.2169C12.4368 43.1945 12.4044 43.1621 12.3682 43.1347C12.3371 43.1097 12.3009 43.0898 12.2735 43.0624L12.271 43.0586C12.2386 43.0275 12.2162 42.9888 12.1887 42.9539C12.1638 42.9203 12.1339 42.8916 12.114 42.8567L12.1127 42.853C12.0903 42.8156 12.0766 42.7707 12.0604 42.7283C12.0442 42.6909 12.023 42.656 12.013 42.6161C12.0005 42.5688 11.998 42.5177 11.9931 42.4691C11.9881 42.4317 11.9781 42.3943 11.9781 42.3569V15.5801L6.18848 12.2446L1.99677 9.83281ZM12.9777 2.36177L2.99764 8.10652L12.9752 13.8513L22.9541 8.10527L12.9752 2.36177H12.9777ZM18.1678 38.2138L23.9574 34.8809V9.83281L19.7657 12.2459L13.9749 15.5801V40.6281L18.1678 38.2138ZM48.9133 9.14105L38.9344 14.8858L48.9133 20.6305L58.8909 14.8846L48.9133 9.14105ZM47.9149 22.3593L42.124 19.0252L37.9323 16.6121V27.9844L43.7219 31.3174L47.9149 33.7317V22.3593ZM24.9533 47.987L39.59 39.631L46.9065 35.4555L36.9352 29.7145L25.4544 36.3242L14.9907 42.3482L24.9533 47.987Z"
fill="currentColor"
/>
</svg>
</div>
<nav v-if="canLogin" class="-mx-3 flex flex-1 justify-end">
<Link
v-if="$page.props.auth.user"
:href="route('dashboard')"
class="rounded-md px-3 py-2 text-black ring-1 ring-transparent transition hover:text-black/70 focus:outline-none focus-visible:ring-[#FF2D20] dark:text-white dark:hover:text-white/80 dark:focus-visible:ring-white"
>
Dashboard
</Link>
<template v-else>
<Link
:href="route('login')"
class="rounded-md px-3 py-2 text-black ring-1 ring-transparent transition hover:text-black/70 focus:outline-none focus-visible:ring-[#FF2D20] dark:text-white dark:hover:text-white/80 dark:focus-visible:ring-white"
>
Log in
</Link>
<Link
v-if="canRegister"
:href="route('register')"
class="rounded-md px-3 py-2 text-black ring-1 ring-transparent transition hover:text-black/70 focus:outline-none focus-visible:ring-[#FF2D20] dark:text-white dark:hover:text-white/80 dark:focus-visible:ring-white"
>
Register
</Link>
</template>
</nav>
</header>
<main class="mt-6">
<div class="grid gap-6 lg:grid-cols-2 lg:gap-8">
<a
href="https://laravel.com/docs"
id="docs-card"
class="flex flex-col items-start gap-6 overflow-hidden rounded-lg bg-white p-6 shadow-[0px_14px_34px_0px_rgba(0,0,0,0.08)] ring-1 ring-white/[0.05] transition duration-300 hover:text-black/70 hover:ring-black/20 focus:outline-none focus-visible:ring-[#FF2D20] md:row-span-3 lg:p-10 lg:pb-10 dark:bg-zinc-900 dark:ring-zinc-800 dark:hover:text-white/70 dark:hover:ring-zinc-700 dark:focus-visible:ring-[#FF2D20]"
>
<div
id="screenshot-container"
class="relative flex w-full flex-1 items-stretch"
>
<img
src="https://laravel.com/assets/img/welcome/docs-light.svg"
alt="Laravel documentation screenshot"
class="aspect-video h-full w-full flex-1 rounded-[10px] object-cover object-top drop-shadow-[0px_4px_34px_rgba(0,0,0,0.06)] dark:hidden"
@error="handleImageError"
/>
<img
src="https://laravel.com/assets/img/welcome/docs-dark.svg"
alt="Laravel documentation screenshot"
class="hidden aspect-video h-full w-full flex-1 rounded-[10px] object-cover object-top drop-shadow-[0px_4px_34px_rgba(0,0,0,0.25)] dark:block"
/>
<div
class="absolute -bottom-16 -left-16 h-40 w-[calc(100%+8rem)] bg-gradient-to-b from-transparent via-white to-white dark:via-zinc-900 dark:to-zinc-900"
></div>
</div>
<div
class="relative flex items-center gap-6 lg:items-end"
>
<div
id="docs-card-content"
class="flex items-start gap-6 lg:flex-col"
>
<div
class="flex size-12 shrink-0 items-center justify-center rounded-full bg-[#FF2D20]/10 sm:size-16"
>
<svg
class="size-5 sm:size-6"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<path
fill="#FF2D20"
d="M23 4a1 1 0 0 0-1.447-.894L12.224 7.77a.5.5 0 0 1-.448 0L2.447 3.106A1 1 0 0 0 1 4v13.382a1.99 1.99 0 0 0 1.105 1.79l9.448 4.728c.14.065.293.1.447.1.154-.005.306-.04.447-.105l9.453-4.724a1.99 1.99 0 0 0 1.1-1.789V4ZM3 6.023a.25.25 0 0 1 .362-.223l7.5 3.75a.251.251 0 0 1 .138.223v11.2a.25.25 0 0 1-.362.224l-7.5-3.75a.25.25 0 0 1-.138-.22V6.023Zm18 11.2a.25.25 0 0 1-.138.224l-7.5 3.75a.249.249 0 0 1-.329-.099.249.249 0 0 1-.033-.12V9.772a.251.251 0 0 1 .138-.224l7.5-3.75a.25.25 0 0 1 .362.224v11.2Z"
/>
<path
fill="#FF2D20"
d="m3.55 1.893 8 4.048a1.008 1.008 0 0 0 .9 0l8-4.048a1 1 0 0 0-.9-1.785l-7.322 3.706a.506.506 0 0 1-.452 0L4.454.108a1 1 0 0 0-.9 1.785H3.55Z"
/>
</svg>
</div>
<div class="pt-3 sm:pt-5 lg:pt-0">
<h2
class="text-xl font-semibold text-black dark:text-white"
>
Documentation
</h2>
<p class="mt-4 text-sm/relaxed">
Laravel has wonderful documentation
covering every aspect of the
framework. Whether you are a
newcomer or have prior experience
with Laravel, we recommend reading
our documentation from beginning to
end.
</p>
</div>
</div>
<svg
class="size-6 shrink-0 stroke-[#FF2D20]"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M4.5 12h15m0 0l-6.75-6.75M19.5 12l-6.75 6.75"
/>
</svg>
</div>
</a>
<a
href="https://laracasts.com"
class="flex items-start gap-4 rounded-lg bg-white p-6 shadow-[0px_14px_34px_0px_rgba(0,0,0,0.08)] ring-1 ring-white/[0.05] transition duration-300 hover:text-black/70 hover:ring-black/20 focus:outline-none focus-visible:ring-[#FF2D20] lg:pb-10 dark:bg-zinc-900 dark:ring-zinc-800 dark:hover:text-white/70 dark:hover:ring-zinc-700 dark:focus-visible:ring-[#FF2D20]"
>
<div
class="flex size-12 shrink-0 items-center justify-center rounded-full bg-[#FF2D20]/10 sm:size-16"
>
<svg
class="size-5 sm:size-6"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<g fill="#FF2D20">
<path
d="M24 8.25a.5.5 0 0 0-.5-.5H.5a.5.5 0 0 0-.5.5v12a2.5 2.5 0 0 0 2.5 2.5h19a2.5 2.5 0 0 0 2.5-2.5v-12Zm-7.765 5.868a1.221 1.221 0 0 1 0 2.264l-6.626 2.776A1.153 1.153 0 0 1 8 18.123v-5.746a1.151 1.151 0 0 1 1.609-1.035l6.626 2.776ZM19.564 1.677a.25.25 0 0 0-.177-.427H15.6a.106.106 0 0 0-.072.03l-4.54 4.543a.25.25 0 0 0 .177.427h3.783c.027 0 .054-.01.073-.03l4.543-4.543ZM22.071 1.318a.047.047 0 0 0-.045.013l-4.492 4.492a.249.249 0 0 0 .038.385.25.25 0 0 0 .14.042h5.784a.5.5 0 0 0 .5-.5v-2a2.5 2.5 0 0 0-1.925-2.432ZM13.014 1.677a.25.25 0 0 0-.178-.427H9.101a.106.106 0 0 0-.073.03l-4.54 4.543a.25.25 0 0 0 .177.427H8.4a.106.106 0 0 0 .073-.03l4.54-4.543ZM6.513 1.677a.25.25 0 0 0-.177-.427H2.5A2.5 2.5 0 0 0 0 3.75v2a.5.5 0 0 0 .5.5h1.4a.106.106 0 0 0 .073-.03l4.54-4.543Z"
/>
</g>
</svg>
</div>
<div class="pt-3 sm:pt-5">
<h2
class="text-xl font-semibold text-black dark:text-white"
>
Laracasts
</h2>
<p class="mt-4 text-sm/relaxed">
Laracasts offers thousands of video
tutorials on Laravel, PHP, and JavaScript
development. Check them out, see for
yourself, and massively level up your
development skills in the process.
</p>
</div>
<svg
class="size-6 shrink-0 self-center stroke-[#FF2D20]"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M4.5 12h15m0 0l-6.75-6.75M19.5 12l-6.75 6.75"
/>
</svg>
</a>
<a
href="https://laravel-news.com"
class="flex items-start gap-4 rounded-lg bg-white p-6 shadow-[0px_14px_34px_0px_rgba(0,0,0,0.08)] ring-1 ring-white/[0.05] transition duration-300 hover:text-black/70 hover:ring-black/20 focus:outline-none focus-visible:ring-[#FF2D20] lg:pb-10 dark:bg-zinc-900 dark:ring-zinc-800 dark:hover:text-white/70 dark:hover:ring-zinc-700 dark:focus-visible:ring-[#FF2D20]"
>
<div
class="flex size-12 shrink-0 items-center justify-center rounded-full bg-[#FF2D20]/10 sm:size-16"
>
<svg
class="size-5 sm:size-6"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<g fill="#FF2D20">
<path
d="M8.75 4.5H5.5c-.69 0-1.25.56-1.25 1.25v4.75c0 .69.56 1.25 1.25 1.25h3.25c.69 0 1.25-.56 1.25-1.25V5.75c0-.69-.56-1.25-1.25-1.25Z"
/>
<path
d="M24 10a3 3 0 0 0-3-3h-2V2.5a2 2 0 0 0-2-2H2a2 2 0 0 0-2 2V20a3.5 3.5 0 0 0 3.5 3.5h17A3.5 3.5 0 0 0 24 20V10ZM3.5 21.5A1.5 1.5 0 0 1 2 20V3a.5.5 0 0 1 .5-.5h14a.5.5 0 0 1 .5.5v17c0 .295.037.588.11.874a.5.5 0 0 1-.484.625L3.5 21.5ZM22 20a1.5 1.5 0 1 1-3 0V9.5a.5.5 0 0 1 .5-.5H21a1 1 0 0 1 1 1v10Z"
/>
<path
d="M12.751 6.047h2a.75.75 0 0 1 .75.75v.5a.75.75 0 0 1-.75.75h-2A.75.75 0 0 1 12 7.3v-.5a.75.75 0 0 1 .751-.753ZM12.751 10.047h2a.75.75 0 0 1 .75.75v.5a.75.75 0 0 1-.75.75h-2A.75.75 0 0 1 12 11.3v-.5a.75.75 0 0 1 .751-.753ZM4.751 14.047h10a.75.75 0 0 1 .75.75v.5a.75.75 0 0 1-.75.75h-10A.75.75 0 0 1 4 15.3v-.5a.75.75 0 0 1 .751-.753ZM4.75 18.047h7.5a.75.75 0 0 1 .75.75v.5a.75.75 0 0 1-.75.75h-7.5A.75.75 0 0 1 4 19.3v-.5a.75.75 0 0 1 .75-.753Z"
/>
</g>
</svg>
</div>
<div class="pt-3 sm:pt-5">
<h2
class="text-xl font-semibold text-black dark:text-white"
>
Laravel News
</h2>
<p class="mt-4 text-sm/relaxed">
Laravel News is a community driven portal
and newsletter aggregating all of the latest
and most important news in the Laravel
ecosystem, including new package releases
and tutorials.
</p>
</div>
<svg
class="size-6 shrink-0 self-center stroke-[#FF2D20]"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M4.5 12h15m0 0l-6.75-6.75M19.5 12l-6.75 6.75"
/>
</svg>
</a>
<div
class="flex items-start gap-4 rounded-lg bg-white p-6 shadow-[0px_14px_34px_0px_rgba(0,0,0,0.08)] ring-1 ring-white/[0.05] lg:pb-10 dark:bg-zinc-900 dark:ring-zinc-800"
>
<div
class="flex size-12 shrink-0 items-center justify-center rounded-full bg-[#FF2D20]/10 sm:size-16"
>
<svg
class="size-5 sm:size-6"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<g fill="#FF2D20">
<path
d="M16.597 12.635a.247.247 0 0 0-.08-.237 2.234 2.234 0 0 1-.769-1.68c.001-.195.03-.39.084-.578a.25.25 0 0 0-.09-.267 8.8 8.8 0 0 0-4.826-1.66.25.25 0 0 0-.268.181 2.5 2.5 0 0 1-2.4 1.824.045.045 0 0 0-.045.037 12.255 12.255 0 0 0-.093 3.86.251.251 0 0 0 .208.214c2.22.366 4.367 1.08 6.362 2.118a.252.252 0 0 0 .32-.079 10.09 10.09 0 0 0 1.597-3.733ZM13.616 17.968a.25.25 0 0 0-.063-.407A19.697 19.697 0 0 0 8.91 15.98a.25.25 0 0 0-.287.325c.151.455.334.898.548 1.328.437.827.981 1.594 1.619 2.28a.249.249 0 0 0 .32.044 29.13 29.13 0 0 0 2.506-1.99ZM6.303 14.105a.25.25 0 0 0 .265-.274 13.048 13.048 0 0 1 .205-4.045.062.062 0 0 0-.022-.07 2.5 2.5 0 0 1-.777-.982.25.25 0 0 0-.271-.149 11 11 0 0 0-5.6 2.815.255.255 0 0 0-.075.163c-.008.135-.02.27-.02.406.002.8.084 1.598.246 2.381a.25.25 0 0 0 .303.193 19.924 19.924 0 0 1 5.746-.438ZM9.228 20.914a.25.25 0 0 0 .1-.393 11.53 11.53 0 0 1-1.5-2.22 12.238 12.238 0 0 1-.91-2.465.248.248 0 0 0-.22-.187 18.876 18.876 0 0 0-5.69.33.249.249 0 0 0-.179.336c.838 2.142 2.272 4 4.132 5.353a.254.254 0 0 0 .15.048c1.41-.01 2.807-.282 4.117-.802ZM18.93 12.957l-.005-.008a.25.25 0 0 0-.268-.082 2.21 2.21 0 0 1-.41.081.25.25 0 0 0-.217.2c-.582 2.66-2.127 5.35-5.75 7.843a.248.248 0 0 0-.09.299.25.25 0 0 0 .065.091 28.703 28.703 0 0 0 2.662 2.12.246.246 0 0 0 .209.037c2.579-.701 4.85-2.242 6.456-4.378a.25.25 0 0 0 .048-.189 13.51 13.51 0 0 0-2.7-6.014ZM5.702 7.058a.254.254 0 0 0 .2-.165A2.488 2.488 0 0 1 7.98 5.245a.093.093 0 0 0 .078-.062 19.734 19.734 0 0 1 3.055-4.74.25.25 0 0 0-.21-.41 12.009 12.009 0 0 0-10.4 8.558.25.25 0 0 0 .373.281 12.912 12.912 0 0 1 4.826-1.814ZM10.773 22.052a.25.25 0 0 0-.28-.046c-.758.356-1.55.635-2.365.833a.25.25 0 0 0-.022.48c1.252.43 2.568.65 3.893.65.1 0 .2 0 .3-.008a.25.25 0 0 0 .147-.444c-.526-.424-1.1-.917-1.673-1.465ZM18.744 8.436a.249.249 0 0 0 .15.228 2.246 2.246 0 0 1 1.352 2.054c0 .337-.08.67-.23.972a.25.25 0 0 0 .042.28l.007.009a15.016 15.016 0 0 1 2.52 4.6.25.25 0 0 0 .37.132.25.25 0 0 0 .096-.114c.623-1.464.944-3.039.945-4.63a12.005 12.005 0 0 0-5.78-10.258.25.25 0 0 0-.373.274c.547 2.109.85 4.274.901 6.453ZM9.61 5.38a.25.25 0 0 0 .08.31c.34.24.616.561.8.935a.25.25 0 0 0 .3.127.631.631 0 0 1 .206-.034c2.054.078 4.036.772 5.69 1.991a.251.251 0 0 0 .267.024c.046-.024.093-.047.141-.067a.25.25 0 0 0 .151-.23A29.98 29.98 0 0 0 15.957.764a.25.25 0 0 0-.16-.164 11.924 11.924 0 0 0-2.21-.518.252.252 0 0 0-.215.076A22.456 22.456 0 0 0 9.61 5.38Z"
/>
</g>
</svg>
</div>
<div class="pt-3 sm:pt-5">
<h2
class="text-xl font-semibold text-black dark:text-white"
>
Vibrant Ecosystem
</h2>
<p class="mt-4 text-sm/relaxed">
Laravel's robust library of first-party
tools and libraries, such as
<a
href="https://forge.laravel.com"
class="rounded-sm underline hover:text-black focus:outline-none focus-visible:ring-1 focus-visible:ring-[#FF2D20] dark:hover:text-white dark:focus-visible:ring-[#FF2D20]"
>Forge</a
>,
<a
href="https://vapor.laravel.com"
class="rounded-sm underline hover:text-black focus:outline-none focus-visible:ring-1 focus-visible:ring-[#FF2D20] dark:hover:text-white"
>Vapor</a
>,
<a
href="https://nova.laravel.com"
class="rounded-sm underline hover:text-black focus:outline-none focus-visible:ring-1 focus-visible:ring-[#FF2D20] dark:hover:text-white"
>Nova</a
>,
<a
href="https://envoyer.io"
class="rounded-sm underline hover:text-black focus:outline-none focus-visible:ring-1 focus-visible:ring-[#FF2D20] dark:hover:text-white"
>Envoyer</a
>, and
<a
href="https://herd.laravel.com"
class="rounded-sm underline hover:text-black focus:outline-none focus-visible:ring-1 focus-visible:ring-[#FF2D20] dark:hover:text-white"
>Herd</a
>
help you take your projects to the next
level. Pair them with powerful open source
libraries like
<a
href="https://laravel.com/docs/billing"
class="rounded-sm underline hover:text-black focus:outline-none focus-visible:ring-1 focus-visible:ring-[#FF2D20] dark:hover:text-white"
>Cashier</a
>,
<a
href="https://laravel.com/docs/dusk"
class="rounded-sm underline hover:text-black focus:outline-none focus-visible:ring-1 focus-visible:ring-[#FF2D20] dark:hover:text-white"
>Dusk</a
>,
<a
href="https://laravel.com/docs/broadcasting"
class="rounded-sm underline hover:text-black focus:outline-none focus-visible:ring-1 focus-visible:ring-[#FF2D20] dark:hover:text-white"
>Echo</a
>,
<a
href="https://laravel.com/docs/horizon"
class="rounded-sm underline hover:text-black focus:outline-none focus-visible:ring-1 focus-visible:ring-[#FF2D20] dark:hover:text-white"
>Horizon</a
>,
<a
href="https://laravel.com/docs/sanctum"
class="rounded-sm underline hover:text-black focus:outline-none focus-visible:ring-1 focus-visible:ring-[#FF2D20] dark:hover:text-white"
>Sanctum</a
>,
<a
href="https://laravel.com/docs/telescope"
class="rounded-sm underline hover:text-black focus:outline-none focus-visible:ring-1 focus-visible:ring-[#FF2D20] dark:hover:text-white"
>Telescope</a
>, and more.
</p>
</div>
</div>
</div>
</main>
<footer
class="py-16 text-center text-sm text-black dark:text-white/70"
>
Laravel v{{ laravelVersion }} (PHP v{{ phpVersion }})
</footer>
</div>
</div>
</div>
</template>