feat: Initialize core application structure including authentication, role-based dashboards, service task management, and integration workflows.

This commit is contained in:
jeremy bayse
2026-02-16 09:30:23 +01:00
commit af060a8847
208 changed files with 26822 additions and 0 deletions

View File

@@ -0,0 +1,72 @@
<script setup>
import { computed } from 'vue';
const props = defineProps({
activities: Array,
});
const formatEvent = (activity) => {
const events = {
'created': 'Création',
'updated': 'Mise à jour',
'deleted': 'Suppression',
};
return events[activity.description] || activity.description;
};
const getBadgeColor = (description) => {
if (description === 'created') return 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200';
if (description === 'updated') return 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200';
return 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200';
};
</script>
<template>
<div class="bg-white dark:bg-gray-800 shadow sm:rounded-lg border border-gray-200 dark:border-gray-700">
<div class="px-4 py-5 sm:px-6">
<h3 class="text-lg font-medium leading-6 text-gray-900 dark:text-gray-100">Historique d'activité</h3>
</div>
<div class="border-t border-gray-200 dark:border-gray-700 px-4 py-5 sm:p-6">
<div class="flow-root">
<ul role="list" class="-mb-8">
<li v-for="(activity, index) in activities" :key="activity.id">
<div class="relative pb-8">
<span v-if="index !== activities.length - 1" class="absolute top-4 left-4 -ml-px h-full w-0.5 bg-gray-200 dark:bg-gray-700" aria-hidden="true"></span>
<div class="relative flex space-x-3">
<div>
<span :class="[getBadgeColor(activity.description), 'h-8 w-8 rounded-full flex items-center justify-center ring-8 ring-white dark:ring-gray-800']">
<svg v-if="activity.description === 'created'" class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path></svg>
<svg v-else-if="activity.description === 'updated'" class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z"></path></svg>
<svg v-else class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
</span>
</div>
<div class="flex min-w-0 flex-1 justify-between space-x-4 pt-1.5">
<div>
<p class="text-sm text-gray-500 dark:text-gray-400">
<span class="font-medium text-gray-900 dark:text-gray-100">{{ activity.causer?.name || 'Système' }}</span>
{{ activity.description === 'created' ? ' a créé ' : ' a mis à jour ' }}
<span class="font-medium text-gray-900 dark:text-gray-100">
{{ activity.subject_type.split('\\').pop() }}
</span>
</p>
<div v-if="activity.properties?.attributes" class="mt-2 text-xs text-gray-400 bg-gray-50 dark:bg-gray-900/50 p-2 rounded">
<div v-for="(val, key) in activity.properties.attributes" :key="key">
<span v-if="key === 'status'">Nouveau statut: <span class="font-bold text-blue-500">{{ val }}</span></span>
</div>
</div>
</div>
<div class="whitespace-nowrap text-right text-sm text-gray-500 dark:text-gray-400">
<time :datetime="activity.created_at">{{ new Date(activity.created_at).toLocaleString() }}</time>
</div>
</div>
</div>
</div>
</li>
<li v-if="activities.length === 0" class="text-sm text-gray-500 italic pb-4">
Aucune activité enregistrée.
</li>
</ul>
</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,92 @@
<script setup>
import { useForm } from '@inertiajs/vue3';
import { ref } from 'vue';
const props = defineProps({
attachments: {
type: Array,
default: () => [],
},
taskId: {
type: Number,
required: true,
},
canManage: Boolean,
});
const fileInput = ref(null);
const form = useForm({
file: null,
});
const uploadFile = () => {
if (fileInput.value.files[0]) {
form.file = fileInput.value.files[0];
form.post(route('attachments.store', props.taskId), {
preserveScroll: true,
onSuccess: () => {
form.reset();
fileInput.value.value = '';
},
});
}
};
const deleteAttachment = (id) => {
if (confirm('Supprimer ce fichier ?')) {
router.delete(route('attachments.destroy', id), {
preserveScroll: true,
});
}
};
const formatSize = (bytes) => {
if (!bytes) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
};
</script>
<template>
<div class="mt-4 border-t border-gray-100 dark:border-gray-700 pt-4">
<h4 class="text-xs font-bold uppercase text-gray-400 mb-2">Pièces jointes ({{ attachments.length }})</h4>
<div class="space-y-2">
<div v-for="file in attachments" :key="file.id" class="flex items-center justify-between p-2 bg-gray-50 dark:bg-gray-900/30 rounded border border-gray-100 dark:border-gray-700">
<div class="flex items-center min-w-0">
<svg class="w-4 h-4 text-gray-400 mr-2 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15.172 7l-6.586 6.586a2 2 0 102.828 2.828l6.414-6.586a4 4 0 00-5.656-5.656l-6.415 6.585a6 6 0 108.486 8.486L20.5 13"></path></svg>
<span class="text-sm text-gray-700 dark:text-gray-300 truncate" :title="file.original_name">
{{ file.original_name }}
</span>
<span class="ml-2 text-[10px] text-gray-400">({{ formatSize(file.size) }})</span>
</div>
<div class="flex items-center space-x-3 ml-4">
<a :href="route('attachments.download', file.id)" class="text-blue-500 hover:text-blue-700 text-xs font-bold">
Télécharger
</a>
<button v-if="canManage" @click="deleteAttachment(file.id)" class="text-red-400 hover:text-red-600">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path></svg>
</button>
</div>
</div>
<div v-if="attachments.length === 0" class="text-xs text-gray-500 italic">Aucun fichier joint.</div>
<div v-if="canManage" class="mt-4">
<label class="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1">Ajouter un fichier</label>
<div class="flex items-center space-x-2">
<input
type="file"
ref="fileInput"
@change="uploadFile"
class="block w-full text-xs text-gray-500 file:mr-4 file:py-1 file:px-2 file:rounded-md file:border-0 file:text-xs file:font-semibold file:bg-blue-50 file:text-blue-700 hover:file:bg-blue-100 dark:file:bg-gray-700 dark:file:text-gray-300"
>
<div v-if="form.processing" class="text-[10px] text-blue-500 animate-pulse">Upload...</div>
</div>
</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,76 @@
<script setup>
import { useForm } from '@inertiajs/vue3';
import { ref } from 'vue';
import PrimaryButton from '@/Components/PrimaryButton.vue';
const props = defineProps({
comments: {
type: Array,
default: () => [],
},
commentableId: Number,
commentableType: String,
});
const isExpanded = ref(false);
const form = useForm({
content: '',
commentable_id: props.commentableId,
commentable_type: props.commentableType,
});
const submit = () => {
form.post(route('comments.store'), {
preserveScroll: true,
onSuccess: () => {
form.reset('content');
},
});
};
</script>
<template>
<div class="mt-4 border-t border-gray-100 dark:border-gray-700 pt-4">
<div class="flex items-center justify-between mb-2">
<h4 class="text-xs font-bold uppercase text-gray-400">Commentaires ({{ comments.length }})</h4>
<button @click="isExpanded = !isExpanded" class="text-xs text-blue-500 hover:underline">
{{ isExpanded ? 'Masquer' : 'Afficher' }}
</button>
</div>
<div v-if="isExpanded" class="space-y-4">
<div v-for="comment in comments" :key="comment.id" class="flex space-x-3">
<div class="flex-shrink-0">
<div class="h-8 w-8 rounded-full bg-gray-200 dark:bg-gray-700 flex items-center justify-center text-xs font-bold text-gray-500">
{{ comment.user.name.charAt(0) }}
</div>
</div>
<div class="flex-1 bg-gray-50 dark:bg-gray-900/50 p-3 rounded-lg text-sm">
<div class="flex items-center justify-between mb-1">
<span class="font-bold text-gray-900 dark:text-gray-100">{{ comment.user.name }}</span>
<span class="text-[10px] text-gray-400">{{ new Date(comment.created_at).toLocaleString() }}</span>
</div>
<p class="text-gray-700 dark:text-gray-300 whitespace-pre-wrap">{{ comment.content }}</p>
</div>
</div>
<form @submit.prevent="submit" class="mt-4">
<div>
<textarea
v-model="form.content"
placeholder="Ajouter un commentaire..."
class="w-full text-sm border-gray-300 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-300 focus:border-blue-500 rounded-md shadow-sm"
rows="2"
required
></textarea>
</div>
<div class="mt-2 flex justify-end">
<PrimaryButton :class="{ 'opacity-25': form.processing }" :disabled="form.processing" class="!px-3 !py-1 !text-[10px]">
Publier
</PrimaryButton>
</div>
</form>
</div>
</div>
</template>

