feat: add sous-devis generation from commandes

From a commande, users can now create one or more sub-quotes
(sous-devis) to ventilate purchases across communes for approval.

- Add parent_devis_id and commande_id FKs on devis table
- Order model: sousDevis() hasMany relation
- Devis model: commande() and parent() relations
- DevisController: createFromCommande / storeFromCommande methods
- OrderController: load and pass sous_devis on show
- Commandes/Show: "Créer un sous-devis" button + ventilation panel
- Devis/CreateFromCommande: form pre-loaded with commande context
- Devis/Index: badge "Sous-devis · CMD-XXXX" on linked quotes
- Devis/Show: back-link to originating commande

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
mrKamoo
2026-06-19 15:14:45 +02:00
co-authored by Claude Sonnet 4.6
parent 7312c7640b
commit 8d51f4de3e
12 changed files with 874 additions and 15 deletions
@@ -0,0 +1,248 @@
<script setup>
import { ref, computed } from 'vue';
import { Head, Link, useForm } from '@inertiajs/vue3';
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue';
const props = defineProps({
commande: Object,
});
const itemTypes = ['Matériel', 'Licence', 'Prestation', 'Infrastructure', 'Autre'];
const form = useForm({
title: '',
destinataire_type: 'commune',
destinataire_nom: '',
referent: '',
date_devis: new Date().toISOString().split('T')[0],
status: 'draft',
items: [
{ type: 'Matériel', designation: '', quantite: 1, prix_unitaire: 0 }
],
tva_rate: 20,
notes: '',
});
const addItem = () => {
form.items.push({ type: 'Matériel', designation: '', quantite: 1, prix_unitaire: 0 });
};
const removeItem = (index) => {
form.items.splice(index, 1);
if (form.items.length === 0) addItem();
};
const totalHt = computed(() =>
form.items.reduce((sum, i) =>
sum + (parseFloat(i.quantite) || 0) * (parseFloat(i.prix_unitaire) || 0), 0)
);
const tvaAmount = computed(() =>
totalHt.value * ((parseFloat(form.tva_rate) || 0) / 100)
);
const totalTtc = computed(() => totalHt.value + tvaAmount.value);
const formatCurrency = (v) =>
new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(v);
const submit = () => {
form.post(route('commandes.sous-devis.store', props.commande.id));
};
</script>
<template>
<Head title="Créer un Sous-Devis" />
<AuthenticatedLayout>
<template #header>
<div class="flex items-center justify-between">
<div>
<h2 class="text-xl font-bold leading-tight text-slate-800 dark:text-slate-200">
Nouveau Sous-Devis
</h2>
<p class="text-sm text-slate-500 dark:text-slate-400 mt-0.5">
Commande
<Link :href="route('commandes.show', commande.id)" class="text-sky-600 dark:text-sky-400 font-semibold hover:underline">
{{ commande.number }}
</Link>
{{ commande.label }}
<span v-if="commande.supplier" class="text-slate-400"> · {{ commande.supplier }}</span>
<span v-if="commande.quote_number" class="font-mono text-xs text-slate-400"> (Réf. devis fournisseur : {{ commande.quote_number }})</span>
</p>
</div>
<Link
:href="route('commandes.show', commande.id)"
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 commande
</Link>
</div>
</template>
<div class="py-6">
<div class="max-w-5xl mx-auto px-4 sm:px-6 lg:px-8">
<form @submit.prevent="submit" class="space-y-6">
<!-- Informations générales -->
<div class="bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-xl shadow-sm p-6 space-y-6">
<h3 class="text-md font-bold text-slate-800 dark:text-slate-200 border-b border-slate-100 dark:border-slate-850 pb-3">
Informations Générales
</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="md:col-span-2">
<label class="block text-sm font-bold text-slate-750 dark:text-slate-300 mb-1">Objet du sous-devis :</label>
<input
v-model="form.title"
type="text"
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"
placeholder="Ex: Ventilation postes de travail Mairie de Corneilhan"
required
>
<p v-if="form.errors.title" class="text-xs text-rose-600 mt-1">{{ form.errors.title }}</p>
</div>
<div>
<label class="block text-sm font-bold text-slate-755 dark:text-slate-300 mb-1">Type de destinataire :</label>
<select v-model="form.destinataire_type" 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="commune">Commune (Collectivité extérieure)</option>
<option value="service_interne">Service Interne (Direction / Service)</option>
</select>
</div>
<div>
<label class="block text-sm font-bold text-slate-755 dark:text-slate-300 mb-1">Nom du destinataire :</label>
<input
v-model="form.destinataire_nom"
type="text"
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"
placeholder="Ex: Mairie de Corneilhan"
required
>
<p v-if="form.errors.destinataire_nom" class="text-xs text-rose-600 mt-1">{{ form.errors.destinataire_nom }}</p>
</div>
<div>
<label class="block text-sm font-bold text-slate-755 dark:text-slate-300 mb-1">Référent / Contact :</label>
<input
v-model="form.referent"
type="text"
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"
placeholder="Ex: Jean Dupont"
>
</div>
<div>
<label class="block text-sm font-bold text-slate-755 dark:text-slate-300 mb-1">Date du devis :</label>
<input v-model="form.date_devis" type="date" 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" required>
</div>
<div>
<label class="block text-sm font-bold text-slate-755 dark:text-slate-300 mb-1">Statut :</label>
<select v-model="form.status" 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="draft">Brouillon</option>
<option value="sent">Envoyé</option>
<option value="accepted">Accepté</option>
<option value="rejected">Refusé</option>
</select>
</div>
<div>
<label class="block text-sm font-bold text-slate-755 dark:text-slate-300 mb-1">Taux TVA (%) :</label>
<input v-model="form.tva_rate" type="number" min="0" max="100" step="0.1" 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>
</div>
</div>
<!-- Lignes à ventiler -->
<div class="bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-xl shadow-sm p-6 space-y-4">
<div class="flex items-center justify-between border-b border-slate-100 dark:border-slate-850 pb-3">
<div>
<h3 class="text-md font-bold text-slate-800 dark:text-slate-200">
Lignes à ventiler
</h3>
<p class="text-xs text-slate-500 dark:text-slate-400 mt-0.5">
Saisissez les éléments du devis fournisseur ({{ commande.quote_number || commande.number }}) à refacturer à cette collectivité.
</p>
</div>
<button type="button" @click="addItem" class="text-xs font-semibold text-sky-600 hover:text-sky-700 bg-sky-50 dark:bg-sky-950/40 px-3 py-1.5 rounded-lg border border-sky-100 dark:border-sky-900/60 transition-colors">
+ Ajouter une ligne
</button>
</div>
<div class="space-y-3">
<div v-for="(item, index) in form.items" :key="index" class="flex items-start gap-3 bg-slate-50 dark:bg-slate-950/50 p-4 rounded-xl border border-slate-100 dark:border-slate-850">
<div class="flex-1 grid grid-cols-1 sm:grid-cols-12 gap-3">
<div class="sm:col-span-3">
<label class="block text-xxs font-bold text-slate-400 uppercase tracking-wide mb-1">Catégorie</label>
<select v-model="item.type" class="w-full text-xs rounded-lg border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:bg-slate-900 dark:border-slate-800 dark:text-slate-200">
<option v-for="t in itemTypes" :key="t" :value="t">{{ t }}</option>
</select>
</div>
<div class="sm:col-span-5">
<label class="block text-xxs font-bold text-slate-400 uppercase tracking-wide mb-1">Description</label>
<input v-model="item.designation" type="text" placeholder="Désignation" class="w-full text-xs rounded-lg border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:bg-slate-900 dark:border-slate-800 dark:text-slate-200" required>
</div>
<div class="sm:col-span-2">
<label class="block text-xxs font-bold text-slate-400 uppercase tracking-wide mb-1">Quantité</label>
<input v-model="item.quantite" type="number" min="1" step="1" class="w-full text-xs rounded-lg border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:bg-slate-900 dark:border-slate-800 dark:text-slate-200" required>
</div>
<div class="sm:col-span-2">
<label class="block text-xxs font-bold text-slate-400 uppercase tracking-wide mb-1">P.U. HT ()</label>
<input v-model="item.prix_unitaire" type="number" min="0" step="0.01" class="w-full text-xs rounded-lg border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:bg-slate-900 dark:border-slate-800 dark:text-slate-200" required>
</div>
</div>
<button type="button" @click="removeItem(index)" class="mt-5 p-1.5 text-rose-500 hover:bg-rose-50 dark:hover:bg-rose-950/40 rounded-md border border-transparent hover:border-rose-200 dark:hover:border-rose-900/60">
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"/></svg>
</button>
</div>
</div>
<p v-if="form.errors.items" class="text-xs text-rose-600">{{ form.errors.items }}</p>
<!-- Récapitulatif -->
<div class="bg-slate-50 dark:bg-slate-950 p-4 rounded-xl border border-slate-200 dark:border-slate-800 flex flex-col items-end space-y-2 text-sm">
<div class="flex justify-between w-64">
<span class="text-slate-500">Total HT :</span>
<span class="font-bold text-slate-850 dark:text-slate-200">{{ formatCurrency(totalHt) }}</span>
</div>
<div class="flex justify-between w-64 border-b border-slate-200 dark:border-slate-800 pb-2">
<span class="text-slate-500">TVA ({{ form.tva_rate }}%) :</span>
<span class="font-semibold text-slate-850 dark:text-slate-200">{{ formatCurrency(tvaAmount) }}</span>
</div>
<div class="flex justify-between w-64 text-base font-extrabold text-sky-600 dark:text-sky-400 pt-1">
<span>Total TTC :</span>
<span>{{ formatCurrency(totalTtc) }}</span>
</div>
</div>
</div>
<!-- Notes -->
<div class="bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-xl shadow-sm p-6">
<label class="block text-sm font-bold text-slate-755 dark:text-slate-300 mb-1">Notes / Conditions :</label>
<textarea v-model="form.notes" rows="3" 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" placeholder="Ex: Devis valable 30 jours."></textarea>
</div>
<!-- Actions -->
<div class="flex items-center justify-end gap-3 pt-6 border-t border-slate-200 dark:border-slate-800">
<Link
:href="route('commandes.show', commande.id)"
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 shadow-sm"
>
Annuler
</Link>
<button
type="submit"
:disabled="form.processing"
class="inline-flex items-center px-4 py-2 text-sm font-semibold text-white bg-violet-600 hover:bg-violet-500 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-violet-500 transition-colors disabled:opacity-50"
>
Créer le Sous-Devis
</button>
</div>
</form>
</div>
</div>
</AuthenticatedLayout>
</template>
@@ -0,0 +1,272 @@
<script setup>
import { ref, computed } from 'vue';
import { Head, Link, useForm } from '@inertiajs/vue3';
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue';
const props = defineProps({
parentDevis: Object,
});
// Each parent item has a `selected` toggle + editable quantite
const parentItems = ref(
(props.parentDevis.items ?? []).map(item => ({
...item,
selected: true,
quantite: item.quantite,
}))
);
const form = useForm({
title: '',
destinataire_type: 'commune',
destinataire_nom: '',
referent: '',
date_devis: new Date().toISOString().split('T')[0],
status: 'draft',
tva_rate: props.parentDevis.tva_rate ?? 20,
notes: '',
items: [],
});
const selectedItems = computed(() =>
parentItems.value.filter(i => i.selected)
);
const totalHt = computed(() =>
selectedItems.value.reduce((sum, i) =>
sum + (parseFloat(i.quantite) || 0) * (parseFloat(i.prix_unitaire) || 0), 0)
);
const tvaAmount = computed(() =>
totalHt.value * ((parseFloat(form.tva_rate) || 0) / 100)
);
const totalTtc = computed(() => totalHt.value + tvaAmount.value);
const formatCurrency = (v) =>
new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(v);
const submit = () => {
form.items = selectedItems.value.map(({ selected, ...item }) => item);
form.post(route('devis.sous-devis.store', props.parentDevis.id));
};
</script>
<template>
<Head title="Créer un Sous-Devis" />
<AuthenticatedLayout>
<template #header>
<div class="flex items-center justify-between">
<div>
<h2 class="text-xl font-bold leading-tight text-slate-800 dark:text-slate-200">
Nouveau Sous-Devis
</h2>
<p class="text-sm text-slate-500 dark:text-slate-400 mt-0.5">
Basé sur
<Link :href="route('devis.show', parentDevis.id)" class="text-sky-600 dark:text-sky-400 font-semibold hover:underline">
{{ parentDevis.number }}
</Link>
{{ parentDevis.title }}
</p>
</div>
<Link
:href="route('devis.show', parentDevis.id)"
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-5xl mx-auto px-4 sm:px-6 lg:px-8">
<form @submit.prevent="submit" class="space-y-6">
<!-- Informations générales -->
<div class="bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-xl shadow-sm p-6 space-y-6">
<h3 class="text-md font-bold text-slate-800 dark:text-slate-200 border-b border-slate-100 dark:border-slate-850 pb-3">
Informations Générales
</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="md:col-span-2">
<label class="block text-sm font-bold text-slate-750 dark:text-slate-300 mb-1">Objet du sous-devis :</label>
<input
v-model="form.title"
type="text"
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"
placeholder="Ex: Sous-devis Mairie de Corneilhan postes de travail"
required
>
<p v-if="form.errors.title" class="text-xs text-rose-600 mt-1">{{ form.errors.title }}</p>
</div>
<div>
<label class="block text-sm font-bold text-slate-755 dark:text-slate-300 mb-1">Type de destinataire :</label>
<select v-model="form.destinataire_type" 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="commune">Commune (Collectivité extérieure)</option>
<option value="service_interne">Service Interne (Direction / Service)</option>
</select>
</div>
<div>
<label class="block text-sm font-bold text-slate-755 dark:text-slate-300 mb-1">Nom du destinataire :</label>
<input
v-model="form.destinataire_nom"
type="text"
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"
placeholder="Ex: Mairie de Corneilhan"
required
>
<p v-if="form.errors.destinataire_nom" class="text-xs text-rose-600 mt-1">{{ form.errors.destinataire_nom }}</p>
</div>
<div>
<label class="block text-sm font-bold text-slate-755 dark:text-slate-300 mb-1">Référent :</label>
<input
v-model="form.referent"
type="text"
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"
placeholder="Ex: Jean Dupont"
>
</div>
<div>
<label class="block text-sm font-bold text-slate-755 dark:text-slate-300 mb-1">Date du devis :</label>
<input v-model="form.date_devis" type="date" 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" required>
</div>
<div>
<label class="block text-sm font-bold text-slate-755 dark:text-slate-300 mb-1">Statut :</label>
<select v-model="form.status" 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="draft">Brouillon</option>
<option value="sent">Envoyé</option>
<option value="accepted">Accepté</option>
<option value="rejected">Refusé</option>
</select>
</div>
<div>
<label class="block text-sm font-bold text-slate-755 dark:text-slate-300 mb-1">Taux TVA (%) :</label>
<input v-model="form.tva_rate" type="number" min="0" max="100" step="0.1" 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>
</div>
</div>
<!-- Sélection des lignes du devis parent -->
<div class="bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-xl shadow-sm p-6 space-y-4">
<div class="border-b border-slate-100 dark:border-slate-850 pb-3">
<h3 class="text-md font-bold text-slate-800 dark:text-slate-200">
Sélection des lignes du devis fournisseur
</h3>
<p class="text-xs text-slate-500 dark:text-slate-400 mt-1">
Cochez les éléments à inclure. Les quantités sont modifiables (max : quantité du devis fournisseur).
</p>
</div>
<div v-if="parentItems.length === 0" class="text-sm text-slate-400 py-4 text-center">
Le devis parent ne contient aucune ligne.
</div>
<div class="space-y-3">
<div
v-for="(item, index) in parentItems"
:key="index"
:class="[
'flex items-center gap-4 p-4 rounded-xl border transition-colors',
item.selected
? 'bg-sky-50 dark:bg-sky-950/20 border-sky-200 dark:border-sky-900/60'
: 'bg-slate-50 dark:bg-slate-950/50 border-slate-100 dark:border-slate-850 opacity-50'
]"
>
<!-- Checkbox -->
<input
v-model="item.selected"
type="checkbox"
class="w-4 h-4 text-sky-600 rounded border-slate-300 focus:ring-sky-500 dark:border-slate-700 dark:bg-slate-800 shrink-0"
>
<!-- Désignation + type -->
<div class="flex-1 min-w-0">
<p class="text-sm font-semibold text-slate-900 dark:text-slate-100 truncate">{{ item.designation }}</p>
<p class="text-xxs font-bold uppercase tracking-wider text-slate-400">{{ item.type }}</p>
</div>
<!-- Prix unitaire (lecture seule) -->
<div class="text-right shrink-0">
<p class="text-xs text-slate-500 dark:text-slate-400">P.U. HT</p>
<p class="text-sm font-bold text-slate-800 dark:text-slate-200">{{ formatCurrency(item.prix_unitaire) }}</p>
</div>
<!-- Quantité modifiable -->
<div class="shrink-0">
<label class="block text-xxs font-bold text-slate-400 uppercase tracking-wide mb-1">Qté</label>
<input
v-model="item.quantite"
type="number"
min="1"
:max="item.quantite"
step="1"
:disabled="!item.selected"
class="w-20 text-xs rounded-lg border-slate-300 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:bg-slate-900 dark:border-slate-800 dark:text-slate-200 disabled:opacity-40"
>
</div>
<!-- Sous-total -->
<div class="text-right shrink-0 w-24">
<p class="text-xs text-slate-500 dark:text-slate-400">Sous-total</p>
<p class="text-sm font-black text-slate-900 dark:text-slate-100">
{{ formatCurrency((parseFloat(item.quantite) || 0) * (parseFloat(item.prix_unitaire) || 0)) }}
</p>
</div>
</div>
</div>
<p v-if="form.errors.items" class="text-xs text-rose-600 mt-1">{{ form.errors.items }}</p>
<!-- Récapitulatif -->
<div class="bg-slate-50 dark:bg-slate-950 p-4 rounded-xl border border-slate-200 dark:border-slate-800 flex flex-col items-end space-y-2 text-sm">
<div class="flex justify-between w-64">
<span class="text-slate-500">Total HT :</span>
<span class="font-bold text-slate-850 dark:text-slate-200">{{ formatCurrency(totalHt) }}</span>
</div>
<div class="flex justify-between w-64 border-b border-slate-200 dark:border-slate-800 pb-2">
<span class="text-slate-500">TVA ({{ form.tva_rate }}%) :</span>
<span class="font-semibold text-slate-850 dark:text-slate-200">{{ formatCurrency(tvaAmount) }}</span>
</div>
<div class="flex justify-between w-64 text-base font-extrabold text-sky-600 dark:text-sky-400 pt-1">
<span>Total TTC :</span>
<span>{{ formatCurrency(totalTtc) }}</span>
</div>
</div>
</div>
<!-- Notes -->
<div class="bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-xl shadow-sm p-6">
<label class="block text-sm font-bold text-slate-755 dark:text-slate-300 mb-1">Notes / Conditions :</label>
<textarea v-model="form.notes" rows="3" 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" placeholder="Ex: Devis valable 30 jours."></textarea>
</div>
<!-- Actions -->
<div class="flex items-center justify-end gap-3 pt-6 border-t border-slate-200 dark:border-slate-800">
<Link
:href="route('devis.show', parentDevis.id)"
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 shadow-sm"
>
Annuler
</Link>
<button
type="submit"
:disabled="form.processing || selectedItems.length === 0"
class="inline-flex items-center px-4 py-2 text-sm font-semibold text-white bg-sky-600 hover:bg-sky-500 dark:bg-sky-500 dark:hover:bg-sky-400 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-sky-500 transition-colors disabled:opacity-50"
>
Créer le Sous-Devis
</button>
</div>
</form>
</div>
</div>
</AuthenticatedLayout>
</template>
+15 -4
View File
@@ -1,11 +1,14 @@
<script setup>
import { computed } from 'vue';
import { Head, Link } from '@inertiajs/vue3';
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue';
defineProps({
const props = defineProps({
devis: Array,
});
const orderedDevis = computed(() => props.devis);
const getStatusClasses = (status) => {
switch (status) {
case 'draft':
@@ -96,15 +99,23 @@ const formatCurrency = (value) => {
</thead>
<tbody class="divide-y divide-slate-100 dark:divide-slate-850 text-sm">
<tr
v-for="d in devis"
v-for="d in orderedDevis"
:key="d.id"
class="hover:bg-slate-50/50 dark:hover:bg-slate-950/20 transition-colors"
:class="[
'hover:bg-slate-50/50 dark:hover:bg-slate-950/20 transition-colors',
!d._isParent ? 'bg-violet-50/30 dark:bg-violet-950/10' : ''
]"
>
<!-- Numéro -->
<td class="px-6 py-4 font-mono text-xs font-bold text-sky-600 dark:text-sky-400">
<Link :href="route('devis.show', d.id)" class="hover:underline">
{{ d.number }}
</Link>
<div v-if="d.commande_id" class="mt-0.5">
<span class="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-xxs font-bold bg-violet-100 text-violet-700 dark:bg-violet-950/40 dark:text-violet-300 border border-violet-200 dark:border-violet-900/60">
Sous-devis · {{ d.commande_number }}
</span>
</div>
</td>
<!-- Objet -->
<td class="px-6 py-4 font-medium text-slate-800 dark:text-slate-200">
@@ -169,7 +180,7 @@ const formatCurrency = (value) => {
</div>
</td>
</tr>
<tr v-if="devis.length === 0">
<tr v-if="orderedDevis.length === 0">
<td colspan="7" class="px-6 py-12 text-center">
<div class="flex flex-col items-center justify-center text-slate-500 dark:text-slate-400">
<svg class="w-12 h-12 mb-4 text-slate-300 dark:text-slate-600" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
+72
View File
@@ -68,6 +68,16 @@ const printDevis = () => {
>
Modifier
</Link>
<Link
v-if="!devis.parent_devis_id"
:href="route('devis.sous-devis.create', devis.id)"
class="inline-flex items-center px-4 py-2 text-sm font-semibold text-white bg-violet-600 hover:bg-violet-500 rounded-lg transition-colors shadow-sm"
>
<svg class="w-4 h-4 mr-1.5" fill="none" viewBox="0 0 24 24" stroke-width="2" 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.25M9 16.5v.75m3-3v3M15 12v5.25m-4.5-15H5.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>
Créer un sous-devis
</Link>
<a
:href="route('devis.pdf', devis.id)"
class="inline-flex items-center px-4 py-2 text-sm font-semibold text-white bg-sky-600 hover:bg-sky-550 rounded-lg transition-colors shadow-sm"
@@ -224,6 +234,68 @@ const printDevis = () => {
</div>
<!-- Badge : lien vers la commande d'origine si sous-devis -->
<div v-if="devis.commande_id" class="pt-6 border-t border-slate-200 dark:border-slate-800">
<div class="flex items-center gap-3 text-sm">
<span class="inline-flex items-center px-2.5 py-1 rounded-full text-xs font-bold bg-violet-100 text-violet-800 dark:bg-violet-950/40 dark:text-violet-300 border border-violet-200 dark:border-violet-900/60">
Sous-devis
</span>
<span class="text-slate-500 dark:text-slate-400">Issu de la commande :</span>
<Link :href="route('commandes.show', devis.commande_id)" class="font-mono text-xs font-bold text-sky-600 dark:text-sky-400 hover:underline">
{{ devis.commande_number }}
</Link>
</div>
</div>
<!-- Badge : lien vers le devis parent si sous-devis (legacy) -->
<div v-if="devis.parent_devis_id && devis.parent && !devis.commande_id" class="pt-6 border-t border-slate-200 dark:border-slate-800">
<div class="flex items-center gap-3 text-sm">
<span class="inline-flex items-center px-2.5 py-1 rounded-full text-xs font-bold bg-violet-100 text-violet-800 dark:bg-violet-950/40 dark:text-violet-300 border border-violet-200 dark:border-violet-900/60">
Sous-devis
</span>
<span class="text-slate-500 dark:text-slate-400">Issu du devis fournisseur :</span>
<Link :href="route('devis.show', devis.parent.id)" class="font-mono text-xs font-bold text-sky-600 dark:text-sky-400 hover:underline">
{{ devis.parent.number }}
</Link>
<span class="text-slate-400 dark:text-slate-600"></span>
<span class="text-slate-600 dark:text-slate-400">{{ devis.parent.title }}</span>
</div>
</div>
<!-- Section sous-devis enfants (si devis parent) -->
<div v-if="!devis.parent_devis_id && devis.sous_devis && devis.sous_devis.length > 0" class="pt-6 border-t border-slate-200 dark:border-slate-800 space-y-4 no-print">
<h3 class="text-sm font-black uppercase tracking-wider text-slate-500 dark:text-slate-400">
Sous-devis générés ({{ devis.sous_devis.length }})
</h3>
<div class="space-y-2">
<div
v-for="sd in devis.sous_devis"
:key="sd.id"
class="flex items-center justify-between px-4 py-3 rounded-xl border border-slate-200 dark:border-slate-800 bg-slate-50 dark:bg-slate-950/40"
>
<div class="flex items-center gap-3">
<span class="font-mono text-xs font-bold text-violet-600 dark:text-violet-400">{{ sd.number }}</span>
<span class="text-sm text-slate-700 dark:text-slate-300">{{ sd.title }}</span>
<span class="text-xs text-slate-500 dark:text-slate-400"> {{ sd.destinataire_nom }}</span>
</div>
<div class="flex items-center gap-3">
<span class="text-sm font-bold text-slate-900 dark:text-slate-100">
{{ new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(sd.total_ttc) }}
</span>
<Link :href="route('devis.show', sd.id)" class="text-xs font-semibold text-sky-600 dark:text-sky-400 hover:underline">
Voir
</Link>
</div>
</div>
</div>
<Link
:href="route('devis.sous-devis.create', devis.id)"
class="inline-flex items-center text-xs font-semibold text-violet-600 dark:text-violet-400 hover:underline"
>
+ Ajouter un sous-devis
</Link>
</div>
</div>
</div>
</div>