Ventes et modules transverses: devis, factures, messages, stats et webmailing
This commit is contained in:
parent
dc87b0f720
commit
11750a3ffc
@ -9,9 +9,7 @@
|
||||
<small class="text-muted">Communiquez avec votre équipe</small>
|
||||
</div>
|
||||
<div>
|
||||
<span class="badge bg-info">
|
||||
{{ unreadCount }} non lu(s)
|
||||
</span>
|
||||
<span class="badge bg-info"> {{ unreadCount }} non lu(s) </span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -22,19 +20,21 @@
|
||||
<button
|
||||
class="nav-link"
|
||||
:class="{ active: activeTab === 'inbox' }"
|
||||
@click="activeTab = 'inbox'"
|
||||
type="button"
|
||||
@click="activeTab = 'inbox'"
|
||||
>
|
||||
<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>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button
|
||||
class="nav-link"
|
||||
:class="{ active: activeTab === 'compose' }"
|
||||
@click="activeTab = 'compose'"
|
||||
type="button"
|
||||
@click="activeTab = 'compose'"
|
||||
>
|
||||
<i class="fas fa-pen"></i> Nouveau message
|
||||
</button>
|
||||
@ -43,8 +43,8 @@
|
||||
<button
|
||||
class="nav-link"
|
||||
:class="{ active: activeTab === 'sent' }"
|
||||
@click="activeTab = 'sent'"
|
||||
type="button"
|
||||
@click="activeTab = 'sent'"
|
||||
>
|
||||
<i class="fas fa-paper-plane"></i> Envoyés
|
||||
</button>
|
||||
@ -72,17 +72,10 @@
|
||||
@form-data-change="updateNewMessage"
|
||||
/>
|
||||
<div class="mt-4 d-flex justify-content-end gap-2">
|
||||
<soft-button
|
||||
color="secondary"
|
||||
variant="outline"
|
||||
@click="resetForm"
|
||||
>
|
||||
<soft-button color="secondary" variant="outline" @click="resetForm">
|
||||
<i class="fas fa-redo"></i> Réinitialiser
|
||||
</soft-button>
|
||||
<soft-button
|
||||
color="success"
|
||||
@click="sendMessage"
|
||||
>
|
||||
<soft-button color="success" @click="sendMessage">
|
||||
<i class="fas fa-check"></i> Envoyer
|
||||
</soft-button>
|
||||
</div>
|
||||
@ -260,7 +253,9 @@ const sendMessage = () => {
|
||||
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 = {
|
||||
id: sent.value.length + 101,
|
||||
|
||||
@ -6,7 +6,10 @@
|
||||
</soft-button>
|
||||
</template>
|
||||
<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-item
|
||||
prev
|
||||
@ -31,12 +34,26 @@
|
||||
</div>
|
||||
</template>
|
||||
<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
|
||||
</soft-button>
|
||||
<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><a class="dropdown-item border-radius-md" href="javascript:;">Par statut</a></li>
|
||||
<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>
|
||||
</template>
|
||||
<template #intervention-other-action>
|
||||
@ -104,7 +121,7 @@ const props = defineProps({
|
||||
pagination: {
|
||||
type: Object,
|
||||
default: null,
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// Emits
|
||||
@ -115,8 +132,9 @@ const go = () => {
|
||||
};
|
||||
|
||||
const changePage = (page) => {
|
||||
if (typeof page !== 'number') return;
|
||||
if (page < 1 || (props.pagination && page > props.pagination.last_page)) return;
|
||||
if (typeof page !== "number") return;
|
||||
if (page < 1 || (props.pagination && page > props.pagination.last_page))
|
||||
return;
|
||||
if (page === props.pagination.current_page) return;
|
||||
emit("page-change", page);
|
||||
};
|
||||
@ -126,7 +144,11 @@ const visiblePages = computed(() => {
|
||||
const { current_page, last_page } = props.pagination;
|
||||
const delta = 2;
|
||||
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);
|
||||
}
|
||||
|
||||
@ -173,18 +195,24 @@ const visiblePages = computed(() => {
|
||||
for (let i = 1; i <= total; i++) p.push(i);
|
||||
} else {
|
||||
p.push(1);
|
||||
if (current_page > 3) p.push('...');
|
||||
if (current_page > 3) p.push("...");
|
||||
|
||||
let midStart = Math.max(2, current_page - 1);
|
||||
let midEnd = Math.min(total - 1, current_page + 1);
|
||||
|
||||
// pinned logic adjustment
|
||||
if (current_page < 4) { midStart = 2; midEnd = 4; }
|
||||
if (current_page > total - 3) { midStart = total - 3; midEnd = total - 1; }
|
||||
if (current_page < 4) {
|
||||
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);
|
||||
|
||||
if (current_page < total - 2) p.push('...');
|
||||
if (current_page < total - 2) p.push("...");
|
||||
p.push(total);
|
||||
}
|
||||
return p;
|
||||
|
||||
@ -23,7 +23,11 @@
|
||||
<div>
|
||||
<h6 class="mb-3 text-sm">Historique</h6>
|
||||
<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">
|
||||
{{ formatDate(entry.changed_at) }}
|
||||
</span>
|
||||
@ -38,13 +42,15 @@
|
||||
<div>
|
||||
<h6 class="mb-3 text-sm">Informations Client</h6>
|
||||
<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 class="text-xs text-secondary mb-1">
|
||||
{{ invoice.client ? invoice.client.email : '' }}
|
||||
{{ invoice.client ? invoice.client.email : "" }}
|
||||
</p>
|
||||
<p class="text-xs text-secondary mb-0">
|
||||
{{ invoice.client ? invoice.client.phone : '' }}
|
||||
{{ invoice.client ? invoice.client.phone : "" }}
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
@ -71,14 +77,17 @@
|
||||
<ul
|
||||
v-if="dropdownOpen"
|
||||
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">
|
||||
<a
|
||||
class="dropdown-item"
|
||||
:class="{ active: status === invoice.status }"
|
||||
href="javascript:;"
|
||||
@click="changeStatus(status); dropdownOpen = false;"
|
||||
@click="
|
||||
changeStatus(status);
|
||||
dropdownOpen = false;
|
||||
"
|
||||
>
|
||||
{{ getStatusLabel(status) }}
|
||||
</a>
|
||||
@ -180,7 +189,7 @@ const changeStatus = async (newStatus) => {
|
||||
invoice.value = updated;
|
||||
|
||||
notificationStore.success(
|
||||
'Statut mis à jour',
|
||||
"Statut mis à jour",
|
||||
`La facture est maintenant "${getStatusLabel(newStatus)}"`,
|
||||
3000
|
||||
);
|
||||
@ -188,8 +197,8 @@ const changeStatus = async (newStatus) => {
|
||||
} catch (e) {
|
||||
console.error("Failed to update status", e);
|
||||
notificationStore.error(
|
||||
'Erreur',
|
||||
'Impossible de mettre à jour le statut',
|
||||
"Erreur",
|
||||
"Impossible de mettre à jour le statut",
|
||||
3000
|
||||
);
|
||||
} finally {
|
||||
|
||||
@ -54,14 +54,17 @@
|
||||
<ul
|
||||
v-if="dropdownOpen"
|
||||
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">
|
||||
<a
|
||||
class="dropdown-item"
|
||||
:class="{ active: status === quote.status }"
|
||||
href="javascript:;"
|
||||
@click="changeStatus(status); dropdownOpen = false;"
|
||||
@click="
|
||||
changeStatus(status);
|
||||
dropdownOpen = false;
|
||||
"
|
||||
>
|
||||
{{ getStatusLabel(status) }}
|
||||
</a>
|
||||
@ -164,7 +167,7 @@ const changeStatus = async (newStatus) => {
|
||||
|
||||
// Show success notification
|
||||
notificationStore.success(
|
||||
'Statut mis à jour',
|
||||
"Statut mis à jour",
|
||||
`Le devis est maintenant "${getStatusLabel(newStatus)}"`,
|
||||
3000
|
||||
);
|
||||
@ -172,8 +175,8 @@ const changeStatus = async (newStatus) => {
|
||||
} catch (e) {
|
||||
console.error("Failed to update status", e);
|
||||
notificationStore.error(
|
||||
'Erreur',
|
||||
'Impossible de mettre à jour le statut',
|
||||
"Erreur",
|
||||
"Impossible de mettre à jour le statut",
|
||||
3000
|
||||
);
|
||||
} finally {
|
||||
|
||||
@ -113,9 +113,7 @@ const practitioners = ref([
|
||||
const applyFilter = (filterData) => {
|
||||
filterStartDate.value = filterData.startDate;
|
||||
filterEndDate.value = filterData.endDate;
|
||||
alert(
|
||||
`Filtre appliqué: ${filterData.startDate} à ${filterData.endDate}`
|
||||
);
|
||||
alert(`Filtre appliqué: ${filterData.startDate} à ${filterData.endDate}`);
|
||||
};
|
||||
|
||||
const exportPDF = () => {
|
||||
|
||||
@ -3,10 +3,10 @@
|
||||
<template #webmailing-header>
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<div>
|
||||
<h3 class="mb-0">
|
||||
<i class="fas fa-envelope"></i> Webmailing
|
||||
</h3>
|
||||
<small class="text-muted">Gérez vos campagnes d'email marketing</small>
|
||||
<h3 class="mb-0"><i class="fas fa-envelope"></i> Webmailing</h3>
|
||||
<small class="text-muted"
|
||||
>Gérez vos campagnes d'email marketing</small
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -17,8 +17,8 @@
|
||||
<button
|
||||
class="nav-link"
|
||||
:class="{ active: activeTab === 'compose' }"
|
||||
@click="activeTab = 'compose'"
|
||||
type="button"
|
||||
@click="activeTab = 'compose'"
|
||||
>
|
||||
<i class="fas fa-pen"></i> Composer
|
||||
</button>
|
||||
@ -27,8 +27,8 @@
|
||||
<button
|
||||
class="nav-link"
|
||||
:class="{ active: activeTab === 'history' }"
|
||||
@click="activeTab = 'history'"
|
||||
type="button"
|
||||
@click="activeTab = 'history'"
|
||||
>
|
||||
<i class="fas fa-history"></i> Historique
|
||||
</button>
|
||||
@ -45,17 +45,10 @@
|
||||
@form-data-change="updateFormData"
|
||||
/>
|
||||
<div class="mt-4 d-flex justify-content-end gap-2">
|
||||
<soft-button
|
||||
color="secondary"
|
||||
variant="outline"
|
||||
@click="resetForm"
|
||||
>
|
||||
<soft-button color="secondary" variant="outline" @click="resetForm">
|
||||
<i class="fas fa-redo"></i> Réinitialiser
|
||||
</soft-button>
|
||||
<soft-button
|
||||
color="success"
|
||||
@click="sendEmail"
|
||||
>
|
||||
<soft-button color="success" @click="sendEmail">
|
||||
<i class="fas fa-paper-plane"></i> Envoyer
|
||||
</soft-button>
|
||||
</div>
|
||||
|
||||
@ -8,21 +8,13 @@
|
||||
@change="handleChange"
|
||||
>
|
||||
<option value="">-- Sélectionner un type --</option>
|
||||
<option value="text">
|
||||
<i class="fas fa-comment"></i> Texte
|
||||
</option>
|
||||
<option value="text"><i class="fas fa-comment"></i> Texte</option>
|
||||
<option value="phone">
|
||||
<i class="fas fa-phone"></i> Appel téléphonique
|
||||
</option>
|
||||
<option value="email">
|
||||
<i class="fas fa-envelope"></i> Email
|
||||
</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>
|
||||
<option value="email"><i class="fas fa-envelope"></i> Email</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>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -22,7 +22,11 @@
|
||||
/>
|
||||
</div>
|
||||
<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="today">Aujourd'hui</option>
|
||||
<option value="week">Cette semaine</option>
|
||||
|
||||
@ -54,9 +54,7 @@ const trendClass = computed(() => {
|
||||
});
|
||||
|
||||
const trendIcon = computed(() => {
|
||||
return props.trendPositive
|
||||
? "fas fa-arrow-up"
|
||||
: "fas fa-arrow-down";
|
||||
return props.trendPositive ? "fas fa-arrow-up" : "fas fa-arrow-down";
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@ -10,7 +10,9 @@
|
||||
multiple
|
||||
@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>
|
||||
</template>
|
||||
|
||||
|
||||
@ -24,15 +24,9 @@
|
||||
class="form-select"
|
||||
@change="emitFormData"
|
||||
>
|
||||
<option value="low">
|
||||
<i class="fas fa-arrow-down"></i> Basse
|
||||
</option>
|
||||
<option value="normal">
|
||||
<i class="fas fa-minus"></i> Normale
|
||||
</option>
|
||||
<option value="high">
|
||||
<i class="fas fa-arrow-up"></i> Haute
|
||||
</option>
|
||||
<option value="low"><i class="fas fa-arrow-down"></i> Basse</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>
|
||||
</div>
|
||||
</div>
|
||||
@ -69,10 +63,7 @@
|
||||
</div>
|
||||
|
||||
<div class="form-section mb-4">
|
||||
<message-content
|
||||
v-model="formData.content"
|
||||
@blur="emitFormData"
|
||||
/>
|
||||
<message-content v-model="formData.content" @blur="emitFormData" />
|
||||
</div>
|
||||
|
||||
<div class="form-section">
|
||||
|
||||
@ -4,7 +4,11 @@
|
||||
<i class="fas fa-info-circle"></i> Aucun message
|
||||
</div>
|
||||
<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="d-flex justify-content-between align-items-start">
|
||||
<div>
|
||||
@ -14,19 +18,15 @@
|
||||
{{ getTypeLabel(message.type) }}
|
||||
</span>
|
||||
</h6>
|
||||
<small class="text-muted">{{ formatDate(message.createdDate) }}</small>
|
||||
<small class="text-muted">{{
|
||||
formatDate(message.createdDate)
|
||||
}}</small>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
<span
|
||||
v-if="message.priority === 'high'"
|
||||
class="badge bg-danger"
|
||||
>
|
||||
<span v-if="message.priority === 'high'" class="badge bg-danger">
|
||||
<i class="fas fa-exclamation-circle"></i> Haute priorité
|
||||
</span>
|
||||
<span
|
||||
v-if="message.isUrgent"
|
||||
class="badge bg-warning"
|
||||
>
|
||||
<span v-if="message.isUrgent" class="badge bg-warning">
|
||||
<i class="fas fa-fire"></i> Urgent
|
||||
</span>
|
||||
<span :class="getStatusClass(message.read)">
|
||||
@ -57,13 +57,22 @@
|
||||
</div>
|
||||
</div>
|
||||
<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
|
||||
</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
|
||||
</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
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@ -3,22 +3,34 @@
|
||||
<table class="table align-items-center mb-0">
|
||||
<thead>
|
||||
<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
|
||||
</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
|
||||
</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é
|
||||
</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.
|
||||
</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
|
||||
</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
|
||||
</th>
|
||||
</tr>
|
||||
@ -26,22 +38,34 @@
|
||||
<tbody>
|
||||
<tr v-for="(line, index) in lines" :key="index">
|
||||
<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>
|
||||
<span class="text-xs text-secondary">{{ line.description || '-' }}</span>
|
||||
<span class="text-xs text-secondary">{{
|
||||
line.description || "-"
|
||||
}}</span>
|
||||
</td>
|
||||
<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 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 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 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>
|
||||
</tr>
|
||||
<tr v-if="!lines || lines.length === 0">
|
||||
|
||||
@ -12,7 +12,9 @@
|
||||
<hr class="horizontal dark my-2" />
|
||||
<div class="d-flex justify-content-between">
|
||||
<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>
|
||||
</template>
|
||||
|
||||
@ -92,8 +92,8 @@ const props = defineProps({
|
||||
});
|
||||
|
||||
const formatCurrency = (value) => {
|
||||
const numberValue = typeof value === 'string' ? parseFloat(value) : value;
|
||||
if (isNaN(numberValue)) return '0,00 €';
|
||||
const numberValue = typeof value === "string" ? parseFloat(value) : value;
|
||||
if (isNaN(numberValue)) return "0,00 €";
|
||||
|
||||
return new Intl.NumberFormat("fr-FR", {
|
||||
style: "currency",
|
||||
|
||||
@ -32,8 +32,8 @@ const props = defineProps({
|
||||
});
|
||||
|
||||
const formatCurrency = (value) => {
|
||||
const numberValue = typeof value === 'string' ? parseFloat(value) : value;
|
||||
if (isNaN(numberValue)) return '0,00 €';
|
||||
const numberValue = typeof value === "string" ? parseFloat(value) : value;
|
||||
if (isNaN(numberValue)) return "0,00 €";
|
||||
|
||||
return new Intl.NumberFormat("fr-FR", {
|
||||
style: "currency",
|
||||
|
||||
@ -33,7 +33,12 @@
|
||||
</td>
|
||||
<td>
|
||||
<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>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@ -35,7 +35,7 @@
|
||||
<!-- Due Date -->
|
||||
<td class="font-weight-bold">
|
||||
<span class="my-2 text-xs" :class="getDueDateClass(invoice)">{{
|
||||
formatDate(invoice.due_date) || '-'
|
||||
formatDate(invoice.due_date) || "-"
|
||||
}}</span>
|
||||
</td>
|
||||
|
||||
|
||||
@ -21,23 +21,24 @@
|
||||
|
||||
<div class="form-section mb-4">
|
||||
<h5 class="mb-3">Contenu du message</h5>
|
||||
<webmailing-body-input
|
||||
v-model="formData.body"
|
||||
@blur="validateBody"
|
||||
/>
|
||||
<webmailing-body-input v-model="formData.body" @blur="validateBody" />
|
||||
</div>
|
||||
|
||||
<div class="form-section mb-4">
|
||||
<h5 class="mb-3">Pièces jointes</h5>
|
||||
<webmailing-attachment
|
||||
@files-selected="handleFilesSelected"
|
||||
/>
|
||||
<webmailing-attachment @files-selected="handleFilesSelected" />
|
||||
<div v-if="formData.attachments.length > 0" class="mt-3">
|
||||
<h6>Fichiers sélectionnés:</h6>
|
||||
<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>
|
||||
<small class="ms-2 text-muted">({{ formatFileSize(file.size) }})</small>
|
||||
<small class="ms-2 text-muted"
|
||||
>({{ formatFileSize(file.size) }})</small
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@ -129,7 +130,7 @@ const formatFileSize = (bytes) => {
|
||||
const k = 1024;
|
||||
const sizes = ["Bytes", "KB", "MB", "GB"];
|
||||
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>
|
||||
|
||||
|
||||
@ -34,7 +34,10 @@
|
||||
<button class="btn btn-sm btn-info" @click="viewEmail(email.id)">
|
||||
<i class="fas fa-eye"></i>
|
||||
</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>
|
||||
</button>
|
||||
</td>
|
||||
|
||||
@ -215,7 +215,7 @@ watch(
|
||||
(newErrors) => {
|
||||
fieldErrors.value = { ...newErrors };
|
||||
},
|
||||
{ deep: true },
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
// Watch for success from parent
|
||||
@ -225,7 +225,7 @@ watch(
|
||||
if (newSuccess) {
|
||||
resetForm();
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const submitForm = async () => {
|
||||
|
||||
@ -240,8 +240,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="form-check mb-3">
|
||||
<input
|
||||
id="isDefaultCheckbox"
|
||||
|
||||
@ -61,7 +61,9 @@ export const TvaRateService = {
|
||||
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 }>({
|
||||
url: `/api/tva-rates/${id}`,
|
||||
method: "delete",
|
||||
|
||||
@ -81,7 +81,9 @@ export const useInvoiceStore = defineStore("invoice", () => {
|
||||
return response;
|
||||
} catch (err: any) {
|
||||
const errorMessage =
|
||||
err.response?.data?.message || err.message || "Failed to fetch invoices";
|
||||
err.response?.data?.message ||
|
||||
err.message ||
|
||||
"Failed to fetch invoices";
|
||||
setError(errorMessage);
|
||||
throw err;
|
||||
} finally {
|
||||
@ -124,7 +126,9 @@ export const useInvoiceStore = defineStore("invoice", () => {
|
||||
return response.data;
|
||||
} catch (err: any) {
|
||||
const errorMessage =
|
||||
err.response?.data?.message || err.message || "Failed to create invoice";
|
||||
err.response?.data?.message ||
|
||||
err.message ||
|
||||
"Failed to create invoice";
|
||||
setError(errorMessage);
|
||||
throw err;
|
||||
} finally {
|
||||
@ -146,7 +150,9 @@ export const useInvoiceStore = defineStore("invoice", () => {
|
||||
return response.data;
|
||||
} catch (err: any) {
|
||||
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);
|
||||
throw err;
|
||||
} finally {
|
||||
@ -174,14 +180,19 @@ export const useInvoiceStore = defineStore("invoice", () => {
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
return updatedInvoice;
|
||||
} catch (err: any) {
|
||||
const errorMessage =
|
||||
err.response?.data?.message || err.message || "Failed to update invoice";
|
||||
err.response?.data?.message ||
|
||||
err.message ||
|
||||
"Failed to update invoice";
|
||||
setError(errorMessage);
|
||||
throw err;
|
||||
} finally {
|
||||
@ -210,7 +221,9 @@ export const useInvoiceStore = defineStore("invoice", () => {
|
||||
return response;
|
||||
} catch (err: any) {
|
||||
const errorMessage =
|
||||
err.response?.data?.message || err.message || "Failed to delete invoice";
|
||||
err.response?.data?.message ||
|
||||
err.message ||
|
||||
"Failed to delete invoice";
|
||||
setError(errorMessage);
|
||||
throw err;
|
||||
} finally {
|
||||
|
||||
@ -6,7 +6,12 @@
|
||||
<div class="card-header pb-0 p-3">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<h6 class="mb-0">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
|
||||
</soft-button>
|
||||
</div>
|
||||
@ -14,11 +19,16 @@
|
||||
<div class="card-body p-3">
|
||||
<div class="alert alert-info">
|
||||
<i class="fas fa-info-circle me-2"></i>
|
||||
La création de facture directe sera disponible prochainement.
|
||||
Pour l'instant, vous pouvez créer une facture à partir d'un devis accepté.
|
||||
La création de facture directe sera disponible prochainement. Pour
|
||||
l'instant, vous pouvez créer une facture à partir d'un devis
|
||||
accepté.
|
||||
</div>
|
||||
<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>
|
||||
Voir les Devis
|
||||
</soft-button>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user