Gestion des avoirs

This commit is contained in:
kevin 2026-01-29 16:44:31 +03:00
parent c0868b6acb
commit 4af8ea2c60
8 changed files with 1274 additions and 323 deletions

View File

@ -7,68 +7,155 @@
<div v-else-if="error" class="text-center py-5 text-danger"> <div v-else-if="error" class="text-center py-5 text-danger">
{{ error }} {{ error }}
</div> </div>
<avoir-detail-template v-else-if="avoir"> <div v-else-if="avoir" class="avoir-detail">
<template #header> <!-- Header Section -->
<avoir-header <div class="form-section">
:avoir-number="avoir.number" <div class="section-title">
:date="avoir.date" <i class="fas fa-file-invoice-dollar"></i>
/> Informations générales
</template> </div>
<template #lines> <!-- Row 1: Avoir Number, Date, Status -->
<avoir-lines-table :lines="avoir.lines" /> <div class="row g-3 mb-3">
</template> <div class="col-md-4">
<label class="form-label">Numéro avoir</label>
<div class="info-value">{{ avoir.avoir_number }}</div>
</div>
<div class="col-md-4">
<label class="form-label">Date d'émission</label>
<div class="info-value">{{ formatDate(avoir.avoir_date) }}</div>
</div>
<div class="col-md-4">
<label class="form-label">Statut</label>
<div class="status-badge" :class="getStatusClass(avoir.status)">
<i :class="getStatusIcon(avoir.status)"></i>
{{ getStatusLabel(avoir.status) }}
</div>
</div>
</div>
<template #timeline> <!-- Row 2: Client, Facture d'origine -->
<div> <div class="row g-3 mb-3">
<h6 class="mb-3 text-sm">Historique</h6> <div class="col-md-6">
<div v-if="avoir.history && avoir.history.length > 0"> <label class="form-label">Client</label>
<div v-for="(entry, index) in avoir.history" :key="index" class="mb-2"> <div class="info-value">{{ avoir.client?.name || 'Client inconnu' }}</div>
<span class="text-xs text-secondary"> </div>
{{ formatDate(entry.changed_at) }} <div class="col-md-6">
<label class="form-label">Facture d'origine</label>
<div class="info-value">{{ avoir.invoice?.invoice_number || 'Non spécifiée' }}</div>
</div>
</div>
<!-- Row 3: Motif, Mode de remboursement -->
<div class="mb-0">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label">Motif</label>
<div class="info-value">{{ getReasonLabel(avoir.reason_type) }}</div>
</div>
<div class="col-md-6">
<label class="form-label">Mode de remboursement</label>
<div class="info-value">{{ getRefundMethodLabel(avoir.refund_method) }}</div>
</div>
</div>
</div>
</div>
<!-- Articles Section -->
<div class="form-section">
<div class="section-header">
<div class="section-title">
<i class="fas fa-boxes"></i>
Articles de l'avoir
</div>
</div>
<div class="lines-container">
<div
v-for="line in avoir.lines"
:key="line.id"
class="line-item"
>
<div class="row g-2 align-items-center">
<div class="col-md-6">
<label class="form-label text-xs">Désignation</label>
<div class="line-designation">{{ line.description }}</div>
</div>
<div class="col-md-2">
<label class="form-label text-xs">Quantité</label>
<div class="line-quantity">{{ line.quantity }}</div>
</div>
<div class="col-md-2">
<label class="form-label text-xs">Prix HT</label>
<div class="line-price">{{ formatCurrency(line.unit_price) }}</div>
</div>
<div class="col-md-2 d-flex flex-column align-items-end">
<label class="form-label text-xs">Total HT</label>
<span class="line-total">
{{ formatCurrency(line.total_ht) }}
</span> </span>
<p class="text-xs mb-0">{{ entry.comment }}</p>
</div> </div>
</div> </div>
<p v-else class="text-xs text-secondary">Aucun historique</p>
</div> </div>
</template> </div>
</div>
<template #billing> <!-- Totals Section -->
<div> <div class="totals-section">
<h6 class="mb-3 text-sm">Informations</h6> <div class="totals-content">
<p class="text-sm mb-1"> <div class="total-row">
<strong>Client:</strong> {{ avoir.clientName }} <span class="total-label">Total HT</span>
</p> <span class="total-value">{{ formatCurrency(avoir.total_ht) }}</span>
<p class="text-sm mb-1"> </div>
<strong>Facture d'origine:</strong> {{ avoir.invoiceNumber }} <div class="total-row">
</p> <span class="total-label">TVA (20%)</span>
<p class="text-sm mb-1"> <span class="total-value">{{ formatCurrency(avoir.total_tva) }}</span>
<strong>Motif:</strong> {{ avoir.reason }} </div>
</p> <div class="total-row total-final">
<p class="text-xs text-secondary mb-0"> <span class="total-label">Total TTC</span>
<strong>Créé le:</strong> {{ formatDate(avoir.date) }} <span class="total-amount">{{ formatCurrency(avoir.total_ttc) }}</span>
</p> </div>
</div>
</div> </div>
</template>
<template #summary> <!-- Additional Info Section -->
<avoir-summary <div class="form-section">
:ht="avoir.total_ht || avoir.amount" <div class="section-title">
:tva="avoir.total_tva || 0" <i class="fas fa-info-circle"></i>
:ttc="avoir.total_ttc || avoir.amount" Informations supplémentaires
/> </div>
</template>
<template #actions> <div class="row g-3">
<div class="d-flex justify-content-end"> <div class="col-md-6">
<div class="position-relative d-inline-block me-2"> <label class="form-label">Date de création</label>
<div class="info-value">{{ formatDate(avoir.created_at) }}</div>
</div>
<div class="col-md-6">
<label class="form-label">Contact client</label>
<div class="info-value">{{ avoir.client?.email || avoir.client?.phone || 'Non spécifié' }}</div>
</div>
</div>
<div v-if="avoir.reason_description" class="mt-3">
<label class="form-label">Détail du motif</label>
<div class="info-value notes-content">{{ avoir.reason_description }}</div>
</div>
</div>
<!-- Action Buttons -->
<div class="action-buttons">
<div class="position-relative d-inline-block">
<soft-button <soft-button
color="secondary" color="secondary"
variant="gradient" variant="gradient"
@click="dropdownOpen = !dropdownOpen" @click="dropdownOpen = !dropdownOpen"
class="btn-status"
> >
{{ getStatusLabel(avoir.status) }} <i class="fas fa-exchange-alt me-2"></i>
Changer le statut
<i class="fas fa-chevron-down ms-2"></i> <i class="fas fa-chevron-down ms-2"></i>
</soft-button> </soft-button>
<ul <ul
@ -83,28 +170,26 @@
href="javascript:;" href="javascript:;"
@click="changeStatus(status); dropdownOpen = false;" @click="changeStatus(status); dropdownOpen = false;"
> >
<i :class="getStatusIcon(status) + ' me-2'"></i>
{{ getStatusLabel(status) }} {{ getStatusLabel(status) }}
</a> </a>
</li> </li>
</ul> </ul>
</div> </div>
<soft-button color="info" variant="outline"> <soft-button color="info" variant="outline" class="btn-pdf">
<i class="fas fa-file-pdf me-1"></i> Télécharger PDF <i class="fas fa-file-pdf me-2"></i> Télécharger PDF
</soft-button> </soft-button>
</div> </div>
</template> </div>
</avoir-detail-template>
</template> </template>
<script setup> <script setup>
import { ref, defineProps } from "vue"; import { ref, defineProps, onMounted } from "vue";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import AvoirDetailTemplate from "@/components/templates/Avoir/AvoirDetailTemplate.vue";
import AvoirHeader from "@/components/molecules/Avoir/AvoirHeader.vue";
import AvoirLinesTable from "@/components/molecules/Avoir/AvoirLinesTable.vue";
import AvoirSummary from "@/components/molecules/Avoir/AvoirSummary.vue";
import SoftButton from "@/components/SoftButton.vue"; import SoftButton from "@/components/SoftButton.vue";
import { useAvoirStore } from "@/stores/avoirStore";
import { useNotificationStore } from "@/stores/notification";
const props = defineProps({ const props = defineProps({
avoirId: { avoirId: {
@ -114,60 +199,22 @@ const props = defineProps({
}); });
const router = useRouter(); const router = useRouter();
const avoir = ref(null); const avoirStore = useAvoirStore();
const loading = ref(true); const notificationStore = useNotificationStore();
const error = ref(null); const { avoir, loading, error } = avoirStore;
const dropdownOpen = ref(false); const dropdownOpen = ref(false);
// Sample avoir data const availableStatuses = ["brouillon", "emis", "applique", "annule"];
const sampleAvoir = {
id: props.avoirId,
number: "AV-2026-02341",
invoiceNumber: "F-2026-00001",
clientName: "Caroline Lepetit thanatopraxie",
amount: 168.0,
status: "emis",
date: new Date(2026, 0, 23),
reason: "Erreur de facturation",
total_ht: 168.0,
total_tva: 0,
total_ttc: 168.0,
lines: [
{
id: 1,
description: "Forfait thanatopraxie",
quantity: 1,
price_ht: 168.0,
total_ht: 168.0,
},
],
history: [
{
changed_at: new Date(2026, 0, 23),
comment: "Avoir créé",
},
{
changed_at: new Date(2026, 0, 23, 10, 30),
comment: "Statut changé en Émis",
},
],
};
loading.value = false;
avoir.value = sampleAvoir;
const formatDate = (dateString) => { const formatDate = (dateString) => {
if (!dateString) return "-"; if (!dateString) return "-";
return new Date(dateString).toLocaleDateString("fr-FR"); return new Date(dateString).toLocaleDateString("fr-FR", {
day: "numeric",
month: "long",
year: "numeric",
});
}; };
const availableStatuses = [
"brouillon",
"emis",
"applique",
"annule",
];
const getStatusLabel = (status) => { const getStatusLabel = (status) => {
const labels = { const labels = {
brouillon: "Brouillon", brouillon: "Brouillon",
@ -178,9 +225,318 @@ const getStatusLabel = (status) => {
return labels[status] || status; return labels[status] || status;
}; };
const changeStatus = (newStatus) => { const getStatusIcon = (status) => {
if (!avoir.value) return; const icons = {
avoir.value.status = newStatus; brouillon: "fas fa-file-alt",
alert(`Statut changé en ${getStatusLabel(newStatus)}`); emis: "fas fa-paper-plane",
applique: "fas fa-check-circle",
annule: "fas fa-times-circle",
}; };
return icons[status] || "fas fa-question-circle";
};
const getStatusClass = (status) => {
const classes = {
brouillon: "status-draft",
emis: "status-issued",
applique: "status-applied",
annule: "status-cancelled",
};
return classes[status] || "";
};
const getReasonLabel = (reason) => {
const labels = {
erreur_facturation: "Erreur de facturation",
retour_marchandise: "Retour de marchandise",
geste_commercial: "Geste commercial",
annulation_prestation: "Annulation de prestation",
autre: "Autre",
};
return labels[reason] || reason;
};
const getRefundMethodLabel = (method) => {
const labels = {
deduction_facture: "Déduction sur prochaine facture",
virement: "Virement bancaire",
cheque: "Chèque",
especes: "Espèces",
non_rembourse: "Non remboursé",
};
return labels[method] || method;
};
const formatCurrency = (value) => {
return new Intl.NumberFormat("fr-FR", {
style: "currency",
currency: "EUR",
}).format(value || 0);
};
const changeStatus = async (newStatus) => {
if (!avoir.value) return;
try {
await avoirStore.updateAvoir({
id: avoir.value.id,
status: newStatus
});
notificationStore.success("Succès", `Statut mis à jour : ${getStatusLabel(newStatus)}`);
} catch (err) {
notificationStore.error("Erreur", "Échec de la mise à jour du statut.");
}
};
onMounted(async () => {
try {
await avoirStore.fetchAvoir(props.avoirId);
} catch (err) {
console.error("Error fetching avoir:", err);
}
});
</script> </script>
<style scoped>
.avoir-detail {
max-width: 1200px;
margin: 0 auto;
}
/* Form Sections */
.form-section {
background: #fff;
border-radius: 12px;
padding: 1.5rem;
margin-bottom: 1.5rem;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
border: 1px solid #e9ecef;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.section-title {
font-size: 1rem;
font-weight: 600;
color: #2c3e50;
display: flex;
align-items: center;
gap: 0.5rem;
}
.section-title i {
color: #6c757d;
font-size: 0.9rem;
}
/* Form Labels */
.form-label {
font-size: 0.875rem;
font-weight: 500;
color: #495057;
margin-bottom: 0.5rem;
}
.form-label.text-xs {
font-size: 0.8rem;
}
/* Info Value Display */
.info-value {
background: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 8px;
padding: 0.625rem 1rem;
font-size: 0.9rem;
color: #212529;
font-weight: 500;
}
.notes-content {
white-space: pre-wrap;
line-height: 1.5;
}
/* Status Badge */
.status-badge {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
border-radius: 8px;
font-size: 0.875rem;
font-weight: 600;
}
.status-badge i {
font-size: 0.9rem;
}
.status-draft {
background: #e9ecef;
color: #6c757d;
}
.status-issued {
background: #d1e7dd;
color: #0f5132;
}
.status-applied {
background: #cff4fc;
color: #055160;
}
.status-cancelled {
background: #f8d7da;
color: #842029;
}
/* Lines Container */
.lines-container {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.line-item {
background: #f8f9fa;
border-radius: 10px;
padding: 1rem;
border: 1px solid #e9ecef;
transition: box-shadow 0.2s, border-color 0.2s;
}
.line-item:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
border-color: #dee2e6;
}
.line-designation {
font-weight: 600;
color: #212529;
font-size: 0.9rem;
}
.line-quantity {
font-weight: 500;
color: #495057;
font-size: 0.9rem;
}
.line-price {
font-weight: 500;
color: #495057;
font-size: 0.9rem;
}
.line-total {
font-weight: 600;
color: #28a745;
font-size: 0.95rem;
white-space: nowrap;
}
/* Totals Section - Clean Design */
.totals-section {
background: #fff;
border-radius: 12px;
padding: 1.5rem;
margin-bottom: 1.5rem;
border: 1px solid #e9ecef;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
}
.totals-content {
max-width: 350px;
margin-left: auto;
}
.total-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.5rem 0;
border-bottom: 1px solid #e9ecef;
}
.total-row:last-child {
border-bottom: none;
}
.total-label {
font-size: 0.9rem;
color: #6c757d;
}
.total-value {
font-size: 0.95rem;
font-weight: 500;
color: #495057;
}
.total-final {
padding-top: 0.75rem;
margin-top: 0.5rem;
}
.total-final .total-label {
font-size: 1rem;
font-weight: 600;
color: #212529;
}
.total-amount {
font-size: 1.35rem;
font-weight: 700;
color: #212529;
}
/* Action Buttons */
.action-buttons {
display: flex;
justify-content: flex-end;
gap: 0.75rem;
padding-top: 0.5rem;
}
.btn-status,
.btn-pdf {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.625rem 1.25rem;
font-size: 0.9rem;
}
/* Responsive */
@media (max-width: 768px) {
.form-section {
padding: 1rem;
}
.section-header {
flex-direction: column;
align-items: flex-start;
gap: 0.75rem;
}
.totals-content {
max-width: 100%;
}
.action-buttons {
flex-direction: column-reverse;
}
.btn-status,
.btn-pdf {
width: 100%;
justify-content: center;
}
}
</style>

View File

@ -25,9 +25,11 @@ import AvoirListControls from "@/components/molecules/Avoir/AvoirListControls.vu
import AvoirTable from "@/components/molecules/Tables/Avoirs/AvoirTable.vue"; import AvoirTable from "@/components/molecules/Tables/Avoirs/AvoirTable.vue";
import { useAvoirStore } from "@/stores/avoirStore"; import { useAvoirStore } from "@/stores/avoirStore";
import { storeToRefs } from "pinia"; import { storeToRefs } from "pinia";
import { useNotificationStore } from "@/stores/notification";
const router = useRouter(); const router = useRouter();
const avoirStore = useAvoirStore(); const avoirStore = useAvoirStore();
const notificationStore = useNotificationStore();
const { avoirs, loading, error } = storeToRefs(avoirStore); const { avoirs, loading, error } = storeToRefs(avoirStore);
const activeFilter = ref(null); const activeFilter = ref(null);
@ -80,6 +82,8 @@ const handleExport = () => {
document.body.appendChild(link); document.body.appendChild(link);
link.click(); link.click();
document.body.removeChild(link); document.body.removeChild(link);
notificationStore.success("Export", "Fichier CSV exporté avec succès");
}; };
const getStatusLabel = (status) => { const getStatusLabel = (status) => {
@ -96,9 +100,9 @@ const handleDelete = async (id) => {
if (confirm("Êtes-vous sûr de vouloir supprimer cet avoir ?")) { if (confirm("Êtes-vous sûr de vouloir supprimer cet avoir ?")) {
try { try {
await avoirStore.deleteAvoir(id); await avoirStore.deleteAvoir(id);
alert("Avoir supprimé avec succès"); notificationStore.success("Succès", "Avoir supprimé avec succès");
} catch (err) { } catch (err) {
alert("Erreur lors de la suppression de l'avoir"); notificationStore.error("Erreur", "Erreur lors de la suppression de l'avoir");
} }
} }
}; };

View File

@ -24,15 +24,41 @@
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import NewAvoirForm from "@/components/molecules/Avoir/NewAvoirForm.vue"; import NewAvoirForm from "@/components/molecules/Avoir/NewAvoirForm.vue";
import SoftButton from "@/components/SoftButton.vue"; import SoftButton from "@/components/SoftButton.vue";
import { useAvoirStore } from "@/stores/avoirStore";
import { useNotificationStore } from "@/stores/notification";
const router = useRouter(); const router = useRouter();
const avoirStore = useAvoirStore();
const notificationStore = useNotificationStore();
const goBack = () => { const goBack = () => {
router.back(); router.back();
}; };
const handleSubmit = (formData) => { const handleSubmit = async (formData) => {
alert(`Avoir créé avec succès: ${formData.number}`); try {
const payload = {
client_id: formData.clientId,
invoice_id: formData.invoiceId,
avoir_number: formData.number,
status: formData.status,
avoir_date: formData.date,
reason_type: formData.reason,
reason_description: formData.reasonDetail,
lines: formData.lines.map(line => ({
description: line.designation,
quantity: line.quantity,
unit_price: line.priceHt,
tva_rate: 0,
})),
};
await avoirStore.createAvoir(payload);
notificationStore.success("Succès", `Avoir créé avec succès: ${formData.number}`);
router.push("/avoirs"); router.push("/avoirs");
} catch (err) {
console.error("Error creating avoir:", err);
notificationStore.error("Erreur", "Erreur lors de la création de l'avoir");
}
}; };
</script> </script>

View File

@ -7,68 +7,147 @@
<div v-else-if="error" class="text-center py-5 text-danger"> <div v-else-if="error" class="text-center py-5 text-danger">
{{ error }} {{ error }}
</div> </div>
<commande-detail-template v-else-if="commande"> <div v-else-if="commande" class="commande-detail">
<template #header> <!-- Header Section -->
<commande-header <div class="form-section">
:commande-number="commande.number" <div class="section-title">
:date="commande.date" <i class="fas fa-file-invoice"></i>
/> Informations générales
</template> </div>
<template #lines> <!-- Row 1: Commande Number, Date, Status -->
<commande-lines-table :lines="commande.lines" /> <div class="row g-3 mb-3">
</template> <div class="col-md-4">
<label class="form-label">Numéro commande</label>
<div class="info-value">{{ commande.number }}</div>
</div>
<div class="col-md-4">
<label class="form-label">Date commande</label>
<div class="info-value">{{ formatDate(commande.date) }}</div>
</div>
<div class="col-md-4">
<label class="form-label">Statut</label>
<div class="status-badge" :class="getStatusClass(commande.status)">
<i :class="getStatusIcon(commande.status)"></i>
{{ getStatusLabel(commande.status) }}
</div>
</div>
</div>
<template #timeline> <!-- Row 2: Fournisseur, Contact -->
<div> <div class="row g-3 mb-3">
<h6 class="mb-3 text-sm">Historique</h6> <div class="col-md-6">
<div v-if="commande.history && commande.history.length > 0"> <label class="form-label">Fournisseur</label>
<div v-for="(entry, index) in commande.history" :key="index" class="mb-2"> <div class="info-value">{{ commande.supplierName }}</div>
<span class="text-xs text-secondary"> </div>
{{ formatDate(entry.changed_at) }} <div class="col-md-6">
<label class="form-label">Contact</label>
<div class="info-value">{{ commande.supplierContact }}</div>
</div>
</div>
<!-- Row 3: Adresse livraison -->
<div class="mb-0">
<label class="form-label">Adresse livraison</label>
<div class="info-value">{{ commande.deliveryAddress || 'Non spécifiée' }}</div>
</div>
</div>
<!-- Articles Section -->
<div class="form-section">
<div class="section-header">
<div class="section-title">
<i class="fas fa-boxes"></i>
Articles commandés
</div>
</div>
<div class="lines-container">
<div
v-for="line in commande.lines"
:key="line.id"
class="line-item"
>
<div class="row g-2 align-items-center">
<div class="col-md-5">
<label class="form-label text-xs">Désignation</label>
<div class="line-designation">{{ line.designation }}</div>
</div>
<div class="col-md-2">
<label class="form-label text-xs">Quantité</label>
<div class="line-quantity">{{ line.quantity }}</div>
</div>
<div class="col-md-2">
<label class="form-label text-xs">Prix HT</label>
<div class="line-price">{{ formatCurrency(line.price_ht) }}</div>
</div>
<div class="col-md-3 d-flex flex-column align-items-end">
<label class="form-label text-xs">Total HT</label>
<span class="line-total">
{{ formatCurrency(line.total_ht) }}
</span> </span>
<p class="text-xs mb-0">{{ entry.comment }}</p>
</div> </div>
</div> </div>
<p v-else class="text-xs text-secondary">Aucun historique</p>
</div> </div>
</template> </div>
</div>
<template #billing> <!-- Totals Section -->
<div> <div class="totals-section">
<h6 class="mb-3 text-sm">Informations</h6> <div class="totals-content">
<p class="text-sm mb-1"> <div class="total-row">
<strong>Fournisseur:</strong> {{ commande.supplierName }} <span class="total-label">Total HT</span>
</p> <span class="total-value">{{ formatCurrency(commande.total_ht) }}</span>
<p class="text-sm mb-1"> </div>
<strong>Adresse:</strong> {{ commande.supplierAddress }} <div class="total-row">
</p> <span class="total-label">TVA (20%)</span>
<p class="text-sm mb-1"> <span class="total-value">{{ formatCurrency(commande.total_tva) }}</span>
<strong>Contact:</strong> {{ commande.supplierContact }} </div>
</p> <div class="total-row total-final">
<p class="text-xs text-secondary mb-0"> <span class="total-label">Total TTC</span>
<strong>Créée le:</strong> {{ formatDate(commande.date) }} <span class="total-amount">{{ formatCurrency(commande.total_ttc) }}</span>
</p> </div>
</div>
</div> </div>
</template>
<template #summary> <!-- Additional Info Section -->
<commande-summary <div class="form-section">
:ht="commande.total_ht" <div class="section-title">
:tva="commande.total_tva" <i class="fas fa-info-circle"></i>
:ttc="commande.total_ttc" Informations supplémentaires
/> </div>
</template>
<template #actions> <div class="row g-3">
<div class="d-flex justify-content-end"> <div class="col-md-6">
<div class="position-relative d-inline-block me-2"> <label class="form-label">Adresse fournisseur</label>
<div class="info-value">{{ commande.supplierAddress }}</div>
</div>
<div class="col-md-6">
<label class="form-label">Date de création</label>
<div class="info-value">{{ formatDate(commande.date) }}</div>
</div>
</div>
<div v-if="commande.notes" class="mt-3">
<label class="form-label">Notes</label>
<div class="info-value notes-content">{{ commande.notes }}</div>
</div>
</div>
<!-- Action Buttons -->
<div class="action-buttons">
<div class="position-relative d-inline-block">
<soft-button <soft-button
color="secondary" color="secondary"
variant="gradient" variant="gradient"
@click="dropdownOpen = !dropdownOpen" @click="dropdownOpen = !dropdownOpen"
class="btn-status"
> >
{{ getStatusLabel(commande.status) }} <i class="fas fa-exchange-alt me-2"></i>
Changer le statut
<i class="fas fa-chevron-down ms-2"></i> <i class="fas fa-chevron-down ms-2"></i>
</soft-button> </soft-button>
<ul <ul
@ -83,27 +162,23 @@
href="javascript:;" href="javascript:;"
@click="changeStatus(status); dropdownOpen = false;" @click="changeStatus(status); dropdownOpen = false;"
> >
<i :class="getStatusIcon(status) + ' me-2'"></i>
{{ getStatusLabel(status) }} {{ getStatusLabel(status) }}
</a> </a>
</li> </li>
</ul> </ul>
</div> </div>
<soft-button color="info" variant="outline"> <soft-button color="info" variant="outline" class="btn-pdf">
<i class="fas fa-file-pdf me-1"></i> Télécharger PDF <i class="fas fa-file-pdf me-2"></i> Télécharger PDF
</soft-button> </soft-button>
</div> </div>
</template> </div>
</commande-detail-template>
</template> </template>
<script setup> <script setup>
import { ref, defineProps, onMounted } from "vue"; import { ref, defineProps, onMounted } from "vue";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import CommandeDetailTemplate from "@/components/templates/Commande/CommandeDetailTemplate.vue";
import CommandeHeader from "@/components/molecules/Commande/CommandeHeader.vue";
import CommandeLinesTable from "@/components/molecules/Commande/CommandeLinesTable.vue";
import CommandeSummary from "@/components/molecules/Commande/CommandeSummary.vue";
import SoftButton from "@/components/SoftButton.vue"; import SoftButton from "@/components/SoftButton.vue";
import { PurchaseOrderService } from "@/services/purchaseOrder"; import { PurchaseOrderService } from "@/services/purchaseOrder";
import { useNotificationStore } from "@/stores/notification"; import { useNotificationStore } from "@/stores/notification";
@ -126,7 +201,11 @@ const availableStatuses = ["brouillon", "confirmee", "livree", "facturee", "annu
const formatDate = (dateString) => { const formatDate = (dateString) => {
if (!dateString) return "-"; if (!dateString) return "-";
return new Date(dateString).toLocaleDateString("fr-FR"); return new Date(dateString).toLocaleDateString("fr-FR", {
day: "numeric",
month: "long",
year: "numeric",
});
}; };
const getStatusLabel = (status) => { const getStatusLabel = (status) => {
@ -140,6 +219,35 @@ const getStatusLabel = (status) => {
return labels[status] || status; return labels[status] || status;
}; };
const getStatusIcon = (status) => {
const icons = {
brouillon: "fas fa-file-alt",
confirmee: "fas fa-check-circle",
livree: "fas fa-truck",
facturee: "fas fa-file-invoice-dollar",
annulee: "fas fa-times-circle",
};
return icons[status] || "fas fa-question-circle";
};
const getStatusClass = (status) => {
const classes = {
brouillon: "status-draft",
confirmee: "status-confirmed",
livree: "status-delivered",
facturee: "status-invoiced",
annulee: "status-cancelled",
};
return classes[status] || "";
};
const formatCurrency = (value) => {
return new Intl.NumberFormat("fr-FR", {
style: "currency",
currency: "EUR",
}).format(value || 0);
};
const fetchCommande = async () => { const fetchCommande = async () => {
loading.value = true; loading.value = true;
error.value = null; error.value = null;
@ -203,3 +311,253 @@ onMounted(() => {
fetchCommande(); fetchCommande();
}); });
</script> </script>
<style scoped>
.commande-detail {
max-width: 1200px;
margin: 0 auto;
}
/* Form Sections */
.form-section {
background: #fff;
border-radius: 12px;
padding: 1.5rem;
margin-bottom: 1.5rem;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
border: 1px solid #e9ecef;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.section-title {
font-size: 1rem;
font-weight: 600;
color: #2c3e50;
display: flex;
align-items: center;
gap: 0.5rem;
}
.section-title i {
color: #6c757d;
font-size: 0.9rem;
}
/* Form Labels */
.form-label {
font-size: 0.875rem;
font-weight: 500;
color: #495057;
margin-bottom: 0.5rem;
}
.form-label.text-xs {
font-size: 0.8rem;
}
/* Info Value Display */
.info-value {
background: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 8px;
padding: 0.625rem 1rem;
font-size: 0.9rem;
color: #212529;
font-weight: 500;
}
.notes-content {
white-space: pre-wrap;
line-height: 1.5;
}
/* Status Badge */
.status-badge {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
border-radius: 8px;
font-size: 0.875rem;
font-weight: 600;
}
.status-badge i {
font-size: 0.9rem;
}
.status-draft {
background: #e9ecef;
color: #6c757d;
}
.status-confirmed {
background: #d1e7dd;
color: #0f5132;
}
.status-delivered {
background: #cff4fc;
color: #055160;
}
.status-invoiced {
background: #fff3cd;
color: #664d03;
}
.status-cancelled {
background: #f8d7da;
color: #842029;
}
/* Lines Container */
.lines-container {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.line-item {
background: #f8f9fa;
border-radius: 10px;
padding: 1rem;
border: 1px solid #e9ecef;
transition: box-shadow 0.2s, border-color 0.2s;
}
.line-item:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
border-color: #dee2e6;
}
.line-designation {
font-weight: 600;
color: #212529;
font-size: 0.9rem;
}
.line-quantity {
font-weight: 500;
color: #495057;
font-size: 0.9rem;
}
.line-price {
font-weight: 500;
color: #495057;
font-size: 0.9rem;
}
.line-total {
font-weight: 600;
color: #28a745;
font-size: 0.95rem;
white-space: nowrap;
}
/* Totals Section - Clean Design */
.totals-section {
background: #fff;
border-radius: 12px;
padding: 1.5rem;
margin-bottom: 1.5rem;
border: 1px solid #e9ecef;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
}
.totals-content {
max-width: 350px;
margin-left: auto;
}
.total-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.5rem 0;
border-bottom: 1px solid #e9ecef;
}
.total-row:last-child {
border-bottom: none;
}
.total-label {
font-size: 0.9rem;
color: #6c757d;
}
.total-value {
font-size: 0.95rem;
font-weight: 500;
color: #495057;
}
.total-final {
padding-top: 0.75rem;
margin-top: 0.5rem;
}
.total-final .total-label {
font-size: 1rem;
font-weight: 600;
color: #212529;
}
.total-amount {
font-size: 1.35rem;
font-weight: 700;
color: #212529;
}
/* Action Buttons */
.action-buttons {
display: flex;
justify-content: flex-end;
gap: 0.75rem;
padding-top: 0.5rem;
}
.btn-status,
.btn-pdf {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.625rem 1.25rem;
font-size: 0.9rem;
}
/* Responsive */
@media (max-width: 768px) {
.form-section {
padding: 1rem;
}
.section-header {
flex-direction: column;
align-items: flex-start;
gap: 0.75rem;
}
.totals-content {
max-width: 100%;
}
.action-buttons {
flex-direction: column-reverse;
}
.btn-status,
.btn-pdf {
width: 100%;
justify-content: center;
}
}
</style>

View File

@ -6,7 +6,12 @@
<div class="card-header pb-0 p-3"> <div class="card-header pb-0 p-3">
<div class="d-flex justify-content-between align-items-center"> <div class="d-flex justify-content-between align-items-center">
<h6 class="mb-0">Créer une nouvelle commande</h6> <h6 class="mb-0">Créer une nouvelle commande</h6>
<soft-button color="secondary" variant="outline" size="sm" @click="goBack"> <soft-button
color="secondary"
variant="outline"
size="sm"
@click="goBack"
>
<i class="fas fa-arrow-left me-2"></i>Retour <i class="fas fa-arrow-left me-2"></i>Retour
</soft-button> </soft-button>
</div> </div>
@ -32,7 +37,6 @@ const goBack = () => {
}; };
const handleSubmit = (formData) => { const handleSubmit = (formData) => {
alert(`Commande créée avec succès: ${formData.number}`);
router.push("/fournisseurs/commandes"); router.push("/fournisseurs/commandes");
}; };
</script> </script>

View File

@ -29,16 +29,50 @@
</div> </div>
</div> </div>
<!-- Row 2: Facture d'origine, Client --> <!-- Row 2: Facture d'origine (with search), Client -->
<div class="row g-3 mb-4"> <div class="row g-3 mb-4">
<div class="col-md-6"> <div class="col-md-6 position-relative invoice-search-container">
<label class="form-label">Facture d'origine</label> <label class="form-label">Facture d'origine</label>
<select v-model="formData.invoiceId" class="form-select" @change="updateClientFromInvoice"> <div class="search-input-wrapper">
<option value="">-- Sélectionner une facture --</option> <i class="fas fa-search search-icon"></i>
<option v-for="invoice in invoices" :key="invoice.id" :value="invoice.id"> <soft-input
{{ invoice.number }} - {{ invoice.clientName }} ({{ formatCurrency(invoice.amount) }}) v-model="invoiceSearchQuery"
</option> type="text"
</select> placeholder="Rechercher une facture..."
@input="handleInvoiceSearch"
@focus="showInvoiceResults = true"
class="search-input"
/>
</div>
<!-- Search Results Dropdown -->
<div
v-if="showInvoiceResults && (invoiceSearchResults.length > 0 || isSearchingInvoices)"
class="search-dropdown"
>
<div v-if="isSearchingInvoices" class="dropdown-loading">
<div class="spinner-border spinner-border-sm text-primary" role="status">
<span class="visually-hidden">Chargement...</span>
</div>
</div>
<div v-else-if="invoiceSearchResults.length === 0" class="dropdown-empty">
Aucune facture trouvée
</div>
<template v-else>
<button
v-for="invoice in invoiceSearchResults"
:key="invoice.id"
type="button"
class="dropdown-item"
@click="selectInvoice(invoice)"
>
<span class="item-name">{{ invoice.invoice_number }}</span>
<span class="item-details">
{{ invoice.clientName }} {{ formatCurrency(invoice.amount) }}
</span>
</button>
</template>
</div>
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label">Client</label> <label class="form-label">Client</label>
@ -81,7 +115,7 @@
<label class="form-label">Détail du motif</label> <label class="form-label">Détail du motif</label>
<textarea <textarea
v-model="formData.reasonDetail" v-model="formData.reasonDetail"
class="form-control" class="form-control notes-textarea"
placeholder="Précisez le motif de l'avoir..." placeholder="Précisez le motif de l'avoir..."
rows="2" rows="2"
></textarea> ></textarea>
@ -105,7 +139,7 @@
<div <div
v-for="(line, index) in formData.lines" v-for="(line, index) in formData.lines"
:key="index" :key="index"
class="row g-2 align-items-end bg-light p-2 rounded" class="row g-2 align-items-end bg-light p-2 rounded line-item"
> >
<div class="col-md-5"> <div class="col-md-5">
<soft-input <soft-input
@ -150,7 +184,7 @@
<!-- Totaux --> <!-- Totaux -->
<div class="row"> <div class="row">
<div class="col-12"> <div class="col-12">
<div class=""> <div class="totals-section">
<div class="d-flex justify-content-end"> <div class="d-flex justify-content-end">
<div class="text-end"> <div class="text-end">
<p class="mb-2"> <p class="mb-2">
@ -175,7 +209,7 @@
<label class="form-label">Notes internes</label> <label class="form-label">Notes internes</label>
<textarea <textarea
v-model="formData.notes" v-model="formData.notes"
class="form-control" class="form-control notes-textarea"
placeholder="Notes..." placeholder="Notes..."
rows="2" rows="2"
></textarea> ></textarea>
@ -202,59 +236,139 @@
</template> </template>
<script setup> <script setup>
import { ref, defineEmits } from "vue"; import { ref, defineEmits, onMounted, onUnmounted } from "vue";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import SoftInput from "@/components/SoftInput.vue"; import SoftInput from "@/components/SoftInput.vue";
import SoftButton from "@/components/SoftButton.vue"; import SoftButton from "@/components/SoftButton.vue";
import { useAvoirStore } from "@/stores/avoirStore";
import { useNotificationStore } from "@/stores/notification";
const router = useRouter(); const router = useRouter();
const avoirStore = useAvoirStore();
const notificationStore = useNotificationStore();
const emit = defineEmits(["submit"]); const emit = defineEmits(["submit"]);
// Invoice Search States
const invoiceSearchQuery = ref("");
const invoiceSearchResults = ref([]);
const isSearchingInvoices = ref(false);
const showInvoiceResults = ref(false);
let searchTimeout = null;
// Sample invoices data // Sample invoices data
const invoices = ref([ const sampleInvoices = [
{ {
id: "1", id: "1",
number: "F-2026-00001", invoice_number: "F-2026-00001",
clientName: "Caroline Lepetit thanatopraxie", clientName: "Caroline Lepetit thanatopraxie",
amount: 168.0, amount: 168.0,
}, },
{ {
id: "2", id: "2",
number: "FAC-202512-0002", invoice_number: "FAC-202512-0002",
clientName: "Hygiène Funéraire 50", clientName: "Hygiène Funéraire 50",
amount: 168.0, amount: 168.0,
}, },
{ {
id: "3", id: "3",
number: "FAC-202512-0001", invoice_number: "FAC-202512-0001",
clientName: "Hygiène Funéraire 50", clientName: "Hygiène Funéraire 50",
amount: 168.0, amount: 168.0,
}, },
{ {
id: "4", id: "4",
number: "FAC-202512-0006", invoice_number: "FAC-202512-0006",
clientName: "Pompes Funèbres Martin", clientName: "Pompes Funèbres Martin",
amount: 216.0, amount: 216.0,
}, },
{ {
id: "5", id: "5",
number: "FAC-202512-0008", invoice_number: "FAC-202512-0008",
clientName: "Pompes Funèbres Martin", clientName: "Pompes Funèbres Martin",
amount: 312.0, amount: 312.0,
}, },
{ {
id: "6", id: "6",
number: "FACT-2024-003", invoice_number: "FACT-2024-003",
clientName: "PF Premium", clientName: "PF Premium",
amount: 144.0, amount: 144.0,
}, },
]); ];
const handleInvoiceSearch = () => {
if (invoiceSearchQuery.value.length < 2) {
invoiceSearchResults.value = [];
if (searchTimeout) clearTimeout(searchTimeout);
isSearchingInvoices.value = false;
return;
}
if (searchTimeout) clearTimeout(searchTimeout);
searchTimeout = setTimeout(async () => {
if (invoiceSearchQuery.value.trim() === "") return;
isSearchingInvoices.value = true;
showInvoiceResults.value = true;
try {
// Simulate API search
await new Promise(resolve => setTimeout(resolve, 300));
const query = invoiceSearchQuery.value.toLowerCase();
invoiceSearchResults.value = sampleInvoices.filter(invoice =>
invoice.invoice_number.toLowerCase().includes(query) ||
invoice.clientName.toLowerCase().includes(query)
);
} catch (error) {
console.error("Error searching invoices:", error);
} finally {
isSearchingInvoices.value = false;
}
}, 300);
};
const selectInvoice = (invoice) => {
if (!invoice || !invoice.id) return;
if (searchTimeout) clearTimeout(searchTimeout);
formData.value.invoiceId = invoice.id;
formData.value.clientId = invoice.clientId;
formData.value.clientName = invoice.clientName;
// Optionally pre-fill a line based on invoice
formData.value.lines = [
{
designation: `Remboursement partiel de ${invoice.invoice_number}`,
quantity: 1,
priceHt: invoice.amount,
},
];
invoiceSearchQuery.value = invoice.invoice_number;
showInvoiceResults.value = false;
};
// Close dropdowns on click outside
const handleClickOutside = (event) => {
const invoiceContainer = document.querySelector('.invoice-search-container');
if (invoiceContainer && !invoiceContainer.contains(event.target)) {
showInvoiceResults.value = false;
}
};
onMounted(() => {
document.addEventListener('click', handleClickOutside);
});
onUnmounted(() => {
document.removeEventListener('click', handleClickOutside);
});
const formData = ref({ const formData = ref({
number: "AV-2026-02341", number: "AV-" + new Date().getFullYear() + "-" + String(Date.now()).slice(-5),
date: new Date().toISOString().split("T")[0], date: new Date().toISOString().split("T")[0],
status: "brouillon", status: "brouillon",
invoiceId: "", invoiceId: "",
clientId: "",
clientName: "", clientName: "",
reason: "erreur_facturation", reason: "erreur_facturation",
refundMethod: "deduction_facture", refundMethod: "deduction_facture",
@ -269,21 +383,6 @@ const formData = ref({
notes: "", notes: "",
}); });
const updateClientFromInvoice = () => {
const selectedInvoice = invoices.value.find((i) => i.id === formData.value.invoiceId);
if (selectedInvoice) {
formData.value.clientName = selectedInvoice.clientName;
// Optionally pre-fill a line based on the invoice
formData.value.lines = [
{
designation: `Remboursement partiel de ${selectedInvoice.number}`,
quantity: 1,
priceHt: selectedInvoice.amount,
},
];
}
};
const addLine = () => { const addLine = () => {
formData.value.lines.push({ formData.value.lines.push({
designation: "", designation: "",
@ -321,11 +420,11 @@ const formatCurrency = (value) => {
const submitForm = () => { const submitForm = () => {
if (!formData.value.invoiceId) { if (!formData.value.invoiceId) {
alert("Veuillez sélectionner une facture d'origine"); notificationStore.error("Erreur", "Veuillez sélectionner une facture d'origine");
return; return;
} }
if (formData.value.lines.length === 0) { if (formData.value.lines.length === 0) {
alert("Veuillez ajouter au moins une ligne"); notificationStore.error("Erreur", "Veuillez ajouter au moins une ligne");
return; return;
} }
@ -345,4 +444,124 @@ const cancelForm = () => {
.gap-3 { .gap-3 {
gap: 1rem; gap: 1rem;
} }
/* Search Input with Icon */
.search-input-wrapper {
position: relative;
display: flex;
align-items: center;
}
.search-icon {
position: absolute;
left: 12px;
color: #6c757d;
font-size: 0.9rem;
z-index: 10;
pointer-events: none;
}
.search-input :deep(input) {
padding-left: 2.5rem !important;
}
/* Search Dropdown */
.search-dropdown {
position: absolute;
top: 100%;
left: 0;
right: 0;
background: #fff;
border: 1px solid #dee2e6;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
max-height: 250px;
overflow-y: auto;
z-index: 9999;
margin-top: 4px;
}
.dropdown-item {
display: flex;
flex-direction: column;
padding: 0.75rem 1rem;
border-bottom: 1px solid #f1f3f5;
background: none;
border-left: none;
border-right: none;
border-top: none;
width: 100%;
text-align: left;
cursor: pointer;
transition: background-color 0.15s;
}
.dropdown-item:last-child {
border-bottom: none;
}
.dropdown-item:hover {
background-color: #f8f9fa;
}
.item-name {
font-weight: 600;
color: #212529;
font-size: 0.9rem;
}
.item-details {
font-size: 0.75rem;
color: #6c757d;
margin-top: 2px;
}
.dropdown-loading {
padding: 1rem;
text-align: center;
}
.dropdown-empty {
padding: 1rem;
text-align: center;
color: #6c757d;
font-style: italic;
font-size: 0.875rem;
}
.line-item {
background: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 8px;
transition: box-shadow 0.2s, border-color 0.2s;
}
.line-item:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
border-color: #dee2e6;
}
.notes-textarea {
border: 1px solid #dee2e6;
border-radius: 8px;
padding: 0.75rem 1rem;
font-size: 0.875rem;
resize: vertical;
transition: border-color 0.2s, box-shadow 0.2s;
}
.notes-textarea:focus {
border-color: #86b7fe;
box-shadow: 0 0 0 0.2rem rgba(13, 110, 253, 0.15);
outline: none;
}
.totals-section {
background: #fff;
border-radius: 12px;
padding: 1.5rem;
margin-bottom: 1.5rem;
border: 1px solid #e9ecef;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
}
</style> </style>

View File

@ -11,13 +11,12 @@
>Prénom <span class="text-danger">*</span></label >Prénom <span class="text-danger">*</span></label
> >
<soft-input <soft-input
:value="form.first_name" v-model="form.first_name"
class="multisteps-form__input" class="multisteps-form__input"
:class="{ 'is-invalid': fieldErrors.first_name }" :error="!!fieldErrors.first_name"
type="text" type="text"
placeholder="ex. Jean" placeholder="ex. Jean"
maxlength="191" maxlength="191"
@input="form.first_name = $event.target.value"
/> />
<div v-if="fieldErrors.first_name" class="invalid-feedback"> <div v-if="fieldErrors.first_name" class="invalid-feedback">
{{ fieldErrors.first_name }} {{ fieldErrors.first_name }}
@ -28,13 +27,12 @@
>Nom de famille <span class="text-danger">*</span></label >Nom de famille <span class="text-danger">*</span></label
> >
<soft-input <soft-input
:value="form.last_name" v-model="form.last_name"
class="multisteps-form__input" class="multisteps-form__input"
:class="{ 'is-invalid': fieldErrors.last_name }" :error="!!fieldErrors.last_name"
type="text" type="text"
placeholder="ex. Dupont" placeholder="ex. Dupont"
maxlength="191" maxlength="191"
@input="form.last_name = $event.target.value"
/> />
<div v-if="fieldErrors.last_name" class="invalid-feedback"> <div v-if="fieldErrors.last_name" class="invalid-feedback">
{{ fieldErrors.last_name }} {{ fieldErrors.last_name }}
@ -47,13 +45,12 @@
<div class="col-12 col-sm-6"> <div class="col-12 col-sm-6">
<label class="form-label">Email</label> <label class="form-label">Email</label>
<soft-input <soft-input
:value="form.email" v-model="form.email"
class="multisteps-form__input" class="multisteps-form__input"
:class="{ 'is-invalid': fieldErrors.email }" :error="!!fieldErrors.email"
type="email" type="email"
placeholder="ex. jean.dupont@entreprise.com" placeholder="ex. jean.dupont@entreprise.com"
maxlength="191" maxlength="191"
@input="form.email = $event.target.value"
/> />
<div v-if="fieldErrors.email" class="invalid-feedback"> <div v-if="fieldErrors.email" class="invalid-feedback">
{{ fieldErrors.email }} {{ fieldErrors.email }}
@ -62,13 +59,12 @@
<div class="col-12 col-sm-6 mt-3 mt-sm-0"> <div class="col-12 col-sm-6 mt-3 mt-sm-0">
<label class="form-label">Téléphone</label> <label class="form-label">Téléphone</label>
<soft-input <soft-input
:value="form.phone" v-model="form.phone"
class="multisteps-form__input" class="multisteps-form__input"
:class="{ 'is-invalid': fieldErrors.phone }" :error="!!fieldErrors.phone"
type="text" type="text"
placeholder="ex. +261341234567" placeholder="ex. +261341234567"
maxlength="50" maxlength="50"
@input="form.phone = $event.target.value"
/> />
<div v-if="fieldErrors.phone" class="invalid-feedback"> <div v-if="fieldErrors.phone" class="invalid-feedback">
{{ fieldErrors.phone }} {{ fieldErrors.phone }}
@ -81,13 +77,12 @@
<div class="col-12"> <div class="col-12">
<label class="form-label">Intitulé du poste</label> <label class="form-label">Intitulé du poste</label>
<soft-input <soft-input
:value="form.job_title" v-model="form.job_title"
class="multisteps-form__input" class="multisteps-form__input"
:class="{ 'is-invalid': fieldErrors.job_title }" :error="!!fieldErrors.job_title"
type="text" type="text"
placeholder="ex. Développeur Full-Stack" placeholder="ex. Développeur Full-Stack"
maxlength="191" maxlength="191"
@input="form.job_title = $event.target.value"
/> />
<div v-if="fieldErrors.job_title" class="invalid-feedback"> <div v-if="fieldErrors.job_title" class="invalid-feedback">
{{ fieldErrors.job_title }} {{ fieldErrors.job_title }}
@ -100,11 +95,10 @@
<div class="col-12 col-sm-6"> <div class="col-12 col-sm-6">
<label class="form-label">Date d'embauche</label> <label class="form-label">Date d'embauche</label>
<soft-input <soft-input
:value="form.hire_date" v-model="form.hire_date"
class="multisteps-form__input" class="multisteps-form__input"
:class="{ 'is-invalid': fieldErrors.hire_date }" :error="!!fieldErrors.hire_date"
type="date" type="date"
@input="form.hire_date = $event.target.value"
/> />
<div v-if="fieldErrors.hire_date" class="invalid-feedback"> <div v-if="fieldErrors.hire_date" class="invalid-feedback">
{{ fieldErrors.hire_date }} {{ fieldErrors.hire_date }}
@ -113,14 +107,13 @@
<div class="col-12 col-sm-6 mt-3 mt-sm-0"> <div class="col-12 col-sm-6 mt-3 mt-sm-0">
<label class="form-label">Salaire</label> <label class="form-label">Salaire</label>
<soft-input <soft-input
:value="form.salary" v-model="form.salary"
class="multisteps-form__input" class="multisteps-form__input"
:class="{ 'is-invalid': fieldErrors.salary }" :error="!!fieldErrors.salary"
type="number" type="number"
placeholder="ex. 45000" placeholder="ex. 45000"
step="0.01" step="0.01"
min="0" min="0"
@input="form.salary = $event.target.value"
/> />
<div v-if="fieldErrors.salary" class="invalid-feedback"> <div v-if="fieldErrors.salary" class="invalid-feedback">
{{ fieldErrors.salary }} {{ fieldErrors.salary }}
@ -222,7 +215,7 @@ watch(
(newErrors) => { (newErrors) => {
fieldErrors.value = { ...newErrors }; fieldErrors.value = { ...newErrors };
}, },
{ deep: true } { deep: true },
); );
// Watch for success from parent // Watch for success from parent
@ -232,7 +225,7 @@ watch(
if (newSuccess) { if (newSuccess) {
resetForm(); resetForm();
} }
} },
); );
const submitForm = async () => { const submitForm = async () => {

View File

@ -11,12 +11,11 @@
>Nom du fournisseur <span class="text-danger">*</span></label >Nom du fournisseur <span class="text-danger">*</span></label
> >
<soft-input <soft-input
:value="form.name" v-model="form.name"
class="multisteps-form__input" class="multisteps-form__input"
:class="{ 'is-invalid': fieldErrors.name }" :error="!!fieldErrors.name"
type="text" type="text"
placeholder="ex. Nom de l'entreprise" placeholder="ex. Nom de l'entreprise"
@input="form.name = $event.target.value"
/> />
<div v-if="fieldErrors.name" class="invalid-feedback"> <div v-if="fieldErrors.name" class="invalid-feedback">
{{ fieldErrors.name }} {{ fieldErrors.name }}
@ -29,13 +28,12 @@
<div class="col-12 col-sm-6"> <div class="col-12 col-sm-6">
<label class="form-label">Numéro de TVA</label> <label class="form-label">Numéro de TVA</label>
<soft-input <soft-input
:value="form.vat_number" v-model="form.vat_number"
class="multisteps-form__input" class="multisteps-form__input"
:class="{ 'is-invalid': fieldErrors.vat_number }" :error="!!fieldErrors.vat_number"
type="text" type="text"
placeholder="ex. FR12345678901" placeholder="ex. FR12345678901"
maxlength="32" maxlength="32"
@input="form.vat_number = $event.target.value"
/> />
<div v-if="fieldErrors.vat_number" class="invalid-feedback"> <div v-if="fieldErrors.vat_number" class="invalid-feedback">
{{ fieldErrors.vat_number }} {{ fieldErrors.vat_number }}
@ -44,13 +42,12 @@
<div class="col-12 col-sm-6 mt-3 mt-sm-0"> <div class="col-12 col-sm-6 mt-3 mt-sm-0">
<label class="form-label">SIRET</label> <label class="form-label">SIRET</label>
<soft-input <soft-input
:value="form.siret" v-model="form.siret"
class="multisteps-form__input" class="multisteps-form__input"
:class="{ 'is-invalid': fieldErrors.siret }" :error="!!fieldErrors.siret"
type="text" type="text"
placeholder="ex. 12345678901234" placeholder="ex. 12345678901234"
maxlength="20" maxlength="20"
@input="form.siret = $event.target.value"
/> />
<div v-if="fieldErrors.siret" class="invalid-feedback"> <div v-if="fieldErrors.siret" class="invalid-feedback">
{{ fieldErrors.siret }} {{ fieldErrors.siret }}
@ -63,12 +60,11 @@
<div class="col-12 col-sm-6"> <div class="col-12 col-sm-6">
<label class="form-label">Email</label> <label class="form-label">Email</label>
<soft-input <soft-input
:value="form.email" v-model="form.email"
class="multisteps-form__input" class="multisteps-form__input"
:class="{ 'is-invalid': fieldErrors.email }" :error="!!fieldErrors.email"
type="email" type="email"
placeholder="ex. contact@fournisseur.com" placeholder="ex. contact@fournisseur.com"
@input="form.email = $event.target.value"
/> />
<div v-if="fieldErrors.email" class="invalid-feedback"> <div v-if="fieldErrors.email" class="invalid-feedback">
{{ fieldErrors.email }} {{ fieldErrors.email }}
@ -77,13 +73,12 @@
<div class="col-12 col-sm-6 mt-3 mt-sm-0"> <div class="col-12 col-sm-6 mt-3 mt-sm-0">
<label class="form-label">Téléphone</label> <label class="form-label">Téléphone</label>
<soft-input <soft-input
:value="form.phone" v-model="form.phone"
class="multisteps-form__input" class="multisteps-form__input"
:class="{ 'is-invalid': fieldErrors.phone }" :error="!!fieldErrors.phone"
type="text" type="text"
placeholder="ex. +33 1 23 45 67 89" placeholder="ex. +33 1 23 45 67 89"
maxlength="50" maxlength="50"
@input="form.phone = $event.target.value"
/> />
<div v-if="fieldErrors.phone" class="invalid-feedback"> <div v-if="fieldErrors.phone" class="invalid-feedback">
{{ fieldErrors.phone }} {{ fieldErrors.phone }}
@ -96,13 +91,12 @@
<div class="col-12"> <div class="col-12">
<label class="form-label">Adresse ligne 1</label> <label class="form-label">Adresse ligne 1</label>
<soft-input <soft-input
:value="form.billing_address_line1" v-model="form.billing_address_line1"
class="multisteps-form__input" class="multisteps-form__input"
:class="{ 'is-invalid': fieldErrors.billing_address_line1 }" :error="!!fieldErrors.billing_address_line1"
type="text" type="text"
placeholder="ex. 123 Rue Principale" placeholder="ex. 123 Rue Principale"
maxlength="255" maxlength="255"
@input="form.billing_address_line1 = $event.target.value"
/> />
<div <div
v-if="fieldErrors.billing_address_line1" v-if="fieldErrors.billing_address_line1"
@ -117,13 +111,12 @@
<div class="col-12"> <div class="col-12">
<label class="form-label">Adresse ligne 2</label> <label class="form-label">Adresse ligne 2</label>
<soft-input <soft-input
:value="form.billing_address_line2" v-model="form.billing_address_line2"
class="multisteps-form__input" class="multisteps-form__input"
:class="{ 'is-invalid': fieldErrors.billing_address_line2 }" :error="!!fieldErrors.billing_address_line2"
type="text" type="text"
placeholder="ex. Bâtiment, Étage, etc." placeholder="ex. Bâtiment, Étage, etc."
maxlength="255" maxlength="255"
@input="form.billing_address_line2 = $event.target.value"
/> />
<div <div
v-if="fieldErrors.billing_address_line2" v-if="fieldErrors.billing_address_line2"
@ -138,13 +131,12 @@
<div class="col-12 col-sm-4"> <div class="col-12 col-sm-4">
<label class="form-label">Code postal</label> <label class="form-label">Code postal</label>
<soft-input <soft-input
:value="form.billing_postal_code" v-model="form.billing_postal_code"
class="multisteps-form__input" class="multisteps-form__input"
:class="{ 'is-invalid': fieldErrors.billing_postal_code }" :error="!!fieldErrors.billing_postal_code"
type="text" type="text"
placeholder="ex. 75001" placeholder="ex. 75001"
maxlength="20" maxlength="20"
@input="form.billing_postal_code = $event.target.value"
/> />
<div v-if="fieldErrors.billing_postal_code" class="invalid-feedback"> <div v-if="fieldErrors.billing_postal_code" class="invalid-feedback">
{{ fieldErrors.billing_postal_code }} {{ fieldErrors.billing_postal_code }}
@ -153,13 +145,12 @@
<div class="col-12 col-sm-4 mt-3 mt-sm-0"> <div class="col-12 col-sm-4 mt-3 mt-sm-0">
<label class="form-label">Ville</label> <label class="form-label">Ville</label>
<soft-input <soft-input
:value="form.billing_city" v-model="form.billing_city"
class="multisteps-form__input" class="multisteps-form__input"
:class="{ 'is-invalid': fieldErrors.billing_city }" :error="!!fieldErrors.billing_city"
type="text" type="text"
placeholder="ex. Paris" placeholder="ex. Paris"
maxlength="191" maxlength="191"
@input="form.billing_city = $event.target.value"
/> />
<div v-if="fieldErrors.billing_city" class="invalid-feedback"> <div v-if="fieldErrors.billing_city" class="invalid-feedback">
{{ fieldErrors.billing_city }} {{ fieldErrors.billing_city }}
@ -303,7 +294,7 @@ watch(
(newErrors) => { (newErrors) => {
fieldErrors.value = { ...newErrors }; fieldErrors.value = { ...newErrors };
}, },
{ deep: true } { deep: true },
); );
// Watch for success from parent // Watch for success from parent
@ -313,7 +304,7 @@ watch(
if (newSuccess) { if (newSuccess) {
resetForm(); resetForm();
} }
} },
); );
const submitForm = async () => { const submitForm = async () => {