feat: add Devis and BesoinInformatique modules
- Devis: CRUD complet, génération PDF (dompdf), paramètres (DevisSetting) - BesoinInformatique: CRUD complet avec gestion matériels/licences - Migrations pour les 3 nouvelles tables - Navigation mise à jour dans AuthenticatedLayout Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
030d76af53
commit
7312c7640b
@@ -0,0 +1,233 @@
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import { Head, Link, useForm } from '@inertiajs/vue3';
|
||||
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue';
|
||||
|
||||
const props = defineProps({
|
||||
devis: Object,
|
||||
});
|
||||
|
||||
const form = useForm({
|
||||
title: props.devis.title,
|
||||
destinataire_type: props.devis.destinataire_type,
|
||||
destinataire_nom: props.devis.destinataire_nom,
|
||||
referent: props.devis.referent || '',
|
||||
date_devis: props.devis.date_devis ? new Date(props.devis.date_devis).toISOString().split('T')[0] : '',
|
||||
status: props.devis.status,
|
||||
items: props.devis.items || [
|
||||
{ type: 'Matériel', designation: '', quantite: 1, prix_unitaire: 0 }
|
||||
],
|
||||
tva_rate: parseFloat(props.devis.tva_rate) || 20,
|
||||
notes: props.devis.notes || '',
|
||||
});
|
||||
|
||||
const itemTypes = ['Matériel', 'Licence', 'Prestation', 'Infrastructure', 'Autre'];
|
||||
|
||||
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();
|
||||
}
|
||||
};
|
||||
|
||||
// Computes totals on the client side
|
||||
const totalHt = computed(() => {
|
||||
return form.items.reduce((sum, item) => {
|
||||
const qty = parseFloat(item.quantite) || 0;
|
||||
const price = parseFloat(item.prix_unitaire) || 0;
|
||||
return sum + (qty * price);
|
||||
}, 0);
|
||||
});
|
||||
|
||||
const tvaAmount = computed(() => {
|
||||
const rate = parseFloat(form.tva_rate) || 0;
|
||||
return totalHt.value * (rate / 100);
|
||||
});
|
||||
|
||||
const totalTtc = computed(() => {
|
||||
return totalHt.value + tvaAmount.value;
|
||||
});
|
||||
|
||||
const submit = () => {
|
||||
form.put(route('devis.update', props.devis.id));
|
||||
};
|
||||
|
||||
const formatCurrency = (value) => {
|
||||
return new Intl.NumberFormat('fr-FR', { style: 'currency', currency: 'EUR' }).format(value);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head :title="`Modifier le Devis ${devis.number}`" />
|
||||
|
||||
<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">
|
||||
Modifier le Devis {{ devis.number }}
|
||||
</h2>
|
||||
<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>
|
||||
</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">
|
||||
|
||||
<!-- Fiche / En-tête -->
|
||||
<div class="bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-xl shadow-sm overflow-hidden 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">
|
||||
<!-- Objet / Titre -->
|
||||
<div class="md:col-span-2">
|
||||
<label class="block text-sm font-bold text-slate-750 dark:text-slate-300 mb-1">Objet / Description du 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: Devis pour renouvellement de postes de travail" required>
|
||||
</div>
|
||||
|
||||
<!-- Type Destinataire -->
|
||||
<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>
|
||||
|
||||
<!-- Nom Destinataire -->
|
||||
<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, Service Finance..." required>
|
||||
</div>
|
||||
|
||||
<!-- Contact / Référent -->
|
||||
<div>
|
||||
<label class="block text-sm font-bold text-slate-755 dark:text-slate-300 mb-1">Référent / Interlocuteur :</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>
|
||||
|
||||
<!-- Date du Devis -->
|
||||
<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>
|
||||
|
||||
<!-- Statut -->
|
||||
<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>
|
||||
|
||||
<!-- Taux TVA -->
|
||||
<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>
|
||||
|
||||
<!-- Items du Devis -->
|
||||
<div class="bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-xl shadow-sm overflow-hidden p-6 space-y-4">
|
||||
<div class="flex items-center justify-between border-b border-slate-100 dark:border-slate-850 pb-3">
|
||||
<h3 class="text-md font-bold text-slate-800 dark:text-slate-200">
|
||||
Lignes du Devis
|
||||
</h3>
|
||||
<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">
|
||||
<!-- Type -->
|
||||
<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>
|
||||
<!-- Designation -->
|
||||
<div class="sm:col-span-5">
|
||||
<label class="block text-xxs font-bold text-slate-400 uppercase tracking-wide mb-1">Description / Matériel / Prestation</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>
|
||||
<!-- Quantité -->
|
||||
<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>
|
||||
<!-- Prix unitaire -->
|
||||
<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>
|
||||
|
||||
<!-- Récapitulatif Financier -->
|
||||
<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 / Conditions -->
|
||||
<div class="bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-xl shadow-sm overflow-hidden p-6">
|
||||
<label class="block text-sm font-bold text-slate-755 dark:text-slate-300 mb-1">Notes / Conditions de livraison / Remarques :</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. Matériel disponible sous 15 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.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 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-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"
|
||||
>
|
||||
Enregistrer les Modifications
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</AuthenticatedLayout>
|
||||
</template>
|
||||
Reference in New Issue
Block a user