Initial commit: Import existing Laravel project
This commit is contained in:
7
resources/js/Components/ApplicationLogo.vue
Normal file
7
resources/js/Components/ApplicationLogo.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<svg viewBox="0 0 316 316" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M305.8 81.125C305.77 80.995 305.69 80.885 305.65 80.755C305.56 80.525 305.49 80.285 305.37 80.075C305.29 79.935 305.17 79.815 305.07 79.685C304.94 79.515 304.83 79.325 304.68 79.175C304.55 79.045 304.39 78.955 304.25 78.845C304.09 78.715 303.95 78.575 303.77 78.475L251.32 48.275C249.97 47.495 248.31 47.495 246.96 48.275L194.51 78.475C194.33 78.575 194.19 78.725 194.03 78.845C193.89 78.955 193.73 79.045 193.6 79.175C193.45 79.325 193.34 79.515 193.21 79.685C193.11 79.815 192.99 79.935 192.91 80.075C192.79 80.285 192.71 80.525 192.63 80.755C192.58 80.875 192.51 80.995 192.48 81.125C192.38 81.495 192.33 81.875 192.33 82.265V139.625L148.62 164.795V52.575C148.62 52.185 148.57 51.805 148.47 51.435C148.44 51.305 148.36 51.195 148.32 51.065C148.23 50.835 148.16 50.595 148.04 50.385C147.96 50.245 147.84 50.125 147.74 49.995C147.61 49.825 147.5 49.635 147.35 49.485C147.22 49.355 147.06 49.265 146.92 49.155C146.76 49.025 146.62 48.885 146.44 48.785L93.99 18.585C92.64 17.805 90.98 17.805 89.63 18.585L37.18 48.785C37 48.885 36.86 49.035 36.7 49.155C36.56 49.265 36.4 49.355 36.27 49.485C36.12 49.635 36.01 49.825 35.88 49.995C35.78 50.125 35.66 50.245 35.58 50.385C35.46 50.595 35.38 50.835 35.3 51.065C35.25 51.185 35.18 51.305 35.15 51.435C35.05 51.805 35 52.185 35 52.575V232.235C35 233.795 35.84 235.245 37.19 236.025L142.1 296.425C142.33 296.555 142.58 296.635 142.82 296.725C142.93 296.765 143.04 296.835 143.16 296.865C143.53 296.965 143.9 297.015 144.28 297.015C144.66 297.015 145.03 296.965 145.4 296.865C145.5 296.835 145.59 296.775 145.69 296.745C145.95 296.655 146.21 296.565 146.45 296.435L251.36 236.035C252.72 235.255 253.55 233.815 253.55 232.245V174.885L303.81 145.945C305.17 145.165 306 143.725 306 142.155V82.265C305.95 81.875 305.89 81.495 305.8 81.125ZM144.2 227.205L100.57 202.515L146.39 176.135L196.66 147.195L240.33 172.335L208.29 190.625L144.2 227.205ZM244.75 114.995V164.795L226.39 154.225L201.03 139.625V89.825L219.39 100.395L244.75 114.995ZM249.12 57.105L292.81 82.265L249.12 107.425L205.43 82.265L249.12 57.105ZM114.49 184.425L96.13 194.995V85.305L121.49 70.705L139.85 60.135V169.815L114.49 184.425ZM91.76 27.425L135.45 52.585L91.76 77.745L48.07 52.585L91.76 27.425ZM43.67 60.135L62.03 70.705L87.39 85.305V202.545V202.555V202.565C87.39 202.735 87.44 202.895 87.46 203.055C87.49 203.265 87.49 203.485 87.55 203.695V203.705C87.6 203.875 87.69 204.035 87.76 204.195C87.84 204.375 87.89 204.575 87.99 204.745C87.99 204.745 87.99 204.755 88 204.755C88.09 204.905 88.22 205.035 88.33 205.175C88.45 205.335 88.55 205.495 88.69 205.635L88.7 205.645C88.82 205.765 88.98 205.855 89.12 205.965C89.28 206.085 89.42 206.225 89.59 206.325C89.6 206.325 89.6 206.325 89.61 206.335C89.62 206.335 89.62 206.345 89.63 206.345L139.87 234.775V285.065L43.67 229.705V60.135ZM244.75 229.705L148.58 285.075V234.775L219.8 194.115L244.75 179.875V229.705ZM297.2 139.625L253.49 164.795V114.995L278.85 100.395L297.21 89.825V139.625H297.2Z"
|
||||
/>
|
||||
</svg>
|
||||
</template>
|
||||
34
resources/js/Components/Checkbox.vue
Normal file
34
resources/js/Components/Checkbox.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
|
||||
const emit = defineEmits(['update:checked']);
|
||||
|
||||
const props = defineProps({
|
||||
checked: {
|
||||
type: [Array, Boolean],
|
||||
required: true,
|
||||
},
|
||||
value: {
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
const proxyChecked = computed({
|
||||
get() {
|
||||
return props.checked;
|
||||
},
|
||||
|
||||
set(val) {
|
||||
emit('update:checked', val);
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<input
|
||||
type="checkbox"
|
||||
:value="value"
|
||||
v-model="proxyChecked"
|
||||
class="rounded border-gray-300 text-indigo-600 shadow-sm focus:ring-indigo-500 dark:border-gray-700 dark:bg-gray-900 dark:focus:ring-indigo-600 dark:focus:ring-offset-gray-800"
|
||||
/>
|
||||
</template>
|
||||
7
resources/js/Components/DangerButton.vue
Normal file
7
resources/js/Components/DangerButton.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<button
|
||||
class="inline-flex items-center rounded-md border border-transparent bg-red-600 px-4 py-2 text-xs font-semibold uppercase tracking-widest text-white transition duration-150 ease-in-out hover:bg-red-500 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 active:bg-red-700 dark:focus:ring-offset-gray-800"
|
||||
>
|
||||
<slot />
|
||||
</button>
|
||||
</template>
|
||||
84
resources/js/Components/Dropdown.vue
Normal file
84
resources/js/Components/Dropdown.vue
Normal file
@@ -0,0 +1,84 @@
|
||||
<script setup>
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
align: {
|
||||
type: String,
|
||||
default: 'right',
|
||||
},
|
||||
width: {
|
||||
type: String,
|
||||
default: '48',
|
||||
},
|
||||
contentClasses: {
|
||||
type: String,
|
||||
default: 'py-1 bg-white dark:bg-gray-700',
|
||||
},
|
||||
});
|
||||
|
||||
const closeOnEscape = (e) => {
|
||||
if (open.value && e.key === 'Escape') {
|
||||
open.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => document.addEventListener('keydown', closeOnEscape));
|
||||
onUnmounted(() => document.removeEventListener('keydown', closeOnEscape));
|
||||
|
||||
const widthClass = computed(() => {
|
||||
return {
|
||||
48: 'w-48',
|
||||
}[props.width.toString()];
|
||||
});
|
||||
|
||||
const alignmentClasses = computed(() => {
|
||||
if (props.align === 'left') {
|
||||
return 'ltr:origin-top-left rtl:origin-top-right start-0';
|
||||
} else if (props.align === 'right') {
|
||||
return 'ltr:origin-top-right rtl:origin-top-left end-0';
|
||||
} else {
|
||||
return 'origin-top';
|
||||
}
|
||||
});
|
||||
|
||||
const open = ref(false);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="relative">
|
||||
<div @click="open = !open">
|
||||
<slot name="trigger" />
|
||||
</div>
|
||||
|
||||
<!-- Full Screen Dropdown Overlay -->
|
||||
<div
|
||||
v-show="open"
|
||||
class="fixed inset-0 z-40"
|
||||
@click="open = false"
|
||||
></div>
|
||||
|
||||
<Transition
|
||||
enter-active-class="transition ease-out duration-200"
|
||||
enter-from-class="opacity-0 scale-95"
|
||||
enter-to-class="opacity-100 scale-100"
|
||||
leave-active-class="transition ease-in duration-75"
|
||||
leave-from-class="opacity-100 scale-100"
|
||||
leave-to-class="opacity-0 scale-95"
|
||||
>
|
||||
<div
|
||||
v-show="open"
|
||||
class="absolute z-50 mt-2 rounded-md shadow-lg"
|
||||
:class="[widthClass, alignmentClasses]"
|
||||
style="display: none"
|
||||
@click="open = false"
|
||||
>
|
||||
<div
|
||||
class="rounded-md ring-1 ring-black ring-opacity-5"
|
||||
:class="contentClasses"
|
||||
>
|
||||
<slot name="content" />
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</template>
|
||||
19
resources/js/Components/DropdownLink.vue
Normal file
19
resources/js/Components/DropdownLink.vue
Normal file
@@ -0,0 +1,19 @@
|
||||
<script setup>
|
||||
import { Link } from '@inertiajs/vue3';
|
||||
|
||||
defineProps({
|
||||
href: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Link
|
||||
:href="href"
|
||||
class="block w-full px-4 py-2 text-start text-sm leading-5 text-gray-700 transition duration-150 ease-in-out hover:bg-gray-100 focus:bg-gray-100 focus:outline-none dark:text-gray-300 dark:hover:bg-gray-800 dark:focus:bg-gray-800"
|
||||
>
|
||||
<slot />
|
||||
</Link>
|
||||
</template>
|
||||
15
resources/js/Components/InputError.vue
Normal file
15
resources/js/Components/InputError.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<script setup>
|
||||
defineProps({
|
||||
message: {
|
||||
type: String,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-show="message">
|
||||
<p class="text-sm text-red-600 dark:text-red-400">
|
||||
{{ message }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
14
resources/js/Components/InputLabel.vue
Normal file
14
resources/js/Components/InputLabel.vue
Normal file
@@ -0,0 +1,14 @@
|
||||
<script setup>
|
||||
defineProps({
|
||||
value: {
|
||||
type: String,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
<span v-if="value">{{ value }}</span>
|
||||
<span v-else><slot /></span>
|
||||
</label>
|
||||
</template>
|
||||
63
resources/js/Components/MetricCard.vue
Normal file
63
resources/js/Components/MetricCard.vue
Normal file
@@ -0,0 +1,63 @@
|
||||
<script setup>
|
||||
defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
value: {
|
||||
type: [String, Number],
|
||||
required: true,
|
||||
},
|
||||
subtitle: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="overflow-hidden rounded-xl border border-slate-200 bg-white p-6 shadow-sm dark:border-slate-850 dark:bg-slate-900"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex-1 min-w-0">
|
||||
<p
|
||||
class="text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400 truncate"
|
||||
>
|
||||
{{ title }}
|
||||
</p>
|
||||
<h4
|
||||
class="mt-2 text-2xl font-bold tracking-tight text-slate-900 dark:text-white truncate"
|
||||
>
|
||||
{{ value }}
|
||||
</h4>
|
||||
<p
|
||||
v-if="subtitle"
|
||||
class="mt-1 text-xs text-slate-500 dark:text-slate-400 truncate"
|
||||
>
|
||||
{{ subtitle }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex h-12 w-12 shrink-0 items-center justify-center rounded-xl bg-slate-50 text-slate-500 dark:bg-slate-800 dark:text-slate-400 ml-4"
|
||||
>
|
||||
<slot name="icon">
|
||||
<svg
|
||||
class="h-6 w-6"
|
||||
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>
|
||||
</slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
123
resources/js/Components/Modal.vue
Normal file
123
resources/js/Components/Modal.vue
Normal file
@@ -0,0 +1,123 @@
|
||||
<script setup>
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
maxWidth: {
|
||||
type: String,
|
||||
default: '2xl',
|
||||
},
|
||||
closeable: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['close']);
|
||||
const dialog = ref();
|
||||
const showSlot = ref(props.show);
|
||||
|
||||
watch(
|
||||
() => props.show,
|
||||
() => {
|
||||
if (props.show) {
|
||||
document.body.style.overflow = 'hidden';
|
||||
showSlot.value = true;
|
||||
|
||||
dialog.value?.showModal();
|
||||
} else {
|
||||
document.body.style.overflow = '';
|
||||
|
||||
setTimeout(() => {
|
||||
dialog.value?.close();
|
||||
showSlot.value = false;
|
||||
}, 200);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const close = () => {
|
||||
if (props.closeable) {
|
||||
emit('close');
|
||||
}
|
||||
};
|
||||
|
||||
const closeOnEscape = (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
|
||||
if (props.show) {
|
||||
close();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => document.addEventListener('keydown', closeOnEscape));
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('keydown', closeOnEscape);
|
||||
|
||||
document.body.style.overflow = '';
|
||||
});
|
||||
|
||||
const maxWidthClass = computed(() => {
|
||||
return {
|
||||
sm: 'sm:max-w-sm',
|
||||
md: 'sm:max-w-md',
|
||||
lg: 'sm:max-w-lg',
|
||||
xl: 'sm:max-w-xl',
|
||||
'2xl': 'sm:max-w-2xl',
|
||||
}[props.maxWidth];
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<dialog
|
||||
class="z-50 m-0 min-h-full min-w-full overflow-y-auto bg-transparent backdrop:bg-transparent"
|
||||
ref="dialog"
|
||||
>
|
||||
<div
|
||||
class="fixed inset-0 z-50 overflow-y-auto px-4 py-6 sm:px-0"
|
||||
scroll-region
|
||||
>
|
||||
<Transition
|
||||
enter-active-class="ease-out duration-300"
|
||||
enter-from-class="opacity-0"
|
||||
enter-to-class="opacity-100"
|
||||
leave-active-class="ease-in duration-200"
|
||||
leave-from-class="opacity-100"
|
||||
leave-to-class="opacity-0"
|
||||
>
|
||||
<div
|
||||
v-show="show"
|
||||
class="fixed inset-0 transform transition-all"
|
||||
@click="close"
|
||||
>
|
||||
<div
|
||||
class="absolute inset-0 bg-gray-500 opacity-75 dark:bg-gray-900"
|
||||
/>
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
<Transition
|
||||
enter-active-class="ease-out duration-300"
|
||||
enter-from-class="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
enter-to-class="opacity-100 translate-y-0 sm:scale-100"
|
||||
leave-active-class="ease-in duration-200"
|
||||
leave-from-class="opacity-100 translate-y-0 sm:scale-100"
|
||||
leave-to-class="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
>
|
||||
<div
|
||||
v-show="show"
|
||||
class="mb-6 transform overflow-hidden rounded-lg bg-white shadow-xl transition-all sm:mx-auto sm:w-full dark:bg-gray-800"
|
||||
:class="maxWidthClass"
|
||||
>
|
||||
<slot v-if="showSlot" />
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</dialog>
|
||||
</template>
|
||||
26
resources/js/Components/NavLink.vue
Normal file
26
resources/js/Components/NavLink.vue
Normal file
@@ -0,0 +1,26 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { Link } from '@inertiajs/vue3';
|
||||
|
||||
const props = defineProps({
|
||||
href: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
active: {
|
||||
type: Boolean,
|
||||
},
|
||||
});
|
||||
|
||||
const classes = computed(() =>
|
||||
props.active
|
||||
? 'inline-flex items-center px-1 pt-1 border-b-2 border-indigo-400 dark:border-indigo-600 text-sm font-medium leading-5 text-gray-900 dark:text-gray-100 focus:outline-none focus:border-indigo-700 transition duration-150 ease-in-out'
|
||||
: 'inline-flex items-center px-1 pt-1 border-b-2 border-transparent text-sm font-medium leading-5 text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:border-gray-300 dark:hover:border-gray-700 focus:outline-none focus:text-gray-700 dark:focus:text-gray-300 focus:border-gray-300 dark:focus:border-gray-700 transition duration-150 ease-in-out',
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Link :href="href" :class="classes">
|
||||
<slot />
|
||||
</Link>
|
||||
</template>
|
||||
7
resources/js/Components/PrimaryButton.vue
Normal file
7
resources/js/Components/PrimaryButton.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<button
|
||||
class="inline-flex items-center rounded-md border border-transparent bg-gray-800 px-4 py-2 text-xs font-semibold uppercase tracking-widest text-white transition duration-150 ease-in-out hover:bg-gray-700 focus:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 active:bg-gray-900 dark:bg-gray-200 dark:text-gray-800 dark:hover:bg-white dark:focus:bg-white dark:focus:ring-offset-gray-800 dark:active:bg-gray-300"
|
||||
>
|
||||
<slot />
|
||||
</button>
|
||||
</template>
|
||||
26
resources/js/Components/ResponsiveNavLink.vue
Normal file
26
resources/js/Components/ResponsiveNavLink.vue
Normal file
@@ -0,0 +1,26 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { Link } from '@inertiajs/vue3';
|
||||
|
||||
const props = defineProps({
|
||||
href: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
active: {
|
||||
type: Boolean,
|
||||
},
|
||||
});
|
||||
|
||||
const classes = computed(() =>
|
||||
props.active
|
||||
? 'block w-full ps-3 pe-4 py-2 border-l-4 border-indigo-400 dark:border-indigo-600 text-start text-base font-medium text-indigo-700 dark:text-indigo-300 bg-indigo-50 dark:bg-indigo-900/50 focus:outline-none focus:text-indigo-800 dark:focus:text-indigo-200 focus:bg-indigo-100 dark:focus:bg-indigo-900 focus:border-indigo-700 dark:focus:border-indigo-300 transition duration-150 ease-in-out'
|
||||
: 'block w-full ps-3 pe-4 py-2 border-l-4 border-transparent text-start text-base font-medium text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700 hover:border-gray-300 dark:hover:border-gray-600 focus:outline-none focus:text-gray-800 dark:focus:text-gray-200 focus:bg-gray-50 dark:focus:bg-gray-700 focus:border-gray-300 dark:focus:border-gray-600 transition duration-150 ease-in-out',
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Link :href="href" :class="classes">
|
||||
<slot />
|
||||
</Link>
|
||||
</template>
|
||||
17
resources/js/Components/SecondaryButton.vue
Normal file
17
resources/js/Components/SecondaryButton.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<script setup>
|
||||
defineProps({
|
||||
type: {
|
||||
type: String,
|
||||
default: 'button',
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
:type="type"
|
||||
class="inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-xs font-semibold uppercase tracking-widest text-gray-700 shadow-sm transition duration-150 ease-in-out hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 disabled:opacity-25 dark:border-gray-500 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700 dark:focus:ring-offset-gray-800"
|
||||
>
|
||||
<slot />
|
||||
</button>
|
||||
</template>
|
||||
54
resources/js/Components/StatusBadge.vue
Normal file
54
resources/js/Components/StatusBadge.vue
Normal file
@@ -0,0 +1,54 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
status: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const config = computed(() => {
|
||||
switch (props.status) {
|
||||
case 'draft':
|
||||
return {
|
||||
label: 'Brouillon',
|
||||
classes: 'bg-slate-100 text-slate-800 border-slate-200 dark:bg-slate-900/50 dark:text-slate-300 dark:border-slate-800',
|
||||
};
|
||||
case 'validated':
|
||||
return {
|
||||
label: 'Validée',
|
||||
classes: 'bg-sky-100 text-sky-800 border-sky-200 dark:bg-sky-950/50 dark:text-sky-300 dark:border-sky-900',
|
||||
};
|
||||
case 'ordered':
|
||||
return {
|
||||
label: 'Commandée',
|
||||
classes: 'bg-amber-100 text-amber-800 border-amber-200 dark:bg-amber-950/50 dark:text-amber-300 dark:border-amber-900',
|
||||
};
|
||||
case 'delivered':
|
||||
return {
|
||||
label: 'Livrée',
|
||||
classes: 'bg-emerald-100 text-emerald-800 border-emerald-200 dark:bg-emerald-950/50 dark:text-emerald-300 dark:border-emerald-900',
|
||||
};
|
||||
case 'closed':
|
||||
return {
|
||||
label: 'Clôturée',
|
||||
classes: 'bg-purple-100 text-purple-800 border-purple-200 dark:bg-purple-950/50 dark:text-purple-300 dark:border-purple-900',
|
||||
};
|
||||
default:
|
||||
return {
|
||||
label: props.status,
|
||||
classes: 'bg-gray-100 text-gray-800 border-gray-200 dark:bg-gray-800 dark:text-gray-300 dark:border-gray-750',
|
||||
};
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span
|
||||
:class="`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-semibold border ${config.classes}`"
|
||||
>
|
||||
<span class="mr-1.5 h-1.5 w-1.5 rounded-full bg-current"></span>
|
||||
{{ config.label }}
|
||||
</span>
|
||||
</template>
|
||||
26
resources/js/Components/TextInput.vue
Normal file
26
resources/js/Components/TextInput.vue
Normal file
@@ -0,0 +1,26 @@
|
||||
<script setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
const model = defineModel({
|
||||
type: [String, Number],
|
||||
required: true,
|
||||
});
|
||||
|
||||
const input = ref(null);
|
||||
|
||||
onMounted(() => {
|
||||
if (input.value.hasAttribute('autofocus')) {
|
||||
input.value.focus();
|
||||
}
|
||||
});
|
||||
|
||||
defineExpose({ focus: () => input.value.focus() });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<input
|
||||
class="rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-300 dark:focus:border-indigo-600 dark:focus:ring-indigo-600"
|
||||
v-model="model"
|
||||
ref="input"
|
||||
/>
|
||||
</template>
|
||||
222
resources/js/Layouts/AuthenticatedLayout.vue
Normal file
222
resources/js/Layouts/AuthenticatedLayout.vue
Normal file
@@ -0,0 +1,222 @@
|
||||
<script setup>
|
||||
import { ref } from 'vue';
|
||||
import ApplicationLogo from '@/Components/ApplicationLogo.vue';
|
||||
import Dropdown from '@/Components/Dropdown.vue';
|
||||
import DropdownLink from '@/Components/DropdownLink.vue';
|
||||
import NavLink from '@/Components/NavLink.vue';
|
||||
import ResponsiveNavLink from '@/Components/ResponsiveNavLink.vue';
|
||||
import { Link } from '@inertiajs/vue3';
|
||||
|
||||
const showingNavigationDropdown = ref(false);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="min-h-screen bg-gray-100 dark:bg-gray-900">
|
||||
<nav
|
||||
class="border-b border-gray-100 bg-white dark:border-gray-700 dark:bg-gray-800"
|
||||
>
|
||||
<!-- Primary Navigation Menu -->
|
||||
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex h-16 justify-between">
|
||||
<div class="flex">
|
||||
<!-- Logo -->
|
||||
<div class="flex shrink-0 items-center">
|
||||
<Link :href="route('dashboard')">
|
||||
<ApplicationLogo
|
||||
class="block h-9 w-auto fill-current text-gray-800 dark:text-gray-200"
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<!-- Navigation Links -->
|
||||
<div
|
||||
class="hidden space-x-8 sm:-my-px sm:ms-10 sm:flex"
|
||||
>
|
||||
<NavLink
|
||||
:href="route('dashboard')"
|
||||
:active="route().current('dashboard')"
|
||||
>
|
||||
Dashboard
|
||||
</NavLink>
|
||||
<NavLink
|
||||
:href="route('commandes.index')"
|
||||
:active="route().current('commandes.*')"
|
||||
>
|
||||
Commandes
|
||||
</NavLink>
|
||||
<NavLink
|
||||
:href="route('materiels.index')"
|
||||
:active="route().current('materiels.*')"
|
||||
>
|
||||
Matériels
|
||||
</NavLink>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hidden sm:ms-6 sm:flex sm:items-center">
|
||||
<!-- Settings Dropdown -->
|
||||
<div class="relative ms-3">
|
||||
<Dropdown align="right" width="48">
|
||||
<template #trigger>
|
||||
<span class="inline-flex rounded-md">
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center rounded-md border border-transparent bg-white px-3 py-2 text-sm font-medium leading-4 text-gray-500 transition duration-150 ease-in-out hover:text-gray-700 focus:outline-none dark:bg-gray-800 dark:text-gray-400 dark:hover:text-gray-300"
|
||||
>
|
||||
{{ $page.props.auth.user.name }}
|
||||
|
||||
<svg
|
||||
class="-me-0.5 ms-2 h-4 w-4"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<template #content>
|
||||
<DropdownLink
|
||||
:href="route('profile.edit')"
|
||||
>
|
||||
Profile
|
||||
</DropdownLink>
|
||||
<DropdownLink
|
||||
:href="route('logout')"
|
||||
method="post"
|
||||
as="button"
|
||||
>
|
||||
Log Out
|
||||
</DropdownLink>
|
||||
</template>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hamburger -->
|
||||
<div class="-me-2 flex items-center sm:hidden">
|
||||
<button
|
||||
@click="
|
||||
showingNavigationDropdown =
|
||||
!showingNavigationDropdown
|
||||
"
|
||||
class="inline-flex items-center justify-center rounded-md p-2 text-gray-400 transition duration-150 ease-in-out hover:bg-gray-100 hover:text-gray-500 focus:bg-gray-100 focus:text-gray-500 focus:outline-none dark:text-gray-500 dark:hover:bg-gray-900 dark:hover:text-gray-400 dark:focus:bg-gray-900 dark:focus:text-gray-400"
|
||||
>
|
||||
<svg
|
||||
class="h-6 w-6"
|
||||
stroke="currentColor"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
:class="{
|
||||
hidden: showingNavigationDropdown,
|
||||
'inline-flex':
|
||||
!showingNavigationDropdown,
|
||||
}"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M4 6h16M4 12h16M4 18h16"
|
||||
/>
|
||||
<path
|
||||
:class="{
|
||||
hidden: !showingNavigationDropdown,
|
||||
'inline-flex':
|
||||
showingNavigationDropdown,
|
||||
}"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Responsive Navigation Menu -->
|
||||
<div
|
||||
:class="{
|
||||
block: showingNavigationDropdown,
|
||||
hidden: !showingNavigationDropdown,
|
||||
}"
|
||||
class="sm:hidden"
|
||||
>
|
||||
<div class="space-y-1 pb-3 pt-2">
|
||||
<ResponsiveNavLink
|
||||
:href="route('dashboard')"
|
||||
:active="route().current('dashboard')"
|
||||
>
|
||||
Dashboard
|
||||
</ResponsiveNavLink>
|
||||
<ResponsiveNavLink
|
||||
:href="route('commandes.index')"
|
||||
:active="route().current('commandes.*')"
|
||||
>
|
||||
Commandes
|
||||
</ResponsiveNavLink>
|
||||
<ResponsiveNavLink
|
||||
:href="route('materiels.index')"
|
||||
:active="route().current('materiels.*')"
|
||||
>
|
||||
Matériels
|
||||
</ResponsiveNavLink>
|
||||
</div>
|
||||
|
||||
<!-- Responsive Settings Options -->
|
||||
<div
|
||||
class="border-t border-gray-200 pb-1 pt-4 dark:border-gray-600"
|
||||
>
|
||||
<div class="px-4">
|
||||
<div
|
||||
class="text-base font-medium text-gray-800 dark:text-gray-200"
|
||||
>
|
||||
{{ $page.props.auth.user.name }}
|
||||
</div>
|
||||
<div class="text-sm font-medium text-gray-500">
|
||||
{{ $page.props.auth.user.email }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 space-y-1">
|
||||
<ResponsiveNavLink :href="route('profile.edit')">
|
||||
Profile
|
||||
</ResponsiveNavLink>
|
||||
<ResponsiveNavLink
|
||||
:href="route('logout')"
|
||||
method="post"
|
||||
as="button"
|
||||
>
|
||||
Log Out
|
||||
</ResponsiveNavLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Page Heading -->
|
||||
<header
|
||||
class="bg-white shadow dark:bg-gray-800"
|
||||
v-if="$slots.header"
|
||||
>
|
||||
<div class="mx-auto max-w-7xl px-4 py-6 sm:px-6 lg:px-8">
|
||||
<slot name="header" />
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Page Content -->
|
||||
<main>
|
||||
<slot />
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
22
resources/js/Layouts/GuestLayout.vue
Normal file
22
resources/js/Layouts/GuestLayout.vue
Normal file
@@ -0,0 +1,22 @@
|
||||
<script setup>
|
||||
import ApplicationLogo from '@/Components/ApplicationLogo.vue';
|
||||
import { Link } from '@inertiajs/vue3';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex min-h-screen flex-col items-center bg-gray-100 pt-6 sm:justify-center sm:pt-0 dark:bg-gray-900"
|
||||
>
|
||||
<div>
|
||||
<Link href="/">
|
||||
<ApplicationLogo class="h-20 w-20 fill-current text-gray-500" />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="mt-6 w-full overflow-hidden bg-white px-6 py-4 shadow-md sm:max-w-md sm:rounded-lg dark:bg-gray-800"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
55
resources/js/Pages/Auth/ConfirmPassword.vue
Normal file
55
resources/js/Pages/Auth/ConfirmPassword.vue
Normal 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>
|
||||
68
resources/js/Pages/Auth/ForgotPassword.vue
Normal file
68
resources/js/Pages/Auth/ForgotPassword.vue
Normal 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>
|
||||
100
resources/js/Pages/Auth/Login.vue
Normal file
100
resources/js/Pages/Auth/Login.vue
Normal 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>
|
||||
113
resources/js/Pages/Auth/Register.vue
Normal file
113
resources/js/Pages/Auth/Register.vue
Normal 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>
|
||||
101
resources/js/Pages/Auth/ResetPassword.vue
Normal file
101
resources/js/Pages/Auth/ResetPassword.vue
Normal 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>
|
||||
61
resources/js/Pages/Auth/VerifyEmail.vue
Normal file
61
resources/js/Pages/Auth/VerifyEmail.vue
Normal 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>
|
||||
390
resources/js/Pages/Commandes/Form.vue
Normal file
390
resources/js/Pages/Commandes/Form.vue
Normal 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>
|
||||
368
resources/js/Pages/Commandes/Index.vue
Normal file
368
resources/js/Pages/Commandes/Index.vue
Normal 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>
|
||||
371
resources/js/Pages/Commandes/Show.vue
Normal file
371
resources/js/Pages/Commandes/Show.vue
Normal 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>
|
||||
305
resources/js/Pages/Dashboard.vue
Normal file
305
resources/js/Pages/Dashboard.vue
Normal 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>
|
||||
307
resources/js/Pages/Materiels/Form.vue
Normal file
307
resources/js/Pages/Materiels/Form.vue
Normal 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>
|
||||
368
resources/js/Pages/Materiels/Index.vue
Normal file
368
resources/js/Pages/Materiels/Index.vue
Normal 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>
|
||||
318
resources/js/Pages/Materiels/Show.vue
Normal file
318
resources/js/Pages/Materiels/Show.vue
Normal 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>
|
||||
56
resources/js/Pages/Profile/Edit.vue
Normal file
56
resources/js/Pages/Profile/Edit.vue
Normal 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>
|
||||
108
resources/js/Pages/Profile/Partials/DeleteUserForm.vue
Normal file
108
resources/js/Pages/Profile/Partials/DeleteUserForm.vue
Normal 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>
|
||||
122
resources/js/Pages/Profile/Partials/UpdatePasswordForm.vue
Normal file
122
resources/js/Pages/Profile/Partials/UpdatePasswordForm.vue
Normal 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>
|
||||
@@ -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>
|
||||
386
resources/js/Pages/Welcome.vue
Normal file
386
resources/js/Pages/Welcome.vue
Normal 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>
|
||||
27
resources/js/app.js
Normal file
27
resources/js/app.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import '../css/app.css';
|
||||
import './bootstrap';
|
||||
|
||||
import { createInertiaApp } from '@inertiajs/vue3';
|
||||
import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers';
|
||||
import { createApp, h } from 'vue';
|
||||
import { ZiggyVue } from '../../vendor/tightenco/ziggy';
|
||||
|
||||
const appName = import.meta.env.VITE_APP_NAME || 'Laravel';
|
||||
|
||||
createInertiaApp({
|
||||
title: (title) => `${title} - ${appName}`,
|
||||
resolve: (name) =>
|
||||
resolvePageComponent(
|
||||
`./Pages/${name}.vue`,
|
||||
import.meta.glob('./Pages/**/*.vue'),
|
||||
),
|
||||
setup({ el, App, props, plugin }) {
|
||||
return createApp({ render: () => h(App, props) })
|
||||
.use(plugin)
|
||||
.use(ZiggyVue)
|
||||
.mount(el);
|
||||
},
|
||||
progress: {
|
||||
color: '#4B5563',
|
||||
},
|
||||
});
|
||||
4
resources/js/bootstrap.js
vendored
Normal file
4
resources/js/bootstrap.js
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
import axios from 'axios';
|
||||
window.axios = axios;
|
||||
|
||||
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
|
||||
Reference in New Issue
Block a user