Initial commit: Import existing Laravel project
This commit is contained in:
390
resources/js/Pages/Commandes/Form.vue
Normal file
390
resources/js/Pages/Commandes/Form.vue
Normal file
@@ -0,0 +1,390 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { Head, Link, useForm } from '@inertiajs/vue3';
|
||||
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue';
|
||||
import InputLabel from '@/Components/InputLabel.vue';
|
||||
import TextInput from '@/Components/TextInput.vue';
|
||||
import InputError from '@/Components/InputError.vue';
|
||||
import PrimaryButton from '@/Components/PrimaryButton.vue';
|
||||
|
||||
const props = defineProps({
|
||||
order: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
isEdit: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
// Calcul de la date par défaut (+30 jours) au format YYYY-MM-DD
|
||||
const getDefaultDeadline = () => {
|
||||
const date = new Date();
|
||||
date.setDate(date.getDate() + 30);
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
return `${year}-${month}-${day}`;
|
||||
};
|
||||
|
||||
// Initialisation du formulaire Inertia
|
||||
const form = useForm({
|
||||
label: props.order?.data?.label || props.order?.label || '',
|
||||
type: props.order?.data?.type || props.order?.type || 'Matériel réseau / serveur',
|
||||
supplier: props.order?.data?.supplier || props.order?.supplier || '',
|
||||
quote_number: props.order?.data?.quote_number || props.order?.quote_number || '',
|
||||
amount_ht: props.order?.data?.amount_ht !== undefined && props.order?.data?.amount_ht !== null
|
||||
? String(props.order.data.amount_ht)
|
||||
: (props.order?.amount_ht !== undefined && props.order?.amount_ht !== null
|
||||
? String(props.order.amount_ht)
|
||||
: ''),
|
||||
requested_by: props.order?.data?.requested_by || props.order?.requested_by || 'Jérémy',
|
||||
prescriber: props.order?.data?.prescriber || props.order?.prescriber || '',
|
||||
delivery_deadline: props.order?.data?.delivery_deadline || props.order?.delivery_deadline || getDefaultDeadline(),
|
||||
notes: props.order?.data?.notes || props.order?.notes || '',
|
||||
exclude_vat: props.order?.data?.exclude_vat || props.order?.exclude_vat || false,
|
||||
quote_file: null,
|
||||
delivery_note_file: null,
|
||||
invoice_file: null,
|
||||
_method: props.isEdit ? 'put' : 'post', // Pour supporter l'upload de fichier en PHP/Laravel lors de la mise à jour
|
||||
});
|
||||
|
||||
// Calcul automatique du montant TTC (HT + 20% TVA) en temps réel
|
||||
const amountTtc = computed(() => {
|
||||
const ht = parseFloat(form.amount_ht);
|
||||
if (isNaN(ht) || ht < 0) return 0;
|
||||
if (form.exclude_vat) return ht.toFixed(2);
|
||||
return (ht * 1.20).toFixed(2);
|
||||
});
|
||||
|
||||
// Récupérer le fichier en cours d'édition si présent
|
||||
const getExistingFile = (type) => {
|
||||
const attachments = props.order?.data?.attachments || props.order?.attachments;
|
||||
if (!attachments) return null;
|
||||
return attachments.find(a => a.file_type === type);
|
||||
};
|
||||
|
||||
// Soumission du formulaire
|
||||
const submit = () => {
|
||||
if (props.isEdit) {
|
||||
const orderId = props.order?.data?.id || props.order?.id;
|
||||
form.post(route('commandes.update', { commande: orderId }), {
|
||||
forceFormData: true,
|
||||
});
|
||||
} else {
|
||||
form.post(route('commandes.store'));
|
||||
}
|
||||
};
|
||||
|
||||
const orderTypes = [
|
||||
'Matériel réseau / serveur',
|
||||
'Licences logicielles',
|
||||
'Consommables / câblage',
|
||||
'Prestations / services',
|
||||
];
|
||||
|
||||
const demandeurs = ['Jérémy', 'Sylvain', 'Kévin'];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head :title="isEdit ? `Modifier la Commande ${order?.data?.number || order?.number}` : 'Nouvelle Commande'" />
|
||||
|
||||
<AuthenticatedLayout>
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-xl font-bold leading-tight text-slate-800 dark:text-slate-200">
|
||||
{{ isEdit ? `Modifier la Commande ${order?.data?.number || order?.number}` : 'Nouvelle Demande de Commande' }}
|
||||
</h2>
|
||||
<Link
|
||||
:href="isEdit ? route('commandes.show', { commande: order?.data?.id || order?.id }) : 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
|
||||
</Link>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="py-6">
|
||||
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<!-- Formulaire -->
|
||||
<div class="bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-xl shadow-sm overflow-hidden">
|
||||
<form @submit.prevent="submit" class="p-6 space-y-6">
|
||||
|
||||
<!-- Détails Article -->
|
||||
<div class="border-b border-slate-100 dark:border-slate-850 pb-5">
|
||||
<h3 class="text-md font-bold text-slate-800 dark:text-slate-200 mb-1">
|
||||
Informations sur l'article
|
||||
</h3>
|
||||
<p class="text-xs text-slate-500 dark:text-slate-400">
|
||||
Saisissez la référence ou le libellé principal du matériel ou service demandé.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<!-- Libellé article -->
|
||||
<div class="md:col-span-3">
|
||||
<InputLabel for="label" value="Libellé / Référence article" class="font-bold" />
|
||||
<TextInput
|
||||
id="label"
|
||||
type="text"
|
||||
class="mt-1 block w-full"
|
||||
v-model="form.label"
|
||||
required
|
||||
placeholder="Ex: Serveur NAS Synology 8 baies ou 5x Licences Office 365"
|
||||
/>
|
||||
<InputError class="mt-2" :message="form.errors.label" />
|
||||
</div>
|
||||
|
||||
<!-- Type de commande -->
|
||||
<div>
|
||||
<InputLabel for="type" value="Type de commande" class="font-bold" />
|
||||
<select
|
||||
id="type"
|
||||
v-model="form.type"
|
||||
class="mt-1 block w-full text-sm rounded-lg border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:bg-slate-950 dark:border-slate-800 dark:text-slate-200"
|
||||
required
|
||||
>
|
||||
<option v-for="type in orderTypes" :key="type" :value="type">
|
||||
{{ type }}
|
||||
</option>
|
||||
</select>
|
||||
<InputError class="mt-2" :message="form.errors.type" />
|
||||
</div>
|
||||
|
||||
<!-- Demandeur -->
|
||||
<div>
|
||||
<InputLabel for="requested_by" value="Demandeur" class="font-bold" />
|
||||
<select
|
||||
id="requested_by"
|
||||
v-model="form.requested_by"
|
||||
class="mt-1 block w-full text-sm rounded-lg border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:bg-slate-950 dark:border-slate-800 dark:text-slate-200"
|
||||
required
|
||||
>
|
||||
<option v-for="d in demandeurs" :key="d" :value="d">
|
||||
{{ d }}
|
||||
</option>
|
||||
</select>
|
||||
<InputError class="mt-2" :message="form.errors.requested_by" />
|
||||
</div>
|
||||
|
||||
<!-- Prescripteur -->
|
||||
<div>
|
||||
<InputLabel for="prescriber" value="Prescripteur / Service à l'origine" class="font-bold" />
|
||||
<TextInput
|
||||
id="prescriber"
|
||||
type="text"
|
||||
class="mt-1 block w-full"
|
||||
v-model="form.prescriber"
|
||||
required
|
||||
placeholder="Ex: Service RH, Urbanisme..."
|
||||
/>
|
||||
<InputError class="mt-2" :message="form.errors.prescriber" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Fournisseur & Devis -->
|
||||
<div class="border-b border-slate-100 dark:border-slate-850 pb-5 pt-4">
|
||||
<h3 class="text-md font-bold text-slate-800 dark:text-slate-200 mb-1">
|
||||
Fournisseur & Devis
|
||||
</h3>
|
||||
<p class="text-xs text-slate-500 dark:text-slate-400">
|
||||
Références d'acquisition et détails financiers.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<!-- Fournisseur -->
|
||||
<div>
|
||||
<InputLabel for="supplier" value="Fournisseur" class="font-bold" />
|
||||
<TextInput
|
||||
id="supplier"
|
||||
type="text"
|
||||
class="mt-1 block w-full"
|
||||
v-model="form.supplier"
|
||||
required
|
||||
placeholder="Ex: LDLC Pro, Dell France..."
|
||||
/>
|
||||
<InputError class="mt-2" :message="form.errors.supplier" />
|
||||
</div>
|
||||
|
||||
<!-- Numéro de devis -->
|
||||
<div>
|
||||
<InputLabel for="quote_number" value="Numéro de devis" class="font-bold" />
|
||||
<TextInput
|
||||
id="quote_number"
|
||||
type="text"
|
||||
class="mt-1 block w-full"
|
||||
v-model="form.quote_number"
|
||||
required
|
||||
placeholder="Ex: DEV-2026-99182"
|
||||
/>
|
||||
<InputError class="mt-2" :message="form.errors.quote_number" />
|
||||
</div>
|
||||
|
||||
<!-- Montant HT -->
|
||||
<div>
|
||||
<InputLabel for="amount_ht" value="Montant HT (€)" class="font-bold" />
|
||||
<TextInput
|
||||
id="amount_ht"
|
||||
type="number"
|
||||
step="0.01"
|
||||
min="0"
|
||||
class="mt-1 block w-full"
|
||||
v-model="form.amount_ht"
|
||||
required
|
||||
placeholder="0.00"
|
||||
/>
|
||||
<InputError class="mt-2" :message="form.errors.amount_ht" />
|
||||
|
||||
<div class="mt-3 flex items-center">
|
||||
<input
|
||||
id="exclude_vat"
|
||||
type="checkbox"
|
||||
v-model="form.exclude_vat"
|
||||
class="rounded border-gray-300 text-sky-600 shadow-sm focus:ring-sky-500 dark:border-gray-700 dark:bg-gray-900"
|
||||
/>
|
||||
<label for="exclude_vat" class="ml-2 text-xs font-semibold text-slate-600 dark:text-slate-400 cursor-pointer select-none">
|
||||
Exonérer de TVA / Non soumis à la TVA
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Montant TTC (calculé) -->
|
||||
<div>
|
||||
<InputLabel for="amount_ttc" :value="form.exclude_vat ? 'Montant TTC (€) - Sans TVA' : 'Montant TTC (€) - TVA 20 % (Calculé)'" class="font-bold text-slate-400" />
|
||||
<input
|
||||
id="amount_ttc"
|
||||
type="text"
|
||||
readonly
|
||||
class="mt-1 block w-full text-sm rounded-lg border-slate-300 bg-slate-50 text-slate-500 shadow-sm focus:border-slate-300 focus:ring-0 dark:bg-slate-950 dark:border-slate-850 dark:text-slate-400 cursor-not-allowed font-semibold"
|
||||
:value="new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(amountTtc)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Date souhaitée de livraison -->
|
||||
<div>
|
||||
<InputLabel for="delivery_deadline" value="Date souhaitée de livraison" class="font-bold" />
|
||||
<TextInput
|
||||
id="delivery_deadline"
|
||||
type="date"
|
||||
class="mt-1 block w-full"
|
||||
v-model="form.delivery_deadline"
|
||||
required
|
||||
/>
|
||||
<InputError class="mt-2" :message="form.errors.delivery_deadline" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pièces jointes -->
|
||||
<div class="border-b border-slate-100 dark:border-slate-850 pb-5 pt-4">
|
||||
<h3 class="text-md font-bold text-slate-800 dark:text-slate-200 mb-1">
|
||||
Pièces jointes (PDF, Images, Word, Excel)
|
||||
</h3>
|
||||
<p class="text-xs text-slate-500 dark:text-slate-400">
|
||||
Joignez les fichiers officiels pour le cycle de vie de la commande.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<!-- Devis -->
|
||||
<div class="space-y-2">
|
||||
<InputLabel for="quote_file" value="Devis initial (PDF, Image...)" class="font-bold" />
|
||||
<input
|
||||
id="quote_file"
|
||||
type="file"
|
||||
@input="form.quote_file = $event.target.files[0]"
|
||||
class="block w-full text-xs text-slate-500 file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-xs file:font-semibold file:bg-sky-50 file:text-sky-700 hover:file:bg-sky-100 dark:file:bg-slate-800 dark:file:text-slate-300"
|
||||
/>
|
||||
<div v-if="getExistingFile('quote')" class="text-xs mt-1 text-slate-500">
|
||||
Fichier existant :
|
||||
<a :href="getExistingFile('quote').url" target="_blank" class="text-sky-600 dark:text-sky-400 hover:underline inline-flex items-center">
|
||||
{{ getExistingFile('quote').file_name }}
|
||||
</a>
|
||||
</div>
|
||||
<InputError class="mt-2" :message="form.errors.quote_file" />
|
||||
</div>
|
||||
|
||||
<!-- Bon de livraison -->
|
||||
<div class="space-y-2">
|
||||
<InputLabel for="delivery_note_file" value="Bon de livraison" class="font-bold" />
|
||||
<input
|
||||
id="delivery_note_file"
|
||||
type="file"
|
||||
@input="form.delivery_note_file = $event.target.files[0]"
|
||||
class="block w-full text-xs text-slate-500 file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-xs file:font-semibold file:bg-sky-50 file:text-sky-700 hover:file:bg-sky-100 dark:file:bg-slate-800 dark:file:text-slate-300"
|
||||
/>
|
||||
<div v-if="getExistingFile('delivery_note')" class="text-xs mt-1 text-slate-500">
|
||||
Fichier existant :
|
||||
<a :href="getExistingFile('delivery_note').url" target="_blank" class="text-sky-600 dark:text-sky-400 hover:underline inline-flex items-center">
|
||||
{{ getExistingFile('delivery_note').file_name }}
|
||||
</a>
|
||||
</div>
|
||||
<InputError class="mt-2" :message="form.errors.delivery_note_file" />
|
||||
</div>
|
||||
|
||||
<!-- Facture -->
|
||||
<div class="space-y-2">
|
||||
<InputLabel for="invoice_file" value="Facture d'achat" class="font-bold" />
|
||||
<input
|
||||
id="invoice_file"
|
||||
type="file"
|
||||
@input="form.invoice_file = $event.target.files[0]"
|
||||
class="block w-full text-xs text-slate-500 file:mr-4 file:py-2 file:px-4 file:rounded-md file:border-0 file:text-xs file:font-semibold file:bg-sky-50 file:text-sky-700 hover:file:bg-sky-100 dark:file:bg-slate-800 dark:file:text-slate-300"
|
||||
/>
|
||||
<div v-if="getExistingFile('invoice')" class="text-xs mt-1 text-slate-500">
|
||||
Fichier existant :
|
||||
<a :href="getExistingFile('invoice').url" target="_blank" class="text-sky-600 dark:text-sky-400 hover:underline inline-flex items-center">
|
||||
{{ getExistingFile('invoice').file_name }}
|
||||
</a>
|
||||
</div>
|
||||
<InputError class="mt-2" :message="form.errors.invoice_file" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Notes libres -->
|
||||
<div class="border-b border-slate-100 dark:border-slate-850 pb-5 pt-4">
|
||||
<h3 class="text-md font-bold text-slate-800 dark:text-slate-200 mb-1">
|
||||
Notes & Commentaires
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<InputLabel for="notes" value="Notes libres" class="font-bold" />
|
||||
<textarea
|
||||
id="notes"
|
||||
rows="4"
|
||||
class="mt-1 block w-full text-sm rounded-lg border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:bg-slate-950 dark:border-slate-800 dark:text-slate-200"
|
||||
v-model="form.notes"
|
||||
placeholder="Commentaires libres sur la commande, le suivi fournisseur, ou l'utilisation du matériel..."
|
||||
></textarea>
|
||||
<InputError class="mt-2" :message="form.errors.notes" />
|
||||
</div>
|
||||
|
||||
<!-- Barre de boutons -->
|
||||
<div class="flex items-center justify-end gap-3 pt-6 border-t border-slate-100 dark:border-slate-850">
|
||||
<Link
|
||||
:href="isEdit ? route('commandes.show', { commande: order?.data?.id || order?.id }) : route('commandes.index')"
|
||||
class="inline-flex items-center px-4 py-2 border border-slate-300 text-sm font-semibold rounded-lg text-slate-700 bg-white hover:bg-slate-50 dark:bg-slate-900 dark:text-slate-300 dark:border-slate-850 dark:hover:bg-slate-800 transition-colors"
|
||||
>
|
||||
Annuler
|
||||
</Link>
|
||||
|
||||
<PrimaryButton
|
||||
type="submit"
|
||||
:disabled="form.processing"
|
||||
class="inline-flex items-center px-5 py-2.5 text-sm font-semibold text-white bg-sky-600 hover:bg-sky-500 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-sky-500 transition-colors"
|
||||
>
|
||||
<svg v-if="form.processing" class="animate-spin -ml-1 mr-3 h-4 w-4 text-white" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
{{ isEdit ? 'Mettre à jour' : 'Créer la demande' }}
|
||||
</PrimaryButton>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AuthenticatedLayout>
|
||||
</template>
|
||||
368
resources/js/Pages/Commandes/Index.vue
Normal file
368
resources/js/Pages/Commandes/Index.vue
Normal file
@@ -0,0 +1,368 @@
|
||||
<script setup>
|
||||
import { ref, reactive } from 'vue';
|
||||
import { Head, Link, router } from '@inertiajs/vue3';
|
||||
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue';
|
||||
import StatusBadge from '@/Components/StatusBadge.vue';
|
||||
|
||||
const props = defineProps({
|
||||
orders: Object,
|
||||
filters: Object,
|
||||
});
|
||||
|
||||
// État des filtres réactifs
|
||||
const form = reactive({
|
||||
search: props.filters?.search || '',
|
||||
status: props.filters?.status || '',
|
||||
requested_by: props.filters?.requested_by || '',
|
||||
type: props.filters?.type || '',
|
||||
date_start: props.filters?.date_start || '',
|
||||
date_end: props.filters?.date_end || '',
|
||||
});
|
||||
|
||||
// Appliquer les filtres
|
||||
const applyFilters = () => {
|
||||
router.get(route('commandes.index'), form, {
|
||||
preserveState: true,
|
||||
replace: true,
|
||||
});
|
||||
};
|
||||
|
||||
// Réinitialiser les filtres
|
||||
const resetFilters = () => {
|
||||
form.search = '';
|
||||
form.status = '';
|
||||
form.requested_by = '';
|
||||
form.type = '';
|
||||
form.date_start = '';
|
||||
form.date_end = '';
|
||||
applyFilters();
|
||||
};
|
||||
|
||||
// Exporter en CSV
|
||||
const exportCsv = () => {
|
||||
// Génère l'URL d'export avec les filtres courants
|
||||
const params = new URLSearchParams(form).toString();
|
||||
window.location.href = `${route('commandes.index')}?export=1&${params}`;
|
||||
};
|
||||
|
||||
// Types de commandes pour le filtre
|
||||
const orderTypes = [
|
||||
'Matériel réseau / serveur',
|
||||
'Licences logicielles',
|
||||
'Consommables / câblage',
|
||||
'Prestations / services',
|
||||
];
|
||||
|
||||
// Demandeur pour le filtre
|
||||
const demandeurs = ['Jérémy', 'Sylvain', 'Kévin'];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head title="Suivi des Commandes" />
|
||||
|
||||
<AuthenticatedLayout>
|
||||
<template #header>
|
||||
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
||||
<h2 class="text-xl font-bold leading-tight text-slate-800 dark:text-slate-200">
|
||||
Suivi des Commandes
|
||||
</h2>
|
||||
<div class="flex gap-2">
|
||||
<button
|
||||
@click="exportCsv"
|
||||
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 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-sky-500 dark:bg-slate-900 dark:text-slate-300 dark:border-slate-755 dark:hover:bg-slate-800 transition-colors"
|
||||
>
|
||||
<svg class="w-4 h-4 mr-2" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75V16.5M16.5 12 12 16.5m0 0L7.5 12m4.5 4.5V3" />
|
||||
</svg>
|
||||
Exporter en CSV
|
||||
</button>
|
||||
<Link
|
||||
:href="route('commandes.create')"
|
||||
class="inline-flex items-center px-4 py-2 text-sm font-semibold text-white bg-sky-600 rounded-lg hover:bg-sky-500 shadow-sm focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-sky-500 dark:bg-sky-500 dark:hover:bg-sky-400 transition-colors"
|
||||
>
|
||||
<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="M12 4.5v15m7.5-7.5h-15" />
|
||||
</svg>
|
||||
Créer une demande
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="py-6">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<!-- Session Flash Messages -->
|
||||
<div v-if="$page.props.flash?.success" class="mb-6 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>
|
||||
|
||||
<!-- Filtres -->
|
||||
<div class="bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-xl p-5 shadow-sm mb-6">
|
||||
<h3 class="text-sm font-bold text-slate-800 dark:text-slate-200 mb-4 flex items-center uppercase tracking-wider">
|
||||
<svg class="w-4 h-4 mr-2 text-slate-400" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 3c2.755 0 5.455.232 8.083.678.533.09.917.556.917 1.096v1.044a2.25 2.25 0 0 1-.659 1.591l-5.432 5.432a2.25 2.25 0 0 0-.659 1.591v2.927a2.25 2.25 0 0 1-1.244 2.013L9.75 21v-6.562a2.25 2.25 0 0 0-.659-1.591L3.659 7.409A2.25 2.25 0 0 1 3 5.818V4.774c0-.54.384-1.006.917-1.096A48.32 48.32 0 0 1 12 3Z" />
|
||||
</svg>
|
||||
Filtres de recherche
|
||||
</h3>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
||||
<!-- Recherche textuelle -->
|
||||
<div class="md:col-span-2 lg:col-span-1">
|
||||
<label class="block text-xs font-semibold text-slate-500 dark:text-slate-400 mb-1">Recherche</label>
|
||||
<input
|
||||
type="text"
|
||||
v-model="form.search"
|
||||
placeholder="Numéro, libellé, fournisseur..."
|
||||
@keydown.enter="applyFilters"
|
||||
class="w-full text-sm rounded-lg border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:bg-slate-950 dark:border-slate-800 dark:text-slate-200"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Statut -->
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-slate-500 dark:text-slate-400 mb-1">Statut</label>
|
||||
<select
|
||||
v-model="form.status"
|
||||
@change="applyFilters"
|
||||
class="w-full text-sm rounded-lg border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:bg-slate-950 dark:border-slate-800 dark:text-slate-200"
|
||||
>
|
||||
<option value="">Tous les statuts</option>
|
||||
<option value="draft">Brouillon</option>
|
||||
<option value="validated">Validée</option>
|
||||
<option value="ordered">Commandée</option>
|
||||
<option value="delivered">Livrée</option>
|
||||
<option value="closed">Clôturée</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Demandeur -->
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-slate-500 dark:text-slate-400 mb-1">Demandeur</label>
|
||||
<select
|
||||
v-model="form.requested_by"
|
||||
@change="applyFilters"
|
||||
class="w-full text-sm rounded-lg border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:bg-slate-950 dark:border-slate-800 dark:text-slate-200"
|
||||
>
|
||||
<option value="">Tous les demandeurs</option>
|
||||
<option v-for="d in demandeurs" :key="d" :value="d">{{ d }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Type -->
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-slate-500 dark:text-slate-400 mb-1">Type de commande</label>
|
||||
<select
|
||||
v-model="form.type"
|
||||
@change="applyFilters"
|
||||
class="w-full text-sm rounded-lg border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:bg-slate-950 dark:border-slate-800 dark:text-slate-200"
|
||||
>
|
||||
<option value="">Tous les types</option>
|
||||
<option v-for="t in orderTypes" :key="t" :value="t">{{ t }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Période début -->
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-slate-500 dark:text-slate-400 mb-1">Livraison souhaitée du</label>
|
||||
<input
|
||||
type="date"
|
||||
v-model="form.date_start"
|
||||
@change="applyFilters"
|
||||
class="w-full text-sm rounded-lg border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:bg-slate-950 dark:border-slate-800 dark:text-slate-200"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Période fin -->
|
||||
<div>
|
||||
<label class="block text-xs font-semibold text-slate-500 dark:text-slate-400 mb-1">Au</label>
|
||||
<input
|
||||
type="date"
|
||||
v-model="form.date_end"
|
||||
@change="applyFilters"
|
||||
class="w-full text-sm rounded-lg border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:bg-slate-950 dark:border-slate-800 dark:text-slate-200"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Actions filtres -->
|
||||
<div class="flex items-end gap-2 md:col-span-3 lg:col-span-2">
|
||||
<button
|
||||
@click="applyFilters"
|
||||
class="inline-flex items-center justify-center px-4 py-2 border border-transparent text-sm font-semibold rounded-lg text-white bg-slate-800 hover:bg-slate-700 dark:bg-slate-800 dark:hover:bg-slate-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-slate-500 transition-colors w-full sm:w-auto"
|
||||
>
|
||||
Filtrer
|
||||
</button>
|
||||
<button
|
||||
@click="resetFilters"
|
||||
class="inline-flex items-center justify-center px-4 py-2 border border-slate-300 text-sm font-semibold rounded-lg text-slate-700 bg-white hover:bg-slate-50 dark:bg-slate-900 dark:text-slate-300 dark:border-slate-800 dark:hover:bg-slate-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-slate-500 transition-colors w-full sm:w-auto"
|
||||
>
|
||||
Réinitialiser
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tableau des commandes -->
|
||||
<div class="bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-xl shadow-sm overflow-hidden">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-slate-200 dark:divide-slate-800 text-left">
|
||||
<thead class="bg-slate-50 dark:bg-slate-950">
|
||||
<tr>
|
||||
<th scope="col" class="px-6 py-4 text-xs font-bold uppercase tracking-wider text-slate-500 dark:text-slate-400">Numéro</th>
|
||||
<th scope="col" class="px-6 py-4 text-xs font-bold uppercase tracking-wider text-slate-500 dark:text-slate-400">Libellé / Article</th>
|
||||
<th scope="col" class="px-6 py-4 text-xs font-bold uppercase tracking-wider text-slate-500 dark:text-slate-400">Type</th>
|
||||
<th scope="col" class="px-6 py-4 text-xs font-bold uppercase tracking-wider text-slate-500 dark:text-slate-400">Fournisseur</th>
|
||||
<th scope="col" class="px-6 py-4 text-xs font-bold uppercase tracking-wider text-slate-500 dark:text-slate-400">Demandeur / Service</th>
|
||||
<th scope="col" class="px-6 py-4 text-xs font-bold uppercase tracking-wider text-slate-500 dark:text-slate-400">Date souhaitée</th>
|
||||
<th scope="col" class="px-6 py-4 text-xs font-bold uppercase tracking-wider text-slate-500 dark:text-slate-400 text-right">Montant TTC</th>
|
||||
<th scope="col" class="px-6 py-4 text-xs font-bold uppercase tracking-wider text-slate-500 dark:text-slate-400">Statut</th>
|
||||
<th scope="col" class="px-6 py-4 text-xs font-bold uppercase tracking-wider text-slate-500 dark:text-slate-400 text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-200 dark:divide-slate-850">
|
||||
<tr
|
||||
v-for="order in orders?.data || []"
|
||||
:key="order.id"
|
||||
class="hover:bg-slate-50/50 dark:hover:bg-slate-900/40 transition-colors"
|
||||
>
|
||||
<!-- Numéro -->
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-semibold text-sky-600 dark:text-sky-400">
|
||||
<Link :href="route('commandes.show', { commande: order.id })" class="hover:underline">
|
||||
{{ order.number }}
|
||||
</Link>
|
||||
</td>
|
||||
|
||||
<!-- Libellé -->
|
||||
<td class="px-6 py-4 text-sm text-slate-900 dark:text-slate-200 max-w-xs truncate">
|
||||
<span class="font-medium">{{ order.label }}</span>
|
||||
</td>
|
||||
|
||||
<!-- Type -->
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-slate-500 dark:text-slate-400">
|
||||
{{ order.type }}
|
||||
</td>
|
||||
|
||||
<!-- Fournisseur -->
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-slate-900 dark:text-slate-300 font-medium">
|
||||
{{ order.supplier }}
|
||||
</td>
|
||||
|
||||
<!-- Demandeur / Service -->
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-slate-900 dark:text-slate-350">
|
||||
<div class="font-medium text-slate-800 dark:text-slate-200">{{ order.requested_by }}</div>
|
||||
<div class="text-xs text-slate-450 dark:text-slate-400 font-semibold">{{ order.prescriber }}</div>
|
||||
</td>
|
||||
|
||||
<!-- Date -->
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-slate-500 dark:text-slate-400">
|
||||
<div class="flex items-center gap-1.5">
|
||||
<span>{{ order.delivery_deadline_formatted }}</span>
|
||||
<!-- Alerte de retard -->
|
||||
<span
|
||||
v-if="order.is_overdue"
|
||||
class="inline-flex items-center px-1.5 py-0.5 rounded text-xxs 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"
|
||||
title="Date souhaitée de livraison dépassée"
|
||||
>
|
||||
RETARD
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<!-- Montant TTC -->
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-slate-900 dark:text-slate-100 text-right font-bold">
|
||||
{{ new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(order.amount_ttc) }}
|
||||
</td>
|
||||
|
||||
<!-- Statut -->
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm">
|
||||
<StatusBadge :status="order.status" />
|
||||
</td>
|
||||
|
||||
<!-- Actions -->
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-semibold text-right space-x-3">
|
||||
<Link
|
||||
:href="route('commandes.show', { commande: order.id })"
|
||||
class="text-slate-600 hover:text-slate-900 dark:text-slate-400 dark:hover:text-slate-200"
|
||||
>
|
||||
Voir
|
||||
</Link>
|
||||
<Link
|
||||
v-if="order.can.update"
|
||||
:href="route('commandes.edit', { commande: order.id })"
|
||||
class="text-sky-600 hover:text-sky-900 dark:text-sky-400 dark:hover:text-sky-300"
|
||||
>
|
||||
Éditer
|
||||
</Link>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr v-if="!orders?.data || orders.data.length === 0">
|
||||
<td colspan="9" class="px-6 py-8 text-center text-sm text-slate-500 dark:text-slate-400">
|
||||
Aucune commande ne correspond aux critères de recherche.
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div
|
||||
v-if="orders.meta && orders.meta.links && orders.meta.links.length > 3"
|
||||
class="bg-slate-50 dark:bg-slate-950 border-t border-slate-200 dark:border-slate-800 px-6 py-4 flex items-center justify-between"
|
||||
>
|
||||
<div class="flex-1 flex justify-between sm:hidden">
|
||||
<Link
|
||||
:href="orders.links.prev || '#'"
|
||||
:disabled="!orders.links.prev"
|
||||
class="relative inline-flex items-center px-4 py-2 border border-slate-300 text-sm font-semibold rounded-md text-slate-700 bg-white hover:bg-slate-50 disabled:opacity-50"
|
||||
>
|
||||
Précédent
|
||||
</Link>
|
||||
<Link
|
||||
:href="orders.links.next || '#'"
|
||||
:disabled="!orders.links.next"
|
||||
class="relative inline-flex items-center px-4 py-2 border border-slate-300 text-sm font-semibold rounded-md text-slate-700 bg-white hover:bg-slate-50 disabled:opacity-50"
|
||||
>
|
||||
Suivant
|
||||
</Link>
|
||||
</div>
|
||||
<div class="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
|
||||
<div>
|
||||
<p class="text-sm text-slate-700 dark:text-slate-400">
|
||||
Affichage de la page
|
||||
<span class="font-bold">{{ orders.meta.current_page }}</span>
|
||||
sur
|
||||
<span class="font-bold">{{ orders.meta.last_page }}</span>
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<nav class="relative z-0 inline-flex rounded-md shadow-sm -space-x-px" aria-label="Pagination">
|
||||
<template v-for="(link, key) in orders.meta.links" :key="key">
|
||||
<div
|
||||
v-if="link.url === null"
|
||||
class="relative inline-flex items-center px-3 py-2 border border-slate-300 dark:border-slate-800 text-xs font-semibold text-slate-400 bg-white dark:bg-slate-900 rounded-md cursor-default select-none"
|
||||
v-html="link.label"
|
||||
/>
|
||||
<Link
|
||||
v-else
|
||||
:href="link.url"
|
||||
class="relative inline-flex items-center px-3 py-2 border text-xs font-semibold transition-colors"
|
||||
:class="[
|
||||
link.active
|
||||
? 'z-10 bg-sky-50 border-sky-500 text-sky-600 dark:bg-sky-950/40 dark:border-sky-500 dark:text-sky-300'
|
||||
: 'bg-white border-slate-300 text-slate-500 hover:bg-slate-50 dark:bg-slate-900 dark:border-slate-800 dark:text-slate-400 dark:hover:bg-slate-800'
|
||||
]"
|
||||
v-html="link.label"
|
||||
/>
|
||||
</template>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AuthenticatedLayout>
|
||||
</template>
|
||||
371
resources/js/Pages/Commandes/Show.vue
Normal file
371
resources/js/Pages/Commandes/Show.vue
Normal file
@@ -0,0 +1,371 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user