372 lines
25 KiB
Vue
372 lines
25 KiB
Vue
<script setup>
|
|
import { Head, Link, router } from '@inertiajs/vue3';
|
|
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue';
|
|
import StatusBadge from '@/Components/StatusBadge.vue';
|
|
|
|
const props = defineProps({
|
|
order: Object,
|
|
});
|
|
|
|
// Lancer la transition de statut
|
|
const transitionTo = (status) => {
|
|
router.post(route('commandes.transition', { commande: props.order.data.id }), {
|
|
new_status: status,
|
|
}, {
|
|
preserveScroll: true,
|
|
});
|
|
};
|
|
|
|
// Supprimer la commande
|
|
const deleteOrder = () => {
|
|
if (confirm("Êtes-vous sûr de vouloir supprimer définitivement cette commande ainsi que ses pièces jointes ? Cette action est irréversible.")) {
|
|
router.delete(route('commandes.destroy', { commande: props.order.data.id }));
|
|
}
|
|
};
|
|
|
|
// Label français des statuts pour l'historique
|
|
const getStatusLabel = (status) => {
|
|
switch (status) {
|
|
case 'draft': return 'Brouillon';
|
|
case 'validated': return 'Validée';
|
|
case 'ordered': return 'Commandée';
|
|
case 'delivered': return 'Livrée';
|
|
case 'closed': return 'Clôturée';
|
|
default: return status || 'Création';
|
|
}
|
|
};
|
|
|
|
// Obtenir la couleur du point de la timeline
|
|
const getTimelineColor = (status) => {
|
|
switch (status) {
|
|
case 'draft': return 'bg-slate-400 dark:bg-slate-500';
|
|
case 'validated': return 'bg-sky-500 dark:bg-sky-400';
|
|
case 'ordered': return 'bg-amber-500 dark:bg-amber-400';
|
|
case 'delivered': return 'bg-emerald-500 dark:bg-emerald-400';
|
|
case 'closed': return 'bg-purple-500 dark:bg-purple-400';
|
|
default: return 'bg-gray-400';
|
|
}
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<Head :title="`Commande ${order.data.number}`" />
|
|
|
|
<AuthenticatedLayout>
|
|
<template #header>
|
|
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
|
<div class="flex items-center gap-3">
|
|
<h2 class="text-xl font-bold leading-tight text-slate-800 dark:text-slate-200">
|
|
Commande {{ order.data.number }}
|
|
</h2>
|
|
<StatusBadge :status="order.data.status" />
|
|
<span
|
|
v-if="order.data.is_overdue"
|
|
class="inline-flex items-center px-2 py-0.5 rounded text-xs font-bold bg-rose-100 text-rose-800 border border-rose-200 dark:bg-rose-950/40 dark:text-rose-300 dark:border-rose-900"
|
|
>
|
|
En Retard de Livraison
|
|
</span>
|
|
</div>
|
|
|
|
<div class="flex gap-2">
|
|
<Link
|
|
:href="route('commandes.index')"
|
|
class="inline-flex items-center px-4 py-2 text-sm font-semibold text-slate-700 bg-white border border-slate-300 rounded-lg hover:bg-slate-50 dark:bg-slate-900 dark:text-slate-300 dark:border-slate-850 dark:hover:bg-slate-800 transition-colors"
|
|
>
|
|
Retour à la liste
|
|
</Link>
|
|
|
|
<Link
|
|
v-if="order.data.can.update"
|
|
:href="route('commandes.edit', { commande: order.data.id })"
|
|
class="inline-flex items-center px-4 py-2 text-sm font-semibold text-white bg-sky-600 rounded-lg hover:bg-sky-500 dark:bg-sky-500 dark:hover:bg-sky-400 transition-colors shadow-sm"
|
|
>
|
|
<svg class="w-4 h-4 mr-1.5" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L6.83 18.75a4.48 4.48 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10" />
|
|
</svg>
|
|
Éditer
|
|
</Link>
|
|
|
|
<button
|
|
v-if="order.data.can.delete"
|
|
@click="deleteOrder"
|
|
class="inline-flex items-center px-4 py-2 text-sm font-semibold text-white bg-rose-600 rounded-lg hover:bg-rose-500 dark:bg-rose-650 dark:hover:bg-rose-550 transition-colors shadow-sm"
|
|
>
|
|
<svg class="w-4 h-4 mr-1.5" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" />
|
|
</svg>
|
|
Supprimer
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<div class="py-6">
|
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 space-y-6">
|
|
<!-- Notifications flash -->
|
|
<div v-if="$page.props.flash?.success" class="p-4 bg-emerald-50 border border-emerald-200 text-emerald-800 rounded-lg dark:bg-emerald-950/40 dark:border-emerald-900 dark:text-emerald-300 flex items-center shadow-sm">
|
|
<svg class="w-5 h-5 mr-3 shrink-0" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75 11.25 15 15 9.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
|
|
</svg>
|
|
<span>{{ $page.props.flash?.success }}</span>
|
|
</div>
|
|
|
|
<div v-if="$page.props.errors.error" class="p-4 bg-rose-50 border border-rose-200 text-rose-800 rounded-lg dark:bg-rose-950/40 dark:border-rose-900 dark:text-rose-300 flex items-center shadow-sm">
|
|
<svg class="w-5 h-5 mr-3 shrink-0" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m9-.75a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9 3.75h.008v.008H12v-.008Z" />
|
|
</svg>
|
|
<span>{{ $page.props.errors.error }}</span>
|
|
</div>
|
|
|
|
<!-- Section des transitions contextuelles de statut -->
|
|
<div class="bg-gradient-to-r from-sky-50 to-indigo-50 border border-sky-100 dark:from-slate-900 dark:to-slate-900 dark:border-slate-800 rounded-xl p-6 shadow-sm flex flex-col md:flex-row md:items-center justify-between gap-4">
|
|
<div>
|
|
<h3 class="text-sm font-bold text-slate-850 dark:text-slate-200">
|
|
Actions sur le cycle de vie de la commande
|
|
</h3>
|
|
<p class="text-xs text-slate-500 dark:text-slate-400 mt-0.5">
|
|
Faites avancer cette demande à l'étape suivante en fonction de votre rôle.
|
|
</p>
|
|
</div>
|
|
|
|
<div class="flex flex-wrap gap-2">
|
|
<!-- Transition Draft -> Validated -->
|
|
<button
|
|
v-if="order.data.can_transition_to.validated"
|
|
@click="transitionTo('validated')"
|
|
class="inline-flex items-center px-4 py-2 text-sm font-semibold text-white bg-sky-600 rounded-lg hover:bg-sky-500 transition-colors shadow-sm"
|
|
>
|
|
<svg class="w-4 h-4 mr-2" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="m4.5 12.75 6 6 9-13.5" />
|
|
</svg>
|
|
Valider la commande (Émettre BC)
|
|
</button>
|
|
|
|
<!-- Transition Validated -> Ordered -->
|
|
<button
|
|
v-if="order.data.can_transition_to.ordered"
|
|
@click="transitionTo('ordered')"
|
|
class="inline-flex items-center px-4 py-2 text-sm font-semibold text-white bg-amber-600 rounded-lg hover:bg-amber-500 transition-colors shadow-sm"
|
|
>
|
|
<svg class="w-4 h-4 mr-2" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M6 12 3.269 3.125A59.769 59.769 0 0 1 21.485 12 59.768 59.768 0 0 1 3.27 20.875L5.999 12Zm0 0h7.5" />
|
|
</svg>
|
|
Marquer comme Commandée
|
|
</button>
|
|
|
|
<!-- Transition Ordered -> Delivered -->
|
|
<button
|
|
v-if="order.data.can_transition_to.delivered"
|
|
@click="transitionTo('delivered')"
|
|
class="inline-flex items-center px-4 py-2 text-sm font-semibold text-white bg-emerald-600 rounded-lg hover:bg-emerald-500 transition-colors shadow-sm"
|
|
>
|
|
<svg class="w-4 h-4 mr-2" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 18.75a1.5 1.5 0 0 1-3 0m3 0a1.5 1.5 0 0 0-3 0m3 0h6m-9 0H3.375a1.125 1.125 0 0 1-1.125-1.125V14.25m17.25 4.5a1.5 1.5 0 0 1-3 0m3 0a1.5 1.5 0 0 0-3 0m3 0h1.125c.621 0 1.129-.504 1.129-1.125V11.25M3 14.25h16.5M5.25 14.25V4.5A2.25 2.25 0 0 1 7.5 2.25h6A2.25 2.25 0 0 1 15.75 4.5V14.25m-3-12h.008v.008H12.75V2.25Z" />
|
|
</svg>
|
|
Marquer comme Livrée (Réceptionnée)
|
|
</button>
|
|
|
|
<!-- Transition Delivered -> Closed -->
|
|
<button
|
|
v-if="order.data.can_transition_to.closed"
|
|
@click="transitionTo('closed')"
|
|
class="inline-flex items-center px-4 py-2 text-sm font-semibold text-white bg-purple-600 rounded-lg hover:bg-purple-500 transition-colors shadow-sm"
|
|
>
|
|
<svg class="w-4 h-4 mr-2" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="m20.25 7.5-.625 10.632a2.25 2.25 0 0 1-2.247 2.118H6.622a2.25 2.25 0 0 1-2.247-2.118L3.75 7.5M10 11.25h4M3.375 7.5h17.25c.621 0 1.125-.504 1.125-1.125v-1.5c0-.621-.504-1.125-1.125-1.125H3.375c-.621 0-1.125.504-1.125 1.125v1.5c0 .621.504 1.125 1.125 1.125Z" />
|
|
</svg>
|
|
Clôturer & Archiver la commande
|
|
</button>
|
|
|
|
<!-- Aucune transition possible -->
|
|
<span
|
|
v-if="!order.data.can_transition_to.validated && !order.data.can_transition_to.ordered && !order.data.can_transition_to.delivered && !order.data.can_transition_to.closed"
|
|
class="text-xs font-semibold text-slate-500 bg-slate-100 dark:bg-slate-800 dark:text-slate-400 px-3 py-2 rounded-lg"
|
|
>
|
|
{{ order.data.status === 'closed' ? 'Cette commande est archivée.' : 'En attente d\'une action par un profil autorisé.' }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Grille Principale (Fiche + Pièces Jointes / Historique) -->
|
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
|
|
<!-- Fiche Détails (Col 1 & 2) -->
|
|
<div class="lg:col-span-2 space-y-6">
|
|
<div class="bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-xl shadow-sm overflow-hidden">
|
|
<div class="border-b border-slate-100 dark:border-slate-850 px-6 py-4">
|
|
<h3 class="text-sm font-bold text-slate-800 dark:text-slate-200 uppercase tracking-wider">
|
|
Détails de la demande
|
|
</h3>
|
|
</div>
|
|
|
|
<dl class="grid grid-cols-1 md:grid-cols-2 divide-y divide-slate-100 dark:divide-slate-850 md:divide-y-0 text-sm">
|
|
<div class="px-6 py-4 space-y-1">
|
|
<dt class="text-xs font-semibold uppercase tracking-wider text-slate-400">Libellé / Article</dt>
|
|
<dd class="text-base font-bold text-slate-900 dark:text-white">{{ order.data.label }}</dd>
|
|
</div>
|
|
|
|
<div class="px-6 py-4 space-y-1">
|
|
<dt class="text-xs font-semibold uppercase tracking-wider text-slate-400">Type de commande</dt>
|
|
<dd class="text-slate-800 dark:text-slate-200 font-medium">{{ order.data.type }}</dd>
|
|
</div>
|
|
|
|
<div class="px-6 py-4 space-y-1 md:border-t border-slate-100 dark:border-slate-850">
|
|
<dt class="text-xs font-semibold uppercase tracking-wider text-slate-400">Fournisseur</dt>
|
|
<dd class="text-slate-800 dark:text-slate-200 font-medium">{{ order.data.supplier }}</dd>
|
|
</div>
|
|
|
|
<div class="px-6 py-4 space-y-1 md:border-t border-slate-100 dark:border-slate-850">
|
|
<dt class="text-xs font-semibold uppercase tracking-wider text-slate-400">Numéro de Devis</dt>
|
|
<dd class="text-slate-800 dark:text-slate-200 font-mono">{{ order.data.quote_number }}</dd>
|
|
</div>
|
|
|
|
<div class="px-6 py-4 space-y-1 border-t border-slate-100 dark:border-slate-850">
|
|
<dt class="text-xs font-semibold uppercase tracking-wider text-slate-400">Demandeur</dt>
|
|
<dd class="text-slate-800 dark:text-slate-200 font-medium">{{ order.data.requested_by }}</dd>
|
|
</div>
|
|
|
|
<div class="px-6 py-4 space-y-1 border-t border-slate-100 dark:border-slate-850">
|
|
<dt class="text-xs font-semibold uppercase tracking-wider text-slate-400">Prescripteur / Service à l'origine</dt>
|
|
<dd class="text-slate-800 dark:text-slate-200 font-semibold">{{ order.data.prescriber || 'Non spécifié' }}</dd>
|
|
</div>
|
|
|
|
<div class="px-6 py-4 space-y-1 border-t border-slate-100 dark:border-slate-850">
|
|
<dt class="text-xs font-semibold uppercase tracking-wider text-slate-400">Livraison souhaitée</dt>
|
|
<dd class="text-slate-800 dark:text-slate-200 font-medium">{{ order.data.delivery_deadline_formatted }}</dd>
|
|
</div>
|
|
|
|
<div class="px-6 py-4 space-y-1 border-t border-slate-100 dark:border-slate-850">
|
|
<dt class="text-xs font-semibold uppercase tracking-wider text-slate-400">Montant HT</dt>
|
|
<dd class="text-slate-800 dark:text-slate-200 font-semibold">
|
|
{{ new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(order.data.amount_ht) }}
|
|
</dd>
|
|
</div>
|
|
|
|
<div class="px-6 py-4 space-y-1 border-t border-slate-100 dark:border-slate-850 bg-slate-50/50 dark:bg-slate-950/20">
|
|
<dt class="text-xs font-semibold uppercase tracking-wider text-slate-400">
|
|
{{ order.data.exclude_vat ? 'Montant TTC (Sans TVA)' : 'Montant TTC (TVA 20%)' }}
|
|
</dt>
|
|
<dd class="text-lg font-bold text-sky-600 dark:text-sky-400">
|
|
{{ new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(order.data.amount_ttc) }}
|
|
</dd>
|
|
</div>
|
|
</dl>
|
|
</div>
|
|
|
|
<!-- Notes libres -->
|
|
<div class="bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-xl shadow-sm overflow-hidden">
|
|
<div class="border-b border-slate-100 dark:border-slate-850 px-6 py-4">
|
|
<h3 class="text-sm font-bold text-slate-800 dark:text-slate-200 uppercase tracking-wider">
|
|
Notes & Commentaires
|
|
</h3>
|
|
</div>
|
|
<div class="p-6 text-sm text-slate-700 dark:text-slate-300 leading-relaxed whitespace-pre-line">
|
|
{{ order.data.notes || 'Aucun commentaire sur cette commande.' }}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Pièces Jointes -->
|
|
<div class="bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-xl shadow-sm overflow-hidden">
|
|
<div class="border-b border-slate-100 dark:border-slate-850 px-6 py-4">
|
|
<h3 class="text-sm font-bold text-slate-800 dark:text-slate-200 uppercase tracking-wider">
|
|
Documents joints
|
|
</h3>
|
|
</div>
|
|
<div class="p-6">
|
|
<div v-if="order.data.attachments && order.data.attachments.length > 0" class="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
<div
|
|
v-for="file in order.data.attachments"
|
|
:key="file.id"
|
|
class="flex items-start p-3 border border-slate-200 dark:border-slate-800 hover:border-slate-300 dark:hover:border-slate-700 rounded-xl bg-slate-50/50 dark:bg-slate-950/20 hover:bg-slate-50 dark:hover:bg-slate-950/55 transition-colors"
|
|
>
|
|
<div class="p-2 bg-sky-50 dark:bg-slate-800 text-sky-600 dark:text-sky-400 rounded-lg mr-3">
|
|
<!-- Icône générique de fichier -->
|
|
<svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m2.25 0H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z" />
|
|
</svg>
|
|
</div>
|
|
<div class="flex-1 min-w-0">
|
|
<span class="block text-xxs font-semibold uppercase tracking-wider text-slate-400">
|
|
{{
|
|
file.file_type === 'quote' ? 'Devis' :
|
|
file.file_type === 'delivery_note' ? 'Bon de livraison' :
|
|
file.file_type === 'invoice' ? 'Facture' : file.file_type
|
|
}}
|
|
</span>
|
|
<a
|
|
:href="file.url"
|
|
target="_blank"
|
|
class="block text-sm font-bold text-slate-800 dark:text-slate-200 truncate hover:text-sky-600 dark:hover:text-sky-400 hover:underline mt-0.5"
|
|
>
|
|
{{ file.file_name }}
|
|
</a>
|
|
<span class="block text-xxs text-slate-400 mt-0.5">Ajouté le {{ file.created_at }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div v-else class="text-center py-6 text-sm text-slate-500 dark:text-slate-400">
|
|
Aucune pièce jointe n'a été téléversée pour le moment.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Journal d'historique (Col 3) -->
|
|
<div class="bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-xl shadow-sm overflow-hidden h-fit">
|
|
<div class="border-b border-slate-100 dark:border-slate-850 px-6 py-4">
|
|
<h3 class="text-sm font-bold text-slate-800 dark:text-slate-200 uppercase tracking-wider">
|
|
Historique des statuts
|
|
</h3>
|
|
</div>
|
|
<div class="p-6">
|
|
<ul v-if="order.data.status_logs && order.data.status_logs.length > 0" class="relative border-l border-slate-200 dark:border-slate-800 space-y-6 ml-2 pl-4">
|
|
<li
|
|
v-for="log in order.data.status_logs"
|
|
:key="log.id"
|
|
class="relative"
|
|
>
|
|
<!-- Point coloré -->
|
|
<div
|
|
:class="`absolute -left-6.5 top-1.5 h-3.5 w-3.5 rounded-full border-2 border-white dark:border-slate-900 ${getTimelineColor(log.new_status)} shadow-sm`"
|
|
></div>
|
|
|
|
<div class="space-y-1">
|
|
<div class="flex items-center justify-between gap-2">
|
|
<span class="text-sm font-bold text-slate-800 dark:text-slate-200">
|
|
{{ getStatusLabel(log.new_status) }}
|
|
</span>
|
|
<span class="text-xxs font-medium text-slate-400">
|
|
{{ log.changed_at }}
|
|
</span>
|
|
</div>
|
|
<p class="text-xs text-slate-500 dark:text-slate-400">
|
|
Auteur : <span class="font-semibold text-slate-600 dark:text-slate-300">{{ log.user.name }}</span>
|
|
<span class="text-slate-400 text-xxs font-normal">({{ log.user.role === 'chef_service' ? 'Chef de service' : 'Admin réseau' }})</span>
|
|
</p>
|
|
<p v-if="log.old_status" class="text-xxs text-slate-400">
|
|
Transition depuis : {{ getStatusLabel(log.old_status) }}
|
|
</p>
|
|
<p v-else class="text-xxs text-slate-400 font-semibold italic text-sky-600 dark:text-sky-400">
|
|
Création initiale de la demande
|
|
</p>
|
|
</div>
|
|
</li>
|
|
</ul>
|
|
<div v-else class="text-center py-6 text-sm text-slate-500 dark:text-slate-400">
|
|
Aucun journal de statut disponible.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</AuthenticatedLayout>
|
|
</template>
|
|
|
|
<style scoped>
|
|
/* Ajustement de la position du point de timeline sur la bordure gauche */
|
|
.-left-6\.5 {
|
|
left: -23px;
|
|
}
|
|
</style>
|