Avoirs et factures fournisseur: harmonisation des écrans, formulaires et stores

This commit is contained in:
nyavokevin 2026-03-02 15:46:25 +03:00
parent ecfe25d3ca
commit dc87b0f720
20 changed files with 411 additions and 246 deletions

View File

@ -14,7 +14,7 @@
<i class="fas fa-file-invoice-dollar"></i> <i class="fas fa-file-invoice-dollar"></i>
Informations générales Informations générales
</div> </div>
<!-- Row 1: Avoir Number, Date, Status --> <!-- Row 1: Avoir Number, Date, Status -->
<div class="row g-3 mb-3"> <div class="row g-3 mb-3">
<div class="col-md-4"> <div class="col-md-4">
@ -38,11 +38,15 @@
<div class="row g-3 mb-3"> <div class="row g-3 mb-3">
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label">Client</label> <label class="form-label">Client</label>
<div class="info-value">{{ avoir.client?.name || 'Client inconnu' }}</div> <div class="info-value">
{{ avoir.client?.name || "Client inconnu" }}
</div>
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label">Facture d'origine</label> <label class="form-label">Facture d'origine</label>
<div class="info-value">{{ avoir.invoice?.invoice_number || 'Non spécifiée' }}</div> <div class="info-value">
{{ avoir.invoice?.invoice_number || "Non spécifiée" }}
</div>
</div> </div>
</div> </div>
@ -51,11 +55,15 @@
<div class="row g-3"> <div class="row g-3">
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label">Motif</label> <label class="form-label">Motif</label>
<div class="info-value">{{ getReasonLabel(avoir.reason_type) }}</div> <div class="info-value">
{{ getReasonLabel(avoir.reason_type) }}
</div>
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label">Mode de remboursement</label> <label class="form-label">Mode de remboursement</label>
<div class="info-value">{{ getRefundMethodLabel(avoir.refund_method) }}</div> <div class="info-value">
{{ getRefundMethodLabel(avoir.refund_method) }}
</div>
</div> </div>
</div> </div>
</div> </div>
@ -71,27 +79,25 @@
</div> </div>
<div class="lines-container"> <div class="lines-container">
<div <div v-for="line in avoir.lines" :key="line.id" class="line-item">
v-for="line in avoir.lines"
:key="line.id"
class="line-item"
>
<div class="row g-2 align-items-center"> <div class="row g-2 align-items-center">
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label text-xs">Désignation</label> <label class="form-label text-xs">Désignation</label>
<div class="line-designation">{{ line.description }}</div> <div class="line-designation">{{ line.description }}</div>
</div> </div>
<div class="col-md-2"> <div class="col-md-2">
<label class="form-label text-xs">Quantité</label> <label class="form-label text-xs">Quantité</label>
<div class="line-quantity">{{ line.quantity }}</div> <div class="line-quantity">{{ line.quantity }}</div>
</div> </div>
<div class="col-md-2"> <div class="col-md-2">
<label class="form-label text-xs">Prix HT</label> <label class="form-label text-xs">Prix HT</label>
<div class="line-price">{{ formatCurrency(line.unit_price) }}</div> <div class="line-price">
{{ formatCurrency(line.unit_price) }}
</div>
</div> </div>
<div class="col-md-2 d-flex flex-column align-items-end"> <div class="col-md-2 d-flex flex-column align-items-end">
<label class="form-label text-xs">Total HT</label> <label class="form-label text-xs">Total HT</label>
<span class="line-total"> <span class="line-total">
@ -116,7 +122,9 @@
</div> </div>
<div class="total-row total-final"> <div class="total-row total-final">
<span class="total-label">Total TTC</span> <span class="total-label">Total TTC</span>
<span class="total-amount">{{ formatCurrency(avoir.total_ttc) }}</span> <span class="total-amount">{{
formatCurrency(avoir.total_ttc)
}}</span>
</div> </div>
</div> </div>
</div> </div>
@ -127,7 +135,7 @@
<i class="fas fa-info-circle"></i> <i class="fas fa-info-circle"></i>
Informations supplémentaires Informations supplémentaires
</div> </div>
<div class="row g-3"> <div class="row g-3">
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label">Date de création</label> <label class="form-label">Date de création</label>
@ -135,13 +143,17 @@
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label">Contact client</label> <label class="form-label">Contact client</label>
<div class="info-value">{{ avoir.client?.email || avoir.client?.phone || 'Non spécifié' }}</div> <div class="info-value">
{{ avoir.client?.email || avoir.client?.phone || "Non spécifié" }}
</div>
</div> </div>
</div> </div>
<div v-if="avoir.reason_description" class="mt-3"> <div v-if="avoir.reason_description" class="mt-3">
<label class="form-label">Détail du motif</label> <label class="form-label">Détail du motif</label>
<div class="info-value notes-content">{{ avoir.reason_description }}</div> <div class="info-value notes-content">
{{ avoir.reason_description }}
</div>
</div> </div>
</div> </div>
@ -151,24 +163,27 @@
<soft-button <soft-button
color="secondary" color="secondary"
variant="gradient" variant="gradient"
@click="dropdownOpen = !dropdownOpen"
class="btn-status" class="btn-status"
@click="dropdownOpen = !dropdownOpen"
> >
<i class="fas fa-exchange-alt me-2"></i> <i class="fas fa-exchange-alt me-2"></i>
Changer le statut 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
v-if="dropdownOpen" v-if="dropdownOpen"
class="dropdown-menu show position-absolute" class="dropdown-menu show position-absolute"
style="top: 100%; left: 0; z-index: 1000;" style="top: 100%; left: 0; z-index: 1000"
> >
<li v-for="status in availableStatuses" :key="status"> <li v-for="status in availableStatuses" :key="status">
<a <a
class="dropdown-item" class="dropdown-item"
:class="{ active: status === avoir.status }" :class="{ active: status === avoir.status }"
href="javascript:;" href="javascript:;"
@click="changeStatus(status); dropdownOpen = false;" @click="
changeStatus(status);
dropdownOpen = false;
"
> >
<i :class="getStatusIcon(status) + ' me-2'"></i> <i :class="getStatusIcon(status) + ' me-2'"></i>
{{ getStatusLabel(status) }} {{ getStatusLabel(status) }}
@ -279,9 +294,12 @@ const changeStatus = async (newStatus) => {
try { try {
await avoirStore.updateAvoir({ await avoirStore.updateAvoir({
id: avoir.value.id, id: avoir.value.id,
status: newStatus status: newStatus,
}); });
notificationStore.success("Succès", `Statut mis à jour : ${getStatusLabel(newStatus)}`); notificationStore.success(
"Succès",
`Statut mis à jour : ${getStatusLabel(newStatus)}`
);
} catch (err) { } catch (err) {
notificationStore.error("Erreur", "Échec de la mise à jour du statut."); notificationStore.error("Erreur", "Échec de la mise à jour du statut.");
} }
@ -518,21 +536,21 @@ onMounted(async () => {
.form-section { .form-section {
padding: 1rem; padding: 1rem;
} }
.section-header { .section-header {
flex-direction: column; flex-direction: column;
align-items: flex-start; align-items: flex-start;
gap: 0.75rem; gap: 0.75rem;
} }
.totals-content { .totals-content {
max-width: 100%; max-width: 100%;
} }
.action-buttons { .action-buttons {
flex-direction: column-reverse; flex-direction: column-reverse;
} }
.btn-status, .btn-status,
.btn-pdf { .btn-pdf {
width: 100%; width: 100%;

View File

@ -1,7 +1,7 @@
<template> <template>
<div class="container-fluid py-4"> <div class="container-fluid py-4">
<avoir-list-controls <avoir-list-controls
@create="openCreateModal" @create="openCreateModal"
@filter="handleFilter" @filter="handleFilter"
@export="handleExport" @export="handleExport"
/> />
@ -76,13 +76,16 @@ const handleExport = () => {
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
link.setAttribute("href", url); link.setAttribute("href", url);
link.setAttribute("download", `avoirs-export-${new Date().toISOString().split('T')[0]}.csv`); link.setAttribute(
"download",
`avoirs-export-${new Date().toISOString().split("T")[0]}.csv`
);
link.style.visibility = "hidden"; link.style.visibility = "hidden";
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"); notificationStore.success("Export", "Fichier CSV exporté avec succès");
}; };
@ -102,7 +105,10 @@ const handleDelete = async (id) => {
await avoirStore.deleteAvoir(id); await avoirStore.deleteAvoir(id);
notificationStore.success("Succès", "Avoir supprimé avec succès"); notificationStore.success("Succès", "Avoir supprimé avec succès");
} catch (err) { } catch (err) {
notificationStore.error("Erreur", "Erreur lors de la suppression de l'avoir"); notificationStore.error(
"Erreur",
"Erreur lors de la suppression de l'avoir"
);
} }
} }
}; };

