feat: Initialize core application structure including authentication, role-based dashboards, service task management, and integration workflows.
This commit is contained in:
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="Confirmer le mot de passe" />
|
||||
|
||||
<div class="mb-4 text-sm text-gray-600 dark:text-gray-400">
|
||||
C'est une zone sécurisée de l'application. Veuillez confirmer votre
|
||||
mot de passe avant de continuer.
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="submit">
|
||||
<div>
|
||||
<InputLabel for="password" value="Mot de passe" />
|
||||
<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"
|
||||
>
|
||||
Confirmer
|
||||
</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="Mot de passe oublié" />
|
||||
|
||||
<div class="mb-4 text-sm text-gray-600 dark:text-gray-400">
|
||||
Mot de passe oublié ? Pas de problème. Indiquez-nous simplement votre adresse email
|
||||
et nous vous enverrons un lien de réinitialisation du mot de passe qui vous permettra
|
||||
d'en choisir un nouveau.
|
||||
</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"
|
||||
>
|
||||
Envoyer le lien de réinitialisation
|
||||
</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="Connexion" />
|
||||
|
||||
<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="Mot de passe" />
|
||||
|
||||
<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"
|
||||
>Se souvenir de moi</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"
|
||||
>
|
||||
Mot de passe oublié ?
|
||||
</Link>
|
||||
|
||||
<PrimaryButton
|
||||
class="ms-4"
|
||||
:class="{ 'opacity-25': form.processing }"
|
||||
:disabled="form.processing"
|
||||
>
|
||||
Se connecter
|
||||
</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="Inscription" />
|
||||
|
||||
<form @submit.prevent="submit">
|
||||
<div>
|
||||
<InputLabel for="name" value="Nom" />
|
||||
|
||||
<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="Mot de passe" />
|
||||
|
||||
<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="Confirmer le mot de passe"
|
||||
/>
|
||||
|
||||
<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"
|
||||
>
|
||||
Déjà inscrit ?
|
||||
</Link>
|
||||
|
||||
<PrimaryButton
|
||||
class="ms-4"
|
||||
:class="{ 'opacity-25': form.processing }"
|
||||
:disabled="form.processing"
|
||||
>
|
||||
S'inscrire
|
||||
</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="Réinitialiser le mot de passe" />
|
||||
|
||||
<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="Mot de passe" />
|
||||
|
||||
<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="Confirmer le mot de passe"
|
||||
/>
|
||||
|
||||
<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"
|
||||
>
|
||||
Réinitialiser le mot de passe
|
||||
</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="Vérification de l'email" />
|
||||
|
||||
<div class="mb-4 text-sm text-gray-600 dark:text-gray-400">
|
||||
Merci de vous être inscrit ! Avant de commencer, pourriez-vous vérifier votre
|
||||
adresse email en cliquant sur le lien que nous venons de vous envoyer ? Si vous
|
||||
n'avez pas reçu l'email, nous vous en enverrons un autre avec plaisir.
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="mb-4 text-sm font-medium text-green-600 dark:text-green-400"
|
||||
v-if="verificationLinkSent"
|
||||
>
|
||||
Un nouveau lien de vérification a été envoyé à l'adresse email que vous
|
||||
avez fournie lors de l'inscription.
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="submit">
|
||||
<div class="mt-4 flex items-center justify-between">
|
||||
<PrimaryButton
|
||||
:class="{ 'opacity-25': form.processing }"
|
||||
:disabled="form.processing"
|
||||
>
|
||||
Renvoyer l'email de vérification
|
||||
</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"
|
||||
>Se déconnecter</Link
|
||||
>
|
||||
</div>
|
||||
</form>
|
||||
</GuestLayout>
|
||||
</template>
|
||||
30
resources/js/Pages/Dashboard.vue
Normal file
30
resources/js/Pages/Dashboard.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<script setup>
|
||||
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue';
|
||||
import { Head } from '@inertiajs/vue3';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head title="Dashboard" />
|
||||
|
||||
<AuthenticatedLayout>
|
||||
<template #header>
|
||||
<h2
|
||||
class="text-xl font-semibold leading-tight text-gray-800 dark:text-gray-200"
|
||||
>
|
||||
Tableau de Bord
|
||||
</h2>
|
||||
</template>
|
||||
|
||||
<div class="py-12">
|
||||
<div class="mx-auto max-w-7xl sm:px-6 lg:px-8">
|
||||
<div
|
||||
class="overflow-hidden bg-white shadow-sm sm:rounded-lg dark:bg-gray-800"
|
||||
>
|
||||
<div class="p-6 text-gray-900 dark:text-gray-100">
|
||||
You're logged in!
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AuthenticatedLayout>
|
||||
</template>
|
||||
108
resources/js/Pages/Dashboard/Admin.vue
Normal file
108
resources/js/Pages/Dashboard/Admin.vue
Normal file
@@ -0,0 +1,108 @@
|
||||
<script setup>
|
||||
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue';
|
||||
import { Head, Link } from '@inertiajs/vue3';
|
||||
import StatCard from '@/Components/Dashboard/StatCard.vue';
|
||||
import StatusBadge from '@/Components/App/StatusBadge.vue';
|
||||
|
||||
const props = defineProps({
|
||||
stats: Object,
|
||||
recent_requests: Array,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head title="Administration" />
|
||||
|
||||
<AuthenticatedLayout>
|
||||
<template #header>
|
||||
<div class="flex justify-between items-center">
|
||||
<h2 class="text-xl font-semibold leading-tight text-gray-800 dark:text-gray-200">
|
||||
Administration
|
||||
</h2>
|
||||
<div>
|
||||
<Link :href="route('templates.index')" class="mr-2 px-4 py-2 bg-gray-200 border border-transparent rounded-md font-semibold text-xs text-gray-800 uppercase tracking-widest hover:bg-gray-300 dark:bg-gray-700 dark:text-gray-200 dark:hover:bg-gray-600 transition ease-in-out duration-150">
|
||||
Gérer les Modèles
|
||||
</Link>
|
||||
<Link :href="route('users.index')" class="px-4 py-2 bg-gray-800 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-gray-700 dark:bg-gray-200 dark:text-gray-800 dark:hover:bg-white transition ease-in-out duration-150">
|
||||
Gérer les Utilisateurs
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="py-12">
|
||||
<div class="mx-auto max-w-7xl sm:px-6 lg:px-8 space-y-8">
|
||||
<!-- Stats -->
|
||||
<div class="grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-5">
|
||||
<StatCard title="Fiches" :value="stats.total_integrations" color="blue" />
|
||||
<StatCard title="En Cours" :value="stats.active_integrations" color="yellow" />
|
||||
<StatCard title="Terminés" :value="stats.completed_integrations" color="green" />
|
||||
<StatCard title="SLA Dépassés" :value="stats.overdue_tasks" color="red" />
|
||||
<StatCard title="Délai Moyen" :value="stats.avg_completion_days + ' j'" color="indigo" />
|
||||
</div>
|
||||
|
||||
<!-- SLA & Service Analytics -->
|
||||
<div class="grid grid-cols-1 gap-6 lg:grid-cols-2">
|
||||
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg dark:bg-gray-800 p-6">
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100 mb-4">Charge par Service</h3>
|
||||
<div class="space-y-4">
|
||||
<div v-for="service in stats.service_distribution" :key="service.name">
|
||||
<div class="flex justify-between text-sm mb-1">
|
||||
<span class="text-gray-700 dark:text-gray-300 font-medium">{{ service.name }}</span>
|
||||
<span class="text-gray-500">{{ service.tasks_count }} tâches</span>
|
||||
</div>
|
||||
<div class="w-full bg-gray-100 dark:bg-gray-700 rounded-full h-2">
|
||||
<div
|
||||
class="bg-blue-500 h-2 rounded-full transition-all duration-1000"
|
||||
:style="{ width: (service.tasks_count / (stats.total_integrations || 1) * 10).toFixed(0) + '%' }"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg dark:bg-gray-800 p-6 flex flex-col justify-center items-center text-center">
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100 mb-2">SLA</h3>
|
||||
<div class="text-5xl font-bold text-green-500 mb-2">
|
||||
{{ stats.total_integrations ? Math.round((stats.completed_integrations / stats.total_integrations) * 100) : 0 }}%
|
||||
</div>
|
||||
<p class="text-gray-500 dark:text-gray-400">Taux de réussite des intégrations</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recent Activities -->
|
||||
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg dark:bg-gray-800">
|
||||
<div class="p-6 border-b border-gray-200 dark:border-gray-700">
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100">Dernières Intégrations</h3>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-left border-collapse">
|
||||
<thead>
|
||||
<tr class="border-b dark:border-gray-700 bg-gray-50 dark:bg-gray-700/50">
|
||||
<th class="px-6 py-3 text-xs font-bold uppercase text-gray-500 dark:text-gray-400">Agent</th>
|
||||
<th class="px-6 py-3 text-xs font-bold uppercase text-gray-500 dark:text-gray-400">Date</th>
|
||||
<th class="px-6 py-3 text-xs font-bold uppercase text-gray-500 dark:text-gray-400">Status</th>
|
||||
<th class="px-6 py-3 text-xs font-bold uppercase text-gray-500 dark:text-gray-400">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<tr v-for="req in recent_requests" :key="req.id" class="hover:bg-gray-50 dark:hover:bg-gray-700/30">
|
||||
<td class="px-6 py-4">
|
||||
<div class="text-sm font-medium text-gray-900 dark:text-white">{{ req.agent.first_name }} {{ req.agent.last_name }}</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm text-gray-500 dark:text-gray-400">{{ new Date(req.created_at).toLocaleDateString() }}</td>
|
||||
<td class="px-6 py-4">
|
||||
<StatusBadge :status="req.status" />
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm">
|
||||
<Link :href="route('integrations.show', req.id)" class="text-blue-600 hover:text-blue-900 dark:text-blue-400 dark:hover:text-blue-300">Détails</Link>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AuthenticatedLayout>
|
||||
</template>
|
||||
79
resources/js/Pages/Dashboard/Prescriber.vue
Normal file
79
resources/js/Pages/Dashboard/Prescriber.vue
Normal file
@@ -0,0 +1,79 @@
|
||||
<script setup>
|
||||
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue';
|
||||
import { Head, Link } from '@inertiajs/vue3';
|
||||
import StatusBadge from '@/Components/App/StatusBadge.vue';
|
||||
|
||||
const props = defineProps({
|
||||
my_requests: Array,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head title="Tableau de Bord Prescripteur" />
|
||||
|
||||
<AuthenticatedLayout>
|
||||
<template #header>
|
||||
<h2 class="text-xl font-semibold leading-tight text-gray-800 dark:text-gray-200">
|
||||
Mes Demandes de fiches agents
|
||||
</h2>
|
||||
</template>
|
||||
|
||||
<div class="py-12">
|
||||
<div class="mx-auto max-w-7xl sm:px-6 lg:px-8">
|
||||
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg dark:bg-gray-800">
|
||||
<div class="p-6 border-b border-gray-200 dark:border-gray-700 flex justify-between items-center">
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100">Historique des demandes</h3>
|
||||
<Link :href="route('integrations.create')" class="inline-flex items-center px-4 py-2 bg-blue-600 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-blue-700 focus:bg-blue-700 active:bg-blue-900 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 transition ease-in-out duration-150">
|
||||
Nouvelle Demande
|
||||
</Link>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-left border-collapse">
|
||||
<thead>
|
||||
<tr class="border-b dark:border-gray-700 bg-gray-50 dark:bg-gray-700/50">
|
||||
<th class="px-6 py-3 text-xs font-bold uppercase text-gray-500 dark:text-gray-400">Agent</th>
|
||||
<th class="px-6 py-3 text-xs font-bold uppercase text-gray-500 dark:text-gray-400">Date d'arrivée</th>
|
||||
<th class="px-6 py-3 text-xs font-bold uppercase text-gray-500 dark:text-gray-400">Status</th>
|
||||
<th class="px-6 py-3 text-xs font-bold uppercase text-gray-500 dark:text-gray-400">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<tr v-for="req in my_requests" :key="req.id" class="hover:bg-gray-50 dark:hover:bg-gray-700/30">
|
||||
<td class="px-6 py-4">
|
||||
<div class="text-sm font-medium text-gray-900 dark:text-white">{{ req.agent.first_name }} {{ req.agent.last_name }}</div>
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">{{ req.agent.position }}</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ new Date(req.agent.arrival_date).toLocaleDateString() }}
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<StatusBadge :status="req.status" />
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm">
|
||||
<div class="flex items-center gap-3">
|
||||
<Link :href="route('integrations.show', req.id)" class="text-blue-600 hover:text-blue-900 dark:text-blue-400 dark:hover:text-blue-300 transition-colors">
|
||||
Détails
|
||||
</Link>
|
||||
<span class="text-gray-300 dark:text-gray-600">|</span>
|
||||
<a :href="route('integrations.pdf', req.id)" target="_blank" class="flex items-center gap-1 text-red-600 hover:text-red-900 dark:text-red-400 dark:hover:text-red-300 font-medium transition-colors" title="Télécharger la fiche PDF">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
PDF
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="my_requests.length === 0">
|
||||
<td colspan="4" class="px-6 py-8 text-center text-gray-500 dark:text-gray-400 italic text-sm">
|
||||
Aucune demande effectuée. Commencez par en créer une !
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AuthenticatedLayout>
|
||||
</template>
|
||||
149
resources/js/Pages/Dashboard/RH.vue
Normal file
149
resources/js/Pages/Dashboard/RH.vue
Normal file
@@ -0,0 +1,149 @@
|
||||
<script setup>
|
||||
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue';
|
||||
import { Head, Link } from '@inertiajs/vue3';
|
||||
import StatCard from '@/Components/Dashboard/StatCard.vue';
|
||||
import StatusBadge from '@/Components/App/StatusBadge.vue';
|
||||
|
||||
const props = defineProps({
|
||||
pending_validation: Array,
|
||||
active_integrations: Array,
|
||||
completed_integrations: Array,
|
||||
stats: Object,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head title="Tableau de Bord RH" />
|
||||
|
||||
<AuthenticatedLayout>
|
||||
<template #header>
|
||||
<h2 class="text-xl font-semibold leading-tight text-gray-800 dark:text-gray-200">
|
||||
Tableau de Bord - Ressources Humaines
|
||||
</h2>
|
||||
</template>
|
||||
|
||||
<div class="py-12">
|
||||
<div class="mx-auto max-w-7xl sm:px-6 lg:px-8 space-y-8">
|
||||
<!-- Stats -->
|
||||
<div class="grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<StatCard title="En Cours" :value="stats.in_progress" color="blue" />
|
||||
<StatCard title="Terminés" :value="stats.completed" color="green" />
|
||||
<StatCard title="En Retard" :value="stats.overdue" color="red" />
|
||||
<StatCard title="À Valider" :value="pending_validation.length" color="yellow" />
|
||||
</div>
|
||||
|
||||
<!-- Pending RH Validation -->
|
||||
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg dark:bg-gray-800">
|
||||
<div class="p-6 border-b border-gray-200 dark:border-gray-700 flex justify-between items-center">
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100">Fiches agents à valider (RH)</h3>
|
||||
<Link :href="route('integrations.create')" class="inline-flex items-center px-4 py-2 bg-blue-600 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-blue-700 focus:bg-blue-700 active:bg-blue-900 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 transition ease-in-out duration-150">
|
||||
Nouvelle fiche agent
|
||||
</Link>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-left border-collapse">
|
||||
<thead>
|
||||
<tr class="border-b dark:border-gray-700 bg-gray-50 dark:bg-gray-700/50">
|
||||
<th class="px-6 py-3 text-xs font-bold uppercase text-gray-500 dark:text-gray-400">Agent</th>
|
||||
<th class="px-6 py-3 text-xs font-bold uppercase text-gray-500 dark:text-gray-400">Arrivée</th>
|
||||
<th class="px-6 py-3 text-xs font-bold uppercase text-gray-500 dark:text-gray-400">Modèle</th>
|
||||
<th class="px-6 py-3 text-xs font-bold uppercase text-gray-500 dark:text-gray-400">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<tr v-for="req in pending_validation" :key="req.id" class="hover:bg-gray-50 dark:hover:bg-gray-700/30">
|
||||
<td class="px-6 py-4">
|
||||
<div class="text-sm font-medium text-gray-900 dark:text-white">{{ req.agent.first_name }} {{ req.agent.last_name }}</div>
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">{{ req.agent.position }}</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm text-gray-500 dark:text-gray-400">{{ new Date(req.agent.arrival_date).toLocaleDateString() }}</td>
|
||||
<td class="px-6 py-4 text-sm text-gray-500 dark:text-gray-400">{{ req.template?.name || 'Standard' }}</td>
|
||||
<td class="px-6 py-4 text-sm">
|
||||
<Link :href="route('integrations.show', req.id)" class="text-blue-600 hover:text-blue-900 dark:text-blue-400 dark:hover:text-blue-300">Valider</Link>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="pending_validation.length === 0">
|
||||
<td colspan="4" class="px-6 py-8 text-center text-gray-500 dark:text-gray-400 italic text-sm">Aucune demande en attente</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Active Integrations -->
|
||||
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg dark:bg-gray-800">
|
||||
<div class="p-6 border-b border-gray-200 dark:border-gray-700">
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100">Fiches agents en cours</h3>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-left border-collapse">
|
||||
<thead>
|
||||
<tr class="bg-gray-50 dark:bg-gray-700/50">
|
||||
<th class="px-6 py-3 text-xs font-bold uppercase text-gray-500 dark:text-gray-400">Agent</th>
|
||||
<th class="px-6 py-3 text-xs font-bold uppercase text-gray-500 dark:text-gray-400">Service</th>
|
||||
<th class="px-6 py-3 text-xs font-bold uppercase text-gray-500 dark:text-gray-400">Progression</th>
|
||||
<th class="px-6 py-3 text-xs font-bold uppercase text-gray-500 dark:text-gray-400">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<tr v-for="req in active_integrations" :key="req.id" class="hover:bg-gray-50 dark:hover:bg-gray-700/30">
|
||||
<td class="px-6 py-4">
|
||||
<div class="text-sm font-medium text-gray-900 dark:text-white">{{ req.agent.first_name }} {{ req.agent.last_name }}</div>
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">{{ req.agent.position }}</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm text-gray-500 dark:text-gray-400">{{ req.agent.department }}</td>
|
||||
<td class="px-6 py-4">
|
||||
<div class="w-full bg-gray-200 rounded-full h-2.5 dark:bg-gray-700">
|
||||
<div class="bg-blue-600 h-2.5 rounded-full" :style="{ width: (req.service_tasks?.filter(t => t.status === 'completed').length / (req.service_tasks?.length || 1) * 100) + '%' }"></div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm">
|
||||
<Link :href="route('integrations.show', req.id)" class="text-blue-600 hover:text-blue-900 dark:text-blue-400 dark:hover:text-blue-300">Voir</Link>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="active_integrations.length === 0">
|
||||
<td colspan="4" class="px-6 py-8 text-center text-gray-500 dark:text-gray-400 italic text-sm">Aucun dossier en cours</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Completed Integrations -->
|
||||
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg dark:bg-gray-800">
|
||||
<div class="p-6 border-b border-gray-200 dark:border-gray-700">
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100">Fiches agents terminées</h3>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-left border-collapse">
|
||||
<thead>
|
||||
<tr class="bg-gray-50 dark:bg-gray-700/50">
|
||||
<th class="px-6 py-3 text-xs font-bold uppercase text-gray-500 dark:text-gray-400">Agent</th>
|
||||
<th class="px-6 py-3 text-xs font-bold uppercase text-gray-500 dark:text-gray-400">Service</th>
|
||||
<th class="px-6 py-3 text-xs font-bold uppercase text-gray-500 dark:text-gray-400">Terminé le</th>
|
||||
<th class="px-6 py-3 text-xs font-bold uppercase text-gray-500 dark:text-gray-400">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<tr v-for="req in completed_integrations" :key="req.id" class="hover:bg-gray-50 dark:hover:bg-gray-700/30">
|
||||
<td class="px-6 py-4">
|
||||
<div class="text-sm font-medium text-gray-900 dark:text-white">{{ req.agent.first_name }} {{ req.agent.last_name }}</div>
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">{{ req.agent.position }}</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm text-gray-500 dark:text-gray-400">{{ req.agent.department }}</td>
|
||||
<td class="px-6 py-4 text-sm text-gray-500 dark:text-gray-400">{{ req.completed_at ? new Date(req.completed_at).toLocaleDateString() : '-' }}</td>
|
||||
<td class="px-6 py-4 text-sm">
|
||||
<Link :href="route('integrations.show', req.id)" class="text-blue-600 hover:text-blue-900 dark:text-blue-400 dark:hover:text-blue-300">Consulter</Link>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="completed_integrations.length === 0">
|
||||
<td colspan="3" class="px-6 py-8 text-center text-gray-500 dark:text-gray-400 italic text-sm">Aucun dossier terminé</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AuthenticatedLayout>
|
||||
</template>
|
||||
89
resources/js/Pages/Dashboard/Service.vue
Normal file
89
resources/js/Pages/Dashboard/Service.vue
Normal file
@@ -0,0 +1,89 @@
|
||||
<script setup>
|
||||
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue';
|
||||
import { Head, Link } from '@inertiajs/vue3';
|
||||
import StatCard from '@/Components/Dashboard/StatCard.vue';
|
||||
import StatusBadge from '@/Components/App/StatusBadge.vue';
|
||||
|
||||
const props = defineProps({
|
||||
role: String,
|
||||
my_tasks: Array,
|
||||
completed_tasks: Array,
|
||||
});
|
||||
|
||||
const getOverdueCount = () => {
|
||||
return props.my_tasks.filter(t => t.sla_deadline && new Date(t.sla_deadline) < new Date()).length;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head :title="'Tableau de Bord ' + role" />
|
||||
|
||||
<AuthenticatedLayout>
|
||||
<template #header>
|
||||
<h2 class="text-xl font-semibold leading-tight text-gray-800 dark:text-gray-200">
|
||||
Tableau de Bord - Service {{ role }}
|
||||
</h2>
|
||||
</template>
|
||||
|
||||
<div class="py-12">
|
||||
<div class="mx-auto max-w-7xl sm:px-6 lg:px-8 space-y-8">
|
||||
<!-- Stats -->
|
||||
<div class="grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<StatCard title="Tâches à faire" :value="my_tasks.length" color="yellow" icon="clipboard-list" />
|
||||
<StatCard title="Urgents / Retards" :value="getOverdueCount()" :color="getOverdueCount() > 0 ? 'red' : 'green'" icon="clock" />
|
||||
<StatCard title="Complétés (Récent)" :value="completed_tasks.length" color="blue" icon="check-circle" />
|
||||
</div>
|
||||
|
||||
<!-- My Tasks -->
|
||||
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg dark:bg-gray-800">
|
||||
<div class="p-6 border-b border-gray-200 dark:border-gray-700 flex justify-between items-center">
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100">Mes Tâches à Traiter</h3>
|
||||
<Link :href="route('integrations.create')" class="inline-flex items-center px-4 py-2 bg-blue-600 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-blue-700 focus:bg-blue-700 active:bg-blue-900 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 transition ease-in-out duration-150">
|
||||
Nouvelle Demande
|
||||
</Link>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-left border-collapse">
|
||||
<thead>
|
||||
<tr class="border-b dark:border-gray-700 bg-gray-50 dark:bg-gray-700/50">
|
||||
<th class="px-6 py-3 text-xs font-bold uppercase text-gray-500 dark:text-gray-400">Agent</th>
|
||||
<th class="px-6 py-3 text-xs font-bold uppercase text-gray-500 dark:text-gray-400">Deadline</th>
|
||||
<th class="px-6 py-3 text-xs font-bold uppercase text-gray-500 dark:text-gray-400">Progression</th>
|
||||
<th class="px-6 py-3 text-xs font-bold uppercase text-gray-500 dark:text-gray-400">Status</th>
|
||||
<th class="px-6 py-3 text-xs font-bold uppercase text-gray-500 dark:text-gray-400">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<tr v-for="task in my_tasks" :key="task.id" class="hover:bg-gray-50 dark:hover:bg-gray-700/30">
|
||||
<td class="px-6 py-4">
|
||||
<div class="text-sm font-medium text-gray-900 dark:text-white">{{ task.integration_request.agent.first_name }} {{ task.integration_request.agent.last_name }}</div>
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">{{ task.integration_request.agent.position }}</div>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<span :class="['text-sm', new Date(task.sla_deadline) < new Date() ? 'text-red-600 font-bold' : 'text-gray-500 dark:text-gray-400']">
|
||||
{{ new Date(task.sla_deadline).toLocaleDateString() }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<div class="w-24 bg-gray-200 dark:bg-gray-700 rounded-full h-1.5">
|
||||
<div class="bg-blue-600 h-1.5 rounded-full" :style="{ width: (task.task_items.filter(i => i.is_completed).length / (task.task_items.length || 1) * 100) + '%' }"></div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<StatusBadge :status="task.status" />
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm">
|
||||
<Link :href="route('integrations.show', task.integration_request_id)" class="text-blue-600 hover:text-blue-900 dark:text-blue-400 dark:hover:text-blue-300">Ouvrir</Link>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="my_tasks.length === 0">
|
||||
<td colspan="5" class="px-6 py-8 text-center text-gray-500 dark:text-gray-400 italic text-sm">Aucune tâche en attente</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AuthenticatedLayout>
|
||||
</template>
|
||||
103
resources/js/Pages/Integration/Create.vue
Normal file
103
resources/js/Pages/Integration/Create.vue
Normal file
@@ -0,0 +1,103 @@
|
||||
<script setup>
|
||||
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue';
|
||||
import { Head, useForm } from '@inertiajs/vue3';
|
||||
import InputError from '@/Components/InputError.vue';
|
||||
import InputLabel from '@/Components/InputLabel.vue';
|
||||
import PrimaryButton from '@/Components/PrimaryButton.vue';
|
||||
import TextInput from '@/Components/TextInput.vue';
|
||||
|
||||
const props = defineProps({
|
||||
templates: Array,
|
||||
});
|
||||
|
||||
const form = useForm({
|
||||
first_name: '',
|
||||
last_name: '',
|
||||
email: '',
|
||||
position: '',
|
||||
department: '',
|
||||
arrival_date: '',
|
||||
template_id: props.templates.length > 0 ? props.templates[0].id : '',
|
||||
});
|
||||
|
||||
const submit = () => {
|
||||
form.post(route('integrations.store'));
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head title="Nouvelle Intégration" />
|
||||
|
||||
<AuthenticatedLayout>
|
||||
<template #header>
|
||||
<h2 class="text-xl font-semibold leading-tight text-gray-800 dark:text-gray-200">
|
||||
Nouvelle fiche agent
|
||||
</h2>
|
||||
</template>
|
||||
|
||||
<div class="py-12">
|
||||
<div class="mx-auto max-w-7xl sm:px-6 lg:px-8">
|
||||
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg dark:bg-gray-800 p-8 max-w-2xl mx-auto">
|
||||
<form @submit.prevent="submit" class="space-y-6">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<InputLabel for="first_name" value="Prénom" />
|
||||
<TextInput id="first_name" type="text" class="mt-1 block w-full" v-model="form.first_name" required autofocus />
|
||||
<InputError class="mt-2" :message="form.errors.first_name" />
|
||||
</div>
|
||||
<div>
|
||||
<InputLabel for="last_name" value="Nom" />
|
||||
<TextInput id="last_name" type="text" class="mt-1 block w-full" v-model="form.last_name" required />
|
||||
<InputError class="mt-2" :message="form.errors.last_name" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<InputLabel for="email" value="Email" />
|
||||
<TextInput id="email" type="email" class="mt-1 block w-full" v-model="form.email" required />
|
||||
<InputError class="mt-2" :message="form.errors.email" />
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<InputLabel for="position" value="Poste" />
|
||||
<TextInput id="position" type="text" class="mt-1 block w-full" v-model="form.position" required />
|
||||
<InputError class="mt-2" :message="form.errors.position" />
|
||||
</div>
|
||||
<div>
|
||||
<InputLabel for="department" value="Service de destination" />
|
||||
<TextInput id="department" type="text" class="mt-1 block w-full" v-model="form.department" required />
|
||||
<InputError class="mt-2" :message="form.errors.department" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<InputLabel for="arrival_date" value="Date d'arrivée" />
|
||||
<TextInput id="arrival_date" type="date" class="mt-1 block w-full" v-model="form.arrival_date" required />
|
||||
<InputError class="mt-2" :message="form.errors.arrival_date" />
|
||||
</div>
|
||||
<div class="warning-box bg-red-500 p-4 rounded-md text-center">
|
||||
<span class="text-white">Attention, si vous selectionner l'option <b><u>AVEC TELEPHONE PORTABLE</u></b> une validation de la direction générale est nécessaire.</span>
|
||||
<br>
|
||||
<span class="text-white"><b><u><i>C'est à vous d'initier celle-ci auprès de votre Direction Générale Adjointe</i></u></b></span>
|
||||
</div>
|
||||
<div>
|
||||
<InputLabel for="template_id" value="Template de parcours" />
|
||||
<select id="template_id" v-model="form.template_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="">Aucun (Pas de tâches)</option>
|
||||
<option v-for="t in templates" :key="t.id" :value="t.id">{{ t.name }}</option>
|
||||
</select>
|
||||
<InputError class="mt-2" :message="form.errors.template_id" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end mt-4">
|
||||
<PrimaryButton class="ml-4" :class="{ 'opacity-25': form.processing }" :disabled="form.processing">
|
||||
Créer la fiche agent
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AuthenticatedLayout>
|
||||
</template>
|
||||
66
resources/js/Pages/Integration/Index.vue
Normal file
66
resources/js/Pages/Integration/Index.vue
Normal file
@@ -0,0 +1,66 @@
|
||||
<script setup>
|
||||
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue';
|
||||
import { Head, Link } from '@inertiajs/vue3';
|
||||
import StatusBadge from '@/Components/App/StatusBadge.vue';
|
||||
|
||||
defineProps({
|
||||
integrations: Array,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head title="Intégrations" />
|
||||
|
||||
<AuthenticatedLayout>
|
||||
<template #header>
|
||||
<div class="flex justify-between items-center">
|
||||
<h2 class="text-xl font-semibold leading-tight text-gray-800 dark:text-gray-200">
|
||||
Processus d'intégration
|
||||
</h2>
|
||||
<Link :href="route('integrations.create')" class="inline-flex items-center px-4 py-2 bg-blue-600 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-blue-700 focus:bg-blue-700 active:bg-blue-900 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 transition ease-in-out duration-150">
|
||||
Déclencher une arrivée
|
||||
</Link>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="py-12">
|
||||
<div class="mx-auto max-w-7xl sm:px-6 lg:px-8">
|
||||
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg dark:bg-gray-800">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-left border-collapse">
|
||||
<thead>
|
||||
<tr class="border-b dark:border-gray-700 bg-gray-50 dark:bg-gray-700/50">
|
||||
<th class="px-6 py-3 text-xs font-bold uppercase text-gray-500 dark:text-gray-400">Agent</th>
|
||||
<th class="px-6 py-3 text-xs font-bold uppercase text-gray-500 dark:text-gray-400">Status</th>
|
||||
<th class="px-6 py-3 text-xs font-bold uppercase text-gray-500 dark:text-gray-400">Date d'arrivée</th>
|
||||
<th class="px-6 py-3 text-xs font-bold uppercase text-gray-500 dark:text-gray-400">Template</th>
|
||||
<th class="px-6 py-3 text-xs font-bold uppercase text-gray-500 dark:text-gray-400">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<tr v-for="integration in integrations" :key="integration.id" class="hover:bg-gray-50 dark:hover:bg-gray-700/30">
|
||||
<td class="px-6 py-4">
|
||||
<div class="text-sm font-medium text-gray-900 dark:text-white">{{ integration.agent.first_name }} {{ integration.agent.last_name }}</div>
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">{{ integration.agent.position }}</div>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<StatusBadge :status="integration.status" />
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ new Date(integration.agent.arrival_date).toLocaleDateString() }}
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ integration.template?.name || 'Standard' }}
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm">
|
||||
<Link :href="route('integrations.show', integration.id)" class="text-blue-600 hover:text-blue-900 dark:text-blue-400 dark:hover:text-blue-300">Gérer</Link>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AuthenticatedLayout>
|
||||
</template>
|
||||
86
resources/js/Pages/Integration/Show.vue
Normal file
86
resources/js/Pages/Integration/Show.vue
Normal file
@@ -0,0 +1,86 @@
|
||||
<script setup>
|
||||
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue';
|
||||
import { Head, Link } from '@inertiajs/vue3';
|
||||
import StatusBadge from '@/Components/App/StatusBadge.vue';
|
||||
import ServiceTaskCard from '@/Components/App/ServiceTaskCard.vue';
|
||||
import ActivityTimeline from '@/Components/App/ActivityTimeline.vue';
|
||||
import { router } from '@inertiajs/vue3';
|
||||
|
||||
const props = defineProps({
|
||||
integration: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
activities: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
const validateRH = () => {
|
||||
if (confirm('Souhaitez-vous valider ce dossier et notifier les services ?')) {
|
||||
router.post(route('integrations.validate-rh', props.integration.id));
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head title="Détails Intégration" />
|
||||
|
||||
<AuthenticatedLayout>
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-xl font-semibold leading-tight text-gray-800 dark:text-gray-200">
|
||||
Integration: {{ integration.agent.first_name }} {{ integration.agent.last_name }}
|
||||
</h2>
|
||||
<div class="flex items-center space-x-4">
|
||||
<button
|
||||
v-if="integration.status === 'pending_rh_validation' || integration.status === 'draft'"
|
||||
@click="validateRH"
|
||||
class="px-4 py-2 bg-green-600 text-white rounded-md text-sm font-bold hover:bg-green-700"
|
||||
>
|
||||
Valider Dossier RH
|
||||
</button>
|
||||
<StatusBadge :status="integration.status" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="py-12">
|
||||
<div class="mx-auto max-w-7xl sm:px-6 lg:px-8">
|
||||
<div class="grid grid-cols-1 gap-6 md:grid-cols-3 items-start">
|
||||
<!-- Right/Side Column: Agent Info & Activity -->
|
||||
<div class="md:col-span-1 space-y-6">
|
||||
<!-- Agent Info -->
|
||||
<div class="overflow-hidden bg-white shadow-sm sm:rounded-lg dark:bg-gray-800 p-6 border border-gray-200 dark:border-gray-700">
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100 mb-4">Informations Agent</h3>
|
||||
<dl class="space-y-4">
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Poste</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 dark:text-gray-100">{{ integration.agent.position }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Service</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 dark:text-gray-100">{{ integration.agent.department }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Date d'arrivée</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 dark:text-gray-100">{{ new Date(integration.agent.arrival_date).toLocaleDateString() }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<ActivityTimeline :activities="activities" />
|
||||
</div>
|
||||
|
||||
<!-- Main Column: Tasks -->
|
||||
<div class="md:col-span-2 space-y-6">
|
||||
<div v-for="task in integration.service_tasks" :key="task.id">
|
||||
<ServiceTaskCard :task="task" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AuthenticatedLayout>
|
||||
</template>
|
||||
64
resources/js/Pages/Offboarding/Create.vue
Normal file
64
resources/js/Pages/Offboarding/Create.vue
Normal file
@@ -0,0 +1,64 @@
|
||||
<script setup>
|
||||
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue';
|
||||
import { Head, useForm } from '@inertiajs/vue3';
|
||||
import InputError from '@/Components/InputError.vue';
|
||||
import InputLabel from '@/Components/InputLabel.vue';
|
||||
import PrimaryButton from '@/Components/PrimaryButton.vue';
|
||||
import TextInput from '@/Components/TextInput.vue';
|
||||
|
||||
const props = defineProps({
|
||||
agent: Object,
|
||||
templates: Array,
|
||||
});
|
||||
|
||||
const form = useForm({
|
||||
agent_id: props.agent.id,
|
||||
template_id: '',
|
||||
departure_date: '',
|
||||
});
|
||||
|
||||
const submit = () => {
|
||||
form.post(route('offboarding.store'));
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head title="Nouvel Offboarding" />
|
||||
|
||||
<AuthenticatedLayout>
|
||||
<template #header>
|
||||
<h2 class="text-xl font-semibold leading-tight text-gray-800 dark:text-gray-200">
|
||||
Préparer le départ de {{ agent.first_name }} {{ agent.last_name }}
|
||||
</h2>
|
||||
</template>
|
||||
|
||||
<div class="py-12">
|
||||
<div class="mx-auto max-w-7xl sm:px-6 lg:px-8">
|
||||
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg dark:bg-gray-800 p-8 max-w-2xl mx-auto">
|
||||
<form @submit.prevent="submit" class="space-y-6">
|
||||
<div>
|
||||
<InputLabel for="departure_date" value="Date effective du départ" />
|
||||
<TextInput id="departure_date" type="date" class="mt-1 block w-full" v-model="form.departure_date" required />
|
||||
<InputError class="mt-2" :message="form.errors.departure_date" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<InputLabel for="template_id" value="Template de sortie (Optionnel)" />
|
||||
<select id="template_id" v-model="form.template_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 rounded-md shadow-sm">
|
||||
<option value="">Standard (Sans template)</option>
|
||||
<option v-for="t in templates" :key="t.id" :value="t.id">{{ t.name }}</option>
|
||||
</select>
|
||||
<InputError class="mt-2" :message="form.errors.template_id" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end">
|
||||
<PrimaryButton class="bg-red-600 hover:bg-red-700" :class="{ 'opacity-25': form.processing }" :disabled="form.processing">
|
||||
Lancer le processus de départ
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AuthenticatedLayout>
|
||||
</template>
|
||||
83
resources/js/Pages/Offboarding/Index.vue
Normal file
83
resources/js/Pages/Offboarding/Index.vue
Normal file
@@ -0,0 +1,83 @@
|
||||
<script setup>
|
||||
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue';
|
||||
import { Head, Link } from '@inertiajs/vue3';
|
||||
|
||||
defineProps({
|
||||
active_agents: Array,
|
||||
current_offboardings: Array,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head title="Gestion des Départs" />
|
||||
|
||||
<AuthenticatedLayout>
|
||||
<template #header>
|
||||
<h2 class="text-xl font-semibold leading-tight text-gray-800 dark:text-gray-200">
|
||||
Gestion des Offboardings (Départs)
|
||||
</h2>
|
||||
</template>
|
||||
|
||||
<div class="py-12">
|
||||
<div class="mx-auto max-w-7xl sm:px-6 lg:px-8 space-y-8">
|
||||
<!-- Current Offboardings -->
|
||||
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg dark:bg-gray-800">
|
||||
<div class="p-6 border-b border-gray-200 dark:border-gray-700">
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100">Départs en cours</h3>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-left border-collapse">
|
||||
<thead>
|
||||
<tr class="border-b dark:border-gray-700 bg-gray-50 dark:bg-gray-700/50">
|
||||
<th class="px-6 py-3 text-xs font-bold uppercase text-gray-500 dark:text-gray-400">Agent</th>
|
||||
<th class="px-6 py-3 text-xs font-bold uppercase text-gray-500 dark:text-gray-400">Date de départ</th>
|
||||
<th class="px-6 py-3 text-xs font-bold uppercase text-gray-500 dark:text-gray-400">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<tr v-for="off in current_offboardings" :key="off.id" class="hover:bg-gray-50 dark:hover:bg-gray-700/30">
|
||||
<td class="px-6 py-4 font-medium text-gray-900 dark:text-white">{{ off.agent.first_name }} {{ off.agent.last_name }}</td>
|
||||
<td class="px-6 py-4 text-sm text-gray-500 dark:text-gray-400">{{ new Date(off.global_deadline).toLocaleDateString() }}</td>
|
||||
<td class="px-6 py-4 text-sm">
|
||||
<Link :href="route('integrations.show', off.id)" class="text-blue-600 hover:text-blue-900">Gérer</Link>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="current_offboardings.length === 0">
|
||||
<td colspan="3" class="px-6 py-8 text-center text-gray-500 dark:text-gray-400 italic text-sm">Aucun départ en cours</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Active Agents List for New Offboarding -->
|
||||
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg dark:bg-gray-800">
|
||||
<div class="p-6 border-b border-gray-200 dark:border-gray-700">
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100">Lancer un nouvel offboarding</h3>
|
||||
<p class="text-sm text-gray-500">Liste des agents actuellement en poste</p>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-left border-collapse">
|
||||
<thead>
|
||||
<tr class="border-b dark:border-gray-700 bg-gray-50 dark:bg-gray-700/50">
|
||||
<th class="px-6 py-3 text-xs font-bold uppercase text-gray-500 dark:text-gray-400">Agent</th>
|
||||
<th class="px-6 py-3 text-xs font-bold uppercase text-gray-500 dark:text-gray-400">Service</th>
|
||||
<th class="px-6 py-3 text-xs font-bold uppercase text-gray-500 dark:text-gray-400">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<tr v-for="agent in active_agents" :key="agent.id" class="hover:bg-gray-50 dark:hover:bg-gray-700/30">
|
||||
<td class="px-6 py-4 text-gray-900 dark:text-white">{{ agent.first_name }} {{ agent.last_name }}</td>
|
||||
<td class="px-6 py-4 text-sm text-gray-500 dark:text-gray-400">{{ agent.department }}</td>
|
||||
<td class="px-6 py-4 text-sm">
|
||||
<Link :href="route('offboarding.create', agent.id)" class="text-red-600 hover:text-red-900 font-bold">Déclencher Départ</Link>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</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="Profil" />
|
||||
|
||||
<AuthenticatedLayout>
|
||||
<template #header>
|
||||
<h2
|
||||
class="text-xl font-semibold leading-tight text-gray-800 dark:text-gray-200"
|
||||
>
|
||||
Profil
|
||||
</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">
|
||||
Supprimer le compte
|
||||
</h2>
|
||||
|
||||
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
|
||||
Une fois votre compte supprimé, toutes ses ressources et données seront
|
||||
définitivement effacées. Avant de supprimer votre compte, veuillez
|
||||
télécharger toutes les données ou informations que vous souhaitez conserver.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<DangerButton @click="confirmUserDeletion">Supprimer le compte</DangerButton>
|
||||
|
||||
<Modal :show="confirmingUserDeletion" @close="closeModal">
|
||||
<div class="p-6">
|
||||
<h2
|
||||
class="text-lg font-medium text-gray-900 dark:text-gray-100"
|
||||
>
|
||||
Êtes-vous sûr de vouloir supprimer votre compte ?
|
||||
</h2>
|
||||
|
||||
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
|
||||
Une fois votre compte supprimé, toutes ses ressources et données
|
||||
seront définitivement effacées. Veuillez saisir votre mot de passe pour
|
||||
confirmer que vous souhaitez supprimer définitivement votre compte.
|
||||
</p>
|
||||
|
||||
<div class="mt-6">
|
||||
<InputLabel
|
||||
for="password"
|
||||
value="Mot de passe"
|
||||
class="sr-only"
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
id="password"
|
||||
ref="passwordInput"
|
||||
v-model="form.password"
|
||||
type="password"
|
||||
class="mt-1 block w-3/4"
|
||||
placeholder="Mot de passe"
|
||||
@keyup.enter="deleteUser"
|
||||
/>
|
||||
|
||||
<InputError :message="form.errors.password" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex justify-end">
|
||||
<SecondaryButton @click="closeModal">
|
||||
Annuler
|
||||
</SecondaryButton>
|
||||
|
||||
<DangerButton
|
||||
class="ms-3"
|
||||
:class="{ 'opacity-25': form.processing }"
|
||||
:disabled="form.processing"
|
||||
@click="deleteUser"
|
||||
>
|
||||
Supprimer le compte
|
||||
</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">
|
||||
Mettre à jour le mot de passe
|
||||
</h2>
|
||||
|
||||
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
|
||||
Assurez-vous que votre compte utilise un mot de passe long et aléatoire pour rester
|
||||
en sécurité.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<form @submit.prevent="updatePassword" class="mt-6 space-y-6">
|
||||
<div>
|
||||
<InputLabel for="current_password" value="Mot de passe actuel" />
|
||||
|
||||
<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="Nouveau mot de passe" />
|
||||
|
||||
<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="Confirmer le mot de passe"
|
||||
/>
|
||||
|
||||
<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">Enregistrer</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"
|
||||
>
|
||||
Enregistré.
|
||||
</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">
|
||||
Informations du profil
|
||||
</h2>
|
||||
|
||||
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
|
||||
Mettez à jour les informations de votre compte et votre adresse email.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<form
|
||||
@submit.prevent="form.patch(route('profile.update'))"
|
||||
class="mt-6 space-y-6"
|
||||
>
|
||||
<div>
|
||||
<InputLabel for="name" value="Nom" />
|
||||
|
||||
<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">
|
||||
Votre adresse email n'est pas vérifiée.
|
||||
<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"
|
||||
>
|
||||
Cliquez ici pour renvoyer l'email de vérification.
|
||||
</Link>
|
||||
</p>
|
||||
|
||||
<div
|
||||
v-show="status === 'verification-link-sent'"
|
||||
class="mt-2 text-sm font-medium text-green-600 dark:text-green-400"
|
||||
>
|
||||
Un nouveau lien de vérification a été envoyé à votre adresse email.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-4">
|
||||
<PrimaryButton :disabled="form.processing">Enregistrer</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"
|
||||
>
|
||||
Enregistré.
|
||||
</p>
|
||||
</Transition>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</template>
|
||||
168
resources/js/Pages/Template/Create.vue
Normal file
168
resources/js/Pages/Template/Create.vue
Normal file
@@ -0,0 +1,168 @@
|
||||
<script setup>
|
||||
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue';
|
||||
import { Head, useForm, Link } from '@inertiajs/vue3';
|
||||
import PrimaryButton from '@/Components/PrimaryButton.vue';
|
||||
import InputError from '@/Components/InputError.vue';
|
||||
import InputLabel from '@/Components/InputLabel.vue';
|
||||
import TextInput from '@/Components/TextInput.vue';
|
||||
import { ref } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
services: Array,
|
||||
});
|
||||
|
||||
const form = useForm({
|
||||
name: '',
|
||||
description: '',
|
||||
is_active: true,
|
||||
items: [],
|
||||
});
|
||||
|
||||
const addItem = () => {
|
||||
form.items.push({
|
||||
service_id: props.services.length > 0 ? props.services[0].id : '',
|
||||
label: '',
|
||||
is_mandatory: false,
|
||||
fields: [],
|
||||
});
|
||||
};
|
||||
|
||||
const removeItem = (index) => {
|
||||
form.items.splice(index, 1);
|
||||
};
|
||||
|
||||
const addField = (item) => {
|
||||
if (!item.fields) item.fields = [];
|
||||
item.fields.push({
|
||||
label: '',
|
||||
type: 'text',
|
||||
required: false,
|
||||
});
|
||||
};
|
||||
|
||||
const removeField = (item, index) => {
|
||||
item.fields.splice(index, 1);
|
||||
};
|
||||
|
||||
const submit = () => {
|
||||
form.post(route('templates.store'));
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head title="Nouveau Modèle" />
|
||||
|
||||
<AuthenticatedLayout>
|
||||
<template #header>
|
||||
<h2 class="text-xl font-semibold leading-tight text-gray-800 dark:text-gray-200">
|
||||
Créer un Modèle d'Intégration
|
||||
</h2>
|
||||
</template>
|
||||
|
||||
<div class="py-12">
|
||||
<div class="mx-auto max-w-7xl sm:px-6 lg:px-8">
|
||||
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg dark:bg-gray-800 p-6">
|
||||
<form @submit.prevent="submit" class="space-y-6">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<InputLabel for="name" value="Nom du modèle" />
|
||||
<TextInput id="name" type="text" class="mt-1 block w-full" v-model="form.name" required autofocus />
|
||||
<InputError class="mt-2" :message="form.errors.name" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<InputLabel for="description" value="Description" />
|
||||
<TextInput id="description" type="text" class="mt-1 block w-full" v-model="form.description" />
|
||||
<InputError class="mt-2" :message="form.errors.description" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox" v-model="form.is_active" 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">
|
||||
<span class="ml-2 text-sm text-gray-600 dark:text-gray-400">Modèle Actif</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="border-t border-gray-200 dark:border-gray-700 pt-6">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100">Tâches par Service</h3>
|
||||
<button type="button" @click="addItem" class="text-sm bg-gray-200 hover:bg-gray-300 dark:bg-gray-700 dark:hover:bg-gray-600 px-3 py-1 rounded">
|
||||
+ Ajouter une tâche
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="form.items.length === 0" class="text-center text-gray-500 italic py-4">
|
||||
Aucune tâche définie. Ajoutez des tâches pour ce modèle.
|
||||
</div>
|
||||
|
||||
<div v-else class="space-y-4">
|
||||
<div v-for="(item, index) in form.items" :key="index" class="p-4 bg-gray-50 dark:bg-gray-700/30 rounded-lg space-y-4">
|
||||
<div class="flex items-start space-x-4">
|
||||
<div class="w-1/4">
|
||||
<InputLabel value="Service" class="mb-1" />
|
||||
<select v-model="item.service_id" class="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 text-sm">
|
||||
<option v-for="service in services" :key="service.id" :value="service.id">
|
||||
{{ service.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<InputLabel value="Libellé de la tâche" class="mb-1" />
|
||||
<TextInput type="text" class="block w-full" v-model="item.label" required placeholder="Ex: Créer compte AD" />
|
||||
</div>
|
||||
<div class="pt-6">
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox" v-model="item.is_mandatory" 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">
|
||||
<span class="ml-2 text-sm text-gray-600 dark:text-gray-400">Obligatoire</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="pt-6">
|
||||
<button type="button" @click="removeItem(index)" class="text-red-500 hover:text-red-700">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Custom Fields -->
|
||||
<div class="pl-4 border-l-2 border-indigo-200 dark:border-indigo-800 ml-4 space-y-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<h4 class="text-xs font-semibold uppercase text-gray-500 dark:text-gray-400">Données à saisir par le service</h4>
|
||||
<button type="button" @click="addField(item)" class="text-xs text-indigo-600 hover:text-indigo-800 dark:text-indigo-400 dark:hover:text-indigo-300 font-medium">
|
||||
+ Ajouter un champ
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-for="(field, fIndex) in item.fields" :key="fIndex" class="flex items-center space-x-2 bg-white dark:bg-gray-800 p-2 rounded shadow-sm border border-gray-200 dark:border-gray-700">
|
||||
<TextInput v-model="field.label" placeholder="Nom du champ (ex: Login)" class="text-xs w-1/3 py-1" />
|
||||
<select v-model="field.type" class="text-xs rounded-md border-gray-300 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-300 focus:border-indigo-500 focus:ring-indigo-500 py-1">
|
||||
<option value="text">Texte</option>
|
||||
<option value="checkbox">Case à cocher</option>
|
||||
<option value="date">Date</option>
|
||||
</select>
|
||||
<label class="flex items-center ml-2">
|
||||
<input type="checkbox" v-model="field.required" 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 h-3 w-3">
|
||||
<span class="ml-1 text-xs text-gray-600 dark:text-gray-400">Requis</span> <!-- FIXED: Correct closing tag for span -->
|
||||
</label> <!-- FIXED: Correct closing tag for label -->
|
||||
<button type="button" @click="removeField(item, fIndex)" class="text-red-500 hover:text-red-700 ml-auto p-1">
|
||||
x
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<InputError class="mt-2" :message="form.errors.items" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-4">
|
||||
<PrimaryButton :disabled="form.processing">Enregistrer</PrimaryButton>
|
||||
<Link :href="route('templates.index')" class="text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 underline">Annuler</Link>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AuthenticatedLayout>
|
||||
</template>
|
||||
173
resources/js/Pages/Template/Edit.vue
Normal file
173
resources/js/Pages/Template/Edit.vue
Normal file
@@ -0,0 +1,173 @@
|
||||
<script setup>
|
||||
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue';
|
||||
import { Head, useForm, Link } from '@inertiajs/vue3';
|
||||
import PrimaryButton from '@/Components/PrimaryButton.vue';
|
||||
import InputError from '@/Components/InputError.vue';
|
||||
import InputLabel from '@/Components/InputLabel.vue';
|
||||
import TextInput from '@/Components/TextInput.vue';
|
||||
|
||||
const props = defineProps({
|
||||
template: Object,
|
||||
services: Array,
|
||||
});
|
||||
|
||||
const form = useForm({
|
||||
name: props.template.name,
|
||||
description: props.template.description,
|
||||
is_active: Boolean(props.template.is_active),
|
||||
items: props.template.service_items.map(item => ({
|
||||
service_id: item.service_id,
|
||||
label: item.label,
|
||||
is_mandatory: Boolean(item.is_mandatory),
|
||||
fields: item.fields || [],
|
||||
})),
|
||||
});
|
||||
|
||||
const addItem = () => {
|
||||
form.items.push({
|
||||
service_id: props.services.length > 0 ? props.services[0].id : '',
|
||||
label: '',
|
||||
is_mandatory: false,
|
||||
fields: [],
|
||||
});
|
||||
};
|
||||
|
||||
const removeItem = (index) => {
|
||||
form.items.splice(index, 1);
|
||||
};
|
||||
|
||||
const addField = (item) => {
|
||||
if (!item.fields) item.fields = [];
|
||||
item.fields.push({
|
||||
label: '',
|
||||
type: 'text',
|
||||
required: false,
|
||||
});
|
||||
};
|
||||
|
||||
const removeField = (item, index) => {
|
||||
item.fields.splice(index, 1);
|
||||
};
|
||||
|
||||
const submit = () => {
|
||||
form.put(route('templates.update', props.template.id));
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head title="Modifier Modèle" />
|
||||
|
||||
<AuthenticatedLayout>
|
||||
<template #header>
|
||||
<h2 class="text-xl font-semibold leading-tight text-gray-800 dark:text-gray-200">
|
||||
Modifier Modèle: {{ template.name }}
|
||||
</h2>
|
||||
</template>
|
||||
|
||||
<div class="py-12">
|
||||
<div class="mx-auto max-w-7xl sm:px-6 lg:px-8">
|
||||
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg dark:bg-gray-800 p-6">
|
||||
<form @submit.prevent="submit" class="space-y-6">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<InputLabel for="name" value="Nom du modèle" />
|
||||
<TextInput id="name" type="text" class="mt-1 block w-full" v-model="form.name" required autofocus />
|
||||
<InputError class="mt-2" :message="form.errors.name" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<InputLabel for="description" value="Description" />
|
||||
<TextInput id="description" type="text" class="mt-1 block w-full" v-model="form.description" />
|
||||
<InputError class="mt-2" :message="form.errors.description" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox" v-model="form.is_active" 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">
|
||||
<span class="ml-2 text-sm text-gray-600 dark:text-gray-400">Modèle Actif</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="border-t border-gray-200 dark:border-gray-700 pt-6">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100">Tâches par Service</h3>
|
||||
<button type="button" @click="addItem" class="text-sm bg-gray-200 hover:bg-gray-300 dark:bg-gray-700 dark:hover:bg-gray-600 px-3 py-1 rounded">
|
||||
+ Ajouter une tâche
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="form.items.length === 0" class="text-center text-gray-500 italic py-4">
|
||||
Aucune tâche définie. Ajoutez des tâches pour ce modèle.
|
||||
</div>
|
||||
|
||||
<div v-else class="space-y-4">
|
||||
<div v-for="(item, index) in form.items" :key="index" class="p-4 bg-gray-50 dark:bg-gray-700/30 rounded-lg space-y-4">
|
||||
<div class="flex items-start space-x-4">
|
||||
<div class="w-1/4">
|
||||
<InputLabel value="Service" class="mb-1" />
|
||||
<select v-model="item.service_id" class="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 text-sm">
|
||||
<option v-for="service in services" :key="service.id" :value="service.id">
|
||||
{{ service.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<InputLabel value="Libellé de la tâche" class="mb-1" />
|
||||
<TextInput type="text" class="block w-full" v-model="item.label" required placeholder="Ex: Créer compte AD" />
|
||||
</div>
|
||||
<div class="pt-6">
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox" v-model="item.is_mandatory" 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">
|
||||
<span class="ml-2 text-sm text-gray-600 dark:text-gray-400">Obligatoire</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="pt-6">
|
||||
<button type="button" @click="removeItem(index)" class="text-red-500 hover:text-red-700">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Custom Fields -->
|
||||
<div class="pl-4 border-l-2 border-indigo-200 dark:border-indigo-800 ml-4 space-y-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<h4 class="text-xs font-semibold uppercase text-gray-500 dark:text-gray-400">Données à saisir par le service</h4>
|
||||
<button type="button" @click="addField(item)" class="text-xs text-indigo-600 hover:text-indigo-800 dark:text-indigo-400 dark:hover:text-indigo-300 font-medium">
|
||||
+ Ajouter un champ
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-for="(field, fIndex) in item.fields" :key="fIndex" class="flex items-center space-x-2 bg-white dark:bg-gray-800 p-2 rounded shadow-sm border border-gray-200 dark:border-gray-700">
|
||||
<TextInput v-model="field.label" placeholder="Nom du champ (ex: Login)" class="text-xs w-1/3 py-1" />
|
||||
<select v-model="field.type" class="text-xs rounded-md border-gray-300 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-300 focus:border-indigo-500 focus:ring-indigo-500 py-1">
|
||||
<option value="text">Texte</option>
|
||||
<option value="checkbox">Case à cocher</option>
|
||||
<option value="date">Date</option>
|
||||
</select>
|
||||
<label class="flex items-center ml-2">
|
||||
<input type="checkbox" v-model="field.required" 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 h-3 w-3">
|
||||
<span class="ml-1 text-xs text-gray-600 dark:text-gray-400">Requis</span>
|
||||
</label>
|
||||
<button type="button" @click="removeField(item, fIndex)" class="text-red-500 hover:text-red-700 ml-auto p-1">
|
||||
x
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<InputError class="mt-2" :message="form.errors.items" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-4">
|
||||
<PrimaryButton :disabled="form.processing">Mettre à jour</PrimaryButton>
|
||||
<Link :href="route('templates.index')" class="text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 underline">Annuler</Link>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AuthenticatedLayout>
|
||||
</template>
|
||||
74
resources/js/Pages/Template/Index.vue
Normal file
74
resources/js/Pages/Template/Index.vue
Normal file
@@ -0,0 +1,74 @@
|
||||
<script setup>
|
||||
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue';
|
||||
import { Head, Link, router } from '@inertiajs/vue3';
|
||||
import PrimaryButton from '@/Components/PrimaryButton.vue';
|
||||
|
||||
const props = defineProps({
|
||||
templates: Array,
|
||||
});
|
||||
|
||||
const deleteTemplate = (template) => {
|
||||
if (confirm('Êtes-vous sûr de vouloir supprimer ce template ?')) {
|
||||
router.delete(route('templates.destroy', template.id));
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head title="Gestion des Templates" />
|
||||
|
||||
<AuthenticatedLayout>
|
||||
<template #header>
|
||||
<div class="flex justify-between items-center">
|
||||
<h2 class="text-xl font-semibold leading-tight text-gray-800 dark:text-gray-200">
|
||||
Gestion des Modèles d'Intégration
|
||||
</h2>
|
||||
<Link :href="route('templates.create')">
|
||||
<PrimaryButton>Nouveau Modèle</PrimaryButton>
|
||||
</Link>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="py-12">
|
||||
<div class="mx-auto max-w-7xl sm:px-6 lg:px-8">
|
||||
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg dark:bg-gray-800">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-left border-collapse">
|
||||
<thead>
|
||||
<tr class="border-b dark:border-gray-700 bg-gray-50 dark:bg-gray-700/50">
|
||||
<th class="px-6 py-3 text-xs font-bold uppercase text-gray-500 dark:text-gray-400">Nom</th>
|
||||
<th class="px-6 py-3 text-xs font-bold uppercase text-gray-500 dark:text-gray-400">Description</th>
|
||||
<th class="px-6 py-3 text-xs font-bold uppercase text-gray-500 dark:text-gray-400">Tâches</th>
|
||||
<th class="px-6 py-3 text-xs font-bold uppercase text-gray-500 dark:text-gray-400">Actif</th>
|
||||
<th class="px-6 py-3 text-xs font-bold uppercase text-gray-500 dark:text-gray-400 text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<tr v-for="template in templates" :key="template.id" class="hover:bg-gray-50 dark:hover:bg-gray-700/30">
|
||||
<td class="px-6 py-4 text-sm font-medium text-gray-900 dark:text-white">
|
||||
{{ template.name }}
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ template.description || '-' }}
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ template.service_items_count }}
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<span :class="['px-2 inline-flex text-xs leading-5 font-semibold rounded-full', template.is_active ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800']">
|
||||
{{ template.is_active ? 'Oui' : 'Non' }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm text-right space-x-2">
|
||||
<Link :href="route('templates.edit', template.id)" class="text-indigo-600 hover:text-indigo-900 dark:text-indigo-400 dark:hover:text-indigo-300">Modifier</Link>
|
||||
<button @click="deleteTemplate(template)" class="text-red-600 hover:text-red-900 dark:text-red-400 dark:hover:text-red-300">Supprimer</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AuthenticatedLayout>
|
||||
</template>
|
||||
106
resources/js/Pages/User/Edit.vue
Normal file
106
resources/js/Pages/User/Edit.vue
Normal file
@@ -0,0 +1,106 @@
|
||||
<script setup>
|
||||
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue';
|
||||
import { Head, useForm, Link } from '@inertiajs/vue3';
|
||||
import PrimaryButton from '@/Components/PrimaryButton.vue';
|
||||
import InputError from '@/Components/InputError.vue';
|
||||
import InputLabel from '@/Components/InputLabel.vue';
|
||||
import TextInput from '@/Components/TextInput.vue';
|
||||
|
||||
const props = defineProps({
|
||||
user: Object,
|
||||
roles: Array,
|
||||
});
|
||||
|
||||
const form = useForm({
|
||||
name: props.user?.name || '',
|
||||
email: props.user?.email || '',
|
||||
password: '',
|
||||
roles: props.user?.roles.map(r => r.name) || [],
|
||||
});
|
||||
|
||||
const submit = () => {
|
||||
if (props.user) {
|
||||
form.put(route('users.update', props.user.id));
|
||||
} else {
|
||||
form.post(route('users.store'));
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head :title="user ? 'Modifier Utilisateur' : 'Nouvel Utilisateur'" />
|
||||
|
||||
<AuthenticatedLayout>
|
||||
<template #header>
|
||||
<h2 class="text-xl font-semibold leading-tight text-gray-800 dark:text-gray-200">
|
||||
{{ user ? 'Modifier Utilisateur' : 'Nouvel Utilisateur' }}
|
||||
</h2>
|
||||
</template>
|
||||
|
||||
<div class="py-12">
|
||||
<div class="mx-auto max-w-7xl sm:px-6 lg:px-8">
|
||||
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg dark:bg-gray-800 p-6">
|
||||
<form @submit.prevent="submit" class="space-y-6 max-w-xl">
|
||||
<div>
|
||||
<InputLabel for="name" value="Nom" />
|
||||
<TextInput
|
||||
id="name"
|
||||
type="text"
|
||||
class="mt-1 block w-full"
|
||||
v-model="form.name"
|
||||
required
|
||||
autofocus
|
||||
/>
|
||||
<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
|
||||
/>
|
||||
<InputError class="mt-2" :message="form.errors.email" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<InputLabel for="password" value="Mot de passe (laisser vide pour ne pas changer)" />
|
||||
<TextInput
|
||||
id="password"
|
||||
type="password"
|
||||
class="mt-1 block w-full"
|
||||
v-model="form.password"
|
||||
:required="!user"
|
||||
/>
|
||||
<InputError class="mt-2" :message="form.errors.password" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Rôles</span>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<label v-for="role in roles" :key="role.id" class="flex items-center space-x-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
:value="role.name"
|
||||
v-model="form.roles"
|
||||
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"
|
||||
>
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">{{ role.name }}</span>
|
||||
</label>
|
||||
</div>
|
||||
<InputError class="mt-2" :message="form.errors.roles" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-4">
|
||||
<PrimaryButton :disabled="form.processing">Enregistrer</PrimaryButton>
|
||||
<Link :href="route('users.index')" class="text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 underline">Annuler</Link>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AuthenticatedLayout>
|
||||
</template>
|
||||
70
resources/js/Pages/User/Index.vue
Normal file
70
resources/js/Pages/User/Index.vue
Normal file
@@ -0,0 +1,70 @@
|
||||
<script setup>
|
||||
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue';
|
||||
import { Head, Link, router } from '@inertiajs/vue3';
|
||||
import PrimaryButton from '@/Components/PrimaryButton.vue';
|
||||
|
||||
const props = defineProps({
|
||||
users: Array,
|
||||
});
|
||||
|
||||
const deleteUser = (user) => {
|
||||
if (confirm('Êtes-vous sûr de vouloir supprimer cet utilisateur ?')) {
|
||||
router.delete(route('users.destroy', user.id));
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head title="Gestion des Utilisateurs" />
|
||||
|
||||
<AuthenticatedLayout>
|
||||
<template #header>
|
||||
<div class="flex justify-between items-center">
|
||||
<h2 class="text-xl font-semibold leading-tight text-gray-800 dark:text-gray-200">
|
||||
Gestion des Utilisateurs
|
||||
</h2>
|
||||
<Link :href="route('users.create')">
|
||||
<PrimaryButton>Nouvel Utilisateur</PrimaryButton>
|
||||
</Link>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="py-12">
|
||||
<div class="mx-auto max-w-7xl sm:px-6 lg:px-8">
|
||||
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg dark:bg-gray-800">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-left border-collapse">
|
||||
<thead>
|
||||
<tr class="border-b dark:border-gray-700 bg-gray-50 dark:bg-gray-700/50">
|
||||
<th class="px-6 py-3 text-xs font-bold uppercase text-gray-500 dark:text-gray-400">Nom</th>
|
||||
<th class="px-6 py-3 text-xs font-bold uppercase text-gray-500 dark:text-gray-400">Email</th>
|
||||
<th class="px-6 py-3 text-xs font-bold uppercase text-gray-500 dark:text-gray-400">Rôles</th>
|
||||
<th class="px-6 py-3 text-xs font-bold uppercase text-gray-500 dark:text-gray-400 text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<tr v-for="user in users" :key="user.id" class="hover:bg-gray-50 dark:hover:bg-gray-700/30">
|
||||
<td class="px-6 py-4 text-sm font-medium text-gray-900 dark:text-white">
|
||||
{{ user.name }}
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ user.email }}
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm text-gray-500 dark:text-gray-400">
|
||||
<span v-for="role in user.roles" :key="role.id" class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-blue-100 text-blue-800 mr-1">
|
||||
{{ role.name }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm text-right space-x-2">
|
||||
<Link :href="route('users.edit', user.id)" class="text-indigo-600 hover:text-indigo-900 dark:text-indigo-400 dark:hover:text-indigo-300">Modifier</Link>
|
||||
<button @click="deleteUser(user)" class="text-red-600 hover:text-red-900 dark:text-red-400 dark:hover:text-red-300">Supprimer</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AuthenticatedLayout>
|
||||
</template>
|
||||
163
resources/js/Pages/Welcome.vue
Normal file
163
resources/js/Pages/Welcome.vue
Normal file
@@ -0,0 +1,163 @@
|
||||
<script setup>
|
||||
import { Head, Link } from '@inertiajs/vue3';
|
||||
import ApplicationLogo from '@/Components/ApplicationLogo.vue';
|
||||
|
||||
defineProps({
|
||||
canLogin: {
|
||||
type: Boolean,
|
||||
},
|
||||
canRegister: {
|
||||
type: Boolean,
|
||||
},
|
||||
laravelVersion: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
phpVersion: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head title="Bienvenue - Fiche Agent" />
|
||||
|
||||
<div class="bg-gray-50 text-black/50 dark:bg-black dark:text-white/50 min-h-screen flex flex-col">
|
||||
<!-- Background Effects -->
|
||||
<div class="fixed inset-0 z-0 overflow-hidden pointer-events-none">
|
||||
<div class="absolute -top-[30%] -left-[10%] w-[70%] h-[70%] rounded-full bg-red-500/10 blur-[120px] dark:bg-red-900/20"></div>
|
||||
<div class="absolute top-[40%] -right-[10%] w-[60%] h-[60%] rounded-full bg-yellow-500/10 blur-[100px] dark:bg-yellow-600/10"></div>
|
||||
</div>
|
||||
|
||||
<div class="relative w-full max-w-7xl mx-auto px-6 lg:px-8 z-10 flex-grow flex flex-col">
|
||||
<!-- Header -->
|
||||
<header class="flex justify-between items-center py-6">
|
||||
<div class="flex items-center gap-2">
|
||||
<ApplicationLogo class="w-12 h-12" />
|
||||
<span class="text-xl font-bold text-gray-900 dark:text-white tracking-tight">
|
||||
CAP <span class="text-red-600">CABM</span>
|
||||
</span>
|
||||
</div>
|
||||
<nav v-if="canLogin" class="flex gap-4">
|
||||
<Link
|
||||
v-if="$page.props.auth.user"
|
||||
:href="route('dashboard')"
|
||||
class="rounded-full px-5 py-2 text-sm font-semibold text-white bg-red-600 hover:bg-red-500 transition focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 dark:focus:ring-offset-black"
|
||||
>
|
||||
Tableau de Bord
|
||||
</Link>
|
||||
|
||||
<template v-else>
|
||||
<Link
|
||||
:href="route('login')"
|
||||
class="rounded-full px-5 py-2 text-sm font-semibold text-gray-900 ring-1 ring-gray-900/10 hover:ring-gray-900/20 dark:text-white dark:ring-white/20 dark:hover:ring-white/30 transition"
|
||||
>
|
||||
Connexion
|
||||
</Link>
|
||||
</template>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<!-- Hero Section -->
|
||||
<main class="flex-grow flex flex-col justify-center items-center text-center mt-10 lg:mt-20 mb-20">
|
||||
<div class="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-red-50 dark:bg-red-900/30 text-red-600 dark:text-red-300 text-xs font-medium mb-6 animate-fade-in-up">
|
||||
<span class="relative flex h-2 w-2">
|
||||
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-red-400 opacity-75"></span>
|
||||
<span class="relative inline-flex rounded-full h-2 w-2 bg-red-500"></span>
|
||||
</span>
|
||||
Construire l’Accueil et le Parcours
|
||||
</div>
|
||||
|
||||
<h1 class="text-5xl md:text-7xl font-extrabold tracking-tight text-gray-900 dark:text-white mb-6 max-w-4xl">
|
||||
CAP <span class="text-transparent bg-clip-text bg-gradient-to-r from-red-600 via-yellow-500 to-blue-600">CABM</span>
|
||||
</h1>
|
||||
|
||||
<p class="text-lg md:text-xl text-gray-600 dark:text-gray-300 max-w-2xl mb-10 leading-relaxed">
|
||||
La plateforme dédiée à l'accueil et au parcours des agents de l'Agglomération Béziers Méditerranée.
|
||||
</p>
|
||||
|
||||
<div class="flex flex-col sm:flex-row gap-4 w-full sm:w-auto">
|
||||
<Link
|
||||
v-if="$page.props.auth.user"
|
||||
:href="route('dashboard')"
|
||||
class="px-8 py-3.5 rounded-full text-base font-bold text-white bg-gradient-to-r from-red-600 to-red-800 hover:from-red-500 hover:to-red-700 shadow-lg shadow-red-500/30 transition transform hover:-translate-y-0.5"
|
||||
>
|
||||
Accéder à mon espace
|
||||
</Link>
|
||||
<Link
|
||||
v-else
|
||||
:href="route('login')"
|
||||
class="px-8 py-3.5 rounded-full text-base font-bold text-white bg-gradient-to-r from-red-600 to-red-800 hover:from-red-500 hover:to-red-700 shadow-lg shadow-red-500/30 transition transform hover:-translate-y-0.5"
|
||||
>
|
||||
Commencer maintenant
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<!-- Features Grid -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-8 mt-24 w-full max-w-6xl text-left">
|
||||
<div class="bg-white/50 dark:bg-gray-800/50 backdrop-blur-sm p-6 rounded-2xl border border-gray-100 dark:border-gray-700 hover:border-red-500/50 transition duration-300 group">
|
||||
<div class="w-12 h-12 bg-red-100 dark:bg-red-900/50 rounded-lg flex items-center justify-center mb-4 group-hover:scale-110 transition duration-300">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-red-600 dark:text-red-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-xl font-bold text-gray-900 dark:text-white mb-2">Ressources Humaines</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
Centralisez les demandes d'arrivée et de départ des agents de l'agglomération.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="bg-white/50 dark:bg-gray-800/50 backdrop-blur-sm p-6 rounded-2xl border border-gray-100 dark:border-gray-700 hover:border-blue-500/50 transition duration-300 group">
|
||||
<div class="w-12 h-12 bg-blue-100 dark:bg-blue-900/50 rounded-lg flex items-center justify-center mb-4 group-hover:scale-110 transition duration-300">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-blue-600 dark:text-blue-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-xl font-bold text-gray-900 dark:text-white mb-2">Services Supports</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
DSI, Bâtiment, Parc Auto : recevez vos ordres de mission automatiquement.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="bg-white/50 dark:bg-gray-800/50 backdrop-blur-sm p-6 rounded-2xl border border-gray-100 dark:border-gray-700 hover:border-yellow-500/50 transition duration-300 group">
|
||||
<div class="w-12 h-12 bg-yellow-100 dark:bg-yellow-900/50 rounded-lg flex items-center justify-center mb-4 group-hover:scale-110 transition duration-300">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-yellow-600 dark:text-yellow-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-xl font-bold text-gray-900 dark:text-white mb-2">Efficacité & Suivi</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
Un tableau de bord aux couleurs de Béziers pour piloter l'activité en temps réel.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="py-8 text-center text-sm text-gray-500 dark:text-gray-400">
|
||||
© {{ new Date().getFullYear() }} CAP CABM. Tous droits réservés.
|
||||
<div class="mt-2 text-xs opacity-70">
|
||||
Laravel v{{ laravelVersion }} (PHP v{{ phpVersion }})
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.animate-fade-in-up {
|
||||
animation: fadeInUp 0.8s ease-out forwards;
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user