189 lines
8.6 KiB
Vue
189 lines
8.6 KiB
Vue
<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>
|