View File

@ -6,7 +6,12 @@
<div class="card-header pb-0 p-3"> <div class="card-header pb-0 p-3">
<div class="d-flex justify-content-between align-items-center"> <div class="d-flex justify-content-between align-items-center">
<h6 class="mb-0">Créer un nouvel avoir</h6> <h6 class="mb-0">Créer un nouvel avoir</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>
@ -45,16 +50,19 @@ const handleSubmit = async (formData) => {
avoir_date: formData.date, avoir_date: formData.date,
reason_type: formData.reason, reason_type: formData.reason,
reason_description: formData.reasonDetail, reason_description: formData.reasonDetail,
lines: formData.lines.map(line => ({ lines: formData.lines.map((line) => ({
description: line.designation, description: line.designation,
quantity: line.quantity, quantity: line.quantity,
unit_price: line.priceHt, unit_price: line.priceHt,
tva_rate: 0, tva_rate: 0,
})), })),
}; };
await avoirStore.createAvoir(payload); await avoirStore.createAvoir(payload);
notificationStore.success("Succès", `Avoir créé avec succès: ${formData.number}`); notificationStore.success(
"Succès",
`Avoir créé avec succès: ${formData.number}`
);
router.push("/avoirs"); router.push("/avoirs");
} catch (err) { } catch (err) {
console.error("Error creating avoir:", err); console.error("Error creating avoir:", err);

View File

@ -38,7 +38,12 @@
<template #actions> <template #actions>
<div class="d-flex justify-content-end gap-2"> <div class="d-flex justify-content-end gap-2">
<soft-button color="secondary" variant="outline" size="sm" @click="handleBack"> <soft-button
color="secondary"
variant="outline"
size="sm"
@click="handleBack"
>
Retour Retour
</soft-button> </soft-button>
<soft-button color="info" size="sm"> <soft-button color="info" size="sm">

View File

@ -1,7 +1,7 @@
<template> <template>
<div class="container-fluid py-4"> <div class="container-fluid py-4">
<facture-fournisseur-list-controls <facture-fournisseur-list-controls
@create="handleCreate" @create="handleCreate"
@filter="handleFilter" @filter="handleFilter"
/> />
<div class="row"> <div class="row">
@ -33,7 +33,7 @@ const currentFilter = ref(null);
const filteredFactures = computed(() => { const filteredFactures = computed(() => {
if (!currentFilter.value) return factures.value; if (!currentFilter.value) return factures.value;
return factures.value.filter(f => f.status === currentFilter.value); return factures.value.filter((f) => f.status === currentFilter.value);
}); });
const handleCreate = () => { const handleCreate = () => {

View File

@ -5,12 +5,14 @@
<div class="card"> <div class="card">
<div class="card-header pb-0"> <div class="card-header pb-0">
<h5 class="mb-0">Nouvelle Facture Fournisseur</h5> <h5 class="mb-0">Nouvelle Facture Fournisseur</h5>
<p class="text-sm mb-0">Saisissez les informations de la facture reçue.</p> <p class="text-sm mb-0">
Saisissez les informations de la facture reçue.
</p>
</div> </div>
<div class="card-body"> <div class="card-body">
<new-facture-fournisseur-form <new-facture-fournisseur-form
@submit="handleSubmit" @submit="handleSubmit"
@cancel="handleCancel" @cancel="handleCancel"
/> />
</div> </div>
</div> </div>

View File

@ -4,9 +4,7 @@
<h3 class="mb-1"> <h3 class="mb-1">
<strong>{{ avoirNumber }}</strong> <strong>{{ avoirNumber }}</strong>
</h3> </h3>
<p class="text-muted mb-0"> <p class="text-muted mb-0">Créé le {{ formatDate(date) }}</p>
Créé le {{ formatDate(date) }}
</p>
</div> </div>
</div> </div>
</template> </template>

View File

@ -1,5 +1,5 @@
<template> <template>
<form @submit.prevent="submitForm" class="space-y-6"> <form class="space-y-6" @submit.prevent="submitForm">
<!-- Row 1: N° Avoir, Date d'émission, Statut --> <!-- Row 1: N° Avoir, Date d'émission, Statut -->
<div class="row g-3 mb-4"> <div class="row g-3 mb-4">
<div class="col-md-3"> <div class="col-md-3">
@ -13,10 +13,7 @@
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
<label class="form-label">Date d'émission</label> <label class="form-label">Date d'émission</label>
<soft-input <soft-input v-model="formData.date" type="date" />
v-model="formData.date"
type="date"
/>
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
<label class="form-label">Statut</label> <label class="form-label">Statut</label>
@ -39,23 +36,32 @@
v-model="invoiceSearchQuery" v-model="invoiceSearchQuery"
type="text" type="text"
placeholder="Rechercher une facture..." placeholder="Rechercher une facture..."
class="search-input"
@input="handleInvoiceSearch" @input="handleInvoiceSearch"
@focus="showInvoiceResults = true" @focus="showInvoiceResults = true"
class="search-input"
/> />
</div> </div>
<!-- Search Results Dropdown --> <!-- Search Results Dropdown -->
<div <div
v-if="showInvoiceResults && (invoiceSearchResults.length > 0 || isSearchingInvoices)" v-if="
showInvoiceResults &&
(invoiceSearchResults.length > 0 || isSearchingInvoices)
"
class="search-dropdown" class="search-dropdown"
> >
<div v-if="isSearchingInvoices" class="dropdown-loading"> <div v-if="isSearchingInvoices" class="dropdown-loading">
<div class="spinner-border spinner-border-sm text-primary" role="status"> <div
class="spinner-border spinner-border-sm text-primary"
role="status"
>
<span class="visually-hidden">Chargement...</span> <span class="visually-hidden">Chargement...</span>
</div> </div>
</div> </div>
<div v-else-if="invoiceSearchResults.length === 0" class="dropdown-empty"> <div
v-else-if="invoiceSearchResults.length === 0"
class="dropdown-empty"
>
Aucune facture trouvée Aucune facture trouvée
</div> </div>
<template v-else> <template v-else>
@ -94,14 +100,18 @@
<option value="erreur_facturation">Erreur de facturation</option> <option value="erreur_facturation">Erreur de facturation</option>
<option value="retour_marchandise">Retour de marchandise</option> <option value="retour_marchandise">Retour de marchandise</option>
<option value="geste_commercial">Geste commercial</option> <option value="geste_commercial">Geste commercial</option>
<option value="annulation_prestation">Annulation de prestation</option> <option value="annulation_prestation">
Annulation de prestation
</option>
<option value="autre">Autre</option> <option value="autre">Autre</option>
</select> </select>
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<label class="form-label">Mode de remboursement</label> <label class="form-label">Mode de remboursement</label>
<select v-model="formData.refundMethod" class="form-select"> <select v-model="formData.refundMethod" class="form-select">
<option value="deduction_facture">Déduction sur prochaine facture</option> <option value="deduction_facture">
Déduction sur prochaine facture
</option>
<option value="virement">Virement bancaire</option> <option value="virement">Virement bancaire</option>
<option value="cheque">Chèque</option> <option value="cheque">Chèque</option>
<option value="especes">Espèces</option> <option value="especes">Espèces</option>
@ -125,12 +135,7 @@
<div class="mb-4"> <div class="mb-4">
<div class="d-flex justify-content-between align-items-center mb-3"> <div class="d-flex justify-content-between align-items-center mb-3">
<label class="form-label mb-0">Lignes de l'avoir</label> <label class="form-label mb-0">Lignes de l'avoir</label>
<soft-button <soft-button type="button" color="primary" size="sm" @click="addLine">
type="button"
color="primary"
size="sm"
@click="addLine"
>
<i class="fas fa-plus me-1"></i> Ajouter une ligne <i class="fas fa-plus me-1"></i> Ajouter une ligne
</soft-button> </soft-button>
</div> </div>
@ -171,8 +176,8 @@
<button <button
type="button" type="button"
class="btn btn-sm btn-danger" class="btn btn-sm btn-danger"
@click="removeLine(index)"
:disabled="formData.lines.length === 1" :disabled="formData.lines.length === 1"
@click="removeLine(index)"
> >
<i class="fas fa-trash"></i> <i class="fas fa-trash"></i>
</button> </button>
@ -196,7 +201,9 @@
<span>{{ formatCurrency(calculateTotalTva()) }}</span> <span>{{ formatCurrency(calculateTotalTva()) }}</span>
</p> </p>
<h5 class="mb-0 text-info"> <h5 class="mb-0 text-info">
<strong>Total TTC : {{ formatCurrency(calculateTotalTtc()) }}</strong> <strong
>Total TTC : {{ formatCurrency(calculateTotalTtc()) }}</strong
>
</h5> </h5>
</div> </div>
</div> </div>
@ -225,12 +232,7 @@
> >
Annuler Annuler
</soft-button> </soft-button>
<soft-button <soft-button type="submit" color="success"> Créer l'avoir </soft-button>
type="submit"
color="success"
>
Créer l'avoir
</soft-button>
</div> </div>
</form> </form>
</template> </template>
@ -312,11 +314,12 @@ const handleInvoiceSearch = () => {
showInvoiceResults.value = true; showInvoiceResults.value = true;
try { try {
// Simulate API search // Simulate API search
await new Promise(resolve => setTimeout(resolve, 300)); await new Promise((resolve) => setTimeout(resolve, 300));
const query = invoiceSearchQuery.value.toLowerCase(); const query = invoiceSearchQuery.value.toLowerCase();
invoiceSearchResults.value = sampleInvoices.filter(invoice => invoiceSearchResults.value = sampleInvoices.filter(
invoice.invoice_number.toLowerCase().includes(query) || (invoice) =>
invoice.clientName.toLowerCase().includes(query) invoice.invoice_number.toLowerCase().includes(query) ||
invoice.clientName.toLowerCase().includes(query)
); );
} catch (error) { } catch (error) {
console.error("Error searching invoices:", error); console.error("Error searching invoices:", error);
@ -329,11 +332,11 @@ const handleInvoiceSearch = () => {
const selectInvoice = (invoice) => { const selectInvoice = (invoice) => {
if (!invoice || !invoice.id) return; if (!invoice || !invoice.id) return;
if (searchTimeout) clearTimeout(searchTimeout); if (searchTimeout) clearTimeout(searchTimeout);
formData.value.invoiceId = invoice.id; formData.value.invoiceId = invoice.id;
formData.value.clientId = invoice.clientId; formData.value.clientId = invoice.clientId;
formData.value.clientName = invoice.clientName; formData.value.clientName = invoice.clientName;
// Optionally pre-fill a line based on invoice // Optionally pre-fill a line based on invoice
formData.value.lines = [ formData.value.lines = [
{ {
@ -342,25 +345,25 @@ const selectInvoice = (invoice) => {
priceHt: invoice.amount, priceHt: invoice.amount,
}, },
]; ];
invoiceSearchQuery.value = invoice.invoice_number; invoiceSearchQuery.value = invoice.invoice_number;
showInvoiceResults.value = false; showInvoiceResults.value = false;
}; };
// Close dropdowns on click outside // Close dropdowns on click outside
const handleClickOutside = (event) => { const handleClickOutside = (event) => {
const invoiceContainer = document.querySelector('.invoice-search-container'); const invoiceContainer = document.querySelector(".invoice-search-container");
if (invoiceContainer && !invoiceContainer.contains(event.target)) { if (invoiceContainer && !invoiceContainer.contains(event.target)) {
showInvoiceResults.value = false; showInvoiceResults.value = false;
} }
}; };
onMounted(() => { onMounted(() => {
document.addEventListener('click', handleClickOutside); document.addEventListener("click", handleClickOutside);
}); });
onUnmounted(() => { onUnmounted(() => {
document.removeEventListener('click', handleClickOutside); document.removeEventListener("click", handleClickOutside);
}); });
const formData = ref({ const formData = ref({
@ -420,7 +423,10 @@ const formatCurrency = (value) => {
const submitForm = () => { const submitForm = () => {
if (!formData.value.invoiceId) { if (!formData.value.invoiceId) {
notificationStore.error("Erreur", "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) {

View File

@ -3,16 +3,24 @@
<table class="table align-items-center mb-0"> <table class="table align-items-center mb-0">
<thead> <thead>
<tr> <tr>
<th class="text-uppercase text-secondary text-xxs font-weight-bolder opacity-7 ps-2"> <th
class="text-uppercase text-secondary text-xxs font-weight-bolder opacity-7 ps-2"
>
Description Description
</th> </th>
<th class="text-center text-uppercase text-secondary text-xxs font-weight-bolder opacity-7"> <th
class="text-center text-uppercase text-secondary text-xxs font-weight-bolder opacity-7"
>
Qté Qté
</th> </th>
<th class="text-center text-uppercase text-secondary text-xxs font-weight-bolder opacity-7"> <th
class="text-center text-uppercase text-secondary text-xxs font-weight-bolder opacity-7"
>
Prix Unit. HT Prix Unit. HT
</th> </th>
<th class="text-center text-uppercase text-secondary text-xxs font-weight-bolder opacity-7"> <th
class="text-center text-uppercase text-secondary text-xxs font-weight-bolder opacity-7"
>
Total HT Total HT
</th> </th>
</tr> </tr>
@ -27,13 +35,19 @@
</div> </div>
</td> </td>
<td class="align-middle text-center text-sm"> <td class="align-middle text-center text-sm">
<span class="text-secondary text-xs font-weight-bold">{{ line.quantity }}</span> <span class="text-secondary text-xs font-weight-bold">{{
line.quantity
}}</span>
</td> </td>
<td class="align-middle text-center text-sm"> <td class="align-middle text-center text-sm">
<span class="text-secondary text-xs font-weight-bold">{{ formatCurrency(line.priceHt) }}</span> <span class="text-secondary text-xs font-weight-bold">{{
formatCurrency(line.priceHt)
}}</span>
</td> </td>
<td class="align-middle text-center text-sm"> <td class="align-middle text-center text-sm">
<span class="text-secondary text-xs font-weight-bold">{{ formatCurrency(line.totalHt) }}</span> <span class="text-secondary text-xs font-weight-bold">{{
formatCurrency(line.totalHt)
}}</span>
</td> </td>
</tr> </tr>
</tbody> </tbody>

View File

@ -1,7 +1,12 @@
<template> <template>
<div class="d-sm-flex justify-content-between"> <div class="d-sm-flex justify-content-between">
<div> <div>
<soft-button color="success" variant="gradient" size="sm" @click="$emit('create')"> <soft-button
color="success"
variant="gradient"
size="sm"
@click="$emit('create')"
>
<i class="fas fa-plus me-1"></i> Ajouter une facture <i class="fas fa-plus me-1"></i> Ajouter une facture
</soft-button> </soft-button>
</div> </div>

View File

@ -9,7 +9,9 @@
<span class="text-sm">Total HT:</span> <span class="text-sm">Total HT:</span>
</td> </td>
<td class="text-end"> <td class="text-end">
<span class="text-sm text-dark font-weight-bold">{{ formatCurrency(ht) }}</span> <span class="text-sm text-dark font-weight-bold">{{
formatCurrency(ht)
}}</span>
</td> </td>
</tr> </tr>
<tr> <tr>
@ -17,15 +19,21 @@
<span class="text-sm">TVA (20%):</span> <span class="text-sm">TVA (20%):</span>
</td> </td>
<td class="text-end"> <td class="text-end">
<span class="text-sm text-dark font-weight-bold">{{ formatCurrency(tva) }}</span> <span class="text-sm text-dark font-weight-bold">{{
formatCurrency(tva)
}}</span>
</td> </td>
</tr> </tr>
<tr> <tr>
<td> <td>
<span class="text-sm font-weight-bold text-info">Total TTC:</span> <span class="text-sm font-weight-bold text-info"
>Total TTC:</span
>
</td> </td>
<td class="text-end"> <td class="text-end">
<span class="text-sm font-weight-bold text-info">{{ formatCurrency(ttc) }}</span> <span class="text-sm font-weight-bold text-info">{{
formatCurrency(ttc)
}}</span>
</td> </td>
</tr> </tr>
</tbody> </tbody>

View File

@ -13,20 +13,25 @@
</div> </div>
<div class="col-md-4"> <div class="col-md-4">
<label class="form-label">Fournisseur *</label> <label class="form-label">Fournisseur *</label>
<select v-model="formData.supplierId" class="form-select" @change="updateSupplierInfo" required> <select
v-model="formData.supplierId"
class="form-select"
required
@change="updateSupplierInfo"
>
<option value="">-- Sélectionner un fournisseur --</option> <option value="">-- Sélectionner un fournisseur --</option>
<option v-for="supplier in suppliers" :key="supplier.id" :value="supplier.id"> <option
v-for="supplier in suppliers"
:key="supplier.id"
:value="supplier.id"
>
{{ supplier.name }} {{ supplier.name }}
</option> </option>
</select> </select>
</div> </div>
<div class="col-md-4"> <div class="col-md-4">
<label class="form-label">Date facture *</label> <label class="form-label">Date facture *</label>
<soft-input <soft-input v-model="formData.date" type="date" required />
v-model="formData.date"
type="date"
required
/>
</div> </div>
</div> </div>
@ -55,13 +60,27 @@
<!-- Articles Section --> <!-- Articles Section -->
<div class="mb-4"> <div class="mb-4">
<div class="flex items-center justify-between mb-4"> <div class="flex items-center justify-between mb-4">
<label class="peer-disabled:cursor-not-allowed peer-disabled:opacity-70 text-lg font-bold">Lignes de facture</label> <label
<button class="peer-disabled:cursor-not-allowed peer-disabled:opacity-70 text-lg font-bold"
class="inline-flex items-center justify-center gap-2 whitespace-nowrap font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 bg-primary text-primary-foreground shadow hover:bg-primary/90 h-8 rounded-md px-3 text-xs" >Lignes de facture</label
>
<button
class="inline-flex items-center justify-center gap-2 whitespace-nowrap font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 bg-primary text-primary-foreground shadow hover:bg-primary/90 h-8 rounded-md px-3 text-xs"
type="button" type="button"
@click="addLine" @click="addLine"
> >
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-plus w-4 h-4 mr-2"> <svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-plus w-4 h-4 mr-2"
>
<path d="M5 12h14"></path> <path d="M5 12h14"></path>
<path d="M12 5v14"></path> <path d="M12 5v14"></path>
</svg> </svg>
@ -114,8 +133,8 @@
<button <button
type="button" type="button"
class="btn btn-sm btn-link text-danger mb-0" class="btn btn-sm btn-link text-danger mb-0"
@click="removeLine(index)"
:disabled="formData.lines.length === 1" :disabled="formData.lines.length === 1"
@click="removeLine(index)"
> >
<i class="fas fa-trash"></i> <i class="fas fa-trash"></i>
</button> </button>
@ -131,16 +150,22 @@
<div class="card-body p-3"> <div class="card-body p-3">
<div class="d-flex justify-content-between mb-2"> <div class="d-flex justify-content-between mb-2">
<span class="text-sm">Total HT:</span> <span class="text-sm">Total HT:</span>
<span class="text-sm font-weight-bold">{{ formatCurrency(calculateTotalHt()) }}</span> <span class="text-sm font-weight-bold">{{
formatCurrency(calculateTotalHt())
}}</span>
</div> </div>
<div class="d-flex justify-content-between mb-2"> <div class="d-flex justify-content-between mb-2">
<span class="text-sm">TVA (20%):</span> <span class="text-sm">TVA (20%):</span>
<span class="text-sm font-weight-bold">{{ formatCurrency(calculateTotalTva()) }}</span> <span class="text-sm font-weight-bold">{{
formatCurrency(calculateTotalTva())
}}</span>
</div> </div>
<hr class="horizontal dark my-2"> <hr class="horizontal dark my-2" />
<div class="d-flex justify-content-between"> <div class="d-flex justify-content-between">
<span class="text-base font-weight-bold">Total TTC:</span> <span class="text-base font-weight-bold">Total TTC:</span>
<span class="text-base font-weight-bold text-info">{{ formatCurrency(calculateTotalTtc()) }}</span> <span class="text-base font-weight-bold text-info">{{
formatCurrency(calculateTotalTtc())
}}</span>
</div> </div>
</div> </div>
</div> </div>
@ -157,10 +182,7 @@
> >
Annuler Annuler
</soft-button> </soft-button>
<soft-button <soft-button type="submit" color="success">
type="submit"
color="success"
>
Enregistrer la facture Enregistrer la facture
</soft-button> </soft-button>
</div> </div>
@ -201,7 +223,7 @@ const formData = ref({
}); });
const updateSupplierInfo = () => { const updateSupplierInfo = () => {
const supplier = suppliers.find(s => s.id === formData.value.supplierId); const supplier = suppliers.find((s) => s.id === formData.value.supplierId);
if (supplier) { if (supplier) {
formData.value.supplierName = supplier.name; formData.value.supplierName = supplier.name;
} }
@ -247,19 +269,19 @@ const submitForm = () => {
alert("Veuillez sélectionner un fournisseur"); alert("Veuillez sélectionner un fournisseur");
return; return;
} }
const payload = { const payload = {
...formData.value, ...formData.value,
totalHt: calculateTotalHt(), totalHt: calculateTotalHt(),
totalTva: calculateTotalTva(), totalTva: calculateTotalTva(),
totalTtc: calculateTotalTtc(), totalTtc: calculateTotalTtc(),
// Map lines to include totalHt for store consistency // Map lines to include totalHt for store consistency
lines: formData.value.lines.map(line => ({ lines: formData.value.lines.map((line) => ({
...line, ...line,
totalHt: line.quantity * line.priceHt totalHt: line.quantity * line.priceHt,
})) })),
}; };
emit("submit", payload); emit("submit", payload);
}; };
</script> </script>
@ -277,21 +299,54 @@ const submitForm = () => {
} }
/* Tailwind-like utilities used in the provided snippet */ /* Tailwind-like utilities used in the provided snippet */
.flex { display: flex; } .flex {
.items-center { align-items: center; } display: flex;
.justify-between { justify-content: space-between; } }
.mb-4 { margin-bottom: 1.5rem; } .items-center {
.text-lg { font-size: 1.125rem; } align-items: center;
.font-bold { font-weight: 700; } }
.gap-2 { gap: 0.5rem; } .justify-between {
.whitespace-nowrap { white-space: nowrap; } justify-content: space-between;
.transition-colors { transition-property: background-color, border-color, color, fill, stroke; transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); transition-duration: 150ms; } }
.shadow { box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06); } .mb-4 {
.rounded-md { border-radius: 0.375rem; } margin-bottom: 1.5rem;
.px-3 { padding-left: 0.75rem; padding-right: 0.75rem; } }
.h-8 { height: 2rem; } .text-lg {
.text-xs { font-size: 0.75rem; } font-size: 1.125rem;
.font-medium { font-weight: 500; } }
.font-bold {
font-weight: 700;
}
.gap-2 {
gap: 0.5rem;
}
.whitespace-nowrap {
white-space: nowrap;
}
.transition-colors {
transition-property: background-color, border-color, color, fill, stroke;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
}
.shadow {
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
}
.rounded-md {
border-radius: 0.375rem;
}
.px-3 {
padding-left: 0.75rem;
padding-right: 0.75rem;
}
.h-8 {
height: 2rem;
}
.text-xs {
font-size: 0.75rem;
}
.font-medium {
font-weight: 500;
}
.space-y-3 > div + div { .space-y-3 > div + div {
margin-top: 1rem; margin-top: 1rem;

View File

@ -36,7 +36,9 @@
<!-- Total TTC --> <!-- Total TTC -->
<td class="text-xs font-weight-bold"> <td class="text-xs font-weight-bold">
<span class="my-2 text-xs">{{ formatCurrency(facture.totalTtc) }}</span> <span class="my-2 text-xs">{{
formatCurrency(facture.totalTtc)
}}</span>
</td> </td>
<!-- Status --> <!-- Status -->
@ -47,7 +49,10 @@
variant="outline" variant="outline"
class="btn-icon-only btn-rounded mb-0 me-2 btn-sm d-flex align-items-center justify-content-center" class="btn-icon-only btn-rounded mb-0 me-2 btn-sm d-flex align-items-center justify-content-center"
> >
<i :class="getStatusIcon(facture.status)" aria-hidden="true"></i> <i
:class="getStatusIcon(facture.status)"
aria-hidden="true"
></i>
</soft-button> </soft-button>
<span>{{ getStatusLabel(facture.status) }}</span> <span>{{ getStatusLabel(facture.status) }}</span>
</div> </div>

View File

@ -5,7 +5,7 @@
<div class="card-header p-3 pb-0"> <div class="card-header p-3 pb-0">
<slot name="header"></slot> <slot name="header"></slot>
</div> </div>
<hr class="horizontal dark my-3"> <hr class="horizontal dark my-3" />
<div class="card-body p-3 pt-0"> <div class="card-body p-3 pt-0">
<div class="row"> <div class="row">
<div class="col-12"> <div class="col-12">
@ -20,7 +20,7 @@
<slot name="summary"></slot> <slot name="summary"></slot>
</div> </div>
</div> </div>
<hr class="horizontal dark mt-4 mb-3"> <hr class="horizontal dark mt-4 mb-3" />
<div class="row"> <div class="row">
<div class="col-12"> <div class="col-12">
<slot name="actions"></slot> <slot name="actions"></slot>

View File

@ -131,7 +131,9 @@ export const AvoirService = {
return response; return response;
}, },
async deleteAvoir(id: number): Promise<{ success: boolean; message: string }> { async deleteAvoir(
id: number
): Promise<{ success: boolean; message: string }> {
const response = await request<{ success: boolean; message: string }>({ const response = await request<{ success: boolean; message: string }>({
url: `/api/avoirs/${id}`, url: `/api/avoirs/${id}`,
method: "delete", method: "delete",

View File

@ -22,7 +22,7 @@ export interface SupplierInvoice {
invoice_number: string; invoice_number: string;
invoice_date: string; invoice_date: string;
due_date: string | null; due_date: string | null;
status: 'brouillon' | 'en_attente' | 'payee' | 'annulee'; status: "brouillon" | "en_attente" | "payee" | "annulee";
currency: string; currency: string;
total_ht: number; total_ht: number;
total_tva: number; total_tva: number;
@ -70,7 +70,8 @@ export interface CreateSupplierInvoicePayload {
lines?: CreateSupplierInvoiceLinePayload[]; lines?: CreateSupplierInvoiceLinePayload[];
} }
export interface UpdateSupplierInvoicePayload extends Partial<CreateSupplierInvoicePayload> { export interface UpdateSupplierInvoicePayload
extends Partial<CreateSupplierInvoicePayload> {
id: number; id: number;
} }
@ -98,7 +99,9 @@ export const SupplierInvoiceService = {
return response; return response;
}, },
async createSupplierInvoice(payload: CreateSupplierInvoicePayload): Promise<SupplierInvoiceResponse> { async createSupplierInvoice(
payload: CreateSupplierInvoicePayload
): Promise<SupplierInvoiceResponse> {
const response = await request<SupplierInvoiceResponse>({ const response = await request<SupplierInvoiceResponse>({
url: "/api/supplier-invoices", url: "/api/supplier-invoices",
method: "post", method: "post",
@ -107,7 +110,9 @@ export const SupplierInvoiceService = {
return response; return response;
}, },
async updateSupplierInvoice(payload: UpdateSupplierInvoicePayload): Promise<SupplierInvoiceResponse> { async updateSupplierInvoice(
payload: UpdateSupplierInvoicePayload
): Promise<SupplierInvoiceResponse> {
const { id, ...updateData } = payload; const { id, ...updateData } = payload;
const response = await request<SupplierInvoiceResponse>({ const response = await request<SupplierInvoiceResponse>({
url: `/api/supplier-invoices/${id}`, url: `/api/supplier-invoices/${id}`,
@ -117,7 +122,9 @@ export const SupplierInvoiceService = {
return response; return response;
}, },
async deleteSupplierInvoice(id: number): Promise<{ success: boolean; message: string }> { async deleteSupplierInvoice(
id: number
): Promise<{ success: boolean; message: string }> {
const response = await request<{ success: boolean; message: string }>({ const response = await request<{ success: boolean; message: string }>({
url: `/api/supplier-invoices/${id}`, url: `/api/supplier-invoices/${id}`,
method: "delete", method: "delete",
@ -125,7 +132,9 @@ export const SupplierInvoiceService = {
return response; return response;
}, },
async getByFournisseur(fournisseurId: number): Promise<SupplierInvoiceListResponse> { async getByFournisseur(
fournisseurId: number
): Promise<SupplierInvoiceListResponse> {
const response = await request<SupplierInvoiceListResponse>({ const response = await request<SupplierInvoiceListResponse>({
url: `/api/fournisseurs/${fournisseurId}/supplier-invoices`, url: `/api/fournisseurs/${fournisseurId}/supplier-invoices`,
method: "get", method: "get",

View File

@ -146,10 +146,7 @@ export const useAvoirStore = defineStore("avoir", () => {
avoirs.value[index] = updatedAvoir; avoirs.value[index] = updatedAvoir;
} }
if ( if (currentAvoir.value && currentAvoir.value.id === updatedAvoir.id) {
currentAvoir.value &&
currentAvoir.value.id === updatedAvoir.id
) {
setCurrentAvoir(updatedAvoir); setCurrentAvoir(updatedAvoir);
} }
@ -173,9 +170,7 @@ export const useAvoirStore = defineStore("avoir", () => {
try { try {
const response = await AvoirService.deleteAvoir(id); const response = await AvoirService.deleteAvoir(id);
avoirs.value = avoirs.value.filter( avoirs.value = avoirs.value.filter((avoir) => avoir.id !== id);
(avoir) => avoir.id !== id
);
if (currentAvoir.value && currentAvoir.value.id === id) { if (currentAvoir.value && currentAvoir.value.id === id) {
setCurrentAvoir(null); setCurrentAvoir(null);

View File

@ -22,88 +22,103 @@ export interface FactureFournisseur {
lines: FactureFournisseurLine[]; lines: FactureFournisseurLine[];
} }
export const useFactureFournisseurStore = defineStore("factureFournisseur", () => { export const useFactureFournisseurStore = defineStore(
// State "factureFournisseur",
const factures = ref<FactureFournisseur[]>([ () => {
{ // State
id: 1, const factures = ref<FactureFournisseur[]>([
number: "FF-2026-0001", {
supplierId: "1", id: 1,
supplierName: "Produits Funéraires Pro", number: "FF-2026-0001",
date: "2026-01-15", supplierId: "1",
status: "payee", supplierName: "Produits Funéraires Pro",
totalHt: 1250.0, date: "2026-01-15",
totalTva: 250.0, status: "payee",
totalTtc: 1500.0, totalHt: 1250.0,
lines: [ totalTva: 250.0,
{ id: 1, designation: "Cercueils Chêne Prestige", quantity: 2, priceHt: 625.0, totalHt: 1250.0 } totalTtc: 1500.0,
] lines: [
}, {
{ id: 1,
id: 2, designation: "Cercueils Chêne Prestige",
number: "FF-2026-0002", quantity: 2,
supplierId: "2", priceHt: 625.0,
supplierName: "Thanatos Supply", totalHt: 1250.0,
date: "2026-01-20", },
status: "en_attente", ],
totalHt: 450.0, },
totalTva: 90.0, {
totalTtc: 540.0, id: 2,
lines: [ number: "FF-2026-0002",
{ id: 2, designation: "Urnes Granit Noir", quantity: 5, priceHt: 90.0, totalHt: 450.0 } supplierId: "2",
] supplierName: "Thanatos Supply",
} date: "2026-01-20",
]); status: "en_attente",
const currentFacture = ref<FactureFournisseur | null>(null); totalHt: 450.0,
const loading = ref(false); totalTva: 90.0,
const error = ref<string | null>(null); totalTtc: 540.0,
lines: [
{
id: 2,
designation: "Urnes Granit Noir",
quantity: 5,
priceHt: 90.0,
totalHt: 450.0,
},
],
},
]);
const currentFacture = ref<FactureFournisseur | null>(null);
const loading = ref(false);
const error = ref<string | null>(null);
// Getters // Getters
const allFactures = computed(() => factures.value); const allFactures = computed(() => factures.value);
const isLoading = computed(() => loading.value); const isLoading = computed(() => loading.value);
// Actions // Actions
const fetchFactures = async () => { const fetchFactures = async () => {
loading.value = true; loading.value = true;
// Mock API call // Mock API call
await new Promise(resolve => setTimeout(resolve, 500)); await new Promise((resolve) => setTimeout(resolve, 500));
loading.value = false; loading.value = false;
};
const fetchFacture = async (id: number) => {
loading.value = true;
await new Promise(resolve => setTimeout(resolve, 300));
const found = factures.value.find(f => f.id === id);
currentFacture.value = found || null;
loading.value = false;
};
const createFacture = async (payload: any) => {
loading.value = true;
await new Promise(resolve => setTimeout(resolve, 800));
const newFacture = {
...payload,
id: factures.value.length + 1,
}; };
factures.value.push(newFacture);
loading.value = false;
return newFacture;
};
const deleteFacture = async (id: number) => { const fetchFacture = async (id: number) => {
factures.value = factures.value.filter(f => f.id !== id); loading.value = true;
}; await new Promise((resolve) => setTimeout(resolve, 300));
const found = factures.value.find((f) => f.id === id);
currentFacture.value = found || null;
loading.value = false;
};
return { const createFacture = async (payload: any) => {
factures, loading.value = true;
currentFacture, await new Promise((resolve) => setTimeout(resolve, 800));
loading, const newFacture = {
error, ...payload,
allFactures, id: factures.value.length + 1,
isLoading, };
fetchFactures, factures.value.push(newFacture);
fetchFacture, loading.value = false;
createFacture, return newFacture;
deleteFacture };
};
}); const deleteFacture = async (id: number) => {
factures.value = factures.value.filter((f) => f.id !== id);
};
return {
factures,
currentFacture,
loading,
error,
allFactures,
isLoading,
fetchFactures,
fetchFacture,
createFacture,
deleteFacture,
};
}
);

View File

@ -84,7 +84,9 @@ export const useSupplierInvoiceStore = defineStore("supplierInvoice", () => {
setError(null); setError(null);
try { try {
const response = await SupplierInvoiceService.getAllSupplierInvoices(params); const response = await SupplierInvoiceService.getAllSupplierInvoices(
params
);
setSupplierInvoices(response.data); setSupplierInvoices(response.data);
if (response.meta) { if (response.meta) {
setPagination(response.meta); setPagination(response.meta);
@ -122,12 +124,16 @@ export const useSupplierInvoiceStore = defineStore("supplierInvoice", () => {
} }
}; };
const createSupplierInvoice = async (payload: CreateSupplierInvoicePayload) => { const createSupplierInvoice = async (
payload: CreateSupplierInvoicePayload
) => {
setLoading(true); setLoading(true);
setError(null); setError(null);
try { try {
const response = await SupplierInvoiceService.createSupplierInvoice(payload); const response = await SupplierInvoiceService.createSupplierInvoice(
payload
);
supplierInvoices.value.push(response.data); supplierInvoices.value.push(response.data);
setCurrentSupplierInvoice(response.data); setCurrentSupplierInvoice(response.data);
return response.data; return response.data;
@ -143,12 +149,16 @@ export const useSupplierInvoiceStore = defineStore("supplierInvoice", () => {
} }
}; };
const updateSupplierInvoice = async (payload: UpdateSupplierInvoicePayload) => { const updateSupplierInvoice = async (
payload: UpdateSupplierInvoicePayload
) => {
setLoading(true); setLoading(true);
setError(null); setError(null);
try { try {
const response = await SupplierInvoiceService.updateSupplierInvoice(payload); const response = await SupplierInvoiceService.updateSupplierInvoice(
payload
);
const updatedInvoice = response.data; const updatedInvoice = response.data;
const index = supplierInvoices.value.findIndex( const index = supplierInvoices.value.findIndex(
@ -189,7 +199,10 @@ export const useSupplierInvoiceStore = defineStore("supplierInvoice", () => {
(invoice) => invoice.id !== id (invoice) => invoice.id !== id
); );
if (currentSupplierInvoice.value && currentSupplierInvoice.value.id === id) { if (
currentSupplierInvoice.value &&
currentSupplierInvoice.value.id === id
) {
setCurrentSupplierInvoice(null); setCurrentSupplierInvoice(null);
} }
@ -211,7 +224,9 @@ export const useSupplierInvoiceStore = defineStore("supplierInvoice", () => {
setError(null); setError(null);
try { try {
const response = await SupplierInvoiceService.getByFournisseur(fournisseurId); const response = await SupplierInvoiceService.getByFournisseur(
fournisseurId
);
setSupplierInvoices(response.data); setSupplierInvoices(response.data);
return response.data; return response.data;
} catch (err: any) { } catch (err: any) {

View File

@ -5,4 +5,3 @@
<script setup> <script setup>
import CommandeListPresentation from "@/components/Organism/Commande/CommandeListPresentation.vue"; import CommandeListPresentation from "@/components/Organism/Commande/CommandeListPresentation.vue";
</script> </script>