2026-01-29 16:44:31 +03:00

568 lines
14 KiB
Vue

<template>
<form @submit.prevent="submitForm" class="space-y-6">
<!-- Row 1: N° Avoir, Date d'émission, Statut -->
<div class="row g-3 mb-4">
<div class="col-md-3">
<label class="form-label">N° Avoir</label>
<soft-input
v-model="formData.number"
type="text"
placeholder="Auto-généré"
:disabled="true"
/>
</div>
<div class="col-md-3">
<label class="form-label">Date d'émission</label>
<soft-input
v-model="formData.date"
type="date"
/>
</div>
<div class="col-md-3">
<label class="form-label">Statut</label>
<select v-model="formData.status" class="form-select">
<option value="brouillon">Brouillon</option>
<option value="emis">Émis</option>
<option value="applique">Appliqué</option>
<option value="annule">Annulé</option>
</select>
</div>
</div>
<!-- Row 2: Facture d'origine (with search), Client -->
<div class="row g-3 mb-4">
<div class="col-md-6 position-relative invoice-search-container">
<label class="form-label">Facture d'origine</label>
<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>
<soft-input
v-model="formData.clientName"
type="text"
placeholder="Automatiquement rempli"
:disabled="true"
/>
</div>
</div>
<!-- Row 3: Motif, Mode de remboursement -->
<div class="row g-3 mb-4">
<div class="col-md-6">
<label class="form-label">Motif</label>
<select v-model="formData.reason" class="form-select">
<option value="">-- Sélectionner un motif --</option>
<option value="erreur_facturation">Erreur de facturation</option>
<option value="retour_marchandise">Retour de marchandise</option>
<option value="geste_commercial">Geste commercial</option>
<option value="annulation_prestation">Annulation de prestation</option>
<option value="autre">Autre</option>
</select>
</div>
<div class="col-md-6">
<label class="form-label">Mode de remboursement</label>
<select v-model="formData.refundMethod" class="form-select">
<option value="deduction_facture">Déduction sur prochaine facture</option>
<option value="virement">Virement bancaire</option>
<option value="cheque">Chèque</option>
<option value="especes">Espèces</option>
<option value="non_rembourse">Non remboursé</option>
</select>
</div>
</div>
<!-- Détail du motif -->
<div class="mb-4">
<label class="form-label">Détail du motif</label>
<textarea
v-model="formData.reasonDetail"
class="form-control notes-textarea"
placeholder="Précisez le motif de l'avoir..."
rows="2"
></textarea>
</div>
<!-- Lignes de l'avoir -->
<div class="mb-4">
<div class="d-flex justify-content-between align-items-center mb-3">
<label class="form-label mb-0">Lignes de l'avoir</label>
<soft-button
type="button"
color="primary"
size="sm"
@click="addLine"
>
<i class="fas fa-plus me-1"></i> Ajouter une ligne
</soft-button>
</div>
<div class="space-y-3">
<div
v-for="(line, index) in formData.lines"
:key="index"
class="row g-2 align-items-end bg-light p-2 rounded line-item"
>
<div class="col-md-5">
<soft-input
v-model="line.designation"
type="text"
placeholder="Désignation"
/>
</div>
<div class="col-md-2">
<soft-input
v-model.number="line.quantity"
type="number"
placeholder="Qté"
:min="1"
/>
</div>
<div class="col-md-2">
<soft-input
v-model.number="line.priceHt"
type="number"
placeholder="Prix HT"
step="0.01"
/>
</div>
<div class="col-md-2 text-end font-weight-bold">
{{ formatCurrency(line.quantity * line.priceHt) }}
</div>
<div class="col-md-1 text-end">
<button
type="button"
class="btn btn-sm btn-danger"
@click="removeLine(index)"
:disabled="formData.lines.length === 1"
>
<i class="fas fa-trash"></i>
</button>
</div>
</div>
</div>
</div>
<!-- Totaux -->
<div class="row">
<div class="col-12">
<div class="totals-section">
<div class="d-flex justify-content-end">
<div class="text-end">
<p class="mb-2">
<strong>Total HT :</strong>
<span>{{ formatCurrency(calculateTotalHt()) }}</span>
</p>
<p class="mb-2">
<strong>TVA :</strong>
<span>{{ formatCurrency(calculateTotalTva()) }}</span>
</p>
<h5 class="mb-0 text-info">
<strong>Total TTC : {{ formatCurrency(calculateTotalTtc()) }}</strong>
</h5>
</div>
</div>
</div>
</div>
</div>
<!-- Notes internes -->
<div class="mb-4">
<label class="form-label">Notes internes</label>
<textarea
v-model="formData.notes"
class="form-control notes-textarea"
placeholder="Notes..."
rows="2"
></textarea>
</div>
<!-- Boutons d'action -->
<div class="d-flex justify-content-end gap-3">
<soft-button
type="button"
color="secondary"
variant="outline"
@click="cancelForm"
>
Annuler
</soft-button>
<soft-button
type="submit"
color="success"
>
Créer l'avoir
</soft-button>
</div>
</form>
</template>
<script setup>
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 sampleInvoices = [
{
id: "1",
invoice_number: "F-2026-00001",
clientName: "Caroline Lepetit thanatopraxie",
amount: 168.0,
},
{
id: "2",
invoice_number: "FAC-202512-0002",
clientName: "Hygiène Funéraire 50",
amount: 168.0,
},
{
id: "3",
invoice_number: "FAC-202512-0001",
clientName: "Hygiène Funéraire 50",
amount: 168.0,
},
{
id: "4",
invoice_number: "FAC-202512-0006",
clientName: "Pompes Funèbres Martin",
amount: 216.0,
},
{
id: "5",
invoice_number: "FAC-202512-0008",
clientName: "Pompes Funèbres Martin",
amount: 312.0,
},
{
id: "6",
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-" + 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",
reasonDetail: "",
lines: [
{
designation: "",
quantity: 1,
priceHt: 0,
},
],
notes: "",
});
const addLine = () => {
formData.value.lines.push({
designation: "",
quantity: 1,
priceHt: 0,
});
};
const removeLine = (index) => {
if (formData.value.lines.length > 1) {
formData.value.lines.splice(index, 1);
}
};
const calculateTotalHt = () => {
return formData.value.lines.reduce((sum, line) => {
return sum + line.quantity * line.priceHt;
}, 0);
};
const calculateTotalTva = () => {
return 0; // For now, no TVA
};
const calculateTotalTtc = () => {
return calculateTotalHt() + calculateTotalTva();
};
const formatCurrency = (value) => {
return new Intl.NumberFormat("fr-FR", {
style: "currency",
currency: "EUR",
}).format(value);
};
const submitForm = () => {
if (!formData.value.invoiceId) {
notificationStore.error("Erreur", "Veuillez sélectionner une facture d'origine");
return;
}
if (formData.value.lines.length === 0) {
notificationStore.error("Erreur", "Veuillez ajouter au moins une ligne");
return;
}
emit("submit", formData.value);
};
const cancelForm = () => {
router.push("/avoirs");
};
</script>
<style scoped>
.space-y-3 > * + * {
margin-top: 1rem;
}
.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>