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>
339 lines
21 KiB
Vue
339 lines
21 KiB
Vue
<script setup>
|
|
import { Head, Link } from '@inertiajs/vue3';
|
|
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue';
|
|
|
|
const props = defineProps({
|
|
devis: Object,
|
|
settings: Object,
|
|
});
|
|
|
|
const getStatusClasses = (status) => {
|
|
switch (status) {
|
|
case 'draft':
|
|
return 'bg-slate-100 text-slate-800 border-slate-200 dark:bg-slate-900/60 dark:text-slate-400 dark:border-slate-800';
|
|
case 'sent':
|
|
return 'bg-blue-50 text-blue-800 border-blue-200 dark:bg-blue-950/40 dark:text-blue-300 dark:border-blue-900/60';
|
|
case 'accepted':
|
|
return 'bg-emerald-50 text-emerald-800 border-emerald-200 dark:bg-emerald-950/40 dark:text-emerald-300 dark:border-emerald-900/60';
|
|
case 'rejected':
|
|
return 'bg-rose-50 text-rose-800 border-rose-200 dark:bg-rose-950/40 dark:text-rose-300 dark:border-rose-900/60';
|
|
default:
|
|
return 'bg-slate-100 text-slate-800 border-slate-200';
|
|
}
|
|
};
|
|
|
|
const getStatusLabel = (status) => {
|
|
switch (status) {
|
|
case 'draft': return 'Brouillon';
|
|
case 'sent': return 'Envoyé';
|
|
case 'accepted': return 'Accepté';
|
|
case 'rejected': return 'Refusé';
|
|
default: return status;
|
|
}
|
|
};
|
|
|
|
const formatCurrency = (value) => {
|
|
return new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(value);
|
|
};
|
|
|
|
const printDevis = () => {
|
|
window.print();
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<Head :title="`Devis ${devis.number}`" />
|
|
|
|
<AuthenticatedLayout>
|
|
<template #header>
|
|
<div class="flex items-center justify-between no-print">
|
|
<div class="flex items-center gap-3">
|
|
<h2 class="text-xl font-bold leading-tight text-slate-800 dark:text-slate-200">
|
|
Visualisation du Devis {{ devis.number }}
|
|
</h2>
|
|
<span :class="`inline-flex items-center px-2 py-0.5 rounded text-xs font-semibold border ${getStatusClasses(devis.status)}`">
|
|
{{ getStatusLabel(devis.status) }}
|
|
</span>
|
|
</div>
|
|
<div class="flex items-center gap-2">
|
|
<Link
|
|
:href="route('devis.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>
|
|
<Link
|
|
:href="route('devis.edit', devis.id)"
|
|
class="inline-flex items-center px-4 py-2 text-sm font-semibold text-white bg-amber-500 hover:bg-amber-600 rounded-lg transition-colors shadow-sm"
|
|
>
|
|
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"
|
|
target="_blank"
|
|
>
|
|
<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="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>
|
|
Télécharger PDF
|
|
</a>
|
|
<button
|
|
@click="printDevis"
|
|
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 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="M6.72 13.829c-.24.03-.48.062-.72.096m.72-.096a42.415 42.415 0 0 1 10.56 0m-10.56 0L6.34 18m10.94-4.171c.24.03.48.062.72.096m-.72-.096L17.66 18m0 0a2.25 2.25 0 0 1-2.25 2.25H8.59A2.25 2.25 0 0 1 6.34 18m11.318-4.171A2.25 2.25 0 0 0 19.5 11.625V8.25a2.25 2.25 0 0 0-2.25-2.25H6.75A2.25 2.25 0 0 0 4.5 8.25v3.375c0 .921.555 1.714 1.342 2.054M15 9V4.5A1.5 1.5 0 0 0 13.5 3h-3A1.5 1.5 0 0 0 9 4.5V9m9 3h.008v.008H18V12Zm-1.5 0h.008v.008H16.5V12Z" />
|
|
</svg>
|
|
Imprimer
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<div class="py-10 print:py-0">
|
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 print:px-0 print:max-w-full flex justify-center">
|
|
|
|
<!-- Format Devis Papier / A4 style (Designer Look) -->
|
|
<div class="print-container w-full max-w-4xl bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-2xl shadow-sm overflow-hidden p-12 md:p-16 space-y-14">
|
|
|
|
<!-- En-tête Devis -->
|
|
<table class="w-full border-b border-slate-200 dark:border-slate-800 pb-10 mb-2 text-sm">
|
|
<tbody><tr>
|
|
<td class="align-top text-left pb-4">
|
|
<div class="flex items-center gap-6">
|
|
<img src="/logo.png" alt="Logo Béziers Méditerranée" class="h-24 w-auto object-contain shrink-0">
|
|
<div class="space-y-2 text-left">
|
|
<div class="text-slate-900 dark:text-white font-black text-2xl tracking-tight">
|
|
{{ settings?.entity_name || "Direction des Systèmes d'Information" }}
|
|
</div>
|
|
<!-- Decorative squares row in "Studio Shodwe" style -->
|
|
<div class="flex justify-start gap-1.5 mt-3">
|
|
<span class="w-4 h-3.5 bg-emerald-700 inline-block"></span>
|
|
<span class="w-4 h-3.5 bg-teal-500 inline-block"></span>
|
|
<span class="w-4 h-3.5 bg-amber-500 inline-block"></span>
|
|
<span class="w-4 h-3.5 bg-rose-500 inline-block"></span>
|
|
<span class="w-4 h-3.5 bg-pink-400 inline-block"></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td class="align-top text-right pb-4 text-slate-500 dark:text-slate-400 leading-loose font-semibold" style="text-align: right !important;">
|
|
<div class="inline-block text-right" style="text-align: right !important;">
|
|
<p class="font-extrabold text-slate-850 dark:text-slate-200 text-right mb-1" style="text-align: right !important;">{{ settings?.entity_sub || "Communauté d'Agglomération Béziers Méditerranée" }}</p>
|
|
<p v-if="settings?.telephone" class="text-right" style="text-align: right !important;">Téléphone : {{ settings.telephone }}</p>
|
|
<p v-if="settings?.email" class="text-right" style="text-align: right !important;">E-mail : {{ settings.email }}</p>
|
|
<p v-if="settings?.adresse" class="text-right" style="text-align: right !important;">Adresse : {{ settings.adresse }}</p>
|
|
</div>
|
|
</td>
|
|
</tr></tbody>
|
|
</table>
|
|
|
|
<!-- Blocs Destinataire et Émetteur (Designer thick outline rounded cards) -->
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-10">
|
|
<div class="border-2 border-slate-900 dark:border-slate-700 rounded-[2rem] p-9 space-y-5">
|
|
<h3 class="text-base font-black uppercase tracking-tight text-slate-900 dark:text-slate-200">
|
|
DEVIS N° : {{ devis.number }}
|
|
</h3>
|
|
<div class="space-y-3 text-sm text-slate-700 dark:text-slate-350">
|
|
<p><span class="font-black text-slate-900 dark:text-slate-250">DATE :</span> {{ new Date(devis.date_devis).toLocaleDateString() }}</p>
|
|
<p><span class="font-black text-slate-900 dark:text-slate-250">OBJET :</span> {{ devis.title }}</p>
|
|
<p v-if="devis.notes"><span class="font-black text-slate-900 dark:text-slate-250">CONDITIONS :</span> {{ devis.notes }}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="border-2 border-slate-900 dark:border-slate-700 rounded-[2rem] p-9 space-y-5">
|
|
<h3 class="text-base font-black uppercase tracking-tight text-slate-900 dark:text-slate-200">
|
|
DESTINATAIRE :
|
|
</h3>
|
|
<div class="space-y-3 text-sm text-slate-700 dark:text-slate-350">
|
|
<p class="font-black text-base text-slate-900 dark:text-white">{{ devis.destinataire_nom }}</p>
|
|
<p class="font-bold text-slate-500">
|
|
{{ devis.destinataire_type === 'commune' ? 'Commune / Collectivité' : 'Service Interne' }}
|
|
</p>
|
|
<p v-if="devis.referent" class="border-t border-slate-200 dark:border-slate-800 pt-3 mt-3">
|
|
<span class="font-black text-slate-900 dark:text-slate-250">À L'ATTENTION DE :</span> {{ devis.referent }}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tableau des Lignes du Devis (Clean rows, grey header bar, dotted lines) -->
|
|
<div class="overflow-x-auto">
|
|
<table class="w-full text-left border-collapse text-sm">
|
|
<thead>
|
|
<tr class="bg-slate-100 dark:bg-slate-800 text-xs font-black uppercase tracking-wider text-slate-900 dark:text-slate-200">
|
|
<th class="px-7 py-4">Description :</th>
|
|
<th class="px-7 py-4 text-right">Prix Unitaire :</th>
|
|
<th class="px-7 py-4 text-center">Quantité :</th>
|
|
<th class="px-7 py-4 text-right">Total :</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="text-slate-750 dark:text-slate-250">
|
|
<tr v-for="(item, index) in devis.items" :key="index" class="border-b-2 border-dotted border-slate-300 dark:border-slate-750">
|
|
<td class="px-7 py-6">
|
|
<div class="font-black text-slate-900 dark:text-slate-100 text-base mb-1">
|
|
{{ item.designation }}
|
|
</div>
|
|
<span class="text-xs font-bold uppercase tracking-wider text-slate-400">
|
|
{{ item.type }}
|
|
</span>
|
|
</td>
|
|
<td class="px-7 py-6 text-right font-medium">
|
|
{{ formatCurrency(item.prix_unitaire) }}
|
|
</td>
|
|
<td class="px-7 py-6 text-center font-bold text-slate-900 dark:text-slate-100">
|
|
{{ item.quantite }}
|
|
</td>
|
|
<td class="px-7 py-6 text-right font-black text-slate-900 dark:text-slate-100 text-base">
|
|
{{ formatCurrency(item.quantite * item.prix_unitaire) }}
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
<!-- Totaux (Compact design block) -->
|
|
<div class="flex justify-end pt-4">
|
|
<div class="w-80 space-y-3 text-right text-slate-900 dark:text-slate-100 font-black text-sm">
|
|
<div class="flex justify-between">
|
|
<span>TOTAL HT :</span>
|
|
<span>{{ formatCurrency(devis.total_ht) }}</span>
|
|
</div>
|
|
<div class="flex justify-between">
|
|
<span>TVA ({{ devis.tva_rate }}%) :</span>
|
|
<span>{{ formatCurrency(devis.total_ttc - devis.total_ht) }}</span>
|
|
</div>
|
|
<div class="flex justify-between text-base border-t-2 border-slate-900 dark:border-slate-700 pt-4 mt-1">
|
|
<span>TOTAL TTC :</span>
|
|
<span class="text-sky-600 dark:text-sky-400 font-extrabold">{{ formatCurrency(devis.total_ttc) }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Signatures and Modalités de paiement (Two columns, separated by vertical bar) -->
|
|
<div class="grid grid-cols-1 md:grid-cols-1 gap-14 pt-14 border-t border-slate-200 dark:border-slate-800 text-sm">
|
|
<div class="space-y-5 text-slate-650 dark:text-slate-350 pr-4">
|
|
<div class="pt-1">
|
|
<p class="font-bold text-xs uppercase tracking-wider text-slate-400 mb-2">Pour le Destinataire (Bon pour accord)</p>
|
|
<div class="border-b border-dashed border-slate-400 w-56 h-12 mb-4"></div>
|
|
<p class="text-xs italic text-slate-400">Nom, Date et Signature</p>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
</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>
|
|
</AuthenticatedLayout>
|
|
</template>
|
|
|
|
<style>
|
|
/* Global print styles: non-scoped to guarantee layout hiding works */
|
|
@media print {
|
|
/* Hide the primary navigation bar and header wrappers in AuthenticatedLayout */
|
|
nav, header, aside, .no-print {
|
|
display: none !important;
|
|
height: 0 !important;
|
|
padding: 0 !important;
|
|
margin: 0 !important;
|
|
}
|
|
|
|
.min-h-screen {
|
|
min-height: auto !important;
|
|
background-color: transparent !important;
|
|
}
|
|
|
|
body, html {
|
|
background: white !important;
|
|
color: black !important;
|
|
}
|
|
|
|
/* Preserve the padding inside print container so it does not hug the edge of the sheet */
|
|
.print-container {
|
|
border: none !important;
|
|
box-shadow: none !important;
|
|
padding: 3rem !important; /* ~48px margins inside the paper sheet */
|
|
margin: 0 !important;
|
|
background: white !important;
|
|
color: black !important;
|
|
width: 100% !important;
|
|
max-width: 100% !important;
|
|
}
|
|
}
|
|
</style>
|