View File

@@ -0,0 +1,188 @@
<script setup>
import StatusBadge from '@/Components/App/StatusBadge.vue';
import { computed, reactive } from 'vue';
import { router, usePage } from '@inertiajs/vue3';
import CommentSection from '@/Components/App/CommentSection.vue';
import AttachmentSection from '@/Components/App/AttachmentSection.vue';
import TextInput from '@/Components/TextInput.vue';
import InputLabel from '@/Components/InputLabel.vue';
const props = defineProps({
task: {
type: Object,
required: true,
},
});
const user = computed(() => usePage().props.auth.user);
const canManage = computed(() => {
return user.value.roles.some(r => r.name === props.task.service.name || r.name === 'Admin');
});
const progress = computed(() => {
if (!props.task.task_items.length) return 0;
const completed = props.task.task_items.filter(i => i.is_completed).length;
return Math.round((completed / props.task.task_items.length) * 100);
});
// Initialize form state for custom fields
const fieldsState = reactive({});
props.task.task_items.forEach(item => {
fieldsState[item.id] = {};
if (item.fields_definition) {
item.fields_definition.forEach(field => {
// Load existing data or empty string
// We assume field.label is the key.
// Check if item.data exists and has this key.
const existingValue = item.data && item.data[field.label] ? item.data[field.label] : '';
fieldsState[item.id][field.label] = existingValue;
});
}
});
const startTask = () => {
router.post(route('service-tasks.start', props.task.id));
};
const toggleItem = (item, event) => {
// If we are marking as complete
if (!item.is_completed) {
// Validate required fields
if (item.fields_definition) {
for (const field of item.fields_definition) {
if (field.required && !fieldsState[item.id][field.label]) {
event.preventDefault();
alert(`Le champ "${field.label}" est obligatoire.`);
return;
}
}
}
}
const data = fieldsState[item.id] || {};
router.post(route('task-items.toggle', item.id), { data }, {
preserveScroll: true,
onFinish: () => {
// Optional: feedback
}
});
};
const validateTask = () => {
if (confirm('Souhaitez-vous valider cette tâche ?')) {
router.post(route('service-tasks.approve', props.task.id));
}
};
const rejectTask = () => {
const reason = prompt('Raison du refus :');
if (reason) {
router.post(route('service-tasks.reject', props.task.id), { reason });
}
};
</script>
<template>
<div class="overflow-hidden bg-white shadow sm:rounded-lg dark:bg-gray-800 border border-gray-200 dark:border-gray-700">
<div class="px-4 py-5 sm:px-6 flex justify-between items-center">
<div>
<h3 class="text-lg font-medium leading-6 text-gray-900 dark:text-gray-100">
{{ task.service.name }}
</h3>
<p class="mt-1 max-w-2xl text-sm text-gray-500 dark:text-gray-400">
Deadline: {{ task.sla_deadline ? new Date(task.sla_deadline).toLocaleString() : 'N/A' }}
</p>
</div>
<div class="flex items-center space-x-2">
<button
v-if="canManage && task.status === 'pending'"
@click="startTask"
class="px-3 py-1 bg-blue-600 text-white rounded text-xs font-bold hover:bg-blue-700"
>
Démarrer
</button>
<div v-if="canManage && task.status === 'waiting_validation'" class="flex space-x-2">
<button @click="validateTask" class="px-3 py-1 bg-green-600 text-white rounded text-xs font-bold hover:bg-green-700">Valider</button>
<button @click="rejectTask" class="px-3 py-1 bg-red-600 text-white rounded text-xs font-bold hover:bg-red-700">Refuser</button>
</div>
<StatusBadge :status="task.status" />
</div>
</div>
<div class="px-4 py-3 bg-gray-50 dark:bg-gray-700/50">
<div class="w-full bg-gray-200 dark:bg-gray-600 rounded-full h-1.5 mb-2">
<div class="bg-blue-600 h-1.5 rounded-full transition-all duration-500" :style="{ width: progress + '%' }"></div>
</div>
<p class="text-xs text-gray-500 dark:text-gray-400 text-right">{{ progress }}% complété</p>
</div>
<div class="border-t border-gray-200 dark:border-gray-700 px-4 py-3 sm:p-0">
<dl class="divide-y divide-gray-200 dark:divide-gray-700">
<div v-for="item in task.task_items" :key="item.id" class="py-4 px-6">
<div class="flex items-center justify-between">
<div class="flex items-center w-full">
<input
type="checkbox"
:checked="item.is_completed"
:disabled="!canManage || (task.status !== 'in_progress' && task.status !== 'waiting_validation')"
@click="toggleItem(item, $event)"
class="h-4 w-4 text-blue-600 border-gray-300 rounded focus:ring-blue-500 dark:bg-gray-900 dark:border-gray-600 cursor-pointer disabled:cursor-not-allowed shrink-0"
>
<div class="ml-3 flex flex-col w-full">
<span :class="['text-sm font-medium', item.is_completed ? 'line-through text-gray-400' : 'text-gray-900 dark:text-gray-100']">
{{ item.label }}
</span>
</div>
</div>
<span v-if="item.is_mandatory" class="ml-2 text-[10px] uppercase font-bold text-red-500 dark:text-red-400 shrink-0">Obligatoire</span>
</div>
<!-- Custom Fields Section -->
<div v-if="item.fields_definition && item.fields_definition.length > 0" class="mt-3 ml-7 grid grid-cols-1 gap-y-3 gap-x-4 sm:grid-cols-2 bg-gray-50 dark:bg-gray-700/30 p-3 rounded-md">
<div v-for="(field, index) in item.fields_definition" :key="index">
<InputLabel :value="field.label + (field.required ? ' *' : '')" class="text-xs mb-1" />
<TextInput
v-if="field.type === 'text'"
type="text"
v-model="fieldsState[item.id][field.label]"
:disabled="item.is_completed || !canManage"
class="w-full text-xs py-1"
/>
<input
v-else-if="field.type === 'date'"
type="date"
v-model="fieldsState[item.id][field.label]"
:disabled="item.is_completed || !canManage"
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-xs py-1"
/>
<div v-else-if="field.type === 'checkbox'" class="flex items-center mt-2">
<input
type="checkbox"
v-model="fieldsState[item.id][field.label]"
:disabled="item.is_completed || !canManage"
class="h-4 w-4 text-indigo-600 border-gray-300 rounded focus:ring-indigo-500 dark:border-gray-700 dark:bg-gray-900"
/>
<span class="ml-2 text-xs text-gray-600 dark:text-gray-400">{{ field.label }}</span>
</div>
</div>
</div>
</div>
</dl>
</div>
<AttachmentSection
:attachments="task.attachments"
:task-id="task.id"
:can-manage="canManage"
class="px-4 pb-2"
/>
<CommentSection
:comments="task.comments"
:commentable-id="task.id"
commentable-type="App\Models\ServiceTask"
class="p-4"
/>
</div>
</template>

