Ventes et modules transverses: devis, factures, messages, stats et webmailing

This commit is contained in:
nyavokevin 2026-03-02 15:46:38 +03:00
parent dc87b0f720
commit 11750a3ffc
25 changed files with 281 additions and 201 deletions

View File

@ -9,9 +9,7 @@
<small class="text-muted">Communiquez avec votre équipe</small> <small class="text-muted">Communiquez avec votre équipe</small>
</div> </div>
<div> <div>
<span class="badge bg-info"> <span class="badge bg-info"> {{ unreadCount }} non lu(s) </span>
{{ unreadCount }} non lu(s)
</span>
</div> </div>
</div> </div>
</template> </template>
@ -22,19 +20,21 @@
<button <button
class="nav-link" class="nav-link"
:class="{ active: activeTab === 'inbox' }" :class="{ active: activeTab === 'inbox' }"
@click="activeTab = 'inbox'"
type="button" type="button"
@click="activeTab = 'inbox'"
> >
<i class="fas fa-inbox"></i> Réception <i class="fas fa-inbox"></i> Réception
<span class="badge bg-danger ms-2" v-if="unreadCount > 0">{{ unreadCount }}</span> <span v-if="unreadCount > 0" class="badge bg-danger ms-2">{{
unreadCount
}}</span>
</button> </button>
</li> </li>
<li class="nav-item" role="presentation"> <li class="nav-item" role="presentation">
<button <button
class="nav-link" class="nav-link"
:class="{ active: activeTab === 'compose' }" :class="{ active: activeTab === 'compose' }"
@click="activeTab = 'compose'"
type="button" type="button"
@click="activeTab = 'compose'"
> >
<i class="fas fa-pen"></i> Nouveau message <i class="fas fa-pen"></i> Nouveau message
</button> </button>
@ -43,8 +43,8 @@
<button <button
class="nav-link" class="nav-link"
:class="{ active: activeTab === 'sent' }" :class="{ active: activeTab === 'sent' }"
@click="activeTab = 'sent'"
type="button" type="button"
@click="activeTab = 'sent'"
> >
<i class="fas fa-paper-plane"></i> Envoyés <i class="fas fa-paper-plane"></i> Envoyés
</button> </button>
@ -72,17 +72,10 @@
@form-data-change="updateNewMessage" @form-data-change="updateNewMessage"
/> />
<div class="mt-4 d-flex justify-content-end gap-2"> <div class="mt-4 d-flex justify-content-end gap-2">
<soft-button <soft-button color="secondary" variant="outline" @click="resetForm">
color="secondary"
variant="outline"
@click="resetForm"
>
<i class="fas fa-redo"></i> Réinitialiser <i class="fas fa-redo"></i> Réinitialiser
</soft-button> </soft-button>
<soft-button <soft-button color="success" @click="sendMessage">
color="success"
@click="sendMessage"
>
<i class="fas fa-check"></i> Envoyer <i class="fas fa-check"></i> Envoyer
</soft-button> </soft-button>
</div> </div>
@ -260,7 +253,9 @@ const sendMessage = () => {
return; return;
} }
const recipient = users.value.find((u) => u.id === parseInt(newMessage.value.recipientId)); const recipient = users.value.find(
(u) => u.id === parseInt(newMessage.value.recipientId)
);
const sentMessage = { const sentMessage = {
id: sent.value.length + 101, id: sent.value.length + 101,

View File

@ -6,7 +6,10 @@
</soft-button> </soft-button>
</template> </template>
<template #header-pagination> <template #header-pagination>
<div class="d-flex justify-content-center" v-if="pagination && pagination.last_page > 1"> <div
v-if="pagination && pagination.last_page > 1"
class="d-flex justify-content-center"
>
<soft-pagination color="success" size="sm"> <soft-pagination color="success" size="sm">
<soft-pagination-item <soft-pagination-item
prev prev
@ -31,12 +34,26 @@
</div> </div>
</template> </template>
<template #select-filter> <template #select-filter>
<soft-button color="dark" variant="outline" class="dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false"> <soft-button
color="dark"
variant="outline"
class="dropdown-toggle"
data-bs-toggle="dropdown"
aria-expanded="false"
>
<i class="fas fa-filter me-2"></i> Filtrer <i class="fas fa-filter me-2"></i> Filtrer
</soft-button> </soft-button>
<ul class="dropdown-menu dropdown-menu-lg-start px-2 py-3"> <ul class="dropdown-menu dropdown-menu-lg-start px-2 py-3">
<li><a class="dropdown-item border-radius-md" href="javascript:;">Par date</a></li> <li>
<li><a class="dropdown-item border-radius-md" href="javascript:;">Par statut</a></li> <a class="dropdown-item border-radius-md" href="javascript:;"
>Par date</a
>
</li>
<li>
<a class="dropdown-item border-radius-md" href="javascript:;"
>Par statut</a
>
</li>
</ul> </ul>
</template> </template>
<template #intervention-other-action> <template #intervention-other-action>
@ -104,7 +121,7 @@ const props = defineProps({
pagination: { pagination: {
type: Object, type: Object,
default: null, default: null,
} },
}); });
// Emits // Emits
@ -115,8 +132,9 @@ const go = () => {
}; };
const changePage = (page) => { const changePage = (page) => {
if (typeof page !== 'number') return; if (typeof page !== "number") return;
if (page < 1 || (props.pagination && page > props.pagination.last_page)) return; if (page < 1 || (props.pagination && page > props.pagination.last_page))
return;
if (page === props.pagination.current_page) return; if (page === props.pagination.current_page) return;
emit("page-change", page); emit("page-change", page);
}; };
@ -126,7 +144,11 @@ const visiblePages = computed(() => {
const { current_page, last_page } = props.pagination; const { current_page, last_page } = props.pagination;
const delta = 2; const delta = 2;
const range = []; const range = [];
for (let i = Math.max(2, current_page - delta); i <= Math.min(last_page - 1, current_page + delta); i++) { for (
let i = Math.max(2, current_page - delta);
i <= Math.min(last_page - 1, current_page + delta);
i++
) {
range.push(i); range.push(i);
} }
@ -173,18 +195,24 @@ const visiblePages = computed(() => {
for (let i = 1; i <= total; i++) p.push(i); for (let i = 1; i <= total; i++) p.push(i);
} else { } else {
p.push(1); p.push(1);
if (current_page > 3) p.push('...'); if (current_page > 3) p.push("...");
let midStart = Math.max(2, current_page - 1); let midStart = Math.max(2, current_page - 1);
let midEnd = Math.min(total - 1, current_page + 1); let midEnd = Math.min(total - 1, current_page + 1);
// pinned logic adjustment // pinned logic adjustment
if (current_page < 4) { midStart = 2; midEnd = 4; } if (current_page < 4) {
if (current_page > total - 3) { midStart = total - 3; midEnd = total - 1; } midStart = 2;
midEnd = 4;
}
if (current_page > total - 3) {
midStart = total - 3;
midEnd = total - 1;
}
for (let i = midStart; i <= midEnd; i++) p.push(i); for (let i = midStart; i <= midEnd; i++) p.push(i);
if (current_page < total - 2) p.push('...'); if (current_page < total - 2) p.push("...");
p.push(total); p.push(total);
} }
return p; return p;

View File

@ -23,7 +23,11 @@
<div> <div>
<h6 class="mb-3 text-sm">Historique</h6> <h6 class="mb-3 text-sm">Historique</h6>
<div v-if="invoice.history && invoice.history.length > 0"> <div v-if="invoice.history && invoice.history.length > 0">
<div v-for="(entry, index) in invoice.history" :key="index" class="mb-2"> <div
v-for="(entry, index) in invoice.history"
:key="index"
class="mb-2"
>
<span class="text-xs text-secondary"> <span class="text-xs text-secondary">
{{ formatDate(entry.changed_at) }} {{ formatDate(entry.changed_at) }}
</span> </span>
@ -38,13 +42,15 @@
<div> <div>
<h6 class="mb-3 text-sm">Informations Client</h6> <h6 class="mb-3 text-sm">Informations Client</h6>
<p class="text-sm mb-1"> <p class="text-sm mb-1">
<strong>{{ invoice.client ? invoice.client.name : 'Client inconnu' }}</strong> <strong>{{
invoice.client ? invoice.client.name : "Client inconnu"
}}</strong>
</p> </p>
<p class="text-xs text-secondary mb-1"> <p class="text-xs text-secondary mb-1">
{{ invoice.client ? invoice.client.email : '' }} {{ invoice.client ? invoice.client.email : "" }}
</p> </p>
<p class="text-xs text-secondary mb-0"> <p class="text-xs text-secondary mb-0">
{{ invoice.client ? invoice.client.phone : '' }} {{ invoice.client ? invoice.client.phone : "" }}
</p> </p>
</div> </div>
</template> </template>
@ -71,14 +77,17 @@
<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 === invoice.status }" :class="{ active: status === invoice.status }"
href="javascript:;" href="javascript:;"
@click="changeStatus(status); dropdownOpen = false;" @click="
changeStatus(status);
dropdownOpen = false;
"
> >
{{ getStatusLabel(status) }} {{ getStatusLabel(status) }}
</a> </a>
@ -180,7 +189,7 @@ const changeStatus = async (newStatus) => {
invoice.value = updated; invoice.value = updated;
notificationStore.success( notificationStore.success(
'Statut mis à jour', "Statut mis à jour",
`La facture est maintenant "${getStatusLabel(newStatus)}"`, `La facture est maintenant "${getStatusLabel(newStatus)}"`,
3000 3000
); );
@ -188,8 +197,8 @@ const changeStatus = async (newStatus) => {
} catch (e) { } catch (e) {
console.error("Failed to update status", e); console.error("Failed to update status", e);
notificationStore.error( notificationStore.error(
'Erreur', "Erreur",
'Impossible de mettre à jour le statut', "Impossible de mettre à jour le statut",
3000 3000
); );
} finally { } finally {

View File

@ -54,14 +54,17 @@
<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 === quote.status }" :class="{ active: status === quote.status }"
href="javascript:;" href="javascript:;"
@click="changeStatus(status); dropdownOpen = false;" @click="
changeStatus(status);
dropdownOpen = false;
"
> >
{{ getStatusLabel(status) }} {{ getStatusLabel(status) }}
</a> </a>
@ -164,7 +167,7 @@ const changeStatus = async (newStatus) => {
// Show success notification // Show success notification
notificationStore.success( notificationStore.success(
'Statut mis à jour', "Statut mis à jour",
`Le devis est maintenant "${getStatusLabel(newStatus)}"`, `Le devis est maintenant "${getStatusLabel(newStatus)}"`,
3000 3000
); );
@ -172,8 +175,8 @@ const changeStatus = async (newStatus) => {
} catch (e) { } catch (e) {
console.error("Failed to update status", e); console.error("Failed to update status", e);
notificationStore.error( notificationStore.error(
'Erreur', "Erreur",
'Impossible de mettre à jour le statut', "Impossible de mettre à jour le statut",
3000 3000
); );
} finally { } finally {

View File

@ -113,9 +113,7 @@ const practitioners = ref([
const applyFilter = (filterData) => { const applyFilter = (filterData) => {
filterStartDate.value = filterData.startDate; filterStartDate.value = filterData.startDate;
filterEndDate.value = filterData.endDate; filterEndDate.value = filterData.endDate;
alert( alert(`Filtre appliqué: ${filterData.startDate} à ${filterData.endDate}`);
`Filtre appliqué: ${filterData.startDate} à ${filterData.endDate}`
);
}; };
const exportPDF = () => { const exportPDF = () => {

View File

@ -3,10 +3,10 @@
<template #webmailing-header> <template #webmailing-header>
<div class="d-flex justify-content-between align-items-center mb-4"> <div class="d-flex justify-content-between align-items-center mb-4">
<div> <div>
<h3 class="mb-0"> <h3 class="mb-0"><i class="fas fa-envelope"></i> Webmailing</h3>
<i class="fas fa-envelope"></i> Webmailing <small class="text-muted"
</h3> >Gérez vos campagnes d'email marketing</small
<small class="text-muted">Gérez vos campagnes d'email marketing</small> >
</div> </div>
</div> </div>
</template> </template>
@ -17,8 +17,8 @@
<button <button
class="nav-link" class="nav-link"
:class="{ active: activeTab === 'compose' }" :class="{ active: activeTab === 'compose' }"
@click="activeTab = 'compose'"
type="button" type="button"
@click="activeTab = 'compose'"
> >
<i class="fas fa-pen"></i> Composer <i class="fas fa-pen"></i> Composer
</button> </button>
@ -27,8 +27,8 @@
<button <button
class="nav-link" class="nav-link"
:class="{ active: activeTab === 'history' }" :class="{ active: activeTab === 'history' }"
@click="activeTab = 'history'"
type="button" type="button"
@click="activeTab = 'history'"
> >
<i class="fas fa-history"></i> Historique <i class="fas fa-history"></i> Historique
</button> </button>
@ -45,17 +45,10 @@
@form-data-change="updateFormData" @form-data-change="updateFormData"
/> />
<div class="mt-4 d-flex justify-content-end gap-2"> <div class="mt-4 d-flex justify-content-end gap-2">
<soft-button <soft-button color="secondary" variant="outline" @click="resetForm">
color="secondary"
variant="outline"
@click="resetForm"
>
<i class="fas fa-redo"></i> Réinitialiser <i class="fas fa-redo"></i> Réinitialiser
</soft-button> </soft-button>
<soft-button <soft-button color="success" @click="sendEmail">
color="success"
@click="sendEmail"
>
<i class="fas fa-paper-plane"></i> Envoyer <i class="fas fa-paper-plane"></i> Envoyer
</soft-button> </soft-button>
</div> </div>

View File

@ -8,21 +8,13 @@
@change="handleChange" @change="handleChange"
> >
<option value="">-- Sélectionner un type --</option> <option value="">-- Sélectionner un type --</option>
<option value="text"> <option value="text"><i class="fas fa-comment"></i> Texte</option>
<i class="fas fa-comment"></i> Texte
</option>
<option value="phone"> <option value="phone">
<i class="fas fa-phone"></i> Appel téléphonique <i class="fas fa-phone"></i> Appel téléphonique
</option> </option>
<option value="email"> <option value="email"><i class="fas fa-envelope"></i> Email</option>
<i class="fas fa-envelope"></i> Email <option value="meeting"><i class="fas fa-calendar"></i> Réunion</option>
</option> <option value="note"><i class="fas fa-sticky-note"></i> Note</option>
<option value="meeting">
<i class="fas fa-calendar"></i> Réunion
</option>
<option value="note">
<i class="fas fa-sticky-note"></i> Note
</option>
</select> </select>
</div> </div>
</template> </template>

View File

@ -22,7 +22,11 @@
/> />
</div> </div>
<div class="col-md-3"> <div class="col-md-3">
<select v-model="localPeriod" class="form-select" @change="handlePeriodChange"> <select
v-model="localPeriod"
class="form-select"
@change="handlePeriodChange"
>
<option value="">-- Période personnalisée --</option> <option value="">-- Période personnalisée --</option>
<option value="today">Aujourd'hui</option> <option value="today">Aujourd'hui</option>
<option value="week">Cette semaine</option> <option value="week">Cette semaine</option>

View File

@ -54,9 +54,7 @@ const trendClass = computed(() => {
}); });
const trendIcon = computed(() => { const trendIcon = computed(() => {
return props.trendPositive return props.trendPositive ? "fas fa-arrow-up" : "fas fa-arrow-down";
? "fas fa-arrow-up"
: "fas fa-arrow-down";
}); });
</script> </script>

View File

@ -10,7 +10,9 @@
multiple multiple
@change="handleFileChange" @change="handleFileChange"
/> />
<small class="text-muted">Formats acceptés: PDF, DOC, DOCX, XLS, XLSX, JPG, PNG</small> <small class="text-muted"
>Formats acceptés: PDF, DOC, DOCX, XLS, XLSX, JPG, PNG</small
>
</div> </div>
</template> </template>

View File

@ -24,15 +24,9 @@
class="form-select" class="form-select"
@change="emitFormData" @change="emitFormData"
> >
<option value="low"> <option value="low"><i class="fas fa-arrow-down"></i> Basse</option>
<i class="fas fa-arrow-down"></i> Basse <option value="normal"><i class="fas fa-minus"></i> Normale</option>
</option> <option value="high"><i class="fas fa-arrow-up"></i> Haute</option>
<option value="normal">
<i class="fas fa-minus"></i> Normale
</option>
<option value="high">
<i class="fas fa-arrow-up"></i> Haute
</option>
</select> </select>
</div> </div>
</div> </div>
@ -69,10 +63,7 @@
</div> </div>
<div class="form-section mb-4"> <div class="form-section mb-4">
<message-content <message-content v-model="formData.content" @blur="emitFormData" />
v-model="formData.content"
@blur="emitFormData"
/>
</div> </div>
<div class="form-section"> <div class="form-section">

View File

@ -4,7 +4,11 @@
<i class="fas fa-info-circle"></i> Aucun message <i class="fas fa-info-circle"></i> Aucun message
</div> </div>
<div v-else> <div v-else>
<div v-for="message in messages" :key="message.id" class="message-item mb-3"> <div
v-for="message in messages"
:key="message.id"
class="message-item mb-3"
>
<div class="message-header"> <div class="message-header">
<div class="d-flex justify-content-between align-items-start"> <div class="d-flex justify-content-between align-items-start">
<div> <div>
@ -14,19 +18,15 @@
{{ getTypeLabel(message.type) }} {{ getTypeLabel(message.type) }}
</span> </span>
</h6> </h6>
<small class="text-muted">{{ formatDate(message.createdDate) }}</small> <small class="text-muted">{{
formatDate(message.createdDate)
}}</small>
</div> </div>
<div class="d-flex gap-2"> <div class="d-flex gap-2">
<span <span v-if="message.priority === 'high'" class="badge bg-danger">
v-if="message.priority === 'high'"
class="badge bg-danger"
>
<i class="fas fa-exclamation-circle"></i> Haute priorité <i class="fas fa-exclamation-circle"></i> Haute priorité
</span> </span>
<span <span v-if="message.isUrgent" class="badge bg-warning">
v-if="message.isUrgent"
class="badge bg-warning"
>
<i class="fas fa-fire"></i> Urgent <i class="fas fa-fire"></i> Urgent
</span> </span>
<span :class="getStatusClass(message.read)"> <span :class="getStatusClass(message.read)">
@ -57,13 +57,22 @@
</div> </div>
</div> </div>
<div class="message-actions mt-2"> <div class="message-actions mt-2">
<button class="btn btn-sm btn-outline-primary" @click="markAsRead(message.id)"> <button
class="btn btn-sm btn-outline-primary"
@click="markAsRead(message.id)"
>
<i class="fas fa-envelope-open"></i> Marquer comme lu <i class="fas fa-envelope-open"></i> Marquer comme lu
</button> </button>
<button class="btn btn-sm btn-outline-info ms-2" @click="replyMessage(message.id)"> <button
class="btn btn-sm btn-outline-info ms-2"
@click="replyMessage(message.id)"
>
<i class="fas fa-reply"></i> Répondre <i class="fas fa-reply"></i> Répondre
</button> </button>
<button class="btn btn-sm btn-outline-danger ms-2" @click="deleteMessage(message.id)"> <button
class="btn btn-sm btn-outline-danger ms-2"
@click="deleteMessage(message.id)"
>
<i class="fas fa-trash"></i> Supprimer <i class="fas fa-trash"></i> Supprimer
</button> </button>
</div> </div>

View File

@ -3,22 +3,34 @@
<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"> <th
class="text-uppercase text-secondary text-xxs font-weight-bolder opacity-7"
>
Produit Produit
</th> </th>
<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"
>
Quantité Quantité
</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. Prix Unit.
</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"
>
Remise Remise
</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>
@ -26,22 +38,34 @@
<tbody> <tbody>
<tr v-for="(line, index) in lines" :key="index"> <tr v-for="(line, index) in lines" :key="index">
<td> <td>
<span class="text-xs font-weight-bold">{{ line.product_name || 'Produit' }}</span> <span class="text-xs font-weight-bold">{{
line.product_name || "Produit"
}}</span>
</td> </td>
<td> <td>
<span class="text-xs text-secondary">{{ line.description || '-' }}</span> <span class="text-xs text-secondary">{{
line.description || "-"
}}</span>
</td> </td>
<td class="text-center"> <td class="text-center">
<span class="text-xs font-weight-bold">{{ line.units_qty || line.qty_base || 1 }}</span> <span class="text-xs font-weight-bold">{{
line.units_qty || line.qty_base || 1
}}</span>
</td> </td>
<td class="text-center"> <td class="text-center">
<span class="text-xs font-weight-bold">{{ formatCurrency(line.unit_price) }}</span> <span class="text-xs font-weight-bold">{{
formatCurrency(line.unit_price)
}}</span>
</td> </td>
<td class="text-center"> <td class="text-center">
<span class="text-xs font-weight-bold">{{ line.discount_pct || 0 }}%</span> <span class="text-xs font-weight-bold"
>{{ line.discount_pct || 0 }}%</span
>
</td> </td>
<td class="text-center"> <td class="text-center">
<span class="text-xs font-weight-bold">{{ formatCurrency(line.total_ht) }}</span> <span class="text-xs font-weight-bold">{{
formatCurrency(line.total_ht)
}}</span>
</td> </td>
</tr> </tr>
<tr v-if="!lines || lines.length === 0"> <tr v-if="!lines || lines.length === 0">

View File

@ -12,7 +12,9 @@
<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-sm font-weight-bold">Total TTC:</span> <span class="text-sm font-weight-bold">Total TTC:</span>
<span class="text-sm font-weight-bold text-success">{{ formatCurrency(ttc) }}</span> <span class="text-sm font-weight-bold text-success">{{
formatCurrency(ttc)
}}</span>
</div> </div>
</div> </div>
</template> </template>

View File

@ -92,8 +92,8 @@ const props = defineProps({
}); });
const formatCurrency = (value) => { const formatCurrency = (value) => {
const numberValue = typeof value === 'string' ? parseFloat(value) : value; const numberValue = typeof value === "string" ? parseFloat(value) : value;
if (isNaN(numberValue)) return '0,00 €'; if (isNaN(numberValue)) return "0,00 €";
return new Intl.NumberFormat("fr-FR", { return new Intl.NumberFormat("fr-FR", {
style: "currency", style: "currency",

View File

@ -32,8 +32,8 @@ const props = defineProps({
}); });
const formatCurrency = (value) => { const formatCurrency = (value) => {
const numberValue = typeof value === 'string' ? parseFloat(value) : value; const numberValue = typeof value === "string" ? parseFloat(value) : value;
if (isNaN(numberValue)) return '0,00 €'; if (isNaN(numberValue)) return "0,00 €";
return new Intl.NumberFormat("fr-FR", { return new Intl.NumberFormat("fr-FR", {
style: "currency", style: "currency",

View File

@ -33,7 +33,12 @@
</td> </td>
<td> <td>
<div class="satisfaction-stars"> <div class="satisfaction-stars">
<span v-for="i in 5" :key="i" class="star" :class="getStarClass(i, practitioner.satisfaction)"> <span
v-for="i in 5"
:key="i"
class="star"
:class="getStarClass(i, practitioner.satisfaction)"
>
<i class="fas fa-star"></i> <i class="fas fa-star"></i>
</span> </span>
</div> </div>

View File

@ -35,7 +35,7 @@
<!-- Due Date --> <!-- Due Date -->
<td class="font-weight-bold"> <td class="font-weight-bold">
<span class="my-2 text-xs" :class="getDueDateClass(invoice)">{{ <span class="my-2 text-xs" :class="getDueDateClass(invoice)">{{
formatDate(invoice.due_date) || '-' formatDate(invoice.due_date) || "-"
}}</span> }}</span>
</td> </td>

View File

@ -21,23 +21,24 @@
<div class="form-section mb-4"> <div class="form-section mb-4">
<h5 class="mb-3">Contenu du message</h5> <h5 class="mb-3">Contenu du message</h5>
<webmailing-body-input <webmailing-body-input v-model="formData.body" @blur="validateBody" />
v-model="formData.body"
@blur="validateBody"
/>
</div> </div>
<div class="form-section mb-4"> <div class="form-section mb-4">
<h5 class="mb-3">Pièces jointes</h5> <h5 class="mb-3">Pièces jointes</h5>
<webmailing-attachment <webmailing-attachment @files-selected="handleFilesSelected" />
@files-selected="handleFilesSelected"
/>
<div v-if="formData.attachments.length > 0" class="mt-3"> <div v-if="formData.attachments.length > 0" class="mt-3">
<h6>Fichiers sélectionnés:</h6> <h6>Fichiers sélectionnés:</h6>
<ul class="list-unstyled"> <ul class="list-unstyled">
<li v-for="file in formData.attachments" :key="file.name" class="mb-2"> <li
v-for="file in formData.attachments"
:key="file.name"
class="mb-2"
>
<span class="badge bg-info">{{ file.name }}</span> <span class="badge bg-info">{{ file.name }}</span>
<small class="ms-2 text-muted">({{ formatFileSize(file.size) }})</small> <small class="ms-2 text-muted"
>({{ formatFileSize(file.size) }})</small
>
</li> </li>
</ul> </ul>
</div> </div>
@ -129,7 +130,7 @@ const formatFileSize = (bytes) => {
const k = 1024; const k = 1024;
const sizes = ["Bytes", "KB", "MB", "GB"]; const sizes = ["Bytes", "KB", "MB", "GB"];
const i = Math.floor(Math.log(bytes) / Math.log(k)); const i = Math.floor(Math.log(bytes) / Math.log(k));
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + " " + sizes[i]; return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + " " + sizes[i];
}; };
</script> </script>

View File

@ -34,7 +34,10 @@
<button class="btn btn-sm btn-info" @click="viewEmail(email.id)"> <button class="btn btn-sm btn-info" @click="viewEmail(email.id)">
<i class="fas fa-eye"></i> <i class="fas fa-eye"></i>
</button> </button>
<button class="btn btn-sm btn-danger ms-2" @click="deleteEmail(email.id)"> <button
class="btn btn-sm btn-danger ms-2"
@click="deleteEmail(email.id)"
>
<i class="fas fa-trash"></i> <i class="fas fa-trash"></i>
</button> </button>
</td> </td>

View File

@ -215,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
@ -225,7 +225,7 @@ watch(
if (newSuccess) { if (newSuccess) {
resetForm(); resetForm();
} }
}, }
); );
const submitForm = async () => { const submitForm = async () => {

View File

@ -240,8 +240,6 @@
</div> </div>
</div> </div>
<div class="form-check mb-3"> <div class="form-check mb-3">
<input <input
id="isDefaultCheckbox" id="isDefaultCheckbox"

View File

@ -61,7 +61,9 @@ export const TvaRateService = {
return response; return response;
}, },
async deleteTvaRate(id: number): Promise<{ success: boolean; message: string }> { async deleteTvaRate(
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/tva-rates/${id}`, url: `/api/tva-rates/${id}`,
method: "delete", method: "delete",

View File

@ -81,7 +81,9 @@ export const useInvoiceStore = defineStore("invoice", () => {
return response; return response;
} catch (err: any) { } catch (err: any) {
const errorMessage = const errorMessage =
err.response?.data?.message || err.message || "Failed to fetch invoices"; err.response?.data?.message ||
err.message ||
"Failed to fetch invoices";
setError(errorMessage); setError(errorMessage);
throw err; throw err;
} finally { } finally {
@ -124,7 +126,9 @@ export const useInvoiceStore = defineStore("invoice", () => {
return response.data; return response.data;
} catch (err: any) { } catch (err: any) {
const errorMessage = const errorMessage =
err.response?.data?.message || err.message || "Failed to create invoice"; err.response?.data?.message ||
err.message ||
"Failed to create invoice";
setError(errorMessage); setError(errorMessage);
throw err; throw err;
} finally { } finally {
@ -146,7 +150,9 @@ export const useInvoiceStore = defineStore("invoice", () => {
return response.data; return response.data;
} catch (err: any) { } catch (err: any) {
const errorMessage = const errorMessage =
err.response?.data?.message || err.message || "Failed to create invoice from quote"; err.response?.data?.message ||
err.message ||
"Failed to create invoice from quote";
setError(errorMessage); setError(errorMessage);
throw err; throw err;
} finally { } finally {
@ -174,14 +180,19 @@ export const useInvoiceStore = defineStore("invoice", () => {
} }
// Update current invoice if it's the one being edited // Update current invoice if it's the one being edited
if (currentInvoice.value && currentInvoice.value.id === updatedInvoice.id) { if (
currentInvoice.value &&
currentInvoice.value.id === updatedInvoice.id
) {
setCurrentInvoice(updatedInvoice); setCurrentInvoice(updatedInvoice);
} }
return updatedInvoice; return updatedInvoice;
} catch (err: any) { } catch (err: any) {
const errorMessage = const errorMessage =
err.response?.data?.message || err.message || "Failed to update invoice"; err.response?.data?.message ||
err.message ||
"Failed to update invoice";
setError(errorMessage); setError(errorMessage);
throw err; throw err;
} finally { } finally {
@ -210,7 +221,9 @@ export const useInvoiceStore = defineStore("invoice", () => {
return response; return response;
} catch (err: any) { } catch (err: any) {
const errorMessage = const errorMessage =
err.response?.data?.message || err.message || "Failed to delete invoice"; err.response?.data?.message ||
err.message ||
"Failed to delete invoice";
setError(errorMessage); setError(errorMessage);
throw err; throw err;
} finally { } finally {

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">Nouvelle Facture</h6> <h6 class="mb-0">Nouvelle Facture</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>
@ -14,11 +19,16 @@
<div class="card-body p-3"> <div class="card-body p-3">
<div class="alert alert-info"> <div class="alert alert-info">
<i class="fas fa-info-circle me-2"></i> <i class="fas fa-info-circle me-2"></i>
La création de facture directe sera disponible prochainement. La création de facture directe sera disponible prochainement. Pour
Pour l'instant, vous pouvez créer une facture à partir d'un devis accepté. l'instant, vous pouvez créer une facture à partir d'un devis
accepté.
</div> </div>
<div class="text-center py-4"> <div class="text-center py-4">
<soft-button color="success" variant="gradient" @click="goToQuotes"> <soft-button
color="success"
variant="gradient"
@click="goToQuotes"
>
<i class="fas fa-file-invoice me-2"></i> <i class="fas fa-file-invoice me-2"></i>
Voir les Devis Voir les Devis
</soft-button> </soft-button>