Gestion des avoirs
This commit is contained in:
parent
c0868b6acb
commit
4af8ea2c60
@ -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>
|
||||||
|
|||||||
@ -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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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 () => {
|
||||||
|
|||||||
@ -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 () => {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user