feat: Initialize core application structure including authentication, role-based dashboards, service task management, and integration workflows.
This commit is contained in:
3
resources/css/app.css
Normal file
3
resources/css/app.css
Normal file
@@ -0,0 +1,3 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
72
resources/js/Components/App/ActivityTimeline.vue
Normal file
72
resources/js/Components/App/ActivityTimeline.vue
Normal 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>
|
||||
92
resources/js/Components/App/AttachmentSection.vue
Normal file
92
resources/js/Components/App/AttachmentSection.vue
Normal 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>
|
||||
76
resources/js/Components/App/CommentSection.vue
Normal file
76
resources/js/Components/App/CommentSection.vue
Normal 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>
|
||||
188
resources/js/Components/App/ServiceTaskCard.vue
Normal file
188
resources/js/Components/App/ServiceTaskCard.vue
Normal 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>
|
||||
34
resources/js/Components/App/StatusBadge.vue
Normal file
34
resources/js/Components/App/StatusBadge.vue
Normal 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>
|
||||
40
resources/js/Components/ApplicationLogo.vue
Normal file
40
resources/js/Components/ApplicationLogo.vue
Normal 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>
|
||||
34
resources/js/Components/Checkbox.vue
Normal file
34
resources/js/Components/Checkbox.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
|
||||
const emit = defineEmits(['update:checked']);
|
||||
|
||||
const props = defineProps({
|
||||
checked: {
|
||||
type: [Array, Boolean],
|
||||
required: true,
|
||||
},
|
||||
value: {
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
const proxyChecked = computed({
|
||||
get() {
|
||||
return props.checked;
|
||||
},
|
||||
|
||||
set(val) {
|
||||
emit('update:checked', val);
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<input
|
||||
type="checkbox"
|
||||
:value="value"
|
||||
v-model="proxyChecked"
|
||||
class="rounded border-gray-300 text-indigo-600 shadow-sm focus:ring-indigo-500 dark:border-gray-700 dark:bg-gray-900 dark:focus:ring-indigo-600 dark:focus:ring-offset-gray-800"
|
||||
/>
|
||||
</template>
|
||||
7
resources/js/Components/DangerButton.vue
Normal file
7
resources/js/Components/DangerButton.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<button
|
||||
class="inline-flex items-center rounded-md border border-transparent bg-red-600 px-4 py-2 text-xs font-semibold uppercase tracking-widest text-white transition duration-150 ease-in-out hover:bg-red-500 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 active:bg-red-700 dark:focus:ring-offset-gray-800"
|
||||
>
|
||||
<slot />
|
||||
</button>
|
||||
</template>
|
||||
37
resources/js/Components/Dashboard/StatCard.vue
Normal file
37
resources/js/Components/Dashboard/StatCard.vue
Normal 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>
|
||||
84
resources/js/Components/Dropdown.vue
Normal file
84
resources/js/Components/Dropdown.vue
Normal file
@@ -0,0 +1,84 @@
|
||||
<script setup>
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
align: {
|
||||
type: String,
|
||||
default: 'right',
|
||||
},
|
||||
width: {
|
||||
type: String,
|
||||
default: '48',
|
||||
},
|
||||
contentClasses: {
|
||||
type: String,
|
||||
default: 'py-1 bg-white dark:bg-gray-700',
|
||||
},
|
||||
});
|
||||
|
||||
const closeOnEscape = (e) => {
|
||||
if (open.value && e.key === 'Escape') {
|
||||
open.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => document.addEventListener('keydown', closeOnEscape));
|
||||
onUnmounted(() => document.removeEventListener('keydown', closeOnEscape));
|
||||
|
||||
const widthClass = computed(() => {
|
||||
return {
|
||||
48: 'w-48',
|
||||
}[props.width.toString()];
|
||||
});
|
||||
|
||||
const alignmentClasses = computed(() => {
|
||||
if (props.align === 'left') {
|
||||
return 'ltr:origin-top-left rtl:origin-top-right start-0';
|
||||
} else if (props.align === 'right') {
|
||||
return 'ltr:origin-top-right rtl:origin-top-left end-0';
|
||||
} else {
|
||||
return 'origin-top';
|
||||
}
|
||||
});
|
||||
|
||||
const open = ref(false);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="relative">
|
||||
<div @click="open = !open">
|
||||
<slot name="trigger" />
|
||||
</div>
|
||||
|
||||
<!-- Full Screen Dropdown Overlay -->
|
||||
<div
|
||||
v-show="open"
|
||||
class="fixed inset-0 z-40"
|
||||
@click="open = false"
|
||||
></div>
|
||||
|
||||
<Transition
|
||||
enter-active-class="transition ease-out duration-200"
|
||||
enter-from-class="opacity-0 scale-95"
|
||||
enter-to-class="opacity-100 scale-100"
|
||||
leave-active-class="transition ease-in duration-75"
|
||||
leave-from-class="opacity-100 scale-100"
|
||||
leave-to-class="opacity-0 scale-95"
|
||||
>
|
||||
<div
|
||||
v-show="open"
|
||||
class="absolute z-50 mt-2 rounded-md shadow-lg"
|
||||
:class="[widthClass, alignmentClasses]"
|
||||
style="display: none"
|
||||
@click="open = false"
|
||||
>
|
||||
<div
|
||||
class="rounded-md ring-1 ring-black ring-opacity-5"
|
||||
:class="contentClasses"
|
||||
>
|
||||
<slot name="content" />
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</template>
|
||||
19
resources/js/Components/DropdownLink.vue
Normal file
19
resources/js/Components/DropdownLink.vue
Normal file
@@ -0,0 +1,19 @@
|
||||
<script setup>
|
||||
import { Link } from '@inertiajs/vue3';
|
||||
|
||||
defineProps({
|
||||
href: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Link
|
||||
:href="href"
|
||||
class="block w-full px-4 py-2 text-start text-sm leading-5 text-gray-700 transition duration-150 ease-in-out hover:bg-gray-100 focus:bg-gray-100 focus:outline-none dark:text-gray-300 dark:hover:bg-gray-800 dark:focus:bg-gray-800"
|
||||
>
|
||||
<slot />
|
||||
</Link>
|
||||
</template>
|
||||
15
resources/js/Components/InputError.vue
Normal file
15
resources/js/Components/InputError.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<script setup>
|
||||
defineProps({
|
||||
message: {
|
||||
type: String,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-show="message">
|
||||
<p class="text-sm text-red-600 dark:text-red-400">
|
||||
{{ message }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
14
resources/js/Components/InputLabel.vue
Normal file
14
resources/js/Components/InputLabel.vue
Normal file
@@ -0,0 +1,14 @@
|
||||
<script setup>
|
||||
defineProps({
|
||||
value: {
|
||||
type: String,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
<span v-if="value">{{ value }}</span>
|
||||
<span v-else><slot /></span>
|
||||
</label>
|
||||
</template>
|
||||
123
resources/js/Components/Modal.vue
Normal file
123
resources/js/Components/Modal.vue
Normal file
@@ -0,0 +1,123 @@
|
||||
<script setup>
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
maxWidth: {
|
||||
type: String,
|
||||
default: '2xl',
|
||||
},
|
||||
closeable: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['close']);
|
||||
const dialog = ref();
|
||||
const showSlot = ref(props.show);
|
||||
|
||||
watch(
|
||||
() => props.show,
|
||||
() => {
|
||||
if (props.show) {
|
||||
document.body.style.overflow = 'hidden';
|
||||
showSlot.value = true;
|
||||
|
||||
dialog.value?.showModal();
|
||||
} else {
|
||||
document.body.style.overflow = '';
|
||||
|
||||
setTimeout(() => {
|
||||
dialog.value?.close();
|
||||
showSlot.value = false;
|
||||
}, 200);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const close = () => {
|
||||
if (props.closeable) {
|
||||
emit('close');
|
||||
}
|
||||
};
|
||||
|
||||
const closeOnEscape = (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
|
||||
if (props.show) {
|
||||
close();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => document.addEventListener('keydown', closeOnEscape));
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('keydown', closeOnEscape);
|
||||
|
||||
document.body.style.overflow = '';
|
||||
});
|
||||
|
||||
const maxWidthClass = computed(() => {
|
||||
return {
|
||||
sm: 'sm:max-w-sm',
|
||||
md: 'sm:max-w-md',
|
||||
lg: 'sm:max-w-lg',
|
||||
xl: 'sm:max-w-xl',
|
||||
'2xl': 'sm:max-w-2xl',
|
||||
}[props.maxWidth];
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<dialog
|
||||
class="z-50 m-0 min-h-full min-w-full overflow-y-auto bg-transparent backdrop:bg-transparent"
|
||||
ref="dialog"
|
||||
>
|
||||
<div
|
||||
class="fixed inset-0 z-50 overflow-y-auto px-4 py-6 sm:px-0"
|
||||
scroll-region
|
||||
>
|
||||
<Transition
|
||||
enter-active-class="ease-out duration-300"
|
||||
enter-from-class="opacity-0"
|
||||
enter-to-class="opacity-100"
|
||||
leave-active-class="ease-in duration-200"
|
||||
leave-from-class="opacity-100"
|
||||
leave-to-class="opacity-0"
|
||||
>
|
||||
<div
|
||||
v-show="show"
|
||||
class="fixed inset-0 transform transition-all"
|
||||
@click="close"
|
||||
>
|
||||
<div
|
||||
class="absolute inset-0 bg-gray-500 opacity-75 dark:bg-gray-900"
|
||||
/>
|
||||
</div>
|
||||
</Transition>
|
||||
|
||||
<Transition
|
||||
enter-active-class="ease-out duration-300"
|
||||
enter-from-class="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
enter-to-class="opacity-100 translate-y-0 sm:scale-100"
|
||||
leave-active-class="ease-in duration-200"
|
||||
leave-from-class="opacity-100 translate-y-0 sm:scale-100"
|
||||
leave-to-class="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||
>
|
||||
<div
|
||||
v-show="show"
|
||||
class="mb-6 transform overflow-hidden rounded-lg bg-white shadow-xl transition-all sm:mx-auto sm:w-full dark:bg-gray-800"
|
||||
:class="maxWidthClass"
|
||||
>
|
||||
<slot v-if="showSlot" />
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
</dialog>
|
||||
</template>
|
||||
26
resources/js/Components/NavLink.vue
Normal file
26
resources/js/Components/NavLink.vue
Normal file
@@ -0,0 +1,26 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { Link } from '@inertiajs/vue3';
|
||||
|
||||
const props = defineProps({
|
||||
href: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
active: {
|
||||
type: Boolean,
|
||||
},
|
||||
});
|
||||
|
||||
const classes = computed(() =>
|
||||
props.active
|
||||
? 'inline-flex items-center px-1 pt-1 border-b-2 border-indigo-400 dark:border-indigo-600 text-sm font-medium leading-5 text-gray-900 dark:text-gray-100 focus:outline-none focus:border-indigo-700 transition duration-150 ease-in-out'
|
||||
: 'inline-flex items-center px-1 pt-1 border-b-2 border-transparent text-sm font-medium leading-5 text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 hover:border-gray-300 dark:hover:border-gray-700 focus:outline-none focus:text-gray-700 dark:focus:text-gray-300 focus:border-gray-300 dark:focus:border-gray-700 transition duration-150 ease-in-out',
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Link :href="href" :class="classes">
|
||||
<slot />
|
||||
</Link>
|
||||
</template>
|
||||
7
resources/js/Components/PrimaryButton.vue
Normal file
7
resources/js/Components/PrimaryButton.vue
Normal file
@@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<button
|
||||
class="inline-flex items-center rounded-md border border-transparent bg-gray-800 px-4 py-2 text-xs font-semibold uppercase tracking-widest text-white transition duration-150 ease-in-out hover:bg-gray-700 focus:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 active:bg-gray-900 dark:bg-gray-200 dark:text-gray-800 dark:hover:bg-white dark:focus:bg-white dark:focus:ring-offset-gray-800 dark:active:bg-gray-300"
|
||||
>
|
||||
<slot />
|
||||
</button>
|
||||
</template>
|
||||
26
resources/js/Components/ResponsiveNavLink.vue
Normal file
26
resources/js/Components/ResponsiveNavLink.vue
Normal file
@@ -0,0 +1,26 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { Link } from '@inertiajs/vue3';
|
||||
|
||||
const props = defineProps({
|
||||
href: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
active: {
|
||||
type: Boolean,
|
||||
},
|
||||
});
|
||||
|
||||
const classes = computed(() =>
|
||||
props.active
|
||||
? 'block w-full ps-3 pe-4 py-2 border-l-4 border-indigo-400 dark:border-indigo-600 text-start text-base font-medium text-indigo-700 dark:text-indigo-300 bg-indigo-50 dark:bg-indigo-900/50 focus:outline-none focus:text-indigo-800 dark:focus:text-indigo-200 focus:bg-indigo-100 dark:focus:bg-indigo-900 focus:border-indigo-700 dark:focus:border-indigo-300 transition duration-150 ease-in-out'
|
||||
: 'block w-full ps-3 pe-4 py-2 border-l-4 border-transparent text-start text-base font-medium text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-700 hover:border-gray-300 dark:hover:border-gray-600 focus:outline-none focus:text-gray-800 dark:focus:text-gray-200 focus:bg-gray-50 dark:focus:bg-gray-700 focus:border-gray-300 dark:focus:border-gray-600 transition duration-150 ease-in-out',
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Link :href="href" :class="classes">
|
||||
<slot />
|
||||
</Link>
|
||||
</template>
|
||||
17
resources/js/Components/SecondaryButton.vue
Normal file
17
resources/js/Components/SecondaryButton.vue
Normal file
@@ -0,0 +1,17 @@
|
||||
<script setup>
|
||||
defineProps({
|
||||
type: {
|
||||
type: String,
|
||||
default: 'button',
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
:type="type"
|
||||
class="inline-flex items-center rounded-md border border-gray-300 bg-white px-4 py-2 text-xs font-semibold uppercase tracking-widest text-gray-700 shadow-sm transition duration-150 ease-in-out hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 disabled:opacity-25 dark:border-gray-500 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700 dark:focus:ring-offset-gray-800"
|
||||
>
|
||||
<slot />
|
||||
</button>
|
||||
</template>
|
||||
26
resources/js/Components/TextInput.vue
Normal file
26
resources/js/Components/TextInput.vue
Normal file
@@ -0,0 +1,26 @@
|
||||
<script setup>
|
||||
import { onMounted, ref } from 'vue';
|
||||
|
||||
const model = defineModel({
|
||||
type: String,
|
||||
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>
|
||||
198
resources/js/Layouts/AuthenticatedLayout.vue
Normal file
198
resources/js/Layouts/AuthenticatedLayout.vue
Normal 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>
|
||||
22
resources/js/Layouts/GuestLayout.vue
Normal file
22
resources/js/Layouts/GuestLayout.vue
Normal file
@@ -0,0 +1,22 @@
|
||||
<script setup>
|
||||
import ApplicationLogo from '@/Components/ApplicationLogo.vue';
|
||||
import { Link } from '@inertiajs/vue3';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex min-h-screen flex-col items-center bg-gray-100 pt-6 sm:justify-center sm:pt-0 dark:bg-gray-900"
|
||||
>
|
||||
<div>
|
||||
<Link href="/">
|
||||
<ApplicationLogo class="h-20 w-20" />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="mt-6 w-full overflow-hidden bg-white px-6 py-4 shadow-md sm:max-w-md sm:rounded-lg dark:bg-gray-800"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
55
resources/js/Pages/Auth/ConfirmPassword.vue
Normal file
55
resources/js/Pages/Auth/ConfirmPassword.vue
Normal file
@@ -0,0 +1,55 @@
|
||||
<script setup>
|
||||
import GuestLayout from '@/Layouts/GuestLayout.vue';
|
||||
import InputError from '@/Components/InputError.vue';
|
||||
import InputLabel from '@/Components/InputLabel.vue';
|
||||
import PrimaryButton from '@/Components/PrimaryButton.vue';
|
||||
import TextInput from '@/Components/TextInput.vue';
|
||||
import { Head, useForm } from '@inertiajs/vue3';
|
||||
|
||||
const form = useForm({
|
||||
password: '',
|
||||
});
|
||||
|
||||
const submit = () => {
|
||||
form.post(route('password.confirm'), {
|
||||
onFinish: () => form.reset(),
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<GuestLayout>
|
||||
<Head title="Confirmer le mot de passe" />
|
||||
|
||||
<div class="mb-4 text-sm text-gray-600 dark:text-gray-400">
|
||||
C'est une zone sécurisée de l'application. Veuillez confirmer votre
|
||||
mot de passe avant de continuer.
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="submit">
|
||||
<div>
|
||||
<InputLabel for="password" value="Mot de passe" />
|
||||
<TextInput
|
||||
id="password"
|
||||
type="password"
|
||||
class="mt-1 block w-full"
|
||||
v-model="form.password"
|
||||
required
|
||||
autocomplete="current-password"
|
||||
autofocus
|
||||
/>
|
||||
<InputError class="mt-2" :message="form.errors.password" />
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex justify-end">
|
||||
<PrimaryButton
|
||||
class="ms-4"
|
||||
:class="{ 'opacity-25': form.processing }"
|
||||
:disabled="form.processing"
|
||||
>
|
||||
Confirmer
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
</form>
|
||||
</GuestLayout>
|
||||
</template>
|
||||
68
resources/js/Pages/Auth/ForgotPassword.vue
Normal file
68
resources/js/Pages/Auth/ForgotPassword.vue
Normal file
@@ -0,0 +1,68 @@
|
||||
<script setup>
|
||||
import GuestLayout from '@/Layouts/GuestLayout.vue';
|
||||
import InputError from '@/Components/InputError.vue';
|
||||
import InputLabel from '@/Components/InputLabel.vue';
|
||||
import PrimaryButton from '@/Components/PrimaryButton.vue';
|
||||
import TextInput from '@/Components/TextInput.vue';
|
||||
import { Head, useForm } from '@inertiajs/vue3';
|
||||
|
||||
defineProps({
|
||||
status: {
|
||||
type: String,
|
||||
},
|
||||
});
|
||||
|
||||
const form = useForm({
|
||||
email: '',
|
||||
});
|
||||
|
||||
const submit = () => {
|
||||
form.post(route('password.email'));
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<GuestLayout>
|
||||
<Head title="Mot de passe oublié" />
|
||||
|
||||
<div class="mb-4 text-sm text-gray-600 dark:text-gray-400">
|
||||
Mot de passe oublié ? Pas de problème. Indiquez-nous simplement votre adresse email
|
||||
et nous vous enverrons un lien de réinitialisation du mot de passe qui vous permettra
|
||||
d'en choisir un nouveau.
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="status"
|
||||
class="mb-4 text-sm font-medium text-green-600 dark:text-green-400"
|
||||
>
|
||||
{{ status }}
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="submit">
|
||||
<div>
|
||||
<InputLabel for="email" value="Email" />
|
||||
|
||||
<TextInput
|
||||
id="email"
|
||||
type="email"
|
||||
class="mt-1 block w-full"
|
||||
v-model="form.email"
|
||||
required
|
||||
autofocus
|
||||
autocomplete="username"
|
||||
/>
|
||||
|
||||
<InputError class="mt-2" :message="form.errors.email" />
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex items-center justify-end">
|
||||
<PrimaryButton
|
||||
:class="{ 'opacity-25': form.processing }"
|
||||
:disabled="form.processing"
|
||||
>
|
||||
Envoyer le lien de réinitialisation
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
</form>
|
||||
</GuestLayout>
|
||||
</template>
|
||||
100
resources/js/Pages/Auth/Login.vue
Normal file
100
resources/js/Pages/Auth/Login.vue
Normal file
@@ -0,0 +1,100 @@
|
||||
<script setup>
|
||||
import Checkbox from '@/Components/Checkbox.vue';
|
||||
import GuestLayout from '@/Layouts/GuestLayout.vue';
|
||||
import InputError from '@/Components/InputError.vue';
|
||||
import InputLabel from '@/Components/InputLabel.vue';
|
||||
import PrimaryButton from '@/Components/PrimaryButton.vue';
|
||||
import TextInput from '@/Components/TextInput.vue';
|
||||
import { Head, Link, useForm } from '@inertiajs/vue3';
|
||||
|
||||
defineProps({
|
||||
canResetPassword: {
|
||||
type: Boolean,
|
||||
},
|
||||
status: {
|
||||
type: String,
|
||||
},
|
||||
});
|
||||
|
||||
const form = useForm({
|
||||
email: '',
|
||||
password: '',
|
||||
remember: false,
|
||||
});
|
||||
|
||||
const submit = () => {
|
||||
form.post(route('login'), {
|
||||
onFinish: () => form.reset('password'),
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<GuestLayout>
|
||||
<Head title="Connexion" />
|
||||
|
||||
<div v-if="status" class="mb-4 text-sm font-medium text-green-600">
|
||||
{{ status }}
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="submit">
|
||||
<div>
|
||||
<InputLabel for="email" value="Email" />
|
||||
|
||||
<TextInput
|
||||
id="email"
|
||||
type="email"
|
||||
class="mt-1 block w-full"
|
||||
v-model="form.email"
|
||||
required
|
||||
autofocus
|
||||
autocomplete="username"
|
||||
/>
|
||||
|
||||
<InputError class="mt-2" :message="form.errors.email" />
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<InputLabel for="password" value="Mot de passe" />
|
||||
|
||||
<TextInput
|
||||
id="password"
|
||||
type="password"
|
||||
class="mt-1 block w-full"
|
||||
v-model="form.password"
|
||||
required
|
||||
autocomplete="current-password"
|
||||
/>
|
||||
|
||||
<InputError class="mt-2" :message="form.errors.password" />
|
||||
</div>
|
||||
|
||||
<div class="mt-4 block">
|
||||
<label class="flex items-center">
|
||||
<Checkbox name="remember" v-model:checked="form.remember" />
|
||||
<span class="ms-2 text-sm text-gray-600 dark:text-gray-400"
|
||||
>Se souvenir de moi</span
|
||||
>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex items-center justify-end">
|
||||
<Link
|
||||
v-if="canResetPassword"
|
||||
:href="route('password.request')"
|
||||
class="rounded-md text-sm text-gray-600 underline hover:text-gray-900 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:text-gray-400 dark:hover:text-gray-100 dark:focus:ring-offset-gray-800"
|
||||
>
|
||||
Mot de passe oublié ?
|
||||
</Link>
|
||||
|
||||
<PrimaryButton
|
||||
class="ms-4"
|
||||
:class="{ 'opacity-25': form.processing }"
|
||||
:disabled="form.processing"
|
||||
>
|
||||
Se connecter
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
</form>
|
||||
</GuestLayout>
|
||||
</template>
|
||||
113
resources/js/Pages/Auth/Register.vue
Normal file
113
resources/js/Pages/Auth/Register.vue
Normal file
@@ -0,0 +1,113 @@
|
||||
<script setup>
|
||||
import GuestLayout from '@/Layouts/GuestLayout.vue';
|
||||
import InputError from '@/Components/InputError.vue';
|
||||
import InputLabel from '@/Components/InputLabel.vue';
|
||||
import PrimaryButton from '@/Components/PrimaryButton.vue';
|
||||
import TextInput from '@/Components/TextInput.vue';
|
||||
import { Head, Link, useForm } from '@inertiajs/vue3';
|
||||
|
||||
const form = useForm({
|
||||
name: '',
|
||||
email: '',
|
||||
password: '',
|
||||
password_confirmation: '',
|
||||
});
|
||||
|
||||
const submit = () => {
|
||||
form.post(route('register'), {
|
||||
onFinish: () => form.reset('password', 'password_confirmation'),
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<GuestLayout>
|
||||
<Head title="Inscription" />
|
||||
|
||||
<form @submit.prevent="submit">
|
||||
<div>
|
||||
<InputLabel for="name" value="Nom" />
|
||||
|
||||
<TextInput
|
||||
id="name"
|
||||
type="text"
|
||||
class="mt-1 block w-full"
|
||||
v-model="form.name"
|
||||
required
|
||||
autofocus
|
||||
autocomplete="name"
|
||||
/>
|
||||
|
||||
<InputError class="mt-2" :message="form.errors.name" />
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<InputLabel for="email" value="Email" />
|
||||
|
||||
<TextInput
|
||||
id="email"
|
||||
type="email"
|
||||
class="mt-1 block w-full"
|
||||
v-model="form.email"
|
||||
required
|
||||
autocomplete="username"
|
||||
/>
|
||||
|
||||
<InputError class="mt-2" :message="form.errors.email" />
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<InputLabel for="password" value="Mot de passe" />
|
||||
|
||||
<TextInput
|
||||
id="password"
|
||||
type="password"
|
||||
class="mt-1 block w-full"
|
||||
v-model="form.password"
|
||||
required
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
|
||||
<InputError class="mt-2" :message="form.errors.password" />
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<InputLabel
|
||||
for="password_confirmation"
|
||||
value="Confirmer le mot de passe"
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
id="password_confirmation"
|
||||
type="password"
|
||||
class="mt-1 block w-full"
|
||||
v-model="form.password_confirmation"
|
||||
required
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
|
||||
<InputError
|
||||
class="mt-2"
|
||||
:message="form.errors.password_confirmation"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex items-center justify-end">
|
||||
<Link
|
||||
:href="route('login')"
|
||||
class="rounded-md text-sm text-gray-600 underline hover:text-gray-900 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:text-gray-400 dark:hover:text-gray-100 dark:focus:ring-offset-gray-800"
|
||||
>
|
||||
Déjà inscrit ?
|
||||
</Link>
|
||||
|
||||
<PrimaryButton
|
||||
class="ms-4"
|
||||
:class="{ 'opacity-25': form.processing }"
|
||||
:disabled="form.processing"
|
||||
>
|
||||
S'inscrire
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
</form>
|
||||
</GuestLayout>
|
||||
</template>
|
||||
101
resources/js/Pages/Auth/ResetPassword.vue
Normal file
101
resources/js/Pages/Auth/ResetPassword.vue
Normal file
@@ -0,0 +1,101 @@
|
||||
<script setup>
|
||||
import GuestLayout from '@/Layouts/GuestLayout.vue';
|
||||
import InputError from '@/Components/InputError.vue';
|
||||
import InputLabel from '@/Components/InputLabel.vue';
|
||||
import PrimaryButton from '@/Components/PrimaryButton.vue';
|
||||
import TextInput from '@/Components/TextInput.vue';
|
||||
import { Head, useForm } from '@inertiajs/vue3';
|
||||
|
||||
const props = defineProps({
|
||||
email: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
token: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const form = useForm({
|
||||
token: props.token,
|
||||
email: props.email,
|
||||
password: '',
|
||||
password_confirmation: '',
|
||||
});
|
||||
|
||||
const submit = () => {
|
||||
form.post(route('password.store'), {
|
||||
onFinish: () => form.reset('password', 'password_confirmation'),
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<GuestLayout>
|
||||
<Head title="Réinitialiser le mot de passe" />
|
||||
|
||||
<form @submit.prevent="submit">
|
||||
<div>
|
||||
<InputLabel for="email" value="Email" />
|
||||
|
||||
<TextInput
|
||||
id="email"
|
||||
type="email"
|
||||
class="mt-1 block w-full"
|
||||
v-model="form.email"
|
||||
required
|
||||
autofocus
|
||||
autocomplete="username"
|
||||
/>
|
||||
|
||||
<InputError class="mt-2" :message="form.errors.email" />
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<InputLabel for="password" value="Mot de passe" />
|
||||
|
||||
<TextInput
|
||||
id="password"
|
||||
type="password"
|
||||
class="mt-1 block w-full"
|
||||
v-model="form.password"
|
||||
required
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
|
||||
<InputError class="mt-2" :message="form.errors.password" />
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<InputLabel
|
||||
for="password_confirmation"
|
||||
value="Confirmer le mot de passe"
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
id="password_confirmation"
|
||||
type="password"
|
||||
class="mt-1 block w-full"
|
||||
v-model="form.password_confirmation"
|
||||
required
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
|
||||
<InputError
|
||||
class="mt-2"
|
||||
:message="form.errors.password_confirmation"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex items-center justify-end">
|
||||
<PrimaryButton
|
||||
:class="{ 'opacity-25': form.processing }"
|
||||
:disabled="form.processing"
|
||||
>
|
||||
Réinitialiser le mot de passe
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
</form>
|
||||
</GuestLayout>
|
||||
</template>
|
||||
61
resources/js/Pages/Auth/VerifyEmail.vue
Normal file
61
resources/js/Pages/Auth/VerifyEmail.vue
Normal file
@@ -0,0 +1,61 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import GuestLayout from '@/Layouts/GuestLayout.vue';
|
||||
import PrimaryButton from '@/Components/PrimaryButton.vue';
|
||||
import { Head, Link, useForm } from '@inertiajs/vue3';
|
||||
|
||||
const props = defineProps({
|
||||
status: {
|
||||
type: String,
|
||||
},
|
||||
});
|
||||
|
||||
const form = useForm({});
|
||||
|
||||
const submit = () => {
|
||||
form.post(route('verification.send'));
|
||||
};
|
||||
|
||||
const verificationLinkSent = computed(
|
||||
() => props.status === 'verification-link-sent',
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<GuestLayout>
|
||||
<Head title="Vérification de l'email" />
|
||||
|
||||
<div class="mb-4 text-sm text-gray-600 dark:text-gray-400">
|
||||
Merci de vous être inscrit ! Avant de commencer, pourriez-vous vérifier votre
|
||||
adresse email en cliquant sur le lien que nous venons de vous envoyer ? Si vous
|
||||
n'avez pas reçu l'email, nous vous en enverrons un autre avec plaisir.
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="mb-4 text-sm font-medium text-green-600 dark:text-green-400"
|
||||
v-if="verificationLinkSent"
|
||||
>
|
||||
Un nouveau lien de vérification a été envoyé à l'adresse email que vous
|
||||
avez fournie lors de l'inscription.
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="submit">
|
||||
<div class="mt-4 flex items-center justify-between">
|
||||
<PrimaryButton
|
||||
:class="{ 'opacity-25': form.processing }"
|
||||
:disabled="form.processing"
|
||||
>
|
||||
Renvoyer l'email de vérification
|
||||
</PrimaryButton>
|
||||
|
||||
<Link
|
||||
:href="route('logout')"
|
||||
method="post"
|
||||
as="button"
|
||||
class="rounded-md text-sm text-gray-600 underline hover:text-gray-900 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:text-gray-400 dark:hover:text-gray-100 dark:focus:ring-offset-gray-800"
|
||||
>Se déconnecter</Link
|
||||
>
|
||||
</div>
|
||||
</form>
|
||||
</GuestLayout>
|
||||
</template>
|
||||
30
resources/js/Pages/Dashboard.vue
Normal file
30
resources/js/Pages/Dashboard.vue
Normal file
@@ -0,0 +1,30 @@
|
||||
<script setup>
|
||||
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue';
|
||||
import { Head } from '@inertiajs/vue3';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head title="Dashboard" />
|
||||
|
||||
<AuthenticatedLayout>
|
||||
<template #header>
|
||||
<h2
|
||||
class="text-xl font-semibold leading-tight text-gray-800 dark:text-gray-200"
|
||||
>
|
||||
Tableau de Bord
|
||||
</h2>
|
||||
</template>
|
||||
|
||||
<div class="py-12">
|
||||
<div class="mx-auto max-w-7xl sm:px-6 lg:px-8">
|
||||
<div
|
||||
class="overflow-hidden bg-white shadow-sm sm:rounded-lg dark:bg-gray-800"
|
||||
>
|
||||
<div class="p-6 text-gray-900 dark:text-gray-100">
|
||||
You're logged in!
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AuthenticatedLayout>
|
||||
</template>
|
||||
108
resources/js/Pages/Dashboard/Admin.vue
Normal file
108
resources/js/Pages/Dashboard/Admin.vue
Normal file
@@ -0,0 +1,108 @@
|
||||
<script setup>
|
||||
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue';
|
||||
import { Head, Link } from '@inertiajs/vue3';
|
||||
import StatCard from '@/Components/Dashboard/StatCard.vue';
|
||||
import StatusBadge from '@/Components/App/StatusBadge.vue';
|
||||
|
||||
const props = defineProps({
|
||||
stats: Object,
|
||||
recent_requests: Array,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head title="Administration" />
|
||||
|
||||
<AuthenticatedLayout>
|
||||
<template #header>
|
||||
<div class="flex justify-between items-center">
|
||||
<h2 class="text-xl font-semibold leading-tight text-gray-800 dark:text-gray-200">
|
||||
Administration
|
||||
</h2>
|
||||
<div>
|
||||
<Link :href="route('templates.index')" class="mr-2 px-4 py-2 bg-gray-200 border border-transparent rounded-md font-semibold text-xs text-gray-800 uppercase tracking-widest hover:bg-gray-300 dark:bg-gray-700 dark:text-gray-200 dark:hover:bg-gray-600 transition ease-in-out duration-150">
|
||||
Gérer les Modèles
|
||||
</Link>
|
||||
<Link :href="route('users.index')" class="px-4 py-2 bg-gray-800 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-gray-700 dark:bg-gray-200 dark:text-gray-800 dark:hover:bg-white transition ease-in-out duration-150">
|
||||
Gérer les Utilisateurs
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="py-12">
|
||||
<div class="mx-auto max-w-7xl sm:px-6 lg:px-8 space-y-8">
|
||||
<!-- Stats -->
|
||||
<div class="grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-5">
|
||||
<StatCard title="Fiches" :value="stats.total_integrations" color="blue" />
|
||||
<StatCard title="En Cours" :value="stats.active_integrations" color="yellow" />
|
||||
<StatCard title="Terminés" :value="stats.completed_integrations" color="green" />
|
||||
<StatCard title="SLA Dépassés" :value="stats.overdue_tasks" color="red" />
|
||||
<StatCard title="Délai Moyen" :value="stats.avg_completion_days + ' j'" color="indigo" />
|
||||
</div>
|
||||
|
||||
<!-- SLA & Service Analytics -->
|
||||
<div class="grid grid-cols-1 gap-6 lg:grid-cols-2">
|
||||
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg dark:bg-gray-800 p-6">
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100 mb-4">Charge par Service</h3>
|
||||
<div class="space-y-4">
|
||||
<div v-for="service in stats.service_distribution" :key="service.name">
|
||||
<div class="flex justify-between text-sm mb-1">
|
||||
<span class="text-gray-700 dark:text-gray-300 font-medium">{{ service.name }}</span>
|
||||
<span class="text-gray-500">{{ service.tasks_count }} tâches</span>
|
||||
</div>
|
||||
<div class="w-full bg-gray-100 dark:bg-gray-700 rounded-full h-2">
|
||||
<div
|
||||
class="bg-blue-500 h-2 rounded-full transition-all duration-1000"
|
||||
:style="{ width: (service.tasks_count / (stats.total_integrations || 1) * 10).toFixed(0) + '%' }"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg dark:bg-gray-800 p-6 flex flex-col justify-center items-center text-center">
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100 mb-2">SLA</h3>
|
||||
<div class="text-5xl font-bold text-green-500 mb-2">
|
||||
{{ stats.total_integrations ? Math.round((stats.completed_integrations / stats.total_integrations) * 100) : 0 }}%
|
||||
</div>
|
||||
<p class="text-gray-500 dark:text-gray-400">Taux de réussite des intégrations</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recent Activities -->
|
||||
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg dark:bg-gray-800">
|
||||
<div class="p-6 border-b border-gray-200 dark:border-gray-700">
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100">Dernières Intégrations</h3>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-left border-collapse">
|
||||
<thead>
|
||||
<tr class="border-b dark:border-gray-700 bg-gray-50 dark:bg-gray-700/50">
|
||||
<th class="px-6 py-3 text-xs font-bold uppercase text-gray-500 dark:text-gray-400">Agent</th>
|
||||
<th class="px-6 py-3 text-xs font-bold uppercase text-gray-500 dark:text-gray-400">Date</th>
|
||||
<th class="px-6 py-3 text-xs font-bold uppercase text-gray-500 dark:text-gray-400">Status</th>
|
||||
<th class="px-6 py-3 text-xs font-bold uppercase text-gray-500 dark:text-gray-400">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<tr v-for="req in recent_requests" :key="req.id" class="hover:bg-gray-50 dark:hover:bg-gray-700/30">
|
||||
<td class="px-6 py-4">
|
||||
<div class="text-sm font-medium text-gray-900 dark:text-white">{{ req.agent.first_name }} {{ req.agent.last_name }}</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm text-gray-500 dark:text-gray-400">{{ new Date(req.created_at).toLocaleDateString() }}</td>
|
||||
<td class="px-6 py-4">
|
||||
<StatusBadge :status="req.status" />
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm">
|
||||
<Link :href="route('integrations.show', req.id)" class="text-blue-600 hover:text-blue-900 dark:text-blue-400 dark:hover:text-blue-300">Détails</Link>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AuthenticatedLayout>
|
||||
</template>
|
||||
79
resources/js/Pages/Dashboard/Prescriber.vue
Normal file
79
resources/js/Pages/Dashboard/Prescriber.vue
Normal file
@@ -0,0 +1,79 @@
|
||||
<script setup>
|
||||
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue';
|
||||
import { Head, Link } from '@inertiajs/vue3';
|
||||
import StatusBadge from '@/Components/App/StatusBadge.vue';
|
||||
|
||||
const props = defineProps({
|
||||
my_requests: Array,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head title="Tableau de Bord Prescripteur" />
|
||||
|
||||
<AuthenticatedLayout>
|
||||
<template #header>
|
||||
<h2 class="text-xl font-semibold leading-tight text-gray-800 dark:text-gray-200">
|
||||
Mes Demandes de fiches agents
|
||||
</h2>
|
||||
</template>
|
||||
|
||||
<div class="py-12">
|
||||
<div class="mx-auto max-w-7xl sm:px-6 lg:px-8">
|
||||
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg dark:bg-gray-800">
|
||||
<div class="p-6 border-b border-gray-200 dark:border-gray-700 flex justify-between items-center">
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100">Historique des demandes</h3>
|
||||
<Link :href="route('integrations.create')" class="inline-flex items-center px-4 py-2 bg-blue-600 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-blue-700 focus:bg-blue-700 active:bg-blue-900 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 transition ease-in-out duration-150">
|
||||
Nouvelle Demande
|
||||
</Link>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-left border-collapse">
|
||||
<thead>
|
||||
<tr class="border-b dark:border-gray-700 bg-gray-50 dark:bg-gray-700/50">
|
||||
<th class="px-6 py-3 text-xs font-bold uppercase text-gray-500 dark:text-gray-400">Agent</th>
|
||||
<th class="px-6 py-3 text-xs font-bold uppercase text-gray-500 dark:text-gray-400">Date d'arrivée</th>
|
||||
<th class="px-6 py-3 text-xs font-bold uppercase text-gray-500 dark:text-gray-400">Status</th>
|
||||
<th class="px-6 py-3 text-xs font-bold uppercase text-gray-500 dark:text-gray-400">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<tr v-for="req in my_requests" :key="req.id" class="hover:bg-gray-50 dark:hover:bg-gray-700/30">
|
||||
<td class="px-6 py-4">
|
||||
<div class="text-sm font-medium text-gray-900 dark:text-white">{{ req.agent.first_name }} {{ req.agent.last_name }}</div>
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">{{ req.agent.position }}</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ new Date(req.agent.arrival_date).toLocaleDateString() }}
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<StatusBadge :status="req.status" />
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm">
|
||||
<div class="flex items-center gap-3">
|
||||
<Link :href="route('integrations.show', req.id)" class="text-blue-600 hover:text-blue-900 dark:text-blue-400 dark:hover:text-blue-300 transition-colors">
|
||||
Détails
|
||||
</Link>
|
||||
<span class="text-gray-300 dark:text-gray-600">|</span>
|
||||
<a :href="route('integrations.pdf', req.id)" target="_blank" class="flex items-center gap-1 text-red-600 hover:text-red-900 dark:text-red-400 dark:hover:text-red-300 font-medium transition-colors" title="Télécharger la fiche PDF">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
PDF
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="my_requests.length === 0">
|
||||
<td colspan="4" class="px-6 py-8 text-center text-gray-500 dark:text-gray-400 italic text-sm">
|
||||
Aucune demande effectuée. Commencez par en créer une !
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AuthenticatedLayout>
|
||||
</template>
|
||||
149
resources/js/Pages/Dashboard/RH.vue
Normal file
149
resources/js/Pages/Dashboard/RH.vue
Normal file
@@ -0,0 +1,149 @@
|
||||
<script setup>
|
||||
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue';
|
||||
import { Head, Link } from '@inertiajs/vue3';
|
||||
import StatCard from '@/Components/Dashboard/StatCard.vue';
|
||||
import StatusBadge from '@/Components/App/StatusBadge.vue';
|
||||
|
||||
const props = defineProps({
|
||||
pending_validation: Array,
|
||||
active_integrations: Array,
|
||||
completed_integrations: Array,
|
||||
stats: Object,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head title="Tableau de Bord RH" />
|
||||
|
||||
<AuthenticatedLayout>
|
||||
<template #header>
|
||||
<h2 class="text-xl font-semibold leading-tight text-gray-800 dark:text-gray-200">
|
||||
Tableau de Bord - Ressources Humaines
|
||||
</h2>
|
||||
</template>
|
||||
|
||||
<div class="py-12">
|
||||
<div class="mx-auto max-w-7xl sm:px-6 lg:px-8 space-y-8">
|
||||
<!-- Stats -->
|
||||
<div class="grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<StatCard title="En Cours" :value="stats.in_progress" color="blue" />
|
||||
<StatCard title="Terminés" :value="stats.completed" color="green" />
|
||||
<StatCard title="En Retard" :value="stats.overdue" color="red" />
|
||||
<StatCard title="À Valider" :value="pending_validation.length" color="yellow" />
|
||||
</div>
|
||||
|
||||
<!-- Pending RH Validation -->
|
||||
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg dark:bg-gray-800">
|
||||
<div class="p-6 border-b border-gray-200 dark:border-gray-700 flex justify-between items-center">
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100">Fiches agents à valider (RH)</h3>
|
||||
<Link :href="route('integrations.create')" class="inline-flex items-center px-4 py-2 bg-blue-600 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-blue-700 focus:bg-blue-700 active:bg-blue-900 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 transition ease-in-out duration-150">
|
||||
Nouvelle fiche agent
|
||||
</Link>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-left border-collapse">
|
||||
<thead>
|
||||
<tr class="border-b dark:border-gray-700 bg-gray-50 dark:bg-gray-700/50">
|
||||
<th class="px-6 py-3 text-xs font-bold uppercase text-gray-500 dark:text-gray-400">Agent</th>
|
||||
<th class="px-6 py-3 text-xs font-bold uppercase text-gray-500 dark:text-gray-400">Arrivée</th>
|
||||
<th class="px-6 py-3 text-xs font-bold uppercase text-gray-500 dark:text-gray-400">Modèle</th>
|
||||
<th class="px-6 py-3 text-xs font-bold uppercase text-gray-500 dark:text-gray-400">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<tr v-for="req in pending_validation" :key="req.id" class="hover:bg-gray-50 dark:hover:bg-gray-700/30">
|
||||
<td class="px-6 py-4">
|
||||
<div class="text-sm font-medium text-gray-900 dark:text-white">{{ req.agent.first_name }} {{ req.agent.last_name }}</div>
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">{{ req.agent.position }}</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm text-gray-500 dark:text-gray-400">{{ new Date(req.agent.arrival_date).toLocaleDateString() }}</td>
|
||||
<td class="px-6 py-4 text-sm text-gray-500 dark:text-gray-400">{{ req.template?.name || 'Standard' }}</td>
|
||||
<td class="px-6 py-4 text-sm">
|
||||
<Link :href="route('integrations.show', req.id)" class="text-blue-600 hover:text-blue-900 dark:text-blue-400 dark:hover:text-blue-300">Valider</Link>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="pending_validation.length === 0">
|
||||
<td colspan="4" class="px-6 py-8 text-center text-gray-500 dark:text-gray-400 italic text-sm">Aucune demande en attente</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Active Integrations -->
|
||||
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg dark:bg-gray-800">
|
||||
<div class="p-6 border-b border-gray-200 dark:border-gray-700">
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100">Fiches agents en cours</h3>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-left border-collapse">
|
||||
<thead>
|
||||
<tr class="bg-gray-50 dark:bg-gray-700/50">
|
||||
<th class="px-6 py-3 text-xs font-bold uppercase text-gray-500 dark:text-gray-400">Agent</th>
|
||||
<th class="px-6 py-3 text-xs font-bold uppercase text-gray-500 dark:text-gray-400">Service</th>
|
||||
<th class="px-6 py-3 text-xs font-bold uppercase text-gray-500 dark:text-gray-400">Progression</th>
|
||||
<th class="px-6 py-3 text-xs font-bold uppercase text-gray-500 dark:text-gray-400">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<tr v-for="req in active_integrations" :key="req.id" class="hover:bg-gray-50 dark:hover:bg-gray-700/30">
|
||||
<td class="px-6 py-4">
|
||||
<div class="text-sm font-medium text-gray-900 dark:text-white">{{ req.agent.first_name }} {{ req.agent.last_name }}</div>
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">{{ req.agent.position }}</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm text-gray-500 dark:text-gray-400">{{ req.agent.department }}</td>
|
||||
<td class="px-6 py-4">
|
||||
<div class="w-full bg-gray-200 rounded-full h-2.5 dark:bg-gray-700">
|
||||
<div class="bg-blue-600 h-2.5 rounded-full" :style="{ width: (req.service_tasks?.filter(t => t.status === 'completed').length / (req.service_tasks?.length || 1) * 100) + '%' }"></div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm">
|
||||
<Link :href="route('integrations.show', req.id)" class="text-blue-600 hover:text-blue-900 dark:text-blue-400 dark:hover:text-blue-300">Voir</Link>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="active_integrations.length === 0">
|
||||
<td colspan="4" class="px-6 py-8 text-center text-gray-500 dark:text-gray-400 italic text-sm">Aucun dossier en cours</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Completed Integrations -->
|
||||
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg dark:bg-gray-800">
|
||||
<div class="p-6 border-b border-gray-200 dark:border-gray-700">
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100">Fiches agents terminées</h3>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-left border-collapse">
|
||||
<thead>
|
||||
<tr class="bg-gray-50 dark:bg-gray-700/50">
|
||||
<th class="px-6 py-3 text-xs font-bold uppercase text-gray-500 dark:text-gray-400">Agent</th>
|
||||
<th class="px-6 py-3 text-xs font-bold uppercase text-gray-500 dark:text-gray-400">Service</th>
|
||||
<th class="px-6 py-3 text-xs font-bold uppercase text-gray-500 dark:text-gray-400">Terminé le</th>
|
||||
<th class="px-6 py-3 text-xs font-bold uppercase text-gray-500 dark:text-gray-400">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<tr v-for="req in completed_integrations" :key="req.id" class="hover:bg-gray-50 dark:hover:bg-gray-700/30">
|
||||
<td class="px-6 py-4">
|
||||
<div class="text-sm font-medium text-gray-900 dark:text-white">{{ req.agent.first_name }} {{ req.agent.last_name }}</div>
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">{{ req.agent.position }}</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm text-gray-500 dark:text-gray-400">{{ req.agent.department }}</td>
|
||||
<td class="px-6 py-4 text-sm text-gray-500 dark:text-gray-400">{{ req.completed_at ? new Date(req.completed_at).toLocaleDateString() : '-' }}</td>
|
||||
<td class="px-6 py-4 text-sm">
|
||||
<Link :href="route('integrations.show', req.id)" class="text-blue-600 hover:text-blue-900 dark:text-blue-400 dark:hover:text-blue-300">Consulter</Link>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="completed_integrations.length === 0">
|
||||
<td colspan="3" class="px-6 py-8 text-center text-gray-500 dark:text-gray-400 italic text-sm">Aucun dossier terminé</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AuthenticatedLayout>
|
||||
</template>
|
||||
89
resources/js/Pages/Dashboard/Service.vue
Normal file
89
resources/js/Pages/Dashboard/Service.vue
Normal file
@@ -0,0 +1,89 @@
|
||||
<script setup>
|
||||
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue';
|
||||
import { Head, Link } from '@inertiajs/vue3';
|
||||
import StatCard from '@/Components/Dashboard/StatCard.vue';
|
||||
import StatusBadge from '@/Components/App/StatusBadge.vue';
|
||||
|
||||
const props = defineProps({
|
||||
role: String,
|
||||
my_tasks: Array,
|
||||
completed_tasks: Array,
|
||||
});
|
||||
|
||||
const getOverdueCount = () => {
|
||||
return props.my_tasks.filter(t => t.sla_deadline && new Date(t.sla_deadline) < new Date()).length;
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head :title="'Tableau de Bord ' + role" />
|
||||
|
||||
<AuthenticatedLayout>
|
||||
<template #header>
|
||||
<h2 class="text-xl font-semibold leading-tight text-gray-800 dark:text-gray-200">
|
||||
Tableau de Bord - Service {{ role }}
|
||||
</h2>
|
||||
</template>
|
||||
|
||||
<div class="py-12">
|
||||
<div class="mx-auto max-w-7xl sm:px-6 lg:px-8 space-y-8">
|
||||
<!-- Stats -->
|
||||
<div class="grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<StatCard title="Tâches à faire" :value="my_tasks.length" color="yellow" icon="clipboard-list" />
|
||||
<StatCard title="Urgents / Retards" :value="getOverdueCount()" :color="getOverdueCount() > 0 ? 'red' : 'green'" icon="clock" />
|
||||
<StatCard title="Complétés (Récent)" :value="completed_tasks.length" color="blue" icon="check-circle" />
|
||||
</div>
|
||||
|
||||
<!-- My Tasks -->
|
||||
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg dark:bg-gray-800">
|
||||
<div class="p-6 border-b border-gray-200 dark:border-gray-700 flex justify-between items-center">
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100">Mes Tâches à Traiter</h3>
|
||||
<Link :href="route('integrations.create')" class="inline-flex items-center px-4 py-2 bg-blue-600 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-blue-700 focus:bg-blue-700 active:bg-blue-900 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 transition ease-in-out duration-150">
|
||||
Nouvelle Demande
|
||||
</Link>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-left border-collapse">
|
||||
<thead>
|
||||
<tr class="border-b dark:border-gray-700 bg-gray-50 dark:bg-gray-700/50">
|
||||
<th class="px-6 py-3 text-xs font-bold uppercase text-gray-500 dark:text-gray-400">Agent</th>
|
||||
<th class="px-6 py-3 text-xs font-bold uppercase text-gray-500 dark:text-gray-400">Deadline</th>
|
||||
<th class="px-6 py-3 text-xs font-bold uppercase text-gray-500 dark:text-gray-400">Progression</th>
|
||||
<th class="px-6 py-3 text-xs font-bold uppercase text-gray-500 dark:text-gray-400">Status</th>
|
||||
<th class="px-6 py-3 text-xs font-bold uppercase text-gray-500 dark:text-gray-400">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<tr v-for="task in my_tasks" :key="task.id" class="hover:bg-gray-50 dark:hover:bg-gray-700/30">
|
||||
<td class="px-6 py-4">
|
||||
<div class="text-sm font-medium text-gray-900 dark:text-white">{{ task.integration_request.agent.first_name }} {{ task.integration_request.agent.last_name }}</div>
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">{{ task.integration_request.agent.position }}</div>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<span :class="['text-sm', new Date(task.sla_deadline) < new Date() ? 'text-red-600 font-bold' : 'text-gray-500 dark:text-gray-400']">
|
||||
{{ new Date(task.sla_deadline).toLocaleDateString() }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<div class="w-24 bg-gray-200 dark:bg-gray-700 rounded-full h-1.5">
|
||||
<div class="bg-blue-600 h-1.5 rounded-full" :style="{ width: (task.task_items.filter(i => i.is_completed).length / (task.task_items.length || 1) * 100) + '%' }"></div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<StatusBadge :status="task.status" />
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm">
|
||||
<Link :href="route('integrations.show', task.integration_request_id)" class="text-blue-600 hover:text-blue-900 dark:text-blue-400 dark:hover:text-blue-300">Ouvrir</Link>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="my_tasks.length === 0">
|
||||
<td colspan="5" class="px-6 py-8 text-center text-gray-500 dark:text-gray-400 italic text-sm">Aucune tâche en attente</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AuthenticatedLayout>
|
||||
</template>
|
||||
103
resources/js/Pages/Integration/Create.vue
Normal file
103
resources/js/Pages/Integration/Create.vue
Normal file
@@ -0,0 +1,103 @@
|
||||
<script setup>
|
||||
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue';
|
||||
import { Head, useForm } from '@inertiajs/vue3';
|
||||
import InputError from '@/Components/InputError.vue';
|
||||
import InputLabel from '@/Components/InputLabel.vue';
|
||||
import PrimaryButton from '@/Components/PrimaryButton.vue';
|
||||
import TextInput from '@/Components/TextInput.vue';
|
||||
|
||||
const props = defineProps({
|
||||
templates: Array,
|
||||
});
|
||||
|
||||
const form = useForm({
|
||||
first_name: '',
|
||||
last_name: '',
|
||||
email: '',
|
||||
position: '',
|
||||
department: '',
|
||||
arrival_date: '',
|
||||
template_id: props.templates.length > 0 ? props.templates[0].id : '',
|
||||
});
|
||||
|
||||
const submit = () => {
|
||||
form.post(route('integrations.store'));
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head title="Nouvelle Intégration" />
|
||||
|
||||
<AuthenticatedLayout>
|
||||
<template #header>
|
||||
<h2 class="text-xl font-semibold leading-tight text-gray-800 dark:text-gray-200">
|
||||
Nouvelle fiche agent
|
||||
</h2>
|
||||
</template>
|
||||
|
||||
<div class="py-12">
|
||||
<div class="mx-auto max-w-7xl sm:px-6 lg:px-8">
|
||||
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg dark:bg-gray-800 p-8 max-w-2xl mx-auto">
|
||||
<form @submit.prevent="submit" class="space-y-6">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<InputLabel for="first_name" value="Prénom" />
|
||||
<TextInput id="first_name" type="text" class="mt-1 block w-full" v-model="form.first_name" required autofocus />
|
||||
<InputError class="mt-2" :message="form.errors.first_name" />
|
||||
</div>
|
||||
<div>
|
||||
<InputLabel for="last_name" value="Nom" />
|
||||
<TextInput id="last_name" type="text" class="mt-1 block w-full" v-model="form.last_name" required />
|
||||
<InputError class="mt-2" :message="form.errors.last_name" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<InputLabel for="email" value="Email" />
|
||||
<TextInput id="email" type="email" class="mt-1 block w-full" v-model="form.email" required />
|
||||
<InputError class="mt-2" :message="form.errors.email" />
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<InputLabel for="position" value="Poste" />
|
||||
<TextInput id="position" type="text" class="mt-1 block w-full" v-model="form.position" required />
|
||||
<InputError class="mt-2" :message="form.errors.position" />
|
||||
</div>
|
||||
<div>
|
||||
<InputLabel for="department" value="Service de destination" />
|
||||
<TextInput id="department" type="text" class="mt-1 block w-full" v-model="form.department" required />
|
||||
<InputError class="mt-2" :message="form.errors.department" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<InputLabel for="arrival_date" value="Date d'arrivée" />
|
||||
<TextInput id="arrival_date" type="date" class="mt-1 block w-full" v-model="form.arrival_date" required />
|
||||
<InputError class="mt-2" :message="form.errors.arrival_date" />
|
||||
</div>
|
||||
<div class="warning-box bg-red-500 p-4 rounded-md text-center">
|
||||
<span class="text-white">Attention, si vous selectionner l'option <b><u>AVEC TELEPHONE PORTABLE</u></b> une validation de la direction générale est nécessaire.</span>
|
||||
<br>
|
||||
<span class="text-white"><b><u><i>C'est à vous d'initier celle-ci auprès de votre Direction Générale Adjointe</i></u></b></span>
|
||||
</div>
|
||||
<div>
|
||||
<InputLabel for="template_id" value="Template de parcours" />
|
||||
<select id="template_id" v-model="form.template_id" class="mt-1 block w-full border-gray-300 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-300 focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 rounded-md shadow-sm">
|
||||
<option value="">Aucun (Pas de tâches)</option>
|
||||
<option v-for="t in templates" :key="t.id" :value="t.id">{{ t.name }}</option>
|
||||
</select>
|
||||
<InputError class="mt-2" :message="form.errors.template_id" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end mt-4">
|
||||
<PrimaryButton class="ml-4" :class="{ 'opacity-25': form.processing }" :disabled="form.processing">
|
||||
Créer la fiche agent
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AuthenticatedLayout>
|
||||
</template>
|
||||
66
resources/js/Pages/Integration/Index.vue
Normal file
66
resources/js/Pages/Integration/Index.vue
Normal file
@@ -0,0 +1,66 @@
|
||||
<script setup>
|
||||
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue';
|
||||
import { Head, Link } from '@inertiajs/vue3';
|
||||
import StatusBadge from '@/Components/App/StatusBadge.vue';
|
||||
|
||||
defineProps({
|
||||
integrations: Array,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head title="Intégrations" />
|
||||
|
||||
<AuthenticatedLayout>
|
||||
<template #header>
|
||||
<div class="flex justify-between items-center">
|
||||
<h2 class="text-xl font-semibold leading-tight text-gray-800 dark:text-gray-200">
|
||||
Processus d'intégration
|
||||
</h2>
|
||||
<Link :href="route('integrations.create')" class="inline-flex items-center px-4 py-2 bg-blue-600 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-blue-700 focus:bg-blue-700 active:bg-blue-900 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 transition ease-in-out duration-150">
|
||||
Déclencher une arrivée
|
||||
</Link>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="py-12">
|
||||
<div class="mx-auto max-w-7xl sm:px-6 lg:px-8">
|
||||
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg dark:bg-gray-800">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-left border-collapse">
|
||||
<thead>
|
||||
<tr class="border-b dark:border-gray-700 bg-gray-50 dark:bg-gray-700/50">
|
||||
<th class="px-6 py-3 text-xs font-bold uppercase text-gray-500 dark:text-gray-400">Agent</th>
|
||||
<th class="px-6 py-3 text-xs font-bold uppercase text-gray-500 dark:text-gray-400">Status</th>
|
||||
<th class="px-6 py-3 text-xs font-bold uppercase text-gray-500 dark:text-gray-400">Date d'arrivée</th>
|
||||
<th class="px-6 py-3 text-xs font-bold uppercase text-gray-500 dark:text-gray-400">Template</th>
|
||||
<th class="px-6 py-3 text-xs font-bold uppercase text-gray-500 dark:text-gray-400">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<tr v-for="integration in integrations" :key="integration.id" class="hover:bg-gray-50 dark:hover:bg-gray-700/30">
|
||||
<td class="px-6 py-4">
|
||||
<div class="text-sm font-medium text-gray-900 dark:text-white">{{ integration.agent.first_name }} {{ integration.agent.last_name }}</div>
|
||||
<div class="text-xs text-gray-500 dark:text-gray-400">{{ integration.agent.position }}</div>
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<StatusBadge :status="integration.status" />
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ new Date(integration.agent.arrival_date).toLocaleDateString() }}
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ integration.template?.name || 'Standard' }}
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm">
|
||||
<Link :href="route('integrations.show', integration.id)" class="text-blue-600 hover:text-blue-900 dark:text-blue-400 dark:hover:text-blue-300">Gérer</Link>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AuthenticatedLayout>
|
||||
</template>
|
||||
86
resources/js/Pages/Integration/Show.vue
Normal file
86
resources/js/Pages/Integration/Show.vue
Normal file
@@ -0,0 +1,86 @@
|
||||
<script setup>
|
||||
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue';
|
||||
import { Head, Link } from '@inertiajs/vue3';
|
||||
import StatusBadge from '@/Components/App/StatusBadge.vue';
|
||||
import ServiceTaskCard from '@/Components/App/ServiceTaskCard.vue';
|
||||
import ActivityTimeline from '@/Components/App/ActivityTimeline.vue';
|
||||
import { router } from '@inertiajs/vue3';
|
||||
|
||||
const props = defineProps({
|
||||
integration: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
activities: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
});
|
||||
|
||||
const validateRH = () => {
|
||||
if (confirm('Souhaitez-vous valider ce dossier et notifier les services ?')) {
|
||||
router.post(route('integrations.validate-rh', props.integration.id));
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head title="Détails Intégration" />
|
||||
|
||||
<AuthenticatedLayout>
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-xl font-semibold leading-tight text-gray-800 dark:text-gray-200">
|
||||
Integration: {{ integration.agent.first_name }} {{ integration.agent.last_name }}
|
||||
</h2>
|
||||
<div class="flex items-center space-x-4">
|
||||
<button
|
||||
v-if="integration.status === 'pending_rh_validation' || integration.status === 'draft'"
|
||||
@click="validateRH"
|
||||
class="px-4 py-2 bg-green-600 text-white rounded-md text-sm font-bold hover:bg-green-700"
|
||||
>
|
||||
Valider Dossier RH
|
||||
</button>
|
||||
<StatusBadge :status="integration.status" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="py-12">
|
||||
<div class="mx-auto max-w-7xl sm:px-6 lg:px-8">
|
||||
<div class="grid grid-cols-1 gap-6 md:grid-cols-3 items-start">
|
||||
<!-- Right/Side Column: Agent Info & Activity -->
|
||||
<div class="md:col-span-1 space-y-6">
|
||||
<!-- Agent Info -->
|
||||
<div class="overflow-hidden bg-white shadow-sm sm:rounded-lg dark:bg-gray-800 p-6 border border-gray-200 dark:border-gray-700">
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100 mb-4">Informations Agent</h3>
|
||||
<dl class="space-y-4">
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Poste</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 dark:text-gray-100">{{ integration.agent.position }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Service</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 dark:text-gray-100">{{ integration.agent.department }}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Date d'arrivée</dt>
|
||||
<dd class="mt-1 text-sm text-gray-900 dark:text-gray-100">{{ new Date(integration.agent.arrival_date).toLocaleDateString() }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<ActivityTimeline :activities="activities" />
|
||||
</div>
|
||||
|
||||
<!-- Main Column: Tasks -->
|
||||
<div class="md:col-span-2 space-y-6">
|
||||
<div v-for="task in integration.service_tasks" :key="task.id">
|
||||
<ServiceTaskCard :task="task" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AuthenticatedLayout>
|
||||
</template>
|
||||
64
resources/js/Pages/Offboarding/Create.vue
Normal file
64
resources/js/Pages/Offboarding/Create.vue
Normal file
@@ -0,0 +1,64 @@
|
||||
<script setup>
|
||||
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue';
|
||||
import { Head, useForm } from '@inertiajs/vue3';
|
||||
import InputError from '@/Components/InputError.vue';
|
||||
import InputLabel from '@/Components/InputLabel.vue';
|
||||
import PrimaryButton from '@/Components/PrimaryButton.vue';
|
||||
import TextInput from '@/Components/TextInput.vue';
|
||||
|
||||
const props = defineProps({
|
||||
agent: Object,
|
||||
templates: Array,
|
||||
});
|
||||
|
||||
const form = useForm({
|
||||
agent_id: props.agent.id,
|
||||
template_id: '',
|
||||
departure_date: '',
|
||||
});
|
||||
|
||||
const submit = () => {
|
||||
form.post(route('offboarding.store'));
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head title="Nouvel Offboarding" />
|
||||
|
||||
<AuthenticatedLayout>
|
||||
<template #header>
|
||||
<h2 class="text-xl font-semibold leading-tight text-gray-800 dark:text-gray-200">
|
||||
Préparer le départ de {{ agent.first_name }} {{ agent.last_name }}
|
||||
</h2>
|
||||
</template>
|
||||
|
||||
<div class="py-12">
|
||||
<div class="mx-auto max-w-7xl sm:px-6 lg:px-8">
|
||||
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg dark:bg-gray-800 p-8 max-w-2xl mx-auto">
|
||||
<form @submit.prevent="submit" class="space-y-6">
|
||||
<div>
|
||||
<InputLabel for="departure_date" value="Date effective du départ" />
|
||||
<TextInput id="departure_date" type="date" class="mt-1 block w-full" v-model="form.departure_date" required />
|
||||
<InputError class="mt-2" :message="form.errors.departure_date" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<InputLabel for="template_id" value="Template de sortie (Optionnel)" />
|
||||
<select id="template_id" v-model="form.template_id" class="mt-1 block w-full border-gray-300 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-300 focus:border-indigo-500 rounded-md shadow-sm">
|
||||
<option value="">Standard (Sans template)</option>
|
||||
<option v-for="t in templates" :key="t.id" :value="t.id">{{ t.name }}</option>
|
||||
</select>
|
||||
<InputError class="mt-2" :message="form.errors.template_id" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end">
|
||||
<PrimaryButton class="bg-red-600 hover:bg-red-700" :class="{ 'opacity-25': form.processing }" :disabled="form.processing">
|
||||
Lancer le processus de départ
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AuthenticatedLayout>
|
||||
</template>
|
||||
83
resources/js/Pages/Offboarding/Index.vue
Normal file
83
resources/js/Pages/Offboarding/Index.vue
Normal file
@@ -0,0 +1,83 @@
|
||||
<script setup>
|
||||
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue';
|
||||
import { Head, Link } from '@inertiajs/vue3';
|
||||
|
||||
defineProps({
|
||||
active_agents: Array,
|
||||
current_offboardings: Array,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head title="Gestion des Départs" />
|
||||
|
||||
<AuthenticatedLayout>
|
||||
<template #header>
|
||||
<h2 class="text-xl font-semibold leading-tight text-gray-800 dark:text-gray-200">
|
||||
Gestion des Offboardings (Départs)
|
||||
</h2>
|
||||
</template>
|
||||
|
||||
<div class="py-12">
|
||||
<div class="mx-auto max-w-7xl sm:px-6 lg:px-8 space-y-8">
|
||||
<!-- Current Offboardings -->
|
||||
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg dark:bg-gray-800">
|
||||
<div class="p-6 border-b border-gray-200 dark:border-gray-700">
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100">Départs en cours</h3>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-left border-collapse">
|
||||
<thead>
|
||||
<tr class="border-b dark:border-gray-700 bg-gray-50 dark:bg-gray-700/50">
|
||||
<th class="px-6 py-3 text-xs font-bold uppercase text-gray-500 dark:text-gray-400">Agent</th>
|
||||
<th class="px-6 py-3 text-xs font-bold uppercase text-gray-500 dark:text-gray-400">Date de départ</th>
|
||||
<th class="px-6 py-3 text-xs font-bold uppercase text-gray-500 dark:text-gray-400">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<tr v-for="off in current_offboardings" :key="off.id" class="hover:bg-gray-50 dark:hover:bg-gray-700/30">
|
||||
<td class="px-6 py-4 font-medium text-gray-900 dark:text-white">{{ off.agent.first_name }} {{ off.agent.last_name }}</td>
|
||||
<td class="px-6 py-4 text-sm text-gray-500 dark:text-gray-400">{{ new Date(off.global_deadline).toLocaleDateString() }}</td>
|
||||
<td class="px-6 py-4 text-sm">
|
||||
<Link :href="route('integrations.show', off.id)" class="text-blue-600 hover:text-blue-900">Gérer</Link>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="current_offboardings.length === 0">
|
||||
<td colspan="3" class="px-6 py-8 text-center text-gray-500 dark:text-gray-400 italic text-sm">Aucun départ en cours</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Active Agents List for New Offboarding -->
|
||||
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg dark:bg-gray-800">
|
||||
<div class="p-6 border-b border-gray-200 dark:border-gray-700">
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100">Lancer un nouvel offboarding</h3>
|
||||
<p class="text-sm text-gray-500">Liste des agents actuellement en poste</p>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-left border-collapse">
|
||||
<thead>
|
||||
<tr class="border-b dark:border-gray-700 bg-gray-50 dark:bg-gray-700/50">
|
||||
<th class="px-6 py-3 text-xs font-bold uppercase text-gray-500 dark:text-gray-400">Agent</th>
|
||||
<th class="px-6 py-3 text-xs font-bold uppercase text-gray-500 dark:text-gray-400">Service</th>
|
||||
<th class="px-6 py-3 text-xs font-bold uppercase text-gray-500 dark:text-gray-400">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<tr v-for="agent in active_agents" :key="agent.id" class="hover:bg-gray-50 dark:hover:bg-gray-700/30">
|
||||
<td class="px-6 py-4 text-gray-900 dark:text-white">{{ agent.first_name }} {{ agent.last_name }}</td>
|
||||
<td class="px-6 py-4 text-sm text-gray-500 dark:text-gray-400">{{ agent.department }}</td>
|
||||
<td class="px-6 py-4 text-sm">
|
||||
<Link :href="route('offboarding.create', agent.id)" class="text-red-600 hover:text-red-900 font-bold">Déclencher Départ</Link>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AuthenticatedLayout>
|
||||
</template>
|
||||
56
resources/js/Pages/Profile/Edit.vue
Normal file
56
resources/js/Pages/Profile/Edit.vue
Normal file
@@ -0,0 +1,56 @@
|
||||
<script setup>
|
||||
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue';
|
||||
import DeleteUserForm from './Partials/DeleteUserForm.vue';
|
||||
import UpdatePasswordForm from './Partials/UpdatePasswordForm.vue';
|
||||
import UpdateProfileInformationForm from './Partials/UpdateProfileInformationForm.vue';
|
||||
import { Head } from '@inertiajs/vue3';
|
||||
|
||||
defineProps({
|
||||
mustVerifyEmail: {
|
||||
type: Boolean,
|
||||
},
|
||||
status: {
|
||||
type: String,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head title="Profil" />
|
||||
|
||||
<AuthenticatedLayout>
|
||||
<template #header>
|
||||
<h2
|
||||
class="text-xl font-semibold leading-tight text-gray-800 dark:text-gray-200"
|
||||
>
|
||||
Profil
|
||||
</h2>
|
||||
</template>
|
||||
|
||||
<div class="py-12">
|
||||
<div class="mx-auto max-w-7xl space-y-6 sm:px-6 lg:px-8">
|
||||
<div
|
||||
class="bg-white p-4 shadow sm:rounded-lg sm:p-8 dark:bg-gray-800"
|
||||
>
|
||||
<UpdateProfileInformationForm
|
||||
:must-verify-email="mustVerifyEmail"
|
||||
:status="status"
|
||||
class="max-w-xl"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="bg-white p-4 shadow sm:rounded-lg sm:p-8 dark:bg-gray-800"
|
||||
>
|
||||
<UpdatePasswordForm class="max-w-xl" />
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="bg-white p-4 shadow sm:rounded-lg sm:p-8 dark:bg-gray-800"
|
||||
>
|
||||
<DeleteUserForm class="max-w-xl" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AuthenticatedLayout>
|
||||
</template>
|
||||
108
resources/js/Pages/Profile/Partials/DeleteUserForm.vue
Normal file
108
resources/js/Pages/Profile/Partials/DeleteUserForm.vue
Normal file
@@ -0,0 +1,108 @@
|
||||
<script setup>
|
||||
import DangerButton from '@/Components/DangerButton.vue';
|
||||
import InputError from '@/Components/InputError.vue';
|
||||
import InputLabel from '@/Components/InputLabel.vue';
|
||||
import Modal from '@/Components/Modal.vue';
|
||||
import SecondaryButton from '@/Components/SecondaryButton.vue';
|
||||
import TextInput from '@/Components/TextInput.vue';
|
||||
import { useForm } from '@inertiajs/vue3';
|
||||
import { nextTick, ref } from 'vue';
|
||||
|
||||
const confirmingUserDeletion = ref(false);
|
||||
const passwordInput = ref(null);
|
||||
|
||||
const form = useForm({
|
||||
password: '',
|
||||
});
|
||||
|
||||
const confirmUserDeletion = () => {
|
||||
confirmingUserDeletion.value = true;
|
||||
|
||||
nextTick(() => passwordInput.value.focus());
|
||||
};
|
||||
|
||||
const deleteUser = () => {
|
||||
form.delete(route('profile.destroy'), {
|
||||
preserveScroll: true,
|
||||
onSuccess: () => closeModal(),
|
||||
onError: () => passwordInput.value.focus(),
|
||||
onFinish: () => form.reset(),
|
||||
});
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
confirmingUserDeletion.value = false;
|
||||
|
||||
form.clearErrors();
|
||||
form.reset();
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="space-y-6">
|
||||
<header>
|
||||
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">
|
||||
Supprimer le compte
|
||||
</h2>
|
||||
|
||||
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
|
||||
Une fois votre compte supprimé, toutes ses ressources et données seront
|
||||
définitivement effacées. Avant de supprimer votre compte, veuillez
|
||||
télécharger toutes les données ou informations que vous souhaitez conserver.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<DangerButton @click="confirmUserDeletion">Supprimer le compte</DangerButton>
|
||||
|
||||
<Modal :show="confirmingUserDeletion" @close="closeModal">
|
||||
<div class="p-6">
|
||||
<h2
|
||||
class="text-lg font-medium text-gray-900 dark:text-gray-100"
|
||||
>
|
||||
Êtes-vous sûr de vouloir supprimer votre compte ?
|
||||
</h2>
|
||||
|
||||
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
|
||||
Une fois votre compte supprimé, toutes ses ressources et données
|
||||
seront définitivement effacées. Veuillez saisir votre mot de passe pour
|
||||
confirmer que vous souhaitez supprimer définitivement votre compte.
|
||||
</p>
|
||||
|
||||
<div class="mt-6">
|
||||
<InputLabel
|
||||
for="password"
|
||||
value="Mot de passe"
|
||||
class="sr-only"
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
id="password"
|
||||
ref="passwordInput"
|
||||
v-model="form.password"
|
||||
type="password"
|
||||
class="mt-1 block w-3/4"
|
||||
placeholder="Mot de passe"
|
||||
@keyup.enter="deleteUser"
|
||||
/>
|
||||
|
||||
<InputError :message="form.errors.password" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex justify-end">
|
||||
<SecondaryButton @click="closeModal">
|
||||
Annuler
|
||||
</SecondaryButton>
|
||||
|
||||
<DangerButton
|
||||
class="ms-3"
|
||||
:class="{ 'opacity-25': form.processing }"
|
||||
:disabled="form.processing"
|
||||
@click="deleteUser"
|
||||
>
|
||||
Supprimer le compte
|
||||
</DangerButton>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</section>
|
||||
</template>
|
||||
122
resources/js/Pages/Profile/Partials/UpdatePasswordForm.vue
Normal file
122
resources/js/Pages/Profile/Partials/UpdatePasswordForm.vue
Normal file
@@ -0,0 +1,122 @@
|
||||
<script setup>
|
||||
import InputError from '@/Components/InputError.vue';
|
||||
import InputLabel from '@/Components/InputLabel.vue';
|
||||
import PrimaryButton from '@/Components/PrimaryButton.vue';
|
||||
import TextInput from '@/Components/TextInput.vue';
|
||||
import { useForm } from '@inertiajs/vue3';
|
||||
import { ref } from 'vue';
|
||||
|
||||
const passwordInput = ref(null);
|
||||
const currentPasswordInput = ref(null);
|
||||
|
||||
const form = useForm({
|
||||
current_password: '',
|
||||
password: '',
|
||||
password_confirmation: '',
|
||||
});
|
||||
|
||||
const updatePassword = () => {
|
||||
form.put(route('password.update'), {
|
||||
preserveScroll: true,
|
||||
onSuccess: () => form.reset(),
|
||||
onError: () => {
|
||||
if (form.errors.password) {
|
||||
form.reset('password', 'password_confirmation');
|
||||
passwordInput.value.focus();
|
||||
}
|
||||
if (form.errors.current_password) {
|
||||
form.reset('current_password');
|
||||
currentPasswordInput.value.focus();
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section>
|
||||
<header>
|
||||
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">
|
||||
Mettre à jour le mot de passe
|
||||
</h2>
|
||||
|
||||
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
|
||||
Assurez-vous que votre compte utilise un mot de passe long et aléatoire pour rester
|
||||
en sécurité.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<form @submit.prevent="updatePassword" class="mt-6 space-y-6">
|
||||
<div>
|
||||
<InputLabel for="current_password" value="Mot de passe actuel" />
|
||||
|
||||
<TextInput
|
||||
id="current_password"
|
||||
ref="currentPasswordInput"
|
||||
v-model="form.current_password"
|
||||
type="password"
|
||||
class="mt-1 block w-full"
|
||||
autocomplete="current-password"
|
||||
/>
|
||||
|
||||
<InputError
|
||||
:message="form.errors.current_password"
|
||||
class="mt-2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<InputLabel for="password" value="Nouveau mot de passe" />
|
||||
|
||||
<TextInput
|
||||
id="password"
|
||||
ref="passwordInput"
|
||||
v-model="form.password"
|
||||
type="password"
|
||||
class="mt-1 block w-full"
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
|
||||
<InputError :message="form.errors.password" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<InputLabel
|
||||
for="password_confirmation"
|
||||
value="Confirmer le mot de passe"
|
||||
/>
|
||||
|
||||
<TextInput
|
||||
id="password_confirmation"
|
||||
v-model="form.password_confirmation"
|
||||
type="password"
|
||||
class="mt-1 block w-full"
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
|
||||
<InputError
|
||||
:message="form.errors.password_confirmation"
|
||||
class="mt-2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-4">
|
||||
<PrimaryButton :disabled="form.processing">Enregistrer</PrimaryButton>
|
||||
|
||||
<Transition
|
||||
enter-active-class="transition ease-in-out"
|
||||
enter-from-class="opacity-0"
|
||||
leave-active-class="transition ease-in-out"
|
||||
leave-to-class="opacity-0"
|
||||
>
|
||||
<p
|
||||
v-if="form.recentlySuccessful"
|
||||
class="text-sm text-gray-600 dark:text-gray-400"
|
||||
>
|
||||
Enregistré.
|
||||
</p>
|
||||
</Transition>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</template>
|
||||
@@ -0,0 +1,112 @@
|
||||
<script setup>
|
||||
import InputError from '@/Components/InputError.vue';
|
||||
import InputLabel from '@/Components/InputLabel.vue';
|
||||
import PrimaryButton from '@/Components/PrimaryButton.vue';
|
||||
import TextInput from '@/Components/TextInput.vue';
|
||||
import { Link, useForm, usePage } from '@inertiajs/vue3';
|
||||
|
||||
defineProps({
|
||||
mustVerifyEmail: {
|
||||
type: Boolean,
|
||||
},
|
||||
status: {
|
||||
type: String,
|
||||
},
|
||||
});
|
||||
|
||||
const user = usePage().props.auth.user;
|
||||
|
||||
const form = useForm({
|
||||
name: user.name,
|
||||
email: user.email,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section>
|
||||
<header>
|
||||
<h2 class="text-lg font-medium text-gray-900 dark:text-gray-100">
|
||||
Informations du profil
|
||||
</h2>
|
||||
|
||||
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
|
||||
Mettez à jour les informations de votre compte et votre adresse email.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<form
|
||||
@submit.prevent="form.patch(route('profile.update'))"
|
||||
class="mt-6 space-y-6"
|
||||
>
|
||||
<div>
|
||||
<InputLabel for="name" value="Nom" />
|
||||
|
||||
<TextInput
|
||||
id="name"
|
||||
type="text"
|
||||
class="mt-1 block w-full"
|
||||
v-model="form.name"
|
||||
required
|
||||
autofocus
|
||||
autocomplete="name"
|
||||
/>
|
||||
|
||||
<InputError class="mt-2" :message="form.errors.name" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<InputLabel for="email" value="Email" />
|
||||
|
||||
<TextInput
|
||||
id="email"
|
||||
type="email"
|
||||
class="mt-1 block w-full"
|
||||
v-model="form.email"
|
||||
required
|
||||
autocomplete="username"
|
||||
/>
|
||||
|
||||
<InputError class="mt-2" :message="form.errors.email" />
|
||||
</div>
|
||||
|
||||
<div v-if="mustVerifyEmail && user.email_verified_at === null">
|
||||
<p class="mt-2 text-sm text-gray-800 dark:text-gray-200">
|
||||
Votre adresse email n'est pas vérifiée.
|
||||
<Link
|
||||
:href="route('verification.send')"
|
||||
method="post"
|
||||
as="button"
|
||||
class="rounded-md text-sm text-gray-600 underline hover:text-gray-900 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:text-gray-400 dark:hover:text-gray-100 dark:focus:ring-offset-gray-800"
|
||||
>
|
||||
Cliquez ici pour renvoyer l'email de vérification.
|
||||
</Link>
|
||||
</p>
|
||||
|
||||
<div
|
||||
v-show="status === 'verification-link-sent'"
|
||||
class="mt-2 text-sm font-medium text-green-600 dark:text-green-400"
|
||||
>
|
||||
Un nouveau lien de vérification a été envoyé à votre adresse email.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-4">
|
||||
<PrimaryButton :disabled="form.processing">Enregistrer</PrimaryButton>
|
||||
|
||||
<Transition
|
||||
enter-active-class="transition ease-in-out"
|
||||
enter-from-class="opacity-0"
|
||||
leave-active-class="transition ease-in-out"
|
||||
leave-to-class="opacity-0"
|
||||
>
|
||||
<p
|
||||
v-if="form.recentlySuccessful"
|
||||
class="text-sm text-gray-600 dark:text-gray-400"
|
||||
>
|
||||
Enregistré.
|
||||
</p>
|
||||
</Transition>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</template>
|
||||
168
resources/js/Pages/Template/Create.vue
Normal file
168
resources/js/Pages/Template/Create.vue
Normal file
@@ -0,0 +1,168 @@
|
||||
<script setup>
|
||||
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue';
|
||||
import { Head, useForm, Link } from '@inertiajs/vue3';
|
||||
import PrimaryButton from '@/Components/PrimaryButton.vue';
|
||||
import InputError from '@/Components/InputError.vue';
|
||||
import InputLabel from '@/Components/InputLabel.vue';
|
||||
import TextInput from '@/Components/TextInput.vue';
|
||||
import { ref } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
services: Array,
|
||||
});
|
||||
|
||||
const form = useForm({
|
||||
name: '',
|
||||
description: '',
|
||||
is_active: true,
|
||||
items: [],
|
||||
});
|
||||
|
||||
const addItem = () => {
|
||||
form.items.push({
|
||||
service_id: props.services.length > 0 ? props.services[0].id : '',
|
||||
label: '',
|
||||
is_mandatory: false,
|
||||
fields: [],
|
||||
});
|
||||
};
|
||||
|
||||
const removeItem = (index) => {
|
||||
form.items.splice(index, 1);
|
||||
};
|
||||
|
||||
const addField = (item) => {
|
||||
if (!item.fields) item.fields = [];
|
||||
item.fields.push({
|
||||
label: '',
|
||||
type: 'text',
|
||||
required: false,
|
||||
});
|
||||
};
|
||||
|
||||
const removeField = (item, index) => {
|
||||
item.fields.splice(index, 1);
|
||||
};
|
||||
|
||||
const submit = () => {
|
||||
form.post(route('templates.store'));
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head title="Nouveau Modèle" />
|
||||
|
||||
<AuthenticatedLayout>
|
||||
<template #header>
|
||||
<h2 class="text-xl font-semibold leading-tight text-gray-800 dark:text-gray-200">
|
||||
Créer un Modèle d'Intégration
|
||||
</h2>
|
||||
</template>
|
||||
|
||||
<div class="py-12">
|
||||
<div class="mx-auto max-w-7xl sm:px-6 lg:px-8">
|
||||
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg dark:bg-gray-800 p-6">
|
||||
<form @submit.prevent="submit" class="space-y-6">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<InputLabel for="name" value="Nom du modèle" />
|
||||
<TextInput id="name" type="text" class="mt-1 block w-full" v-model="form.name" required autofocus />
|
||||
<InputError class="mt-2" :message="form.errors.name" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<InputLabel for="description" value="Description" />
|
||||
<TextInput id="description" type="text" class="mt-1 block w-full" v-model="form.description" />
|
||||
<InputError class="mt-2" :message="form.errors.description" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox" v-model="form.is_active" class="rounded border-gray-300 text-indigo-600 shadow-sm focus:ring-indigo-500 dark:border-gray-700 dark:bg-gray-900 dark:focus:ring-indigo-600 dark:focus:ring-offset-gray-800">
|
||||
<span class="ml-2 text-sm text-gray-600 dark:text-gray-400">Modèle Actif</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="border-t border-gray-200 dark:border-gray-700 pt-6">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100">Tâches par Service</h3>
|
||||
<button type="button" @click="addItem" class="text-sm bg-gray-200 hover:bg-gray-300 dark:bg-gray-700 dark:hover:bg-gray-600 px-3 py-1 rounded">
|
||||
+ Ajouter une tâche
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="form.items.length === 0" class="text-center text-gray-500 italic py-4">
|
||||
Aucune tâche définie. Ajoutez des tâches pour ce modèle.
|
||||
</div>
|
||||
|
||||
<div v-else class="space-y-4">
|
||||
<div v-for="(item, index) in form.items" :key="index" class="p-4 bg-gray-50 dark:bg-gray-700/30 rounded-lg space-y-4">
|
||||
<div class="flex items-start space-x-4">
|
||||
<div class="w-1/4">
|
||||
<InputLabel value="Service" class="mb-1" />
|
||||
<select v-model="item.service_id" class="block w-full border-gray-300 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-300 focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 rounded-md shadow-sm text-sm">
|
||||
<option v-for="service in services" :key="service.id" :value="service.id">
|
||||
{{ service.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<InputLabel value="Libellé de la tâche" class="mb-1" />
|
||||
<TextInput type="text" class="block w-full" v-model="item.label" required placeholder="Ex: Créer compte AD" />
|
||||
</div>
|
||||
<div class="pt-6">
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox" v-model="item.is_mandatory" class="rounded border-gray-300 text-indigo-600 shadow-sm focus:ring-indigo-500 dark:border-gray-700 dark:bg-gray-900 dark:focus:ring-indigo-600 dark:focus:ring-offset-gray-800">
|
||||
<span class="ml-2 text-sm text-gray-600 dark:text-gray-400">Obligatoire</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="pt-6">
|
||||
<button type="button" @click="removeItem(index)" class="text-red-500 hover:text-red-700">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Custom Fields -->
|
||||
<div class="pl-4 border-l-2 border-indigo-200 dark:border-indigo-800 ml-4 space-y-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<h4 class="text-xs font-semibold uppercase text-gray-500 dark:text-gray-400">Données à saisir par le service</h4>
|
||||
<button type="button" @click="addField(item)" class="text-xs text-indigo-600 hover:text-indigo-800 dark:text-indigo-400 dark:hover:text-indigo-300 font-medium">
|
||||
+ Ajouter un champ
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-for="(field, fIndex) in item.fields" :key="fIndex" class="flex items-center space-x-2 bg-white dark:bg-gray-800 p-2 rounded shadow-sm border border-gray-200 dark:border-gray-700">
|
||||
<TextInput v-model="field.label" placeholder="Nom du champ (ex: Login)" class="text-xs w-1/3 py-1" />
|
||||
<select v-model="field.type" class="text-xs rounded-md border-gray-300 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-300 focus:border-indigo-500 focus:ring-indigo-500 py-1">
|
||||
<option value="text">Texte</option>
|
||||
<option value="checkbox">Case à cocher</option>
|
||||
<option value="date">Date</option>
|
||||
</select>
|
||||
<label class="flex items-center ml-2">
|
||||
<input type="checkbox" v-model="field.required" class="rounded border-gray-300 text-indigo-600 shadow-sm focus:ring-indigo-500 dark:border-gray-700 dark:bg-gray-900 dark:focus:ring-indigo-600 dark:focus:ring-offset-gray-800 h-3 w-3">
|
||||
<span class="ml-1 text-xs text-gray-600 dark:text-gray-400">Requis</span> <!-- FIXED: Correct closing tag for span -->
|
||||
</label> <!-- FIXED: Correct closing tag for label -->
|
||||
<button type="button" @click="removeField(item, fIndex)" class="text-red-500 hover:text-red-700 ml-auto p-1">
|
||||
x
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<InputError class="mt-2" :message="form.errors.items" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-4">
|
||||
<PrimaryButton :disabled="form.processing">Enregistrer</PrimaryButton>
|
||||
<Link :href="route('templates.index')" class="text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 underline">Annuler</Link>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AuthenticatedLayout>
|
||||
</template>
|
||||
173
resources/js/Pages/Template/Edit.vue
Normal file
173
resources/js/Pages/Template/Edit.vue
Normal file
@@ -0,0 +1,173 @@
|
||||
<script setup>
|
||||
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue';
|
||||
import { Head, useForm, Link } from '@inertiajs/vue3';
|
||||
import PrimaryButton from '@/Components/PrimaryButton.vue';
|
||||
import InputError from '@/Components/InputError.vue';
|
||||
import InputLabel from '@/Components/InputLabel.vue';
|
||||
import TextInput from '@/Components/TextInput.vue';
|
||||
|
||||
const props = defineProps({
|
||||
template: Object,
|
||||
services: Array,
|
||||
});
|
||||
|
||||
const form = useForm({
|
||||
name: props.template.name,
|
||||
description: props.template.description,
|
||||
is_active: Boolean(props.template.is_active),
|
||||
items: props.template.service_items.map(item => ({
|
||||
service_id: item.service_id,
|
||||
label: item.label,
|
||||
is_mandatory: Boolean(item.is_mandatory),
|
||||
fields: item.fields || [],
|
||||
})),
|
||||
});
|
||||
|
||||
const addItem = () => {
|
||||
form.items.push({
|
||||
service_id: props.services.length > 0 ? props.services[0].id : '',
|
||||
label: '',
|
||||
is_mandatory: false,
|
||||
fields: [],
|
||||
});
|
||||
};
|
||||
|
||||
const removeItem = (index) => {
|
||||
form.items.splice(index, 1);
|
||||
};
|
||||
|
||||
const addField = (item) => {
|
||||
if (!item.fields) item.fields = [];
|
||||
item.fields.push({
|
||||
label: '',
|
||||
type: 'text',
|
||||
required: false,
|
||||
});
|
||||
};
|
||||
|
||||
const removeField = (item, index) => {
|
||||
item.fields.splice(index, 1);
|
||||
};
|
||||
|
||||
const submit = () => {
|
||||
form.put(route('templates.update', props.template.id));
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head title="Modifier Modèle" />
|
||||
|
||||
<AuthenticatedLayout>
|
||||
<template #header>
|
||||
<h2 class="text-xl font-semibold leading-tight text-gray-800 dark:text-gray-200">
|
||||
Modifier Modèle: {{ template.name }}
|
||||
</h2>
|
||||
</template>
|
||||
|
||||
<div class="py-12">
|
||||
<div class="mx-auto max-w-7xl sm:px-6 lg:px-8">
|
||||
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg dark:bg-gray-800 p-6">
|
||||
<form @submit.prevent="submit" class="space-y-6">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<InputLabel for="name" value="Nom du modèle" />
|
||||
<TextInput id="name" type="text" class="mt-1 block w-full" v-model="form.name" required autofocus />
|
||||
<InputError class="mt-2" :message="form.errors.name" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<InputLabel for="description" value="Description" />
|
||||
<TextInput id="description" type="text" class="mt-1 block w-full" v-model="form.description" />
|
||||
<InputError class="mt-2" :message="form.errors.description" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox" v-model="form.is_active" class="rounded border-gray-300 text-indigo-600 shadow-sm focus:ring-indigo-500 dark:border-gray-700 dark:bg-gray-900 dark:focus:ring-indigo-600 dark:focus:ring-offset-gray-800">
|
||||
<span class="ml-2 text-sm text-gray-600 dark:text-gray-400">Modèle Actif</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="border-t border-gray-200 dark:border-gray-700 pt-6">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100">Tâches par Service</h3>
|
||||
<button type="button" @click="addItem" class="text-sm bg-gray-200 hover:bg-gray-300 dark:bg-gray-700 dark:hover:bg-gray-600 px-3 py-1 rounded">
|
||||
+ Ajouter une tâche
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="form.items.length === 0" class="text-center text-gray-500 italic py-4">
|
||||
Aucune tâche définie. Ajoutez des tâches pour ce modèle.
|
||||
</div>
|
||||
|
||||
<div v-else class="space-y-4">
|
||||
<div v-for="(item, index) in form.items" :key="index" class="p-4 bg-gray-50 dark:bg-gray-700/30 rounded-lg space-y-4">
|
||||
<div class="flex items-start space-x-4">
|
||||
<div class="w-1/4">
|
||||
<InputLabel value="Service" class="mb-1" />
|
||||
<select v-model="item.service_id" class="block w-full border-gray-300 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-300 focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 rounded-md shadow-sm text-sm">
|
||||
<option v-for="service in services" :key="service.id" :value="service.id">
|
||||
{{ service.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<InputLabel value="Libellé de la tâche" class="mb-1" />
|
||||
<TextInput type="text" class="block w-full" v-model="item.label" required placeholder="Ex: Créer compte AD" />
|
||||
</div>
|
||||
<div class="pt-6">
|
||||
<label class="flex items-center">
|
||||
<input type="checkbox" v-model="item.is_mandatory" class="rounded border-gray-300 text-indigo-600 shadow-sm focus:ring-indigo-500 dark:border-gray-700 dark:bg-gray-900 dark:focus:ring-indigo-600 dark:focus:ring-offset-gray-800">
|
||||
<span class="ml-2 text-sm text-gray-600 dark:text-gray-400">Obligatoire</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="pt-6">
|
||||
<button type="button" @click="removeItem(index)" class="text-red-500 hover:text-red-700">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Custom Fields -->
|
||||
<div class="pl-4 border-l-2 border-indigo-200 dark:border-indigo-800 ml-4 space-y-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<h4 class="text-xs font-semibold uppercase text-gray-500 dark:text-gray-400">Données à saisir par le service</h4>
|
||||
<button type="button" @click="addField(item)" class="text-xs text-indigo-600 hover:text-indigo-800 dark:text-indigo-400 dark:hover:text-indigo-300 font-medium">
|
||||
+ Ajouter un champ
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-for="(field, fIndex) in item.fields" :key="fIndex" class="flex items-center space-x-2 bg-white dark:bg-gray-800 p-2 rounded shadow-sm border border-gray-200 dark:border-gray-700">
|
||||
<TextInput v-model="field.label" placeholder="Nom du champ (ex: Login)" class="text-xs w-1/3 py-1" />
|
||||
<select v-model="field.type" class="text-xs rounded-md border-gray-300 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-300 focus:border-indigo-500 focus:ring-indigo-500 py-1">
|
||||
<option value="text">Texte</option>
|
||||
<option value="checkbox">Case à cocher</option>
|
||||
<option value="date">Date</option>
|
||||
</select>
|
||||
<label class="flex items-center ml-2">
|
||||
<input type="checkbox" v-model="field.required" class="rounded border-gray-300 text-indigo-600 shadow-sm focus:ring-indigo-500 dark:border-gray-700 dark:bg-gray-900 dark:focus:ring-indigo-600 dark:focus:ring-offset-gray-800 h-3 w-3">
|
||||
<span class="ml-1 text-xs text-gray-600 dark:text-gray-400">Requis</span>
|
||||
</label>
|
||||
<button type="button" @click="removeField(item, fIndex)" class="text-red-500 hover:text-red-700 ml-auto p-1">
|
||||
x
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<InputError class="mt-2" :message="form.errors.items" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-4">
|
||||
<PrimaryButton :disabled="form.processing">Mettre à jour</PrimaryButton>
|
||||
<Link :href="route('templates.index')" class="text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 underline">Annuler</Link>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AuthenticatedLayout>
|
||||
</template>
|
||||
74
resources/js/Pages/Template/Index.vue
Normal file
74
resources/js/Pages/Template/Index.vue
Normal file
@@ -0,0 +1,74 @@
|
||||
<script setup>
|
||||
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue';
|
||||
import { Head, Link, router } from '@inertiajs/vue3';
|
||||
import PrimaryButton from '@/Components/PrimaryButton.vue';
|
||||
|
||||
const props = defineProps({
|
||||
templates: Array,
|
||||
});
|
||||
|
||||
const deleteTemplate = (template) => {
|
||||
if (confirm('Êtes-vous sûr de vouloir supprimer ce template ?')) {
|
||||
router.delete(route('templates.destroy', template.id));
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head title="Gestion des Templates" />
|
||||
|
||||
<AuthenticatedLayout>
|
||||
<template #header>
|
||||
<div class="flex justify-between items-center">
|
||||
<h2 class="text-xl font-semibold leading-tight text-gray-800 dark:text-gray-200">
|
||||
Gestion des Modèles d'Intégration
|
||||
</h2>
|
||||
<Link :href="route('templates.create')">
|
||||
<PrimaryButton>Nouveau Modèle</PrimaryButton>
|
||||
</Link>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="py-12">
|
||||
<div class="mx-auto max-w-7xl sm:px-6 lg:px-8">
|
||||
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg dark:bg-gray-800">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-left border-collapse">
|
||||
<thead>
|
||||
<tr class="border-b dark:border-gray-700 bg-gray-50 dark:bg-gray-700/50">
|
||||
<th class="px-6 py-3 text-xs font-bold uppercase text-gray-500 dark:text-gray-400">Nom</th>
|
||||
<th class="px-6 py-3 text-xs font-bold uppercase text-gray-500 dark:text-gray-400">Description</th>
|
||||
<th class="px-6 py-3 text-xs font-bold uppercase text-gray-500 dark:text-gray-400">Tâches</th>
|
||||
<th class="px-6 py-3 text-xs font-bold uppercase text-gray-500 dark:text-gray-400">Actif</th>
|
||||
<th class="px-6 py-3 text-xs font-bold uppercase text-gray-500 dark:text-gray-400 text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<tr v-for="template in templates" :key="template.id" class="hover:bg-gray-50 dark:hover:bg-gray-700/30">
|
||||
<td class="px-6 py-4 text-sm font-medium text-gray-900 dark:text-white">
|
||||
{{ template.name }}
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ template.description || '-' }}
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ template.service_items_count }}
|
||||
</td>
|
||||
<td class="px-6 py-4">
|
||||
<span :class="['px-2 inline-flex text-xs leading-5 font-semibold rounded-full', template.is_active ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800']">
|
||||
{{ template.is_active ? 'Oui' : 'Non' }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm text-right space-x-2">
|
||||
<Link :href="route('templates.edit', template.id)" class="text-indigo-600 hover:text-indigo-900 dark:text-indigo-400 dark:hover:text-indigo-300">Modifier</Link>
|
||||
<button @click="deleteTemplate(template)" class="text-red-600 hover:text-red-900 dark:text-red-400 dark:hover:text-red-300">Supprimer</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AuthenticatedLayout>
|
||||
</template>
|
||||
106
resources/js/Pages/User/Edit.vue
Normal file
106
resources/js/Pages/User/Edit.vue
Normal file
@@ -0,0 +1,106 @@
|
||||
<script setup>
|
||||
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue';
|
||||
import { Head, useForm, Link } from '@inertiajs/vue3';
|
||||
import PrimaryButton from '@/Components/PrimaryButton.vue';
|
||||
import InputError from '@/Components/InputError.vue';
|
||||
import InputLabel from '@/Components/InputLabel.vue';
|
||||
import TextInput from '@/Components/TextInput.vue';
|
||||
|
||||
const props = defineProps({
|
||||
user: Object,
|
||||
roles: Array,
|
||||
});
|
||||
|
||||
const form = useForm({
|
||||
name: props.user?.name || '',
|
||||
email: props.user?.email || '',
|
||||
password: '',
|
||||
roles: props.user?.roles.map(r => r.name) || [],
|
||||
});
|
||||
|
||||
const submit = () => {
|
||||
if (props.user) {
|
||||
form.put(route('users.update', props.user.id));
|
||||
} else {
|
||||
form.post(route('users.store'));
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head :title="user ? 'Modifier Utilisateur' : 'Nouvel Utilisateur'" />
|
||||
|
||||
<AuthenticatedLayout>
|
||||
<template #header>
|
||||
<h2 class="text-xl font-semibold leading-tight text-gray-800 dark:text-gray-200">
|
||||
{{ user ? 'Modifier Utilisateur' : 'Nouvel Utilisateur' }}
|
||||
</h2>
|
||||
</template>
|
||||
|
||||
<div class="py-12">
|
||||
<div class="mx-auto max-w-7xl sm:px-6 lg:px-8">
|
||||
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg dark:bg-gray-800 p-6">
|
||||
<form @submit.prevent="submit" class="space-y-6 max-w-xl">
|
||||
<div>
|
||||
<InputLabel for="name" value="Nom" />
|
||||
<TextInput
|
||||
id="name"
|
||||
type="text"
|
||||
class="mt-1 block w-full"
|
||||
v-model="form.name"
|
||||
required
|
||||
autofocus
|
||||
/>
|
||||
<InputError class="mt-2" :message="form.errors.name" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<InputLabel for="email" value="Email" />
|
||||
<TextInput
|
||||
id="email"
|
||||
type="email"
|
||||
class="mt-1 block w-full"
|
||||
v-model="form.email"
|
||||
required
|
||||
/>
|
||||
<InputError class="mt-2" :message="form.errors.email" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<InputLabel for="password" value="Mot de passe (laisser vide pour ne pas changer)" />
|
||||
<TextInput
|
||||
id="password"
|
||||
type="password"
|
||||
class="mt-1 block w-full"
|
||||
v-model="form.password"
|
||||
:required="!user"
|
||||
/>
|
||||
<InputError class="mt-2" :message="form.errors.password" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<span class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Rôles</span>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<label v-for="role in roles" :key="role.id" class="flex items-center space-x-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
:value="role.name"
|
||||
v-model="form.roles"
|
||||
class="rounded border-gray-300 text-indigo-600 shadow-sm focus:ring-indigo-500 dark:border-gray-700 dark:bg-gray-900 dark:focus:ring-indigo-600 dark:focus:ring-offset-gray-800"
|
||||
>
|
||||
<span class="text-sm text-gray-700 dark:text-gray-300">{{ role.name }}</span>
|
||||
</label>
|
||||
</div>
|
||||
<InputError class="mt-2" :message="form.errors.roles" />
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-4">
|
||||
<PrimaryButton :disabled="form.processing">Enregistrer</PrimaryButton>
|
||||
<Link :href="route('users.index')" class="text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 underline">Annuler</Link>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AuthenticatedLayout>
|
||||
</template>
|
||||
70
resources/js/Pages/User/Index.vue
Normal file
70
resources/js/Pages/User/Index.vue
Normal file
@@ -0,0 +1,70 @@
|
||||
<script setup>
|
||||
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue';
|
||||
import { Head, Link, router } from '@inertiajs/vue3';
|
||||
import PrimaryButton from '@/Components/PrimaryButton.vue';
|
||||
|
||||
const props = defineProps({
|
||||
users: Array,
|
||||
});
|
||||
|
||||
const deleteUser = (user) => {
|
||||
if (confirm('Êtes-vous sûr de vouloir supprimer cet utilisateur ?')) {
|
||||
router.delete(route('users.destroy', user.id));
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head title="Gestion des Utilisateurs" />
|
||||
|
||||
<AuthenticatedLayout>
|
||||
<template #header>
|
||||
<div class="flex justify-between items-center">
|
||||
<h2 class="text-xl font-semibold leading-tight text-gray-800 dark:text-gray-200">
|
||||
Gestion des Utilisateurs
|
||||
</h2>
|
||||
<Link :href="route('users.create')">
|
||||
<PrimaryButton>Nouvel Utilisateur</PrimaryButton>
|
||||
</Link>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="py-12">
|
||||
<div class="mx-auto max-w-7xl sm:px-6 lg:px-8">
|
||||
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg dark:bg-gray-800">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-left border-collapse">
|
||||
<thead>
|
||||
<tr class="border-b dark:border-gray-700 bg-gray-50 dark:bg-gray-700/50">
|
||||
<th class="px-6 py-3 text-xs font-bold uppercase text-gray-500 dark:text-gray-400">Nom</th>
|
||||
<th class="px-6 py-3 text-xs font-bold uppercase text-gray-500 dark:text-gray-400">Email</th>
|
||||
<th class="px-6 py-3 text-xs font-bold uppercase text-gray-500 dark:text-gray-400">Rôles</th>
|
||||
<th class="px-6 py-3 text-xs font-bold uppercase text-gray-500 dark:text-gray-400 text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<tr v-for="user in users" :key="user.id" class="hover:bg-gray-50 dark:hover:bg-gray-700/30">
|
||||
<td class="px-6 py-4 text-sm font-medium text-gray-900 dark:text-white">
|
||||
{{ user.name }}
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ user.email }}
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm text-gray-500 dark:text-gray-400">
|
||||
<span v-for="role in user.roles" :key="role.id" class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-blue-100 text-blue-800 mr-1">
|
||||
{{ role.name }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm text-right space-x-2">
|
||||
<Link :href="route('users.edit', user.id)" class="text-indigo-600 hover:text-indigo-900 dark:text-indigo-400 dark:hover:text-indigo-300">Modifier</Link>
|
||||
<button @click="deleteUser(user)" class="text-red-600 hover:text-red-900 dark:text-red-400 dark:hover:text-red-300">Supprimer</button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AuthenticatedLayout>
|
||||
</template>
|
||||
163
resources/js/Pages/Welcome.vue
Normal file
163
resources/js/Pages/Welcome.vue
Normal file
@@ -0,0 +1,163 @@
|
||||
<script setup>
|
||||
import { Head, Link } from '@inertiajs/vue3';
|
||||
import ApplicationLogo from '@/Components/ApplicationLogo.vue';
|
||||
|
||||
defineProps({
|
||||
canLogin: {
|
||||
type: Boolean,
|
||||
},
|
||||
canRegister: {
|
||||
type: Boolean,
|
||||
},
|
||||
laravelVersion: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
phpVersion: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head title="Bienvenue - Fiche Agent" />
|
||||
|
||||
<div class="bg-gray-50 text-black/50 dark:bg-black dark:text-white/50 min-h-screen flex flex-col">
|
||||
<!-- Background Effects -->
|
||||
<div class="fixed inset-0 z-0 overflow-hidden pointer-events-none">
|
||||
<div class="absolute -top-[30%] -left-[10%] w-[70%] h-[70%] rounded-full bg-red-500/10 blur-[120px] dark:bg-red-900/20"></div>
|
||||
<div class="absolute top-[40%] -right-[10%] w-[60%] h-[60%] rounded-full bg-yellow-500/10 blur-[100px] dark:bg-yellow-600/10"></div>
|
||||
</div>
|
||||
|
||||
<div class="relative w-full max-w-7xl mx-auto px-6 lg:px-8 z-10 flex-grow flex flex-col">
|
||||
<!-- Header -->
|
||||
<header class="flex justify-between items-center py-6">
|
||||
<div class="flex items-center gap-2">
|
||||
<ApplicationLogo class="w-12 h-12" />
|
||||
<span class="text-xl font-bold text-gray-900 dark:text-white tracking-tight">
|
||||
CAP <span class="text-red-600">CABM</span>
|
||||
</span>
|
||||
</div>
|
||||
<nav v-if="canLogin" class="flex gap-4">
|
||||
<Link
|
||||
v-if="$page.props.auth.user"
|
||||
:href="route('dashboard')"
|
||||
class="rounded-full px-5 py-2 text-sm font-semibold text-white bg-red-600 hover:bg-red-500 transition focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 dark:focus:ring-offset-black"
|
||||
>
|
||||
Tableau de Bord
|
||||
</Link>
|
||||
|
||||
<template v-else>
|
||||
<Link
|
||||
:href="route('login')"
|
||||
class="rounded-full px-5 py-2 text-sm font-semibold text-gray-900 ring-1 ring-gray-900/10 hover:ring-gray-900/20 dark:text-white dark:ring-white/20 dark:hover:ring-white/30 transition"
|
||||
>
|
||||
Connexion
|
||||
</Link>
|
||||
</template>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<!-- Hero Section -->
|
||||
<main class="flex-grow flex flex-col justify-center items-center text-center mt-10 lg:mt-20 mb-20">
|
||||
<div class="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-red-50 dark:bg-red-900/30 text-red-600 dark:text-red-300 text-xs font-medium mb-6 animate-fade-in-up">
|
||||
<span class="relative flex h-2 w-2">
|
||||
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-red-400 opacity-75"></span>
|
||||
<span class="relative inline-flex rounded-full h-2 w-2 bg-red-500"></span>
|
||||
</span>
|
||||
Construire l’Accueil et le Parcours
|
||||
</div>
|
||||
|
||||
<h1 class="text-5xl md:text-7xl font-extrabold tracking-tight text-gray-900 dark:text-white mb-6 max-w-4xl">
|
||||
CAP <span class="text-transparent bg-clip-text bg-gradient-to-r from-red-600 via-yellow-500 to-blue-600">CABM</span>
|
||||
</h1>
|
||||
|
||||
<p class="text-lg md:text-xl text-gray-600 dark:text-gray-300 max-w-2xl mb-10 leading-relaxed">
|
||||
La plateforme dédiée à l'accueil et au parcours des agents de l'Agglomération Béziers Méditerranée.
|
||||
</p>
|
||||
|
||||
<div class="flex flex-col sm:flex-row gap-4 w-full sm:w-auto">
|
||||
<Link
|
||||
v-if="$page.props.auth.user"
|
||||
:href="route('dashboard')"
|
||||
class="px-8 py-3.5 rounded-full text-base font-bold text-white bg-gradient-to-r from-red-600 to-red-800 hover:from-red-500 hover:to-red-700 shadow-lg shadow-red-500/30 transition transform hover:-translate-y-0.5"
|
||||
>
|
||||
Accéder à mon espace
|
||||
</Link>
|
||||
<Link
|
||||
v-else
|
||||
:href="route('login')"
|
||||
class="px-8 py-3.5 rounded-full text-base font-bold text-white bg-gradient-to-r from-red-600 to-red-800 hover:from-red-500 hover:to-red-700 shadow-lg shadow-red-500/30 transition transform hover:-translate-y-0.5"
|
||||
>
|
||||
Commencer maintenant
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<!-- Features Grid -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-8 mt-24 w-full max-w-6xl text-left">
|
||||
<div class="bg-white/50 dark:bg-gray-800/50 backdrop-blur-sm p-6 rounded-2xl border border-gray-100 dark:border-gray-700 hover:border-red-500/50 transition duration-300 group">
|
||||
<div class="w-12 h-12 bg-red-100 dark:bg-red-900/50 rounded-lg flex items-center justify-center mb-4 group-hover:scale-110 transition duration-300">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-red-600 dark:text-red-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-xl font-bold text-gray-900 dark:text-white mb-2">Ressources Humaines</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
Centralisez les demandes d'arrivée et de départ des agents de l'agglomération.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="bg-white/50 dark:bg-gray-800/50 backdrop-blur-sm p-6 rounded-2xl border border-gray-100 dark:border-gray-700 hover:border-blue-500/50 transition duration-300 group">
|
||||
<div class="w-12 h-12 bg-blue-100 dark:bg-blue-900/50 rounded-lg flex items-center justify-center mb-4 group-hover:scale-110 transition duration-300">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-blue-600 dark:text-blue-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-xl font-bold text-gray-900 dark:text-white mb-2">Services Supports</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
DSI, Bâtiment, Parc Auto : recevez vos ordres de mission automatiquement.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="bg-white/50 dark:bg-gray-800/50 backdrop-blur-sm p-6 rounded-2xl border border-gray-100 dark:border-gray-700 hover:border-yellow-500/50 transition duration-300 group">
|
||||
<div class="w-12 h-12 bg-yellow-100 dark:bg-yellow-900/50 rounded-lg flex items-center justify-center mb-4 group-hover:scale-110 transition duration-300">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-yellow-600 dark:text-yellow-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3 class="text-xl font-bold text-gray-900 dark:text-white mb-2">Efficacité & Suivi</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-400">
|
||||
Un tableau de bord aux couleurs de Béziers pour piloter l'activité en temps réel.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="py-8 text-center text-sm text-gray-500 dark:text-gray-400">
|
||||
© {{ new Date().getFullYear() }} CAP CABM. Tous droits réservés.
|
||||
<div class="mt-2 text-xs opacity-70">
|
||||
Laravel v{{ laravelVersion }} (PHP v{{ phpVersion }})
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.animate-fade-in-up {
|
||||
animation: fadeInUp 0.8s ease-out forwards;
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
27
resources/js/app.js
Normal file
27
resources/js/app.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import '../css/app.css';
|
||||
import './bootstrap';
|
||||
|
||||
import { createInertiaApp } from '@inertiajs/vue3';
|
||||
import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers';
|
||||
import { createApp, h } from 'vue';
|
||||
import { ZiggyVue } from '../../vendor/tightenco/ziggy';
|
||||
|
||||
const appName = import.meta.env.VITE_APP_NAME || 'Laravel';
|
||||
|
||||
createInertiaApp({
|
||||
title: (title) => `${title} - ${appName}`,
|
||||
resolve: (name) =>
|
||||
resolvePageComponent(
|
||||
`./Pages/${name}.vue`,
|
||||
import.meta.glob('./Pages/**/*.vue'),
|
||||
),
|
||||
setup({ el, App, props, plugin }) {
|
||||
return createApp({ render: () => h(App, props) })
|
||||
.use(plugin)
|
||||
.use(ZiggyVue)
|
||||
.mount(el);
|
||||
},
|
||||
progress: {
|
||||
color: '#4B5563',
|
||||
},
|
||||
});
|
||||
4
resources/js/bootstrap.js
vendored
Normal file
4
resources/js/bootstrap.js
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
import axios from 'axios';
|
||||
window.axios = axios;
|
||||
|
||||
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
|
||||
21
resources/views/app.blade.php
Normal file
21
resources/views/app.blade.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<title inertia>{{ config('app.name', 'Laravel') }}</title>
|
||||
|
||||
<!-- Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.bunny.net">
|
||||
<link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet" />
|
||||
|
||||
<!-- Scripts -->
|
||||
@routes
|
||||
@vite(['resources/js/app.js', "resources/js/Pages/{$page['component']}.vue"])
|
||||
@inertiaHead
|
||||
</head>
|
||||
<body class="font-sans antialiased">
|
||||
@inertia
|
||||
</body>
|
||||
</html>
|
||||
274
resources/views/pdf/integration.blade.php
Normal file
274
resources/views/pdf/integration.blade.php
Normal file
@@ -0,0 +1,274 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
|
||||
<title>Fiche Agent - {{ $agent->first_name }} {{ $agent->last_name }}</title>
|
||||
<style>
|
||||
@page {
|
||||
margin: 0cm 0cm;
|
||||
}
|
||||
body {
|
||||
font-family: 'Helvetica', 'Arial', sans-serif;
|
||||
margin-top: 3cm;
|
||||
margin-left: 2cm;
|
||||
margin-right: 2cm;
|
||||
margin-bottom: 2cm;
|
||||
color: #333;
|
||||
line-height: 1.5;
|
||||
}
|
||||
header {
|
||||
position: fixed;
|
||||
top: 0cm;
|
||||
left: 0cm;
|
||||
right: 0cm;
|
||||
height: 2.5cm;
|
||||
background-color: #f3f4f6; /* gray-100 */
|
||||
border-bottom: 2px solid #dc2626; /* red-600 */
|
||||
color: #1f2937;
|
||||
padding: 0.5cm 2cm;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.header-content {
|
||||
width: 100%;
|
||||
}
|
||||
.logo-text {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #1f2937;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.logo-subtext {
|
||||
color: #dc2626; /* red */
|
||||
}
|
||||
.document-title {
|
||||
text-align: right;
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #1f2937;
|
||||
position: absolute;
|
||||
top: 0.8cm;
|
||||
right: 2cm;
|
||||
}
|
||||
.section-title {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
background-color: #dc2626; /* red-600 */
|
||||
padding: 5px 10px;
|
||||
margin-top: 20px;
|
||||
margin-bottom: 10px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.info-grid {
|
||||
width: 100%;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.info-row {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.label {
|
||||
font-weight: bold;
|
||||
color: #4b5563; /* gray-600 */
|
||||
width: 30%;
|
||||
display: inline-block;
|
||||
}
|
||||
.value {
|
||||
color: #111827; /* gray-900 */
|
||||
font-weight: normal;
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 10px;
|
||||
font-size: 14px;
|
||||
}
|
||||
th {
|
||||
background-color: #f3f4f6;
|
||||
color: #374151;
|
||||
font-weight: bold;
|
||||
text-align: left;
|
||||
padding: 8px;
|
||||
border-bottom: 2px solid #e5e7eb;
|
||||
}
|
||||
td {
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
vertical-align: top;
|
||||
}
|
||||
.service-section {
|
||||
margin-top: 15px;
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
.service-header {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
color: #2563eb; /* blue-600 */
|
||||
border-bottom: 1px solid #bfdbfe;
|
||||
padding-bottom: 4px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.checkbox {
|
||||
display: inline-block;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border: 1px solid #9ca3af;
|
||||
margin-right: 6px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.checkbox.checked {
|
||||
background-color: #10b981; /* green-500 */
|
||||
border-color: #10b981;
|
||||
}
|
||||
footer {
|
||||
position: fixed;
|
||||
bottom: 0cm;
|
||||
left: 0cm;
|
||||
right: 0cm;
|
||||
height: 1.5cm;
|
||||
background-color: #f3f4f6;
|
||||
color: #6b7280;
|
||||
text-align: center;
|
||||
line-height: 1.5cm;
|
||||
font-size: 10px;
|
||||
border-top: 1px solid #e5e7eb;
|
||||
}
|
||||
.status-badge {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 12px;
|
||||
font-size: 10px;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
color: #fff;
|
||||
}
|
||||
.status-completed { background-color: #10b981; }
|
||||
.status-pending { background-color: #fbbf24; color: #78350f; }
|
||||
.status-processing { background-color: #3b82f6; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div class="header-content">
|
||||
<img src="{{ public_path('images/logo_agglo.png') }}" alt="Logo Agglomération Béziers Méditerranée" style="height: 2cm;">
|
||||
<div class="document-title">FICHE AGENT</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<footer>
|
||||
Document généré le {{ $date }} - Confidentiel RH
|
||||
</footer>
|
||||
|
||||
<!-- Agent Information -->
|
||||
<div class="section-title">IDENTITÉ DE L'AGENT</div>
|
||||
<div class="info-grid">
|
||||
<div class="info-row">
|
||||
<span class="label">Nom complet :</span>
|
||||
<span class="value" style="font-size: 16px; font-weight: bold; text-transform: uppercase;">{{ $agent->last_name }} {{ $agent->first_name }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="label">Poste :</span>
|
||||
<span class="value">{{ $agent->position }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="label">Service/Direction :</span>
|
||||
<span class="value">{{ $agent->department }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="label">Date d'arrivée :</span>
|
||||
<span class="value">{{ \Carbon\Carbon::parse($agent->arrival_date)->format('d/m/Y') }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="label">Email :</span>
|
||||
<span class="value">{{ $agent->email }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Integration Details -->
|
||||
<div class="section-title">DÉTAILS DE L'INTÉGRATION</div>
|
||||
<div class="info-grid">
|
||||
<div class="info-row">
|
||||
<span class="label">Type de demande :</span>
|
||||
<span class="value">{{ $integration->type === 'onboarding' ? 'Arrivée (Onboarding)' : 'Départ (Offboarding)' }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="label">Modèle utilisé :</span>
|
||||
<span class="value">{{ $integration->template ? $integration->template->name : 'Aucun (Standard)' }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="label">État global :</span>
|
||||
<span class="value">
|
||||
@if($integration->status === \App\Enums\IntegrationStatus::Completed)
|
||||
<span class="status-badge status-completed">Terminé</span>
|
||||
@elseif($integration->status === \App\Enums\IntegrationStatus::Draft)
|
||||
<span class="status-badge status-pending">Brouillon</span>
|
||||
@else
|
||||
<span class="status-badge status-processing">En Cours</span>
|
||||
@endif
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tasks List -->
|
||||
<div class="section-title">SUIVI DES TÂCHES PAR SERVICE</div>
|
||||
|
||||
@foreach($integration->serviceTasks as $task)
|
||||
<div class="service-section">
|
||||
<div class="service-header">{{ $task->service->name }}</div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="70%">Tâche / Équipement</th>
|
||||
<th width="30%">État</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach($task->taskItems as $item)
|
||||
@php
|
||||
// If integration is completed, skip non-mandatory items that were not completed
|
||||
$isIntegrationCompleted = $integration->status === \App\Enums\IntegrationStatus::Completed;
|
||||
if ($isIntegrationCompleted && !$item->is_mandatory && !$item->is_completed) {
|
||||
continue;
|
||||
}
|
||||
@endphp
|
||||
<tr>
|
||||
<td>
|
||||
<div class="checkbox {{ $item->is_completed ? 'checked' : '' }}"></div>
|
||||
{{ $item->label }}
|
||||
@if($item->data && is_array($item->data) && count($item->data) > 0)
|
||||
<div style="margin-left: 20px; margin-top: 4px; font-size: 12px; color: #4b5563;">
|
||||
@foreach($item->data as $key => $value)
|
||||
<div><strong>{{ ucfirst($key) }} :</strong> {{ $value }}</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
@if($item->is_completed)
|
||||
<div>Fait</div>
|
||||
@if($item->completedBy)
|
||||
<div style="font-size: 10px; color: #6b7280; margin-top: 2px;">
|
||||
par {{ $item->completedBy->name }}
|
||||
</div>
|
||||
@endif
|
||||
@else
|
||||
<span style="color: #6b7280;">À faire</span>
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
@if($task->taskItems->isEmpty())
|
||||
<tr>
|
||||
<td colspan="2" style="font-style: italic; color: #888;">Aucune tâche spécifique listée.</td>
|
||||
</tr>
|
||||
@endif
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@endforeach
|
||||
|
||||
<div style="margin-top: 30px; font-size: 12px; color: #666; border-top: 1px solid #ddd; padding-top: 10px;">
|
||||
<p>Ce document récapitule l'ensemble des actions et équipements prévus pour l'agent. Il peut servir de feuille de route pour les services concernés.</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user