Files
OrderCheck/resources/js/Pages/Devis/CreateFromCommande.vue
T
mrKamooandClaude Sonnet 4.6 8d51f4de3e 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>
2026-06-19 15:14:45 +02:00

249 lines
16 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>