361 lines
9.5 KiB
Vue
361 lines
9.5 KiB
Vue
<template>
|
|
<form @submit.prevent="submitForm">
|
|
<!-- Row 1: N° Facture, Fournisseur, Date -->
|
|
<div class="row g-3 mb-4">
|
|
<div class="col-md-4">
|
|
<label class="form-label">N° Facture Fournisseur *</label>
|
|
<soft-input
|
|
v-model="formData.number"
|
|
type="text"
|
|
placeholder="Ex: FR-2026-001"
|
|
required
|
|
/>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label">Fournisseur *</label>
|
|
<select
|
|
v-model="formData.supplierId"
|
|
class="form-select"
|
|
required
|
|
@change="updateSupplierInfo"
|
|
>
|
|
<option value="">-- Sélectionner un fournisseur --</option>
|
|
<option
|
|
v-for="supplier in suppliers"
|
|
:key="supplier.id"
|
|
:value="supplier.id"
|
|
>
|
|
{{ supplier.name }}
|
|
</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<label class="form-label">Date facture *</label>
|
|
<soft-input v-model="formData.date" type="date" required />
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Row 2: Statut, Notes -->
|
|
<div class="row g-3 mb-4">
|
|
<div class="col-md-6">
|
|
<label class="form-label">Statut</label>
|
|
<select v-model="formData.status" class="form-select">
|
|
<option value="brouillon">Brouillon</option>
|
|
<option value="en_attente">En attente de paiement</option>
|
|
<option value="payee">Payée</option>
|
|
<option value="annulee">Annulée</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label">Notes</label>
|
|
<textarea
|
|
v-model="formData.notes"
|
|
class="form-control"
|
|
placeholder="Remarques éventuelles..."
|
|
rows="2"
|
|
></textarea>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Articles Section -->
|
|
<div class="mb-4">
|
|
<div class="flex items-center justify-between mb-4">
|
|
<label
|
|
class="peer-disabled:cursor-not-allowed peer-disabled:opacity-70 text-lg font-bold"
|
|
>Lignes de facture</label
|
|
>
|
|
<button
|
|
class="inline-flex items-center justify-center gap-2 whitespace-nowrap font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 bg-primary text-primary-foreground shadow hover:bg-primary/90 h-8 rounded-md px-3 text-xs"
|
|
type="button"
|
|
@click="addLine"
|
|
>
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
width="24"
|
|
height="24"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
class="lucide lucide-plus w-4 h-4 mr-2"
|
|
>
|
|
<path d="M5 12h14"></path>
|
|
<path d="M12 5v14"></path>
|
|
</svg>
|
|
Ajouter ligne
|
|
</button>
|
|
</div>
|
|
|
|
<div class="space-y-3 mt-3">
|
|
<div
|
|
v-for="(line, index) in formData.lines"
|
|
:key="index"
|
|
class="row g-2 align-items-end bg-light p-3 rounded mx-0"
|
|
>
|
|
<div class="col-md-5">
|
|
<label class="form-label text-xs mb-1">Désignation *</label>
|
|
<soft-input
|
|
v-model="line.designation"
|
|
type="text"
|
|
placeholder="Nom de l'article ou prestation"
|
|
required
|
|
/>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<label class="form-label text-xs mb-1">Quantité *</label>
|
|
<soft-input
|
|
v-model.number="line.quantity"
|
|
type="number"
|
|
placeholder="Qté"
|
|
:min="1"
|
|
required
|
|
/>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<label class="form-label text-xs mb-1">Prix HT *</label>
|
|
<soft-input
|
|
v-model.number="line.priceHt"
|
|
type="number"
|
|
placeholder="0.00"
|
|
step="0.01"
|
|
required
|
|
/>
|
|
</div>
|
|
<div class="col-md-2 text-end">
|
|
<label class="form-label text-xs mb-1 d-block">Sous-total HT</label>
|
|
<span class="text-sm font-weight-bold">
|
|
{{ formatCurrency(line.quantity * line.priceHt) }}
|
|
</span>
|
|
</div>
|
|
<div class="col-md-1 text-end">
|
|
<button
|
|
type="button"
|
|
class="btn btn-sm btn-link text-danger mb-0"
|
|
:disabled="formData.lines.length === 1"
|
|
@click="removeLine(index)"
|
|
>
|
|
<i class="fas fa-trash"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Totaux -->
|
|
<div class="row mb-4">
|
|
<div class="col-md-5 ms-auto">
|
|
<div class="card bg-gray-100 shadow-none border">
|
|
<div class="card-body p-3">
|
|
<div class="d-flex justify-content-between mb-2">
|
|
<span class="text-sm">Total HT:</span>
|
|
<span class="text-sm font-weight-bold">{{
|
|
formatCurrency(calculateTotalHt())
|
|
}}</span>
|
|
</div>
|
|
<div class="d-flex justify-content-between mb-2">
|
|
<span class="text-sm">TVA (20%):</span>
|
|
<span class="text-sm font-weight-bold">{{
|
|
formatCurrency(calculateTotalTva())
|
|
}}</span>
|
|
</div>
|
|
<hr class="horizontal dark my-2" />
|
|
<div class="d-flex justify-content-between">
|
|
<span class="text-base font-weight-bold">Total TTC:</span>
|
|
<span class="text-base font-weight-bold text-info">{{
|
|
formatCurrency(calculateTotalTtc())
|
|
}}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Boutons d'action -->
|
|
<div class="d-flex justify-content-end gap-3 mt-4">
|
|
<soft-button
|
|
type="button"
|
|
color="secondary"
|
|
variant="outline"
|
|
@click="$emit('cancel')"
|
|
>
|
|
Annuler
|
|
</soft-button>
|
|
<soft-button type="submit" color="success">
|
|
Enregistrer la facture
|
|
</soft-button>
|
|
</div>
|
|
</form>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, defineEmits } from "vue";
|
|
import SoftInput from "@/components/SoftInput.vue";
|
|
import SoftButton from "@/components/SoftButton.vue";
|
|
|
|
const emit = defineEmits(["submit", "cancel"]);
|
|
|
|
const suppliers = [
|
|
{ id: "1", name: "Produits Funéraires Pro" },
|
|
{ id: "2", name: "Thanatos Supply" },
|
|
{ id: "3", name: "ISOFROID" },
|
|
{ id: "4", name: "EEP Co EUROPE" },
|
|
{ id: "5", name: "NEXTECH MEDICAL" },
|
|
{ id: "6", name: "ACTION" },
|
|
{ id: "7", name: "E.LECLERC" },
|
|
];
|
|
|
|
const formData = ref({
|
|
number: "",
|
|
supplierId: "",
|
|
supplierName: "",
|
|
date: new Date().toISOString().split("T")[0],
|
|
status: "brouillon",
|
|
notes: "",
|
|
lines: [
|
|
{
|
|
designation: "",
|
|
quantity: 1,
|
|
priceHt: 0,
|
|
},
|
|
],
|
|
});
|
|
|
|
const updateSupplierInfo = () => {
|
|
const supplier = suppliers.find((s) => s.id === formData.value.supplierId);
|
|
if (supplier) {
|
|
formData.value.supplierName = supplier.name;
|
|
}
|
|
};
|
|
|
|
const formatCurrency = (value) => {
|
|
return new Intl.NumberFormat("fr-FR", {
|
|
style: "currency",
|
|
currency: "EUR",
|
|
}).format(value);
|
|
};
|
|
|
|
const calculateTotalHt = () => {
|
|
return formData.value.lines.reduce((sum, line) => {
|
|
return sum + line.quantity * (line.priceHt || 0);
|
|
}, 0);
|
|
};
|
|
|
|
const calculateTotalTva = () => {
|
|
return calculateTotalHt() * 0.2;
|
|
};
|
|
|
|
const calculateTotalTtc = () => {
|
|
return calculateTotalHt() + calculateTotalTva();
|
|
};
|
|
|
|
const addLine = () => {
|
|
formData.value.lines.push({
|
|
designation: "",
|
|
quantity: 1,
|
|
priceHt: 0,
|
|
});
|
|
};
|
|
|
|
const removeLine = (index) => {
|
|
if (formData.value.lines.length > 1) {
|
|
formData.value.lines.splice(index, 1);
|
|
}
|
|
};
|
|
|
|
const submitForm = () => {
|
|
if (!formData.value.supplierId) {
|
|
alert("Veuillez sélectionner un fournisseur");
|
|
return;
|
|
}
|
|
|
|
const payload = {
|
|
...formData.value,
|
|
totalHt: calculateTotalHt(),
|
|
totalTva: calculateTotalTva(),
|
|
totalTtc: calculateTotalTtc(),
|
|
// Map lines to include totalHt for store consistency
|
|
lines: formData.value.lines.map((line) => ({
|
|
...line,
|
|
totalHt: line.quantity * line.priceHt,
|
|
})),
|
|
};
|
|
|
|
emit("submit", payload);
|
|
};
|
|
</script>
|
|
|
|
<style scoped>
|
|
/* Scoped styles for the custom button from user request */
|
|
.bg-primary {
|
|
background-color: var(--bs-primary, #cb0c9f);
|
|
}
|
|
.text-primary-foreground {
|
|
color: #fff;
|
|
}
|
|
.bg-primary\:hover {
|
|
background-color: #b30b8c;
|
|
}
|
|
|
|
/* Tailwind-like utilities used in the provided snippet */
|
|
.flex {
|
|
display: flex;
|
|
}
|
|
.items-center {
|
|
align-items: center;
|
|
}
|
|
.justify-between {
|
|
justify-content: space-between;
|
|
}
|
|
.mb-4 {
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
.text-lg {
|
|
font-size: 1.125rem;
|
|
}
|
|
.font-bold {
|
|
font-weight: 700;
|
|
}
|
|
.gap-2 {
|
|
gap: 0.5rem;
|
|
}
|
|
.whitespace-nowrap {
|
|
white-space: nowrap;
|
|
}
|
|
.transition-colors {
|
|
transition-property: background-color, border-color, color, fill, stroke;
|
|
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
|
transition-duration: 150ms;
|
|
}
|
|
.shadow {
|
|
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
|
|
}
|
|
.rounded-md {
|
|
border-radius: 0.375rem;
|
|
}
|
|
.px-3 {
|
|
padding-left: 0.75rem;
|
|
padding-right: 0.75rem;
|
|
}
|
|
.h-8 {
|
|
height: 2rem;
|
|
}
|
|
.text-xs {
|
|
font-size: 0.75rem;
|
|
}
|
|
.font-medium {
|
|
font-weight: 500;
|
|
}
|
|
|
|
.space-y-3 > div + div {
|
|
margin-top: 1rem;
|
|
}
|
|
|
|
.form-label {
|
|
font-size: 0.875rem;
|
|
font-weight: 500;
|
|
margin-bottom: 0.5rem;
|
|
}
|
|
</style>
|