View File

@@ -0,0 +1,34 @@
<script setup>
import { computed } from 'vue';
const props = defineProps({
status: {
type: String,
required: true,
},
});
const statusConfig = {
draft: { label: 'Brouillon', class: 'bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-200' },
pending_rh_validation: { label: 'Attente RH', class: 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300' },
in_progress: { label: 'En cours', class: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-300 animate-pulse' },
waiting_services: { label: 'Attente Services', class: 'bg-orange-100 text-orange-800 dark:bg-orange-900/30 dark:text-orange-300' },
completed: { label: 'Terminé', class: 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300' },
cancelled: { label: 'Annulé', class: 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300' },
rejected: { label: 'Refusé', class: 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300' },
pending: { label: 'En attente', class: 'bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-200' },
waiting_validation: { label: 'Attente Validation', class: 'bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-300 animate-pulse' },
};
const config = computed(() => statusConfig[props.status] || { label: props.status, class: 'bg-gray-100 text-gray-800' });
</script>
<template>
<span :class="['inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium', config.class]">
<span v-if="status === 'in_progress' || status === 'waiting_validation'" class="flex h-1.5 w-1.5 mr-1.5">
<span class="animate-ping absolute inline-flex h-1.5 w-1.5 rounded-full bg-current opacity-75"></span>
<span class="relative inline-flex rounded-full h-1.5 w-1.5 bg-current"></span>
</span>
{{ config.label }}
</span>
</template>

View File

@@ -0,0 +1,40 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" fill="none">
<defs>
<linearGradient id="body-gradient" x1="0" y1="100" x2="100" y2="0" gradientUnits="userSpaceOnUse">
<stop offset="0%" stop-color="#FFEC00" /> <!-- Yellow -->
<stop offset="100%" stop-color="#009EE0" /> <!-- Blue -->
</linearGradient>
<linearGradient id="head-gradient" x1="20" y1="20" x2="80" y2="80" gradientUnits="userSpaceOnUse">
<stop offset="0%" stop-color="#E30613" /> <!-- Red -->
<stop offset="100%" stop-color="#B91C1C" /> <!-- Dark Red -->
</linearGradient>
<!-- Glow filter for modern feel -->
<filter id="glow" x="-20%" y="-20%" width="140%" height="140%">
<feGaussianBlur stdDeviation="2" result="blur" />
<feComposite in="SourceGraphic" in2="blur" operator="over" />
</filter>
</defs>
<!-- Modern Abstract Onboarding Icon -->
<!-- The "Receptacle" / Organization / Structure (U-shape) -->
<!-- Stylized as a smooth, continuous curve representing support/integration -->
<path
d="M 20 55 C 20 85, 80 85, 80 55"
stroke="url(#body-gradient)"
stroke-width="12"
stroke-linecap="round"
fill="none"
/>
<!-- The "User" / Agent (Floating Sphere) -->
<!-- Poised to enter or rising up, representing energy/new arrival -->
<circle cx="50" cy="35" r="14" fill="url(#head-gradient)" />
<!-- Dynamic "Connection" Dot (Success/Active status) -->
<circle cx="82" cy="35" r="6" fill="#009EE0" stroke="white" stroke-width="2" />
</svg>
</template>

View File

@@ -0,0 +1,34 @@
<script setup>
import { computed } from 'vue';
const emit = defineEmits(['update:checked']);
const props = defineProps({
checked: {
type: [Array, Boolean],
required: true,
},
value: {
default: null,
},
});
const proxyChecked = computed({
get() {
return props.checked;
},
set(val) {
emit('update:checked', val);
},
});
</script>
<template>
<input
type="checkbox"
:value="value"
v-model="proxyChecked"
class="rounded border-gray-300 text-indigo-600 shadow-sm focus:ring-indigo-500 dark:border-gray-700 dark:bg-gray-900 dark:focus:ring-indigo-600 dark:focus:ring-offset-gray-800"
/>
</template>

View File

@@ -0,0 +1,7 @@
<template>
<button
class="inline-flex items-center rounded-md border border-transparent bg-red-600 px-4 py-2 text-xs font-semibold uppercase tracking-widest text-white transition duration-150 ease-in-out hover:bg-red-500 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 active:bg-red-700 dark:focus:ring-offset-gray-800"
>
<slot />
</button>
</template>

View File

@@ -0,0 +1,37 @@
<script setup>
defineProps({
title: String,
value: [String, Number],
subValue: String,
icon: String,
color: {
type: String,
default: 'blue'
}
});
const colors = {
blue: 'text-blue-600 bg-blue-100 dark:bg-blue-900/30 dark:text-blue-400',
red: 'text-red-600 bg-red-100 dark:bg-red-900/30 dark:text-red-400',
green: 'text-green-600 bg-green-100 dark:bg-green-900/30 dark:text-green-400',
yellow: 'text-yellow-600 bg-yellow-100 dark:bg-yellow-900/30 dark:text-yellow-400',
};
</script>
<template>
<div class="p-6 bg-white rounded-lg shadow dark:bg-gray-800">
<div class="flex items-center">
<div :class="['p-3 rounded-full', colors[color]]">
<!-- Simple Circle as Placeholder for Icon -->
<div class="w-6 h-6 border-2 border-current rounded-full"></div>
</div>
<div class="ml-4 text-gray-500 dark:text-gray-400">
<p class="text-sm font-medium uppercase tracking-wider">{{ title }}</p>
<div class="flex items-baseline">
<p class="text-2xl font-semibold text-gray-900 dark:text-white">{{ value }}</p>
<p v-if="subValue" class="ml-2 text-xs font-semibold text-gray-500">{{ subValue }}</p>
</div>
</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,84 @@
<script setup>
import { computed, onMounted, onUnmounted, ref } from 'vue';
const props = defineProps({
align: {
type: String,
default: 'right',
},
width: {
type: String,
default: '48',
},
contentClasses: {
type: String,
default: 'py-1 bg-white dark:bg-gray-700',
},
});
const closeOnEscape = (e) => {
if (open.value && e.key === 'Escape') {
open.value = false;
}
};
onMounted(() => document.addEventListener('keydown', closeOnEscape));
onUnmounted(() => document.removeEventListener('keydown', closeOnEscape));
const widthClass = computed(() => {
return {
48: 'w-48',
}[props.width.toString()];
});
const alignmentClasses = computed(() => {
if (props.align === 'left') {
return 'ltr:origin-top-left rtl:origin-top-right start-0';
} else if (props.align === 'right') {
return 'ltr:origin-top-right rtl:origin-top-left end-0';
} else {
return 'origin-top';
}
});
const open = ref(false);
</script>
<template>
<div class="relative">
<div @click="open = !open">
<slot name="trigger" />
</div>
<!-- Full Screen Dropdown Overlay -->
<div
v-show="open"
class="fixed inset-0 z-40"
@click="open = false"
></div>
<Transition
enter-active-class="transition ease-out duration-200"
enter-from-class="opacity-0 scale-95"
enter-to-class="opacity-100 scale-100"
leave-active-class="transition ease-in duration-75"
leave-from-class="opacity-100 scale-100"
leave-to-class="opacity-0 scale-95"
>
<div
v-show="open"
class="absolute z-50 mt-2 rounded-md shadow-lg"
:class="[widthClass, alignmentClasses]"
style="display: none"
@click="open = false"
>
<div
class="rounded-md ring-1 ring-black ring-opacity-5"
:class="contentClasses"
>
<slot name="content" />
</div>
</div>
</Transition>
</div>
</template>

View File

@@ -0,0 +1,19 @@
<script setup>
import { Link } from '@inertiajs/vue3';
defineProps({
href: {
type: String,
required: true,
},
});
</script>
<template>
<Link
:href="href"
class="block w-full px-4 py-2 text-start text-sm leading-5 text-gray-700 transition duration-150 ease-in-out hover:bg-gray-100 focus:bg-gray-100 focus:outline-none dark:text-gray-300 dark:hover:bg-gray-800 dark:focus:bg-gray-800"
>
<slot />
</Link>
</template>

View File

@@ -0,0 +1,15 @@
<script setup>
defineProps({
message: {
type: String,
},
});
</script>
<template>
<div v-show="message">
<p class="text-sm text-red-600 dark:text-red-400">
{{ message }}
</p>
</div>
</template>

View File

@@ -0,0 +1,14 @@
<script setup>
defineProps({
value: {
type: String,
},
});
</script>
<template>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">
<span v-if="value">{{ value }}</span>
<span v-else><slot /></span>
</label>
</template>

View File

@@ -0,0 +1,123 @@
<script setup>
import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
const props = defineProps({
show: {
type: Boolean,
default: false,
},
maxWidth: {
type: String,
default: '2xl',
},
closeable: {
type: Boolean,
default: true,
},
});
const emit = defineEmits(['close']);
const dialog = ref();
const showSlot = ref(props.show);
watch(
() => props.show,
() => {
if (props.show) {
document.body.style.overflow = 'hidden';
showSlot.value = true;
dialog.value?.showModal();
} else {
document.body.style.overflow = '';
setTimeout(() => {
dialog.value?.close();
showSlot.value = false;
}, 200);
}
},
);
const close = () => {
if (props.closeable) {
emit('close');
}
};
const closeOnEscape = (e) => {
if (e.key === 'Escape') {
e.preventDefault();
if (props.show) {
close();
}
}
};
onMounted(() => document.addEventListener('keydown', closeOnEscape));
onUnmounted(() => {
document.removeEventListener('keydown', closeOnEscape);
document.body.style.overflow = '';
});
const maxWidthClass = computed(() => {
return {
sm: 'sm:max-w-sm',
md: 'sm:max-w-md',
lg: 'sm:max-w-lg',
xl: 'sm:max-w-xl',
'2xl': 'sm:max-w-2xl',
}[props.maxWidth];
});
</script>
<template>
<dialog
class="z-50 m-0 min-h-full min-w-full overflow-y-auto bg-transparent backdrop:bg-transparent"
ref="dialog"
>
<div
class="fixed inset-0 z-50 overflow-y-auto px-4 py-6 sm:px-0"
scroll-region
>
<Transition
enter-active-class="ease-out duration-300"
enter-from-class="opacity-0"
enter-to-class="opacity-100"
leave-active-class="ease-in duration-200"
leave-from-class="opacity-100"
leave-to-class="opacity-0"
>
<div
v-show="show"
class="fixed inset-0 transform transition-all"
@click="close"
>
<div
class="absolute inset-0 bg-gray-500 opacity-75 dark:bg-gray-900"
/>
</div>
</Transition>
<Transition
enter-active-class="ease-out duration-300"
enter-from-class="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
enter-to-class="opacity-100 translate-y-0 sm:scale-100"
leave-active-class="ease-in duration-200"
leave-from-class="opacity-100 translate-y-0 sm:scale-100"
leave-to-class="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<div
v-show="show"
class="mb-6 transform overflow-hidden rounded-lg bg-white shadow-xl transition-all sm:mx-auto sm:w-full dark:bg-gray-800"
:class="maxWidthClass"
>
<slot v-if="showSlot" />
</div>
</Transition>
</div>
</dialog>
</template>

View File

@@ -0,0 +1,26 @@
<script setup>
import { computed } from 'vue';
import { Link } from '@inertiajs/vue3';
const props = defineProps({
href: {
type: String,
required: true,
},
active: {
type: Boolean,
},
});
const classes = computed(() =>
props.active
? 'inline-flex items-center px-1 pt-1 border-b-2 border-indigo-400 dark:border-indigo-600 text-sm font-medium leading-5 text-gray-900 dark:text-gray-100 focus:outline-none focus:border-indigo-700 transition duration-150 ease-in-out'
: 'inline-flex items-center px-1 pt-1 border-b-2 border-transparent text-sm font-medium leading-5 text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:border-gray-300 dark:hover:border-gray-700 focus:outline-none focus:text-gray-700 dark:focus:text-gray-300 focus:border-gray-300 dark:focus:border-gray-700 transition duration-150 ease-in-out',
);
</script>
<template>
<Link :href="href" :class="classes">
<slot />
</Link>
</template>

View File

@@ -0,0 +1,7 @@
<template>
<button
class="inline-flex items-center rounded-md border border-transparent bg-gray-800 px-4 py-2 text-xs font-semibold uppercase tracking-widest text-white transition duration-150 ease-in-out hover:bg-gray-700 focus:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 active:bg-gray-900 dark:bg-gray-200 dark:text-gray-800 dark:hover:bg-white dark:focus:bg-white dark:focus:ring-offset-gray-800 dark:active:bg-gray-300"
>
<slot />
</button>
</template>

View File

@@ -0,0 +1,26 @@
<script setup>
import { computed } from 'vue';
import { Link } from '@inertiajs/vue3';
const props = defineProps({
href: {
type: String,
required: true,
},
active: {
type: Boolean,
},
});
const classes = computed(() =>
props.active
? 'block w-full ps-3 pe-4 py-2 border-l-4 border-indigo-400 dark:border-indigo-600 text-start text-base font-medium text-indigo-700 dark:text-indigo-300 bg-indigo-50 dark:bg-indigo-900/50 focus:outline-none focus:text-indigo-800 dark:focus:text-indigo-200 focus:bg-indigo-100 dark:focus:bg-indigo-900 focus:border-indigo-700 dark:focus:border-indigo-300 transition duration-150 ease-in-out'
: 'block w-full ps-3 pe-4 py-2 border-l-4 border-transparent text-start text-base font-medium text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700 hover:border-gray-300 dark:hover:border-gray-600 focus:outline-none focus:text-gray-800 dark:focus:text-gray-200 focus:bg-gray-50 dark:focus:bg-gray-700 focus:border-gray-300 dark:focus:border-gray-600 transition duration-150 ease-in-out',
);
</script>
<template>
<Link :href="href" :class="classes">
<slot />
</Link>
</template>

View File

@@ -0,0 +1,17 @@
<script setup>
defineProps({
type: {
type: String,
default: 'button',
},
});
</script>
<template>
<button
:type="type"
class="inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-xs font-semibold uppercase tracking-widest text-gray-700 shadow-sm transition duration-150 ease-in-out hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 disabled:opacity-25 dark:border-gray-500 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700 dark:focus:ring-offset-gray-800"
>
<slot />
</button>
</template>

View File

@@ -0,0 +1,26 @@
<script setup>
import { onMounted, ref } from 'vue';
const model = defineModel({
type: String,
required: true,
});
const input = ref(null);
onMounted(() => {
if (input.value.hasAttribute('autofocus')) {
input.value.focus();
}
});
defineExpose({ focus: () => input.value.focus() });
</script>
<template>
<input
class="rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-300 dark:focus:border-indigo-600 dark:focus:ring-indigo-600"
v-model="model"
ref="input"
/>
</template>

View File

@@ -0,0 +1,198 @@
<script setup>
import { ref } from 'vue';
import ApplicationLogo from '@/Components/ApplicationLogo.vue';
import Dropdown from '@/Components/Dropdown.vue';
import DropdownLink from '@/Components/DropdownLink.vue';
import NavLink from '@/Components/NavLink.vue';
import ResponsiveNavLink from '@/Components/ResponsiveNavLink.vue';
import { Link } from '@inertiajs/vue3';
const showingNavigationDropdown = ref(false);
</script>
<template>
<div>
<div class="min-h-screen bg-gray-100 dark:bg-gray-900">
<nav
class="border-b border-gray-100 bg-white dark:border-gray-700 dark:bg-gray-800"
>
<!-- Primary Navigation Menu -->
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<div class="flex h-16 justify-between">
<div class="flex">
<!-- Logo -->
<div class="flex shrink-0 items-center">
<Link :href="route('dashboard')">
<ApplicationLogo
class="block h-9 w-auto"
/>
</Link>
</div>
<!-- Navigation Links -->
<div
class="hidden space-x-8 sm:-my-px sm:ms-10 sm:flex"
>
<NavLink
:href="route('dashboard')"
:active="route().current('dashboard')"
>
Tableau de Bord
</NavLink>
</div>
</div>
<div class="hidden sm:ms-6 sm:flex sm:items-center">
<!-- Settings Dropdown -->
<div class="relative ms-3">
<Dropdown align="right" width="48">
<template #trigger>
<span class="inline-flex rounded-md">
<button
type="button"
class="inline-flex items-center rounded-md border border-transparent bg-white px-3 py-2 text-sm font-medium leading-4 text-gray-500 transition duration-150 ease-in-out hover:text-gray-700 focus:outline-none dark:bg-gray-800 dark:text-gray-400 dark:hover:text-gray-300"
>
{{ $page.props.auth.user.name }}
<svg
class="-me-0.5 ms-2 h-4 w-4"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fill-rule="evenodd"
d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"
clip-rule="evenodd"
/>
</svg>
</button>
</span>
</template>
<template #content>
<DropdownLink
:href="route('profile.edit')"
>
Profil
</DropdownLink>
<DropdownLink
:href="route('logout')"
method="post"
as="button"
>
Déconnexion
</DropdownLink>
</template>
</Dropdown>
</div>
</div>
<!-- Hamburger -->
<div class="-me-2 flex items-center sm:hidden">
<button
@click="
showingNavigationDropdown =
!showingNavigationDropdown
"
class="inline-flex items-center justify-center rounded-md p-2 text-gray-400 transition duration-150 ease-in-out hover:bg-gray-100 hover:text-gray-500 focus:bg-gray-100 focus:text-gray-500 focus:outline-none dark:text-gray-500 dark:hover:bg-gray-900 dark:hover:text-gray-400 dark:focus:bg-gray-900 dark:focus:text-gray-400"
>
<svg
class="h-6 w-6"
stroke="currentColor"
fill="none"
viewBox="0 0 24 24"
>
<path
:class="{
hidden: showingNavigationDropdown,
'inline-flex':
!showingNavigationDropdown,
}"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 6h16M4 12h16M4 18h16"
/>
<path
:class="{
hidden: !showingNavigationDropdown,
'inline-flex':
showingNavigationDropdown,
}"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
</div>
</div>
</div>
<!-- Responsive Navigation Menu -->
<div
:class="{
block: showingNavigationDropdown,
hidden: !showingNavigationDropdown,
}"
class="sm:hidden"
>
<div class="space-y-1 pb-3 pt-2">
<ResponsiveNavLink
:href="route('dashboard')"
:active="route().current('dashboard')"
>
Tableau de Bord
</ResponsiveNavLink>
</div>
<!-- Responsive Settings Options -->
<div
class="border-t border-gray-200 pb-1 pt-4 dark:border-gray-600"
>
<div class="px-4">
<div
class="text-base font-medium text-gray-800 dark:text-gray-200"
>
{{ $page.props.auth.user.name }}
</div>
<div class="text-sm font-medium text-gray-500">
{{ $page.props.auth.user.email }}
</div>
</div>
<div class="mt-3 space-y-1">
<ResponsiveNavLink :href="route('profile.edit')">
Profil
</ResponsiveNavLink>
<ResponsiveNavLink
:href="route('logout')"
method="post"
as="button"
>
Déconnexion
</ResponsiveNavLink>
</div>
</div>
</div>
</nav>
<!-- Page Heading -->
<header
class="bg-white shadow dark:bg-gray-800"
v-if="$slots.header"
>
<div class="mx-auto max-w-7xl px-4 py-6 sm:px-6 lg:px-8">
<slot name="header" />
</div>
</header>
<!-- Page Content -->
<main>
<slot />
</main>
</div>
</div>
</template>

View File

@@ -0,0 +1,22 @@
<script setup>
import ApplicationLogo from '@/Components/ApplicationLogo.vue';
import { Link } from '@inertiajs/vue3';
</script>
<template>
<div
class="flex min-h-screen flex-col items-center bg-gray-100 pt-6 sm:justify-center sm:pt-0 dark:bg-gray-900"
>
<div>
<Link href="/">
<ApplicationLogo class="h-20 w-20" />
</Link>
</div>
<div
class="mt-6 w-full overflow-hidden bg-white px-6 py-4 shadow-md sm:max-w-md sm:rounded-lg dark:bg-gray-800"
>
<slot />
</div>
</div>
</template>

View File

@@ -0,0 +1,55 @@
<script setup>
import GuestLayout from '@/Layouts/GuestLayout.vue';
import InputError from '@/Components/InputError.vue';
import InputLabel from '@/Components/InputLabel.vue';
import PrimaryButton from '@/Components/PrimaryButton.vue';
import TextInput from '@/Components/TextInput.vue';
import { Head, useForm } from '@inertiajs/vue3';
const form = useForm({
password: '',
});
const submit = () => {
form.post(route('password.confirm'), {
onFinish: () => form.reset(),
});
};
</script>
<template>
<GuestLayout>
<Head title="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>

View File

@@ -0,0 +1,68 @@
<script setup>
import GuestLayout from '@/Layouts/GuestLayout.vue';
import InputError from '@/Components/InputError.vue';
import InputLabel from '@/Components/InputLabel.vue';
import PrimaryButton from '@/Components/PrimaryButton.vue';
import TextInput from '@/Components/TextInput.vue';
import { Head, useForm } from '@inertiajs/vue3';
defineProps({
status: {
type: String,
},
});
const form = useForm({
email: '',
});
const submit = () => {
form.post(route('password.email'));
};
</script>
<template>
<GuestLayout>
<Head title="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>

View File

@@ -0,0 +1,100 @@
<script setup>
import Checkbox from '@/Components/Checkbox.vue';
import GuestLayout from '@/Layouts/GuestLayout.vue';
import InputError from '@/Components/InputError.vue';
import InputLabel from '@/Components/InputLabel.vue';
import PrimaryButton from '@/Components/PrimaryButton.vue';
import TextInput from '@/Components/TextInput.vue';
import { Head, Link, useForm } from '@inertiajs/vue3';
defineProps({
canResetPassword: {
type: Boolean,
},
status: {
type: String,
},
});
const form = useForm({
email: '',
password: '',
remember: false,
});
const submit = () => {
form.post(route('login'), {
onFinish: () => form.reset('password'),
});
};
</script>
<template>
<GuestLayout>
<Head title="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>

View File

@@ -0,0 +1,113 @@
<script setup>
import GuestLayout from '@/Layouts/GuestLayout.vue';
import InputError from '@/Components/InputError.vue';
import InputLabel from '@/Components/InputLabel.vue';
import PrimaryButton from '@/Components/PrimaryButton.vue';
import TextInput from '@/Components/TextInput.vue';
import { Head, Link, useForm } from '@inertiajs/vue3';
const form = useForm({
name: '',
email: '',
password: '',
password_confirmation: '',
});
const submit = () => {
form.post(route('register'), {
onFinish: () => form.reset('password', 'password_confirmation'),
});
};
</script>
<template>
<GuestLayout>
<Head title="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>

View File

@@ -0,0 +1,101 @@
<script setup>
import GuestLayout from '@/Layouts/GuestLayout.vue';
import InputError from '@/Components/InputError.vue';
import InputLabel from '@/Components/InputLabel.vue';
import PrimaryButton from '@/Components/PrimaryButton.vue';
import TextInput from '@/Components/TextInput.vue';
import { Head, useForm } from '@inertiajs/vue3';
const props = defineProps({
email: {
type: String,
required: true,
},
token: {
type: String,
required: true,
},
});
const form = useForm({
token: props.token,
email: props.email,
password: '',
password_confirmation: '',
});
const submit = () => {
form.post(route('password.store'), {
onFinish: () => form.reset('password', 'password_confirmation'),
});
};
</script>
<template>
<GuestLayout>
<Head title="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>

View File

@@ -0,0 +1,61 @@
<script setup>
import { computed } from 'vue';
import GuestLayout from '@/Layouts/GuestLayout.vue';
import PrimaryButton from '@/Components/PrimaryButton.vue';
import { Head, Link, useForm } from '@inertiajs/vue3';
const props = defineProps({
status: {
type: String,
},
});
const form = useForm({});
const submit = () => {
form.post(route('verification.send'));
};
const verificationLinkSent = computed(
() => props.status === 'verification-link-sent',
);
</script>
<template>
<GuestLayout>
<Head title="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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View File

@@ -0,0 +1,56 @@
<script setup>
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue';
import DeleteUserForm from './Partials/DeleteUserForm.vue';
import UpdatePasswordForm from './Partials/UpdatePasswordForm.vue';
import UpdateProfileInformationForm from './Partials/UpdateProfileInformationForm.vue';
import { Head } from '@inertiajs/vue3';
defineProps({
mustVerifyEmail: {
type: Boolean,
},
status: {
type: String,
},
});
</script>
<template>
<Head title="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>

View File

@@ -0,0 +1,108 @@
<script setup>
import DangerButton from '@/Components/DangerButton.vue';
import InputError from '@/Components/InputError.vue';
import InputLabel from '@/Components/InputLabel.vue';
import Modal from '@/Components/Modal.vue';
import SecondaryButton from '@/Components/SecondaryButton.vue';
import TextInput from '@/Components/TextInput.vue';
import { useForm } from '@inertiajs/vue3';
import { nextTick, ref } from 'vue';
const confirmingUserDeletion = ref(false);
const passwordInput = ref(null);
const form = useForm({
password: '',
});
const confirmUserDeletion = () => {
confirmingUserDeletion.value = true;
nextTick(() => passwordInput.value.focus());
};
const deleteUser = () => {
form.delete(route('profile.destroy'), {
preserveScroll: true,
onSuccess: () => closeModal(),
onError: () => passwordInput.value.focus(),
onFinish: () => form.reset(),
});
};
const closeModal = () => {
confirmingUserDeletion.value = false;
form.clearErrors();
form.reset();
};
</script>
<template>
<section class="space-y-6">
<header>
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">
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>

View File

@@ -0,0 +1,122 @@
<script setup>
import InputError from '@/Components/InputError.vue';
import InputLabel from '@/Components/InputLabel.vue';
import PrimaryButton from '@/Components/PrimaryButton.vue';
import TextInput from '@/Components/TextInput.vue';
import { useForm } from '@inertiajs/vue3';
import { ref } from 'vue';
const passwordInput = ref(null);
const currentPasswordInput = ref(null);
const form = useForm({
current_password: '',
password: '',
password_confirmation: '',
});
const updatePassword = () => {
form.put(route('password.update'), {
preserveScroll: true,
onSuccess: () => form.reset(),
onError: () => {
if (form.errors.password) {
form.reset('password', 'password_confirmation');
passwordInput.value.focus();
}
if (form.errors.current_password) {
form.reset('current_password');
currentPasswordInput.value.focus();
}
},
});
};
</script>
<template>
<section>
<header>
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">
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>

View File

@@ -0,0 +1,112 @@
<script setup>
import InputError from '@/Components/InputError.vue';
import InputLabel from '@/Components/InputLabel.vue';
import PrimaryButton from '@/Components/PrimaryButton.vue';
import TextInput from '@/Components/TextInput.vue';
import { Link, useForm, usePage } from '@inertiajs/vue3';
defineProps({
mustVerifyEmail: {
type: Boolean,
},
status: {
type: String,
},
});
const user = usePage().props.auth.user;
const form = useForm({
name: user.name,
email: user.email,
});
</script>
<template>
<section>
<header>
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">
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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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 lAccueil 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">
&copy; {{ 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>

27
resources/js/app.js Normal file
View File

@@ -0,0 +1,27 @@
import '../css/app.css';
import './bootstrap';
import { createInertiaApp } from '@inertiajs/vue3';
import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers';
import { createApp, h } from 'vue';
import { ZiggyVue } from '../../vendor/tightenco/ziggy';
const appName = import.meta.env.VITE_APP_NAME || 'Laravel';
createInertiaApp({
title: (title) => `${title} - ${appName}`,
resolve: (name) =>
resolvePageComponent(
`./Pages/${name}.vue`,
import.meta.glob('./Pages/**/*.vue'),
),
setup({ el, App, props, plugin }) {
return createApp({ render: () => h(App, props) })
.use(plugin)
.use(ZiggyVue)
.mount(el);
},
progress: {
color: '#4B5563',
},
});

4
resources/js/bootstrap.js vendored Normal file
View File

@@ -0,0 +1,4 @@
import axios from 'axios';
window.axios = axios;
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';