Gestion des avoirs
This commit is contained in:
parent
c0868b6acb
commit
4af8ea2c60
@ -7,104 +7,189 @@
|
||||
<div v-else-if="error" class="text-center py-5 text-danger">
|
||||
{{ error }}
|
||||
</div>
|
||||
<avoir-detail-template v-else-if="avoir">
|
||||
<template #header>
|
||||
<avoir-header
|
||||
:avoir-number="avoir.number"
|
||||
:date="avoir.date"
|
||||
/>
|
||||
</template>
|
||||
<div v-else-if="avoir" class="avoir-detail">
|
||||
<!-- Header Section -->
|
||||
<div class="form-section">
|
||||
<div class="section-title">
|
||||
<i class="fas fa-file-invoice-dollar"></i>
|
||||
Informations générales
|
||||
</div>
|
||||
|
||||
<template #lines>
|
||||
<avoir-lines-table :lines="avoir.lines" />
|
||||
</template>
|
||||
|
||||
<template #timeline>
|
||||
<div>
|
||||
<h6 class="mb-3 text-sm">Historique</h6>
|
||||
<div v-if="avoir.history && avoir.history.length > 0">
|
||||
<div v-for="(entry, index) in avoir.history" :key="index" class="mb-2">
|
||||
<span class="text-xs text-secondary">
|
||||
{{ formatDate(entry.changed_at) }}
|
||||
</span>
|
||||
<p class="text-xs mb-0">{{ entry.comment }}</p>
|
||||
<!-- Row 1: Avoir Number, Date, Status -->
|
||||
<div class="row g-3 mb-3">
|
||||
<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>
|
||||
<p v-else class="text-xs text-secondary">Aucun historique</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #billing>
|
||||
<div>
|
||||
<h6 class="mb-3 text-sm">Informations</h6>
|
||||
<p class="text-sm mb-1">
|
||||
<strong>Client:</strong> {{ avoir.clientName }}
|
||||
</p>
|
||||
<p class="text-sm mb-1">
|
||||
<strong>Facture d'origine:</strong> {{ avoir.invoiceNumber }}
|
||||
</p>
|
||||
<p class="text-sm mb-1">
|
||||
<strong>Motif:</strong> {{ avoir.reason }}
|
||||
</p>
|
||||
<p class="text-xs text-secondary mb-0">
|
||||
<strong>Créé le:</strong> {{ formatDate(avoir.date) }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #summary>
|
||||
<avoir-summary
|
||||
:ht="avoir.total_ht || avoir.amount"
|
||||
:tva="avoir.total_tva || 0"
|
||||
:ttc="avoir.total_ttc || avoir.amount"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #actions>
|
||||
<div class="d-flex justify-content-end">
|
||||
<div class="position-relative d-inline-block me-2">
|
||||
<soft-button
|
||||
color="secondary"
|
||||
variant="gradient"
|
||||
@click="dropdownOpen = !dropdownOpen"
|
||||
>
|
||||
{{ getStatusLabel(avoir.status) }}
|
||||
<i class="fas fa-chevron-down ms-2"></i>
|
||||
</soft-button>
|
||||
<ul
|
||||
v-if="dropdownOpen"
|
||||
class="dropdown-menu show position-absolute"
|
||||
style="top: 100%; left: 0; z-index: 1000;"
|
||||
>
|
||||
<li v-for="status in availableStatuses" :key="status">
|
||||
<a
|
||||
class="dropdown-item"
|
||||
:class="{ active: status === avoir.status }"
|
||||
href="javascript:;"
|
||||
@click="changeStatus(status); dropdownOpen = false;"
|
||||
>
|
||||
{{ getStatusLabel(status) }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<!-- Row 2: Client, Facture d'origine -->
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Client</label>
|
||||
<div class="info-value">{{ avoir.client?.name || 'Client inconnu' }}</div>
|
||||
</div>
|
||||
<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>
|
||||
|
||||
<soft-button color="info" variant="outline">
|
||||
<i class="fas fa-file-pdf me-1"></i> Télécharger PDF
|
||||
</soft-button>
|
||||
</div>
|
||||
</template>
|
||||
</avoir-detail-template>
|
||||
|
||||
<!-- 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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Totals Section -->
|
||||
<div class="totals-section">
|
||||
<div class="totals-content">
|
||||
<div class="total-row">
|
||||
<span class="total-label">Total HT</span>
|
||||
<span class="total-value">{{ formatCurrency(avoir.total_ht) }}</span>
|
||||
</div>
|
||||
<div class="total-row">
|
||||
<span class="total-label">TVA (20%)</span>
|
||||
<span class="total-value">{{ formatCurrency(avoir.total_tva) }}</span>
|
||||
</div>
|
||||
<div class="total-row total-final">
|
||||
<span class="total-label">Total TTC</span>
|
||||
<span class="total-amount">{{ formatCurrency(avoir.total_ttc) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Additional Info Section -->
|
||||
<div class="form-section">
|
||||
<div class="section-title">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
Informations supplémentaires
|
||||
</div>
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<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
|
||||
color="secondary"
|
||||
variant="gradient"
|
||||
@click="dropdownOpen = !dropdownOpen"
|
||||
class="btn-status"
|
||||
>
|
||||
<i class="fas fa-exchange-alt me-2"></i>
|
||||
Changer le statut
|
||||
<i class="fas fa-chevron-down ms-2"></i>
|
||||
</soft-button>
|
||||
<ul
|
||||
v-if="dropdownOpen"
|
||||
class="dropdown-menu show position-absolute"
|
||||
style="top: 100%; left: 0; z-index: 1000;"
|
||||
>
|
||||
<li v-for="status in availableStatuses" :key="status">
|
||||
<a
|
||||
class="dropdown-item"
|
||||
:class="{ active: status === avoir.status }"
|
||||
href="javascript:;"
|
||||
@click="changeStatus(status); dropdownOpen = false;"
|
||||
>
|
||||
<i :class="getStatusIcon(status) + ' me-2'"></i>
|
||||
{{ getStatusLabel(status) }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<soft-button color="info" variant="outline" class="btn-pdf">
|
||||
<i class="fas fa-file-pdf me-2"></i> Télécharger PDF
|
||||
</soft-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, defineProps } from "vue";
|
||||
import { ref, defineProps, onMounted } from "vue";
|
||||
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 { useAvoirStore } from "@/stores/avoirStore";
|
||||
import { useNotificationStore } from "@/stores/notification";
|
||||
|
||||
const props = defineProps({
|
||||
avoirId: {
|
||||
@ -114,60 +199,22 @@ const props = defineProps({
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
const avoir = ref(null);
|
||||
const loading = ref(true);
|
||||
const error = ref(null);
|
||||
const avoirStore = useAvoirStore();
|
||||
const notificationStore = useNotificationStore();
|
||||
const { avoir, loading, error } = avoirStore;
|
||||
const dropdownOpen = ref(false);
|
||||
|
||||
// Sample avoir data
|
||||
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 availableStatuses = ["brouillon", "emis", "applique", "annule"];
|
||||
|
||||
const formatDate = (dateString) => {
|
||||
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 labels = {
|
||||
brouillon: "Brouillon",
|
||||
@ -178,9 +225,318 @@ const getStatusLabel = (status) => {
|
||||
return labels[status] || status;
|
||||
};
|
||||
|
||||
const changeStatus = (newStatus) => {
|
||||
if (!avoir.value) return;
|
||||
avoir.value.status = newStatus;
|
||||
alert(`Statut changé en ${getStatusLabel(newStatus)}`);
|
||||
const getStatusIcon = (status) => {
|
||||
const icons = {
|
||||
brouillon: "fas fa-file-alt",
|
||||
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>
|
||||
|
||||
<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>
|
||||
|
||||
@ -25,9 +25,11 @@ import AvoirListControls from "@/components/molecules/Avoir/AvoirListControls.vu
|
||||
import AvoirTable from "@/components/molecules/Tables/Avoirs/AvoirTable.vue";
|
||||
import { useAvoirStore } from "@/stores/avoirStore";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { useNotificationStore } from "@/stores/notification";
|
||||
|
||||
const router = useRouter();
|
||||
const avoirStore = useAvoirStore();
|
||||
const notificationStore = useNotificationStore();
|
||||
const { avoirs, loading, error } = storeToRefs(avoirStore);
|
||||
const activeFilter = ref(null);
|
||||
|
||||
@ -80,6 +82,8 @@ const handleExport = () => {
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
|
||||
notificationStore.success("Export", "Fichier CSV exporté avec succès");
|
||||
};
|
||||
|
||||
const getStatusLabel = (status) => {
|
||||
@ -96,9 +100,9 @@ const handleDelete = async (id) => {
|
||||
if (confirm("Êtes-vous sûr de vouloir supprimer cet avoir ?")) {
|
||||
try {
|
||||
await avoirStore.deleteAvoir(id);
|
||||
alert("Avoir supprimé avec succès");
|
||||
notificationStore.success("Succès", "Avoir supprimé avec succès");
|
||||
} catch (err) {
|
||||
alert("Erreur lors de la suppression de l'avoir");
|
||||
notificationStore.error("Erreur", "Erreur lors de la suppression de l'avoir");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -24,15 +24,41 @@
|
||||
import { useRouter } from "vue-router";
|
||||
import NewAvoirForm from "@/components/molecules/Avoir/NewAvoirForm.vue";
|
||||
import SoftButton from "@/components/SoftButton.vue";
|
||||
import { useAvoirStore } from "@/stores/avoirStore";
|
||||
import { useNotificationStore } from "@/stores/notification";
|
||||
|
||||
const router = useRouter();
|
||||
const avoirStore = useAvoirStore();
|
||||
const notificationStore = useNotificationStore();
|
||||
|
||||
const goBack = () => {
|
||||
router.back();
|
||||
};
|
||||
|
||||
const handleSubmit = (formData) => {
|
||||
alert(`Avoir créé avec succès: ${formData.number}`);
|
||||
router.push("/avoirs");
|
||||
const handleSubmit = async (formData) => {
|
||||
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");
|
||||
} catch (err) {
|
||||
console.error("Error creating avoir:", err);
|
||||
notificationStore.error("Erreur", "Erreur lors de la création de l'avoir");
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
@ -7,103 +7,178 @@
|
||||
<div v-else-if="error" class="text-center py-5 text-danger">
|
||||
{{ error }}
|
||||
</div>
|
||||
<commande-detail-template v-else-if="commande">
|
||||
<template #header>
|
||||
<commande-header
|
||||
:commande-number="commande.number"
|
||||
:date="commande.date"
|
||||
/>
|
||||
</template>
|
||||
<div v-else-if="commande" class="commande-detail">
|
||||
<!-- Header Section -->
|
||||
<div class="form-section">
|
||||
<div class="section-title">
|
||||
<i class="fas fa-file-invoice"></i>
|
||||
Informations générales
|
||||
</div>
|
||||
|
||||
<template #lines>
|
||||
<commande-lines-table :lines="commande.lines" />
|
||||
</template>
|
||||
|
||||
<template #timeline>
|
||||
<div>
|
||||
<h6 class="mb-3 text-sm">Historique</h6>
|
||||
<div v-if="commande.history && commande.history.length > 0">
|
||||
<div v-for="(entry, index) in commande.history" :key="index" class="mb-2">
|
||||
<span class="text-xs text-secondary">
|
||||
{{ formatDate(entry.changed_at) }}
|
||||
</span>
|
||||
<p class="text-xs mb-0">{{ entry.comment }}</p>
|
||||
<!-- Row 1: Commande Number, Date, Status -->
|
||||
<div class="row g-3 mb-3">
|
||||
<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>
|
||||
<p v-else class="text-xs text-secondary">Aucun historique</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #billing>
|
||||
<div>
|
||||
<h6 class="mb-3 text-sm">Informations</h6>
|
||||
<p class="text-sm mb-1">
|
||||
<strong>Fournisseur:</strong> {{ commande.supplierName }}
|
||||
</p>
|
||||
<p class="text-sm mb-1">
|
||||
<strong>Adresse:</strong> {{ commande.supplierAddress }}
|
||||
</p>
|
||||
<p class="text-sm mb-1">
|
||||
<strong>Contact:</strong> {{ commande.supplierContact }}
|
||||
</p>
|
||||
<p class="text-xs text-secondary mb-0">
|
||||
<strong>Créée le:</strong> {{ formatDate(commande.date) }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #summary>
|
||||
<commande-summary
|
||||
:ht="commande.total_ht"
|
||||
:tva="commande.total_tva"
|
||||
:ttc="commande.total_ttc"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #actions>
|
||||
<div class="d-flex justify-content-end">
|
||||
<div class="position-relative d-inline-block me-2">
|
||||
<soft-button
|
||||
color="secondary"
|
||||
variant="gradient"
|
||||
@click="dropdownOpen = !dropdownOpen"
|
||||
>
|
||||
{{ getStatusLabel(commande.status) }}
|
||||
<i class="fas fa-chevron-down ms-2"></i>
|
||||
</soft-button>
|
||||
<ul
|
||||
v-if="dropdownOpen"
|
||||
class="dropdown-menu show position-absolute"
|
||||
style="top: 100%; left: 0; z-index: 1000;"
|
||||
>
|
||||
<li v-for="status in availableStatuses" :key="status">
|
||||
<a
|
||||
class="dropdown-item"
|
||||
:class="{ active: status === commande.status }"
|
||||
href="javascript:;"
|
||||
@click="changeStatus(status); dropdownOpen = false;"
|
||||
>
|
||||
{{ getStatusLabel(status) }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<!-- Row 2: Fournisseur, Contact -->
|
||||
<div class="row g-3 mb-3">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Fournisseur</label>
|
||||
<div class="info-value">{{ commande.supplierName }}</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Contact</label>
|
||||
<div class="info-value">{{ commande.supplierContact }}</div>
|
||||
</div>
|
||||
|
||||
<soft-button color="info" variant="outline">
|
||||
<i class="fas fa-file-pdf me-1"></i> Télécharger PDF
|
||||
</soft-button>
|
||||
</div>
|
||||
</template>
|
||||
</commande-detail-template>
|
||||
|
||||
<!-- 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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Totals Section -->
|
||||
<div class="totals-section">
|
||||
<div class="totals-content">
|
||||
<div class="total-row">
|
||||
<span class="total-label">Total HT</span>
|
||||
<span class="total-value">{{ formatCurrency(commande.total_ht) }}</span>
|
||||
</div>
|
||||
<div class="total-row">
|
||||
<span class="total-label">TVA (20%)</span>
|
||||
<span class="total-value">{{ formatCurrency(commande.total_tva) }}</span>
|
||||
</div>
|
||||
<div class="total-row total-final">
|
||||
<span class="total-label">Total TTC</span>
|
||||
<span class="total-amount">{{ formatCurrency(commande.total_ttc) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Additional Info Section -->
|
||||
<div class="form-section">
|
||||
<div class="section-title">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
Informations supplémentaires
|
||||
</div>
|
||||
|
||||
<div class="row g-3">
|
||||
<div class="col-md-6">
|
||||
<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
|
||||
color="secondary"
|
||||
variant="gradient"
|
||||
@click="dropdownOpen = !dropdownOpen"
|
||||
class="btn-status"
|
||||
>
|
||||
<i class="fas fa-exchange-alt me-2"></i>
|
||||
Changer le statut
|
||||
<i class="fas fa-chevron-down ms-2"></i>
|
||||
</soft-button>
|
||||
<ul
|
||||
v-if="dropdownOpen"
|
||||
class="dropdown-menu show position-absolute"
|
||||
style="top: 100%; left: 0; z-index: 1000;"
|
||||
>
|
||||
<li v-for="status in availableStatuses" :key="status">
|
||||
<a
|
||||
class="dropdown-item"
|
||||
:class="{ active: status === commande.status }"
|
||||
href="javascript:;"
|
||||
@click="changeStatus(status); dropdownOpen = false;"
|
||||
>
|
||||
<i :class="getStatusIcon(status) + ' me-2'"></i>
|
||||
{{ getStatusLabel(status) }}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<soft-button color="info" variant="outline" class="btn-pdf">
|
||||
<i class="fas fa-file-pdf me-2"></i> Télécharger PDF
|
||||
</soft-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, defineProps, onMounted } from "vue";
|
||||
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 { PurchaseOrderService } from "@/services/purchaseOrder";
|
||||
import { useNotificationStore } from "@/stores/notification";
|
||||
@ -126,7 +201,11 @@ const availableStatuses = ["brouillon", "confirmee", "livree", "facturee", "annu
|
||||
|
||||
const formatDate = (dateString) => {
|
||||
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) => {
|
||||
@ -140,6 +219,35 @@ const getStatusLabel = (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 () => {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
@ -203,3 +311,253 @@ onMounted(() => {
|
||||
fetchCommande();
|
||||
});
|
||||
</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>
|
||||
|
||||
@ -6,7 +6,12 @@
|
||||
<div class="card-header pb-0 p-3">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<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
|
||||
</soft-button>
|
||||
</div>
|
||||
@ -32,7 +37,6 @@ const goBack = () => {
|
||||
};
|
||||
|
||||
const handleSubmit = (formData) => {
|
||||
alert(`Commande créée avec succès: ${formData.number}`);
|
||||
router.push("/fournisseurs/commandes");
|
||||
};
|
||||
</script>
|
||||
|
||||
@ -29,16 +29,50 @@
|
||||
</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="col-md-6">
|
||||
<div class="col-md-6 position-relative invoice-search-container">
|
||||
<label class="form-label">Facture d'origine</label>
|
||||
<select v-model="formData.invoiceId" class="form-select" @change="updateClientFromInvoice">
|
||||
<option value="">-- Sélectionner une facture --</option>
|
||||
<option v-for="invoice in invoices" :key="invoice.id" :value="invoice.id">
|
||||
{{ invoice.number }} - {{ invoice.clientName }} ({{ formatCurrency(invoice.amount) }})
|
||||
</option>
|
||||
</select>
|
||||
<div class="search-input-wrapper">
|
||||
<i class="fas fa-search search-icon"></i>
|
||||
<soft-input
|
||||
v-model="invoiceSearchQuery"
|
||||
type="text"
|
||||
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 class="col-md-6">
|
||||
<label class="form-label">Client</label>
|
||||
@ -81,7 +115,7 @@
|
||||
<label class="form-label">Détail du motif</label>
|
||||
<textarea
|
||||
v-model="formData.reasonDetail"
|
||||
class="form-control"
|
||||
class="form-control notes-textarea"
|
||||
placeholder="Précisez le motif de l'avoir..."
|
||||
rows="2"
|
||||
></textarea>
|
||||
@ -105,7 +139,7 @@
|
||||
<div
|
||||
v-for="(line, index) in formData.lines"
|
||||
: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">
|
||||
<soft-input
|
||||
@ -150,7 +184,7 @@
|
||||
<!-- Totaux -->
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="">
|
||||
<div class="totals-section">
|
||||
<div class="d-flex justify-content-end">
|
||||
<div class="text-end">
|
||||
<p class="mb-2">
|
||||
@ -175,7 +209,7 @@
|
||||
<label class="form-label">Notes internes</label>
|
||||
<textarea
|
||||
v-model="formData.notes"
|
||||
class="form-control"
|
||||
class="form-control notes-textarea"
|
||||
placeholder="Notes..."
|
||||
rows="2"
|
||||
></textarea>
|
||||
@ -202,59 +236,139 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, defineEmits } from "vue";
|
||||
import { ref, defineEmits, onMounted, onUnmounted } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import SoftInput from "@/components/SoftInput.vue";
|
||||
import SoftButton from "@/components/SoftButton.vue";
|
||||
import { useAvoirStore } from "@/stores/avoirStore";
|
||||
import { useNotificationStore } from "@/stores/notification";
|
||||
|
||||
const router = useRouter();
|
||||
const avoirStore = useAvoirStore();
|
||||
const notificationStore = useNotificationStore();
|
||||
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
|
||||
const invoices = ref([
|
||||
const sampleInvoices = [
|
||||
{
|
||||
id: "1",
|
||||
number: "F-2026-00001",
|
||||
invoice_number: "F-2026-00001",
|
||||
clientName: "Caroline Lepetit thanatopraxie",
|
||||
amount: 168.0,
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
number: "FAC-202512-0002",
|
||||
invoice_number: "FAC-202512-0002",
|
||||
clientName: "Hygiène Funéraire 50",
|
||||
amount: 168.0,
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
number: "FAC-202512-0001",
|
||||
invoice_number: "FAC-202512-0001",
|
||||
clientName: "Hygiène Funéraire 50",
|
||||
amount: 168.0,
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
number: "FAC-202512-0006",
|
||||
invoice_number: "FAC-202512-0006",
|
||||
clientName: "Pompes Funèbres Martin",
|
||||
amount: 216.0,
|
||||
},
|
||||
{
|
||||
id: "5",
|
||||
number: "FAC-202512-0008",
|
||||
invoice_number: "FAC-202512-0008",
|
||||
clientName: "Pompes Funèbres Martin",
|
||||
amount: 312.0,
|
||||
},
|
||||
{
|
||||
id: "6",
|
||||
number: "FACT-2024-003",
|
||||
invoice_number: "FACT-2024-003",
|
||||
clientName: "PF Premium",
|
||||
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({
|
||||
number: "AV-2026-02341",
|
||||
number: "AV-" + new Date().getFullYear() + "-" + String(Date.now()).slice(-5),
|
||||
date: new Date().toISOString().split("T")[0],
|
||||
status: "brouillon",
|
||||
invoiceId: "",
|
||||
clientId: "",
|
||||
clientName: "",
|
||||
reason: "erreur_facturation",
|
||||
refundMethod: "deduction_facture",
|
||||
@ -269,21 +383,6 @@ const formData = ref({
|
||||
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 = () => {
|
||||
formData.value.lines.push({
|
||||
designation: "",
|
||||
@ -321,11 +420,11 @@ const formatCurrency = (value) => {
|
||||
|
||||
const submitForm = () => {
|
||||
if (!formData.value.invoiceId) {
|
||||
alert("Veuillez sélectionner une facture d'origine");
|
||||
notificationStore.error("Erreur", "Veuillez sélectionner une facture d'origine");
|
||||
return;
|
||||
}
|
||||
if (formData.value.lines.length === 0) {
|
||||
alert("Veuillez ajouter au moins une ligne");
|
||||
notificationStore.error("Erreur", "Veuillez ajouter au moins une ligne");
|
||||
return;
|
||||
}
|
||||
|
||||
@ -345,4 +444,124 @@ const cancelForm = () => {
|
||||
.gap-3 {
|
||||
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>
|
||||
|
||||
@ -11,13 +11,12 @@
|
||||
>Prénom <span class="text-danger">*</span></label
|
||||
>
|
||||
<soft-input
|
||||
:value="form.first_name"
|
||||
v-model="form.first_name"
|
||||
class="multisteps-form__input"
|
||||
:class="{ 'is-invalid': fieldErrors.first_name }"
|
||||
:error="!!fieldErrors.first_name"
|
||||
type="text"
|
||||
placeholder="ex. Jean"
|
||||
maxlength="191"
|
||||
@input="form.first_name = $event.target.value"
|
||||
/>
|
||||
<div v-if="fieldErrors.first_name" class="invalid-feedback">
|
||||
{{ fieldErrors.first_name }}
|
||||
@ -28,13 +27,12 @@
|
||||
>Nom de famille <span class="text-danger">*</span></label
|
||||
>
|
||||
<soft-input
|
||||
:value="form.last_name"
|
||||
v-model="form.last_name"
|
||||
class="multisteps-form__input"
|
||||
:class="{ 'is-invalid': fieldErrors.last_name }"
|
||||
:error="!!fieldErrors.last_name"
|
||||
type="text"
|
||||
placeholder="ex. Dupont"
|
||||
maxlength="191"
|
||||
@input="form.last_name = $event.target.value"
|
||||
/>
|
||||
<div v-if="fieldErrors.last_name" class="invalid-feedback">
|
||||
{{ fieldErrors.last_name }}
|
||||
@ -47,13 +45,12 @@
|
||||
<div class="col-12 col-sm-6">
|
||||
<label class="form-label">Email</label>
|
||||
<soft-input
|
||||
:value="form.email"
|
||||
v-model="form.email"
|
||||
class="multisteps-form__input"
|
||||
:class="{ 'is-invalid': fieldErrors.email }"
|
||||
:error="!!fieldErrors.email"
|
||||
type="email"
|
||||
placeholder="ex. jean.dupont@entreprise.com"
|
||||
maxlength="191"
|
||||
@input="form.email = $event.target.value"
|
||||
/>
|
||||
<div v-if="fieldErrors.email" class="invalid-feedback">
|
||||
{{ fieldErrors.email }}
|
||||
@ -62,13 +59,12 @@
|
||||
<div class="col-12 col-sm-6 mt-3 mt-sm-0">
|
||||
<label class="form-label">Téléphone</label>
|
||||
<soft-input
|
||||
:value="form.phone"
|
||||
v-model="form.phone"
|
||||
class="multisteps-form__input"
|
||||
:class="{ 'is-invalid': fieldErrors.phone }"
|
||||
:error="!!fieldErrors.phone"
|
||||
type="text"
|
||||
placeholder="ex. +261341234567"
|
||||
maxlength="50"
|
||||
@input="form.phone = $event.target.value"
|
||||
/>
|
||||
<div v-if="fieldErrors.phone" class="invalid-feedback">
|
||||
{{ fieldErrors.phone }}
|
||||
@ -81,13 +77,12 @@
|
||||
<div class="col-12">
|
||||
<label class="form-label">Intitulé du poste</label>
|
||||
<soft-input
|
||||
:value="form.job_title"
|
||||
v-model="form.job_title"
|
||||
class="multisteps-form__input"
|
||||
:class="{ 'is-invalid': fieldErrors.job_title }"
|
||||
:error="!!fieldErrors.job_title"
|
||||
type="text"
|
||||
placeholder="ex. Développeur Full-Stack"
|
||||
maxlength="191"
|
||||
@input="form.job_title = $event.target.value"
|
||||
/>
|
||||
<div v-if="fieldErrors.job_title" class="invalid-feedback">
|
||||
{{ fieldErrors.job_title }}
|
||||
@ -100,11 +95,10 @@
|
||||
<div class="col-12 col-sm-6">
|
||||
<label class="form-label">Date d'embauche</label>
|
||||
<soft-input
|
||||
:value="form.hire_date"
|
||||
v-model="form.hire_date"
|
||||
class="multisteps-form__input"
|
||||
:class="{ 'is-invalid': fieldErrors.hire_date }"
|
||||
:error="!!fieldErrors.hire_date"
|
||||
type="date"
|
||||
@input="form.hire_date = $event.target.value"
|
||||
/>
|
||||
<div v-if="fieldErrors.hire_date" class="invalid-feedback">
|
||||
{{ fieldErrors.hire_date }}
|
||||
@ -113,14 +107,13 @@
|
||||
<div class="col-12 col-sm-6 mt-3 mt-sm-0">
|
||||
<label class="form-label">Salaire</label>
|
||||
<soft-input
|
||||
:value="form.salary"
|
||||
v-model="form.salary"
|
||||
class="multisteps-form__input"
|
||||
:class="{ 'is-invalid': fieldErrors.salary }"
|
||||
:error="!!fieldErrors.salary"
|
||||
type="number"
|
||||
placeholder="ex. 45000"
|
||||
step="0.01"
|
||||
min="0"
|
||||
@input="form.salary = $event.target.value"
|
||||
/>
|
||||
<div v-if="fieldErrors.salary" class="invalid-feedback">
|
||||
{{ fieldErrors.salary }}
|
||||
@ -222,7 +215,7 @@ watch(
|
||||
(newErrors) => {
|
||||
fieldErrors.value = { ...newErrors };
|
||||
},
|
||||
{ deep: true }
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
// Watch for success from parent
|
||||
@ -232,7 +225,7 @@ watch(
|
||||
if (newSuccess) {
|
||||
resetForm();
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const submitForm = async () => {
|
||||
|
||||
@ -11,12 +11,11 @@
|
||||
>Nom du fournisseur <span class="text-danger">*</span></label
|
||||
>
|
||||
<soft-input
|
||||
:value="form.name"
|
||||
v-model="form.name"
|
||||
class="multisteps-form__input"
|
||||
:class="{ 'is-invalid': fieldErrors.name }"
|
||||
:error="!!fieldErrors.name"
|
||||
type="text"
|
||||
placeholder="ex. Nom de l'entreprise"
|
||||
@input="form.name = $event.target.value"
|
||||
/>
|
||||
<div v-if="fieldErrors.name" class="invalid-feedback">
|
||||
{{ fieldErrors.name }}
|
||||
@ -29,13 +28,12 @@
|
||||
<div class="col-12 col-sm-6">
|
||||
<label class="form-label">Numéro de TVA</label>
|
||||
<soft-input
|
||||
:value="form.vat_number"
|
||||
v-model="form.vat_number"
|
||||
class="multisteps-form__input"
|
||||
:class="{ 'is-invalid': fieldErrors.vat_number }"
|
||||
:error="!!fieldErrors.vat_number"
|
||||
type="text"
|
||||
placeholder="ex. FR12345678901"
|
||||
maxlength="32"
|
||||
@input="form.vat_number = $event.target.value"
|
||||
/>
|
||||
<div v-if="fieldErrors.vat_number" class="invalid-feedback">
|
||||
{{ fieldErrors.vat_number }}
|
||||
@ -44,13 +42,12 @@
|
||||
<div class="col-12 col-sm-6 mt-3 mt-sm-0">
|
||||
<label class="form-label">SIRET</label>
|
||||
<soft-input
|
||||
:value="form.siret"
|
||||
v-model="form.siret"
|
||||
class="multisteps-form__input"
|
||||
:class="{ 'is-invalid': fieldErrors.siret }"
|
||||
:error="!!fieldErrors.siret"
|
||||
type="text"
|
||||
placeholder="ex. 12345678901234"
|
||||
maxlength="20"
|
||||
@input="form.siret = $event.target.value"
|
||||
/>
|
||||
<div v-if="fieldErrors.siret" class="invalid-feedback">
|
||||
{{ fieldErrors.siret }}
|
||||
@ -63,12 +60,11 @@
|
||||
<div class="col-12 col-sm-6">
|
||||
<label class="form-label">Email</label>
|
||||
<soft-input
|
||||
:value="form.email"
|
||||
v-model="form.email"
|
||||
class="multisteps-form__input"
|
||||
:class="{ 'is-invalid': fieldErrors.email }"
|
||||
:error="!!fieldErrors.email"
|
||||
type="email"
|
||||
placeholder="ex. contact@fournisseur.com"
|
||||
@input="form.email = $event.target.value"
|
||||
/>
|
||||
<div v-if="fieldErrors.email" class="invalid-feedback">
|
||||
{{ fieldErrors.email }}
|
||||
@ -77,13 +73,12 @@
|
||||
<div class="col-12 col-sm-6 mt-3 mt-sm-0">
|
||||
<label class="form-label">Téléphone</label>
|
||||
<soft-input
|
||||
:value="form.phone"
|
||||
v-model="form.phone"
|
||||
class="multisteps-form__input"
|
||||
:class="{ 'is-invalid': fieldErrors.phone }"
|
||||
:error="!!fieldErrors.phone"
|
||||
type="text"
|
||||
placeholder="ex. +33 1 23 45 67 89"
|
||||
maxlength="50"
|
||||
@input="form.phone = $event.target.value"
|
||||
/>
|
||||
<div v-if="fieldErrors.phone" class="invalid-feedback">
|
||||
{{ fieldErrors.phone }}
|
||||
@ -96,13 +91,12 @@
|
||||
<div class="col-12">
|
||||
<label class="form-label">Adresse ligne 1</label>
|
||||
<soft-input
|
||||
:value="form.billing_address_line1"
|
||||
v-model="form.billing_address_line1"
|
||||
class="multisteps-form__input"
|
||||
:class="{ 'is-invalid': fieldErrors.billing_address_line1 }"
|
||||
:error="!!fieldErrors.billing_address_line1"
|
||||
type="text"
|
||||
placeholder="ex. 123 Rue Principale"
|
||||
maxlength="255"
|
||||
@input="form.billing_address_line1 = $event.target.value"
|
||||
/>
|
||||
<div
|
||||
v-if="fieldErrors.billing_address_line1"
|
||||
@ -117,13 +111,12 @@
|
||||
<div class="col-12">
|
||||
<label class="form-label">Adresse ligne 2</label>
|
||||
<soft-input
|
||||
:value="form.billing_address_line2"
|
||||
v-model="form.billing_address_line2"
|
||||
class="multisteps-form__input"
|
||||
:class="{ 'is-invalid': fieldErrors.billing_address_line2 }"
|
||||
:error="!!fieldErrors.billing_address_line2"
|
||||
type="text"
|
||||
placeholder="ex. Bâtiment, Étage, etc."
|
||||
maxlength="255"
|
||||
@input="form.billing_address_line2 = $event.target.value"
|
||||
/>
|
||||
<div
|
||||
v-if="fieldErrors.billing_address_line2"
|
||||
@ -138,13 +131,12 @@
|
||||
<div class="col-12 col-sm-4">
|
||||
<label class="form-label">Code postal</label>
|
||||
<soft-input
|
||||
:value="form.billing_postal_code"
|
||||
v-model="form.billing_postal_code"
|
||||
class="multisteps-form__input"
|
||||
:class="{ 'is-invalid': fieldErrors.billing_postal_code }"
|
||||
:error="!!fieldErrors.billing_postal_code"
|
||||
type="text"
|
||||
placeholder="ex. 75001"
|
||||
maxlength="20"
|
||||
@input="form.billing_postal_code = $event.target.value"
|
||||
/>
|
||||
<div v-if="fieldErrors.billing_postal_code" class="invalid-feedback">
|
||||
{{ fieldErrors.billing_postal_code }}
|
||||
@ -153,13 +145,12 @@
|
||||
<div class="col-12 col-sm-4 mt-3 mt-sm-0">
|
||||
<label class="form-label">Ville</label>
|
||||
<soft-input
|
||||
:value="form.billing_city"
|
||||
v-model="form.billing_city"
|
||||
class="multisteps-form__input"
|
||||
:class="{ 'is-invalid': fieldErrors.billing_city }"
|
||||
:error="!!fieldErrors.billing_city"
|
||||
type="text"
|
||||
placeholder="ex. Paris"
|
||||
maxlength="191"
|
||||
@input="form.billing_city = $event.target.value"
|
||||
/>
|
||||
<div v-if="fieldErrors.billing_city" class="invalid-feedback">
|
||||
{{ fieldErrors.billing_city }}
|
||||
@ -303,7 +294,7 @@ watch(
|
||||
(newErrors) => {
|
||||
fieldErrors.value = { ...newErrors };
|
||||
},
|
||||
{ deep: true }
|
||||
{ deep: true },
|
||||
);
|
||||
|
||||
// Watch for success from parent
|
||||
@ -313,7 +304,7 @@ watch(
|
||||
if (newSuccess) {
|
||||
resetForm();
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const submitForm = async () => {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user