Feat, design, liste avoir, details avoirs, creation avoir, liste commande fournisseur, details commande fournisseur, creation commande fournisseur, webmail

This commit is contained in:
kevin 2026-01-23 16:13:03 +03:00
parent 86472e0de9
commit f62a2db36e
54 changed files with 4981 additions and 73 deletions

View File

@ -1,65 +0,0 @@
APP_NAME=Laravel
APP_ENV=local
APP_KEY=
APP_DEBUG=true
APP_URL=http://localhost
APP_LOCALE=en
APP_FALLBACK_LOCALE=en
APP_FAKER_LOCALE=en_US
APP_MAINTENANCE_DRIVER=file
# APP_MAINTENANCE_STORE=database
PHP_CLI_SERVER_WORKERS=4
BCRYPT_ROUNDS=12
LOG_CHANNEL=stack
LOG_STACK=single
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=thanasoft_back
DB_USERNAME=root
DB_PASSWORD=
SESSION_DRIVER=database
SESSION_LIFETIME=120
SESSION_ENCRYPT=false
SESSION_PATH=/
SESSION_DOMAIN=null
BROADCAST_CONNECTION=log
FILESYSTEM_DISK=local
QUEUE_CONNECTION=database
CACHE_STORE=database
# CACHE_PREFIX=
MEMCACHED_HOST=127.0.0.1
REDIS_CLIENT=phpredis
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
MAIL_MAILER=log
MAIL_SCHEME=null
MAIL_HOST=127.0.0.1
MAIL_PORT=2525
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_FROM_ADDRESS="hello@example.com"
MAIL_FROM_NAME="${APP_NAME}"
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false
VITE_APP_NAME="${APP_NAME}"

View File

@ -0,0 +1,186 @@
<template>
<div v-if="loading" class="text-center py-5">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
<div v-else-if="error" class="text-center py-5 text-danger">
{{ error }}
</div>
<avoir-detail-template v-else-if="avoir">
<template #header>
<avoir-header
:avoir-number="avoir.number"
:date="avoir.date"
/>
</template>
<template #lines>
<avoir-lines-table :lines="avoir.lines" />
</template>
<template #timeline>
<div>
<h6 class="mb-3 text-sm">Historique</h6>
<div v-if="avoir.history && avoir.history.length > 0">
<div v-for="(entry, index) in avoir.history" :key="index" class="mb-2">
<span class="text-xs text-secondary">
{{ formatDate(entry.changed_at) }}
</span>
<p class="text-xs mb-0">{{ entry.comment }}</p>
</div>
</div>
<p v-else class="text-xs text-secondary">Aucun historique</p>
</div>
</template>
<template #billing>
<div>
<h6 class="mb-3 text-sm">Informations</h6>
<p class="text-sm mb-1">
<strong>Client:</strong> {{ avoir.clientName }}
</p>
<p class="text-sm mb-1">
<strong>Facture d'origine:</strong> {{ avoir.invoiceNumber }}
</p>
<p class="text-sm mb-1">
<strong>Motif:</strong> {{ avoir.reason }}
</p>
<p class="text-xs text-secondary mb-0">
<strong>Créé le:</strong> {{ formatDate(avoir.date) }}
</p>
</div>
</template>
<template #summary>
<avoir-summary
:ht="avoir.total_ht || avoir.amount"
:tva="avoir.total_tva || 0"
:ttc="avoir.total_ttc || avoir.amount"
/>
</template>
<template #actions>
<div class="d-flex justify-content-end">
<div class="position-relative d-inline-block me-2">
<soft-button
color="secondary"
variant="gradient"
@click="dropdownOpen = !dropdownOpen"
>
{{ getStatusLabel(avoir.status) }}
<i class="fas fa-chevron-down ms-2"></i>
</soft-button>
<ul
v-if="dropdownOpen"
class="dropdown-menu show position-absolute"
style="top: 100%; left: 0; z-index: 1000;"
>
<li v-for="status in availableStatuses" :key="status">
<a
class="dropdown-item"
:class="{ active: status === avoir.status }"
href="javascript:;"
@click="changeStatus(status); dropdownOpen = false;"
>
{{ getStatusLabel(status) }}
</a>
</li>
</ul>
</div>
<soft-button color="info" variant="outline">
<i class="fas fa-file-pdf me-1"></i> Télécharger PDF
</soft-button>
</div>
</template>
</avoir-detail-template>
</template>
<script setup>
import { ref, defineProps } from "vue";
import { useRouter } from "vue-router";
import AvoirDetailTemplate from "@/components/templates/Avoir/AvoirDetailTemplate.vue";
import AvoirHeader from "@/components/molecules/Avoir/AvoirHeader.vue";
import AvoirLinesTable from "@/components/molecules/Avoir/AvoirLinesTable.vue";
import AvoirSummary from "@/components/molecules/Avoir/AvoirSummary.vue";
import SoftButton from "@/components/SoftButton.vue";
const props = defineProps({
avoirId: {
type: [String, Number],
required: true,
},
});
const router = useRouter();
const avoir = ref(null);
const loading = ref(true);
const error = ref(null);
const dropdownOpen = ref(false);
// Sample avoir data
const sampleAvoir = {
id: props.avoirId,
number: "AV-2026-02341",
invoiceNumber: "F-2026-00001",
clientName: "Caroline Lepetit thanatopraxie",
amount: 168.0,
status: "emis",
date: new Date(2026, 0, 23),
reason: "Erreur de facturation",
total_ht: 168.0,
total_tva: 0,
total_ttc: 168.0,
lines: [
{
id: 1,
description: "Forfait thanatopraxie",
quantity: 1,
price_ht: 168.0,
total_ht: 168.0,
},
],
history: [
{
changed_at: new Date(2026, 0, 23),
comment: "Avoir créé",
},
{
changed_at: new Date(2026, 0, 23, 10, 30),
comment: "Statut changé en Émis",
},
],
};
loading.value = false;
avoir.value = sampleAvoir;
const formatDate = (dateString) => {
if (!dateString) return "-";
return new Date(dateString).toLocaleDateString("fr-FR");
};
const availableStatuses = [
"brouillon",
"emis",
"applique",
"annule",
];
const getStatusLabel = (status) => {
const labels = {
brouillon: "Brouillon",
emis: "Émis",
applique: "Appliqué",
annule: "Annulé",
};
return labels[status] || status;
};
const changeStatus = (newStatus) => {
if (!avoir.value) return;
avoir.value.status = newStatus;
alert(`Statut changé en ${getStatusLabel(newStatus)}`);
};
</script>

View File

@ -0,0 +1,165 @@
<template>
<div class="container-fluid py-4">
<avoir-list-controls
@create="openCreateModal"
@filter="handleFilter"
@export="handleExport"
/>
<div class="row">
<div class="col-12">
<avoir-table
:data="filteredAvoirs"
:loading="loading"
@view="handleView"
@delete="handleDelete"
/>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, computed } from "vue";
import { useRouter } from "vue-router";
import AvoirListControls from "@/components/molecules/Avoir/AvoirListControls.vue";
import AvoirTable from "@/components/molecules/Tables/Avoirs/AvoirTable.vue";
const router = useRouter();
const loading = ref(false);
const activeFilter = ref(null);
// Sample data for avoirs
const avoirs = ref([
{
id: "1",
number: "AV-2026-00001",
invoiceNumber: "F-2026-00001",
clientName: "Caroline Lepetit thanatopraxie",
amount: 168.0,
status: "emis",
date: new Date(2026, 0, 15),
reason: "Erreur de facturation",
},
{
id: "2",
number: "AV-2026-00002",
invoiceNumber: "FAC-202512-0002",
clientName: "Hygiène Funéraire 50",
amount: 84.0,
status: "applique",
date: new Date(2026, 0, 18),
reason: "Retour de marchandise",
},
{
id: "3",
number: "AV-2026-00003",
invoiceNumber: "FAC-202512-0005",
clientName: "Pompes Funèbres Martin",
amount: 54.0,
status: "brouillon",
date: new Date(2026, 0, 20),
reason: "Geste commercial",
},
{
id: "4",
number: "AV-2026-00004",
invoiceNumber: "FAC-202512-0007",
clientName: "Pompes Funèbres Martin",
amount: 108.0,
status: "emis",
date: new Date(2026, 0, 22),
reason: "Annulation de prestation",
},
{
id: "5",
number: "AV-2026-00005",
invoiceNumber: "FACT-2024-003",
clientName: "PF Premium",
amount: 72.0,
status: "annule",
date: new Date(2025, 11, 28),
reason: "Erreur de facturation",
},
]);
// Computed property for filtered avoirs
const filteredAvoirs = computed(() => {
if (!activeFilter.value) {
return avoirs.value;
}
return avoirs.value.filter(avoir => avoir.status === activeFilter.value);
});
const openCreateModal = () => {
router.push("/avoirs/new");
};
const handleView = (id) => {
router.push(`/avoirs/${id}`);
};
const handleFilter = (status) => {
activeFilter.value = status;
};
const handleExport = () => {
// Export filtered avoirs to CSV
const dataToExport = filteredAvoirs.value;
const headers = [
"N° Avoir",
"Date",
"Statut",
"Client",
"Facture d'origine",
"Montant",
];
const csvContent = [
headers.join(","),
...dataToExport.map((avoir) =>
[
avoir.number,
avoir.date.toLocaleDateString("fr-FR"),
getStatusLabel(avoir.status),
avoir.clientName,
avoir.invoiceNumber,
avoir.amount.toFixed(2) + " EUR",
].join(",")
),
].join("\n");
// Create blob and download
const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" });
const link = document.createElement("a");
const url = URL.createObjectURL(blob);
link.setAttribute("href", url);
link.setAttribute("download", `avoirs-export-${new Date().toISOString().split('T')[0]}.csv`);
link.style.visibility = "hidden";
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
const getStatusLabel = (status) => {
const labels = {
brouillon: "Brouillon",
emis: "Émis",
applique: "Appliqué",
annule: "Annulé",
};
return labels[status] || status;
};
const handleDelete = async (id) => {
if (confirm("Êtes-vous sûr de vouloir supprimer cet avoir ?")) {
avoirs.value = avoirs.value.filter((a) => a.id !== id);
alert("Avoir supprimé avec succès");
}
};
onMounted(() => {
loading.value = false;
});
</script>

View File

@ -0,0 +1,38 @@
<template>
<div class="container-fluid py-4">
<div class="row">
<div class="col-lg-8 mx-auto">
<div class="card">
<div class="card-header pb-0 p-3">
<div class="d-flex justify-content-between align-items-center">
<h6 class="mb-0">Créer un nouvel avoir</h6>
<soft-button color="secondary" variant="outline" size="sm" @click="goBack">
<i class="fas fa-arrow-left me-2"></i>Retour
</soft-button>
</div>
</div>
<div class="card-body p-3">
<new-avoir-form @submit="handleSubmit" />
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { useRouter } from "vue-router";
import NewAvoirForm from "@/components/molecules/Avoir/NewAvoirForm.vue";
import SoftButton from "@/components/SoftButton.vue";
const router = useRouter();
const goBack = () => {
router.back();
};
const handleSubmit = (formData) => {
alert(`Avoir créé avec succès: ${formData.number}`);
router.push("/avoirs");
};
</script>

View File

@ -0,0 +1,189 @@
<template>
<div v-if="loading" class="text-center py-5">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
<div v-else-if="error" class="text-center py-5 text-danger">
{{ error }}
</div>
<commande-detail-template v-else-if="commande">
<template #header>
<commande-header
:commande-number="commande.number"
:date="commande.date"
/>
</template>
<template #lines>
<commande-lines-table :lines="commande.lines" />
</template>
<template #timeline>
<div>
<h6 class="mb-3 text-sm">Historique</h6>
<div v-if="commande.history && commande.history.length > 0">
<div v-for="(entry, index) in commande.history" :key="index" class="mb-2">
<span class="text-xs text-secondary">
{{ formatDate(entry.changed_at) }}
</span>
<p class="text-xs mb-0">{{ entry.comment }}</p>
</div>
</div>
<p v-else class="text-xs text-secondary">Aucun historique</p>
</div>
</template>
<template #billing>
<div>
<h6 class="mb-3 text-sm">Informations</h6>
<p class="text-sm mb-1">
<strong>Fournisseur:</strong> {{ commande.supplierName }}
</p>
<p class="text-sm mb-1">
<strong>Adresse:</strong> {{ commande.supplierAddress }}
</p>
<p class="text-sm mb-1">
<strong>Contact:</strong> {{ commande.supplierContact }}
</p>
<p class="text-xs text-secondary mb-0">
<strong>Créée le:</strong> {{ formatDate(commande.date) }}
</p>
</div>
</template>
<template #summary>
<commande-summary
:ht="commande.total_ht"
:tva="commande.total_tva"
:ttc="commande.total_ttc"
/>
</template>
<template #actions>
<div class="d-flex justify-content-end">
<div class="position-relative d-inline-block me-2">
<soft-button
color="secondary"
variant="gradient"
@click="dropdownOpen = !dropdownOpen"
>
{{ getStatusLabel(commande.status) }}
<i class="fas fa-chevron-down ms-2"></i>
</soft-button>
<ul
v-if="dropdownOpen"
class="dropdown-menu show position-absolute"
style="top: 100%; left: 0; z-index: 1000;"
>
<li v-for="status in availableStatuses" :key="status">
<a
class="dropdown-item"
:class="{ active: status === commande.status }"
href="javascript:;"
@click="changeStatus(status); dropdownOpen = false;"
>
{{ getStatusLabel(status) }}
</a>
</li>
</ul>
</div>
<soft-button color="info" variant="outline">
<i class="fas fa-file-pdf me-1"></i> Télécharger PDF
</soft-button>
</div>
</template>
</commande-detail-template>
</template>
<script setup>
import { ref, defineProps } from "vue";
import { useRouter } from "vue-router";
import CommandeDetailTemplate from "@/components/templates/Commande/CommandeDetailTemplate.vue";
import CommandeHeader from "@/components/molecules/Commande/CommandeHeader.vue";
import CommandeLinesTable from "@/components/molecules/Commande/CommandeLinesTable.vue";
import CommandeSummary from "@/components/molecules/Commande/CommandeSummary.vue";
import SoftButton from "@/components/SoftButton.vue";
const props = defineProps({
commandeId: {
type: [String, Number],
required: true,
},
});
const router = useRouter();
const commande = ref(null);
const loading = ref(true);
const error = ref(null);
const dropdownOpen = ref(false);
// Sample commande data
const sampleCommande = {
id: props.commandeId,
number: "CMD-2026-001",
supplierName: "Produits Funéraires Pro",
supplierAddress: "123 rue de Paris, 75001 Paris",
supplierContact: "contact@pfpro.fr",
status: "confirmee",
date: new Date(2026, 0, 15),
total_ht: 2500.0,
total_tva: 500.0,
total_ttc: 3000.0,
lines: [
{
id: 1,
designation: "Fluide artériel Premium 5L",
quantity: 5,
price_ht: 450.0,
total_ht: 2250.0,
},
{
id: 2,
designation: "Aiguilles de suture serpentine 10,80cm",
quantity: 2,
price_ht: 125.0,
total_ht: 250.0,
},
],
history: [
{
changed_at: new Date(2026, 0, 15),
comment: "Commande créée",
},
{
changed_at: new Date(2026, 0, 15, 10, 30),
comment: "Commande confirmée par le fournisseur",
},
],
};
const availableStatuses = ["brouillon", "confirmee", "livree", "facturee", "annulee"];
const formatDate = (dateString) => {
if (!dateString) return "-";
return new Date(dateString).toLocaleDateString("fr-FR");
};
const getStatusLabel = (status) => {
const labels = {
brouillon: "Brouillon",
confirmee: "Confirmée",
livree: "Livrée",
facturee: "Facturée",
annulee: "Annulée",
};
return labels[status] || status;
};
const changeStatus = (newStatus) => {
commande.value.status = newStatus;
};
// Load sample data on mount
setTimeout(() => {
commande.value = sampleCommande;
loading.value = false;
}, 500);
</script>

View File

@ -0,0 +1,161 @@
<template>
<div class="container-fluid py-4">
<commande-list-controls
@create="openCreateModal"
@filter="handleFilter"
@export="handleExport"
/>
<div class="row">
<div class="col-12">
<commande-table
:data="filteredCommandes"
:loading="loading"
@view="handleView"
@delete="handleDelete"
/>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, computed } from "vue";
import { useRouter } from "vue-router";
import CommandeListControls from "@/components/molecules/Fournisseur/CommandeListControls.vue";
import CommandeTable from "@/components/molecules/Tables/Fournisseurs/CommandeTable.vue";
const router = useRouter();
const loading = ref(false);
const activeFilter = ref(null);
// Sample data for commandes
const commandes = ref([
{
id: "1",
number: "CMD-2026-001",
date: new Date(2026, 0, 15),
supplier: "Produits Funéraires Pro",
status: "confirmee",
amount: 2500.0,
items_count: 5,
},
{
id: "2",
number: "CMD-2026-002",
date: new Date(2026, 0, 18),
supplier: "Thanatos Supply",
status: "livree",
amount: 1850.5,
items_count: 3,
},
{
id: "3",
number: "CMD-2026-003",
date: new Date(2026, 0, 20),
supplier: "ISOFROID",
status: "brouillon",
amount: 3200.0,
items_count: 8,
},
{
id: "4",
number: "CMD-2026-004",
date: new Date(2026, 0, 22),
supplier: "EEP Co EUROPE",
status: "confirmee",
amount: 1520.75,
items_count: 4,
},
{
id: "5",
number: "CMD-2026-005",
date: new Date(2025, 11, 28),
supplier: "NEXTECH MEDICAL",
status: "annulee",
amount: 890.0,
items_count: 2,
},
]);
// Computed property for filtered commandes
const filteredCommandes = computed(() => {
if (!activeFilter.value) {
return commandes.value;
}
return commandes.value.filter(cmd => cmd.status === activeFilter.value);
});
const openCreateModal = () => {
router.push("/fournisseurs/commandes/new");
};
const handleView = (id) => {
router.push(`/fournisseurs/commandes/${id}`);
};
const handleFilter = (status) => {
activeFilter.value = status;
};
const handleExport = () => {
// Export filtered commandes to CSV
const dataToExport = filteredCommandes.value;
const headers = [
"N° Commande",
"Date",
"Fournisseur",
"Statut",
"Montant",
"Articles",
];
const csvContent = [
headers.join(","),
...dataToExport.map((cmd) =>
[
cmd.number,
cmd.date.toLocaleDateString("fr-FR"),
cmd.supplier,
getStatusLabel(cmd.status),
cmd.amount.toFixed(2) + " EUR",
cmd.items_count,
].join(",")
),
].join("\n");
// Create blob and download
const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" });
const link = document.createElement("a");
const url = URL.createObjectURL(blob);
link.setAttribute("href", url);
link.setAttribute("download", `commandes-export-${new Date().toISOString().split('T')[0]}.csv`);
link.style.visibility = "hidden";
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
const getStatusLabel = (status) => {
const labels = {
brouillon: "Brouillon",
confirmee: "Confirmée",
livree: "Livrée",
facturee: "Facturée",
annulee: "Annulée",
};
return labels[status] || status;
};
const handleDelete = async (id) => {
if (confirm("Êtes-vous sûr de vouloir supprimer cette commande ?")) {
commandes.value = commandes.value.filter((c) => c.id !== id);
alert("Commande supprimée avec succès");
}
};
onMounted(() => {
loading.value = false;
});
</script>

View File

@ -0,0 +1,38 @@
<template>
<div class="container-fluid py-4">
<div class="row">
<div class="col-lg-8 mx-auto">
<div class="card">
<div class="card-header pb-0 p-3">
<div class="d-flex justify-content-between align-items-center">
<h6 class="mb-0">Créer une nouvelle commande</h6>
<soft-button color="secondary" variant="outline" size="sm" @click="goBack">
<i class="fas fa-arrow-left me-2"></i>Retour
</soft-button>
</div>
</div>
<div class="card-body p-3">
<new-commande-form @submit="handleSubmit" />
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { useRouter } from "vue-router";
import NewCommandeForm from "@/components/molecules/Commande/NewCommandeForm.vue";
import SoftButton from "@/components/SoftButton.vue";
const router = useRouter();
const goBack = () => {
router.back();
};
const handleSubmit = (formData) => {
alert(`Commande créée avec succès: ${formData.number}`);
router.push("/fournisseurs/commandes");
};
</script>

View File

@ -0,0 +1,332 @@
<template>
<internal-message-template>
<template #header>
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h3 class="mb-0">
<i class="fas fa-comments"></i> Messages internes
</h3>
<small class="text-muted">Communiquez avec votre équipe</small>
</div>
<div>
<span class="badge bg-info">
{{ unreadCount }} non lu(s)
</span>
</div>
</div>
</template>
<template #tabs>
<ul class="nav nav-tabs" role="tablist">
<li class="nav-item" role="presentation">
<button
class="nav-link"
:class="{ active: activeTab === 'inbox' }"
@click="activeTab = 'inbox'"
type="button"
>
<i class="fas fa-inbox"></i> Réception
<span class="badge bg-danger ms-2" v-if="unreadCount > 0">{{ unreadCount }}</span>
</button>
</li>
<li class="nav-item" role="presentation">
<button
class="nav-link"
:class="{ active: activeTab === 'compose' }"
@click="activeTab = 'compose'"
type="button"
>
<i class="fas fa-pen"></i> Nouveau message
</button>
</li>
<li class="nav-item" role="presentation">
<button
class="nav-link"
:class="{ active: activeTab === 'sent' }"
@click="activeTab = 'sent'"
type="button"
>
<i class="fas fa-paper-plane"></i> Envoyés
</button>
</li>
</ul>
</template>
<template #content>
<div class="tab-content mt-4">
<!-- Inbox Tab -->
<div v-if="activeTab === 'inbox'" class="tab-pane active">
<internal-message-list
:messages="inbox"
@mark-as-read="handleMarkAsRead"
@reply="handleReply"
@delete="handleDeleteMessage"
/>
</div>
<!-- Compose Tab -->
<div v-if="activeTab === 'compose'" class="tab-pane active">
<internal-message-form
:initial-data="newMessage"
:users="users"
@form-data-change="updateNewMessage"
/>
<div class="mt-4 d-flex justify-content-end gap-2">
<soft-button
color="secondary"
variant="outline"
@click="resetForm"
>
<i class="fas fa-redo"></i> Réinitialiser
</soft-button>
<soft-button
color="success"
@click="sendMessage"
>
<i class="fas fa-check"></i> Envoyer
</soft-button>
</div>
</div>
<!-- Sent Tab -->
<div v-if="activeTab === 'sent'" class="tab-pane active">
<internal-message-list
:messages="sent"
@delete="handleDeleteMessage"
/>
</div>
</div>
</template>
</internal-message-template>
</template>
<script setup>
import { ref, computed } from "vue";
import InternalMessageTemplate from "@/components/templates/InternalMessages/InternalMessageTemplate.vue";
import InternalMessageForm from "@/components/molecules/InternalMessages/InternalMessageForm.vue";
import InternalMessageList from "@/components/molecules/InternalMessages/InternalMessageList.vue";
import SoftButton from "@/components/SoftButton.vue";
const activeTab = ref("inbox");
// Sample users data
const users = ref([
{ id: 1, name: "Alice Martin", role: "Manager" },
{ id: 2, name: "Bob Durand", role: "Thanatopraticien" },
{ id: 3, name: "Catherine Leclerc", role: "Secrétaire" },
{ id: 4, name: "David Moreau", role: "Directeur" },
{ id: 5, name: "Emma Bernard", role: "Thanatopraticienne" },
]);
// Sample inbox messages
const inbox = ref([
{
id: 1,
senderName: "Alice Martin",
type: "text",
content:
"Bonjour, pouvez-vous confirmer votre disponibilité pour la réunion de demain?",
createdDate: new Date(Date.now() - 30 * 60000),
read: false,
priority: "normal",
isUrgent: false,
},
{
id: 2,
senderName: "Bob Durand",
type: "phone",
content: "Je vous appelle concernant le dossier client #125",
phoneNumber: "+33 1 23 45 67 89",
createdDate: new Date(Date.now() - 2 * 60 * 60000),
read: false,
priority: "high",
isUrgent: true,
},
{
id: 3,
senderName: "Catherine Leclerc",
type: "meeting",
content: "Réunion d'équipe programmée",
meetingDate: new Date(Date.now() + 24 * 60 * 60000),
meetingLocation: "Salle de conférence A",
createdDate: new Date(Date.now() - 4 * 60 * 60000),
read: true,
priority: "normal",
isUrgent: false,
},
{
id: 4,
senderName: "David Moreau",
type: "note",
content: "Mise à jour des procédures disponible sur le serveur partagé",
createdDate: new Date(Date.now() - 1 * 24 * 60 * 60000),
read: true,
priority: "low",
isUrgent: false,
},
{
id: 5,
senderName: "Emma Bernard",
type: "text",
content: "Avez-vous reçu mon dernier rapport?",
createdDate: new Date(Date.now() - 3 * 24 * 60 * 60000),
read: true,
priority: "normal",
isUrgent: false,
},
]);
// Sample sent messages
const sent = ref([
{
id: 101,
senderName: "Vous",
type: "text",
content: "Merci pour votre message, j'ai bien reçu",
createdDate: new Date(Date.now() - 1 * 60 * 60000),
read: true,
priority: "normal",
isUrgent: false,
},
{
id: 102,
senderName: "Vous",
type: "email",
content: "Voici les documents que vous aviez demandés",
createdDate: new Date(Date.now() - 5 * 60 * 60000),
read: true,
priority: "normal",
isUrgent: false,
},
{
id: 103,
senderName: "Vous",
type: "meeting",
content: "Réunion de suivi programmée",
meetingDate: new Date(Date.now() + 2 * 24 * 60 * 60000),
meetingLocation: "Bureau",
createdDate: new Date(Date.now() - 2 * 24 * 60 * 60000),
read: true,
priority: "normal",
isUrgent: false,
},
]);
const newMessage = ref({
type: "",
recipientId: "",
priority: "normal",
phoneNumber: "",
meetingDate: "",
meetingLocation: "",
content: "",
markAsUrgent: false,
});
const unreadCount = computed(() => {
return inbox.value.filter((m) => !m.read).length;
});
const updateNewMessage = (data) => {
newMessage.value = data;
};
const resetForm = () => {
newMessage.value = {
type: "",
recipientId: "",
priority: "normal",
phoneNumber: "",
meetingDate: "",
meetingLocation: "",
content: "",
markAsUrgent: false,
};
alert("Formulaire réinitialisé");
};
const sendMessage = () => {
// Validation
if (!newMessage.value.type) {
alert("Veuillez sélectionner un type de message");
return;
}
if (!newMessage.value.recipientId) {
alert("Veuillez sélectionner un destinataire");
return;
}
if (!newMessage.value.content.trim()) {
alert("Veuillez entrer un contenu");
return;
}
const recipient = users.value.find((u) => u.id === parseInt(newMessage.value.recipientId));
const sentMessage = {
id: sent.value.length + 101,
senderName: "Vous",
type: newMessage.value.type,
content: newMessage.value.content,
phoneNumber: newMessage.value.phoneNumber,
meetingDate: newMessage.value.meetingDate,
meetingLocation: newMessage.value.meetingLocation,
createdDate: new Date(),
read: true,
priority: newMessage.value.priority,
isUrgent: newMessage.value.markAsUrgent,
};
sent.value.unshift(sentMessage);
alert(`Message envoyé à ${recipient?.name} avec succès!`);
resetForm();
activeTab.value = "sent";
};
const handleMarkAsRead = (id) => {
const message = inbox.value.find((m) => m.id === id);
if (message) {
message.read = true;
alert("Message marqué comme lu");
}
};
const handleReply = (id) => {
const message = inbox.value.find((m) => m.id === id);
if (message) {
activeTab.value = "compose";
newMessage.value.type = message.type;
alert(`Répondre à ${message.senderName}`);
}
};
const handleDeleteMessage = (id) => {
if (confirm("Êtes-vous sûr de vouloir supprimer ce message?")) {
// Remove from both arrays
const inboxIndex = inbox.value.findIndex((m) => m.id === id);
if (inboxIndex >= 0) {
inbox.value.splice(inboxIndex, 1);
}
const sentIndex = sent.value.findIndex((m) => m.id === id);
if (sentIndex >= 0) {
sent.value.splice(sentIndex, 1);
}
alert("Message supprimé");
}
};
</script>
<style scoped>
.nav-tabs .nav-link {
color: #6c757d;
cursor: pointer;
}
.nav-tabs .nav-link.active {
color: #0d6efd;
border-bottom: 3px solid #0d6efd;
}
.gap-2 {
gap: 10px;
}
</style>

View File

@ -0,0 +1,145 @@
<template>
<statistics-template>
<template #header>
<div class="mb-4">
<h3 class="mb-2">
<i class="fas fa-chart-bar"></i> Statistiques Thanatopractiens
</h3>
<p class="text-muted">
Analyse des performances et des interventions de votre équipe
</p>
</div>
</template>
<template #filter>
<date-range-filter
:start-date="filterStartDate"
:end-date="filterEndDate"
@filter-applied="applyFilter"
/>
</template>
<template #overview>
<statistics-overview />
</template>
<template #chart>
<intervention-chart />
</template>
<template #performance>
<thanatometer-performance :practitioners="practitioners" />
</template>
<template #export>
<div class="export-section">
<h5 class="mb-3">Exporter les données</h5>
<div class="d-flex gap-2">
<soft-button color="info" @click="exportPDF">
<i class="fas fa-file-pdf"></i> Télécharger PDF
</soft-button>
<soft-button color="success" @click="exportExcel">
<i class="fas fa-file-excel"></i> Télécharger Excel
</soft-button>
<soft-button color="secondary" @click="printReport">
<i class="fas fa-print"></i> Imprimer
</soft-button>
</div>
</div>
</template>
</statistics-template>
</template>
<script setup>
import { ref } from "vue";
import StatisticsTemplate from "@/components/templates/Statistics/StatisticsTemplate.vue";
import DateRangeFilter from "@/components/atoms/Statistics/DateRangeFilter.vue";
import StatisticsOverview from "@/components/molecules/Statistics/StatisticsOverview.vue";
import InterventionChart from "@/components/molecules/Statistics/InterventionChart.vue";
import ThanatometerPerformance from "@/components/molecules/Statistics/ThanatometerPerformance.vue";
import SoftButton from "@/components/SoftButton.vue";
const filterStartDate = ref("");
const filterEndDate = ref("");
const practitioners = ref([
{
id: 1,
name: "Alice Dupont",
totalInterventions: 98,
completedInterventions: 94,
completionRate: 96,
satisfaction: 4.8,
status: "active",
},
{
id: 2,
name: "Bob Martin",
totalInterventions: 87,
completedInterventions: 78,
completionRate: 90,
satisfaction: 4.5,
status: "active",
},
{
id: 3,
name: "Catherine Leclerc",
totalInterventions: 105,
completedInterventions: 103,
completionRate: 98,
satisfaction: 4.9,
status: "active",
},
{
id: 4,
name: "David Bernard",
totalInterventions: 92,
completedInterventions: 88,
completionRate: 96,
satisfaction: 4.7,
status: "active",
},
{
id: 5,
name: "Emma Wilson",
totalInterventions: 78,
completedInterventions: 70,
completionRate: 90,
satisfaction: 4.4,
status: "inactive",
},
]);
const applyFilter = (filterData) => {
filterStartDate.value = filterData.startDate;
filterEndDate.value = filterData.endDate;
alert(
`Filtre appliqué: ${filterData.startDate} à ${filterData.endDate}`
);
};
const exportPDF = () => {
alert("Export PDF en cours...\nLe document sera téléchargé sous peu.");
};
const exportExcel = () => {
alert("Export Excel en cours...\nLe fichier sera téléchargé sous peu.");
};
const printReport = () => {
window.print();
};
</script>
<style scoped>
.export-section {
background-color: white;
padding: 1.5rem;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.gap-2 {
gap: 10px;
}
</style>

View File

@ -0,0 +1,215 @@
<template>
<webmailing-template>
<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>
</div>
</div>
</template>
<template #webmailing-tabs>
<ul class="nav nav-tabs" role="tablist">
<li class="nav-item" role="presentation">
<button
class="nav-link"
:class="{ active: activeTab === 'compose' }"
@click="activeTab = 'compose'"
type="button"
>
<i class="fas fa-pen"></i> Composer
</button>
</li>
<li class="nav-item" role="presentation">
<button
class="nav-link"
:class="{ active: activeTab === 'history' }"
@click="activeTab = 'history'"
type="button"
>
<i class="fas fa-history"></i> Historique
</button>
</li>
</ul>
</template>
<template #webmailing-content>
<div class="tab-content mt-4">
<!-- Compose Tab -->
<div v-if="activeTab === 'compose'" class="tab-pane active">
<webmailing-form
:initial-data="formData"
@form-data-change="updateFormData"
/>
<div class="mt-4 d-flex justify-content-end gap-2">
<soft-button
color="secondary"
variant="outline"
@click="resetForm"
>
<i class="fas fa-redo"></i> Réinitialiser
</soft-button>
<soft-button
color="success"
@click="sendEmail"
>
<i class="fas fa-paper-plane"></i> Envoyer
</soft-button>
</div>
</div>
<!-- History Tab -->
<div v-if="activeTab === 'history'" class="tab-pane active">
<webmailing-list
:emails="emailHistory"
@view-email="handleViewEmail"
@delete-email="handleDeleteEmail"
/>
</div>
</div>
</template>
</webmailing-template>
</template>
<script setup>
import { ref } from "vue";
import WebmailingTemplate from "@/components/templates/Webmailing/WebmailingTemplate.vue";
import WebmailingForm from "@/components/molecules/Webmailing/WebmailingForm.vue";
import WebmailingList from "@/components/molecules/Webmailing/WebmailingList.vue";
import SoftButton from "@/components/SoftButton.vue";
const activeTab = ref("compose");
const formData = ref({
recipients: "",
subject: "",
body: "",
attachments: [],
sendCopy: false,
scheduled: false,
scheduledDate: "",
});
// Sample email history data
const emailHistory = ref([
{
id: 1,
recipients: "client1@example.com, client2@example.com",
subject: "Bienvenue chez Thanasoft",
sentDate: new Date(Date.now() - 3 * 24 * 60 * 60 * 1000),
status: "sent",
},
{
id: 2,
recipients: "client3@example.com",
subject: "Rappel de rendez-vous",
sentDate: new Date(Date.now() - 1 * 24 * 60 * 60 * 1000),
status: "sent",
},
{
id: 3,
recipients: "client4@example.com, client5@example.com",
subject: "Nouvelle fonctionnalité disponible",
sentDate: new Date(),
status: "scheduled",
},
{
id: 4,
recipients: "client6@example.com",
subject: "Documentation mise à jour",
sentDate: new Date(Date.now() - 7 * 24 * 60 * 60 * 1000),
status: "sent",
},
{
id: 5,
recipients: "client7@example.com",
subject: "Erreur d'envoi",
sentDate: new Date(Date.now() - 2 * 24 * 60 * 60 * 1000),
status: "failed",
},
]);
const updateFormData = (data) => {
formData.value = data;
};
const resetForm = () => {
formData.value = {
recipients: "",
subject: "",
body: "",
attachments: [],
sendCopy: false,
scheduled: false,
scheduledDate: "",
};
alert("Formulaire réinitialisé");
};
const sendEmail = () => {
// Validation
if (!formData.value.recipients.trim()) {
alert("Veuillez entrer au moins un destinataire");
return;
}
if (!formData.value.subject.trim()) {
alert("Veuillez entrer un sujet");
return;
}
if (!formData.value.body.trim()) {
alert("Veuillez entrer un contenu");
return;
}
// Add new email to history
const newEmail = {
id: emailHistory.value.length + 1,
recipients: formData.value.recipients,
subject: formData.value.subject,
sentDate: new Date(),
status: formData.value.scheduled ? "scheduled" : "sent",
};
emailHistory.value.unshift(newEmail);
alert(
formData.value.scheduled
? "Email programmé avec succès!"
: "Email envoyé avec succès!"
);
resetForm();
activeTab.value = "history";
};
const handleViewEmail = (id) => {
const email = emailHistory.value.find((e) => e.id === id);
if (email) {
alert(`Visualisation de l'email: ${email.subject}`);
}
};
const handleDeleteEmail = (id) => {
if (confirm("Êtes-vous sûr de vouloir supprimer cet email?")) {
emailHistory.value = emailHistory.value.filter((e) => e.id !== id);
alert("Email supprimé");
}
};
</script>
<style scoped>
.nav-tabs .nav-link {
color: #6c757d;
cursor: pointer;
}
.nav-tabs .nav-link.active {
color: #0d6efd;
border-bottom: 3px solid #0d6efd;
}
.gap-2 {
gap: 10px;
}
</style>

View File

@ -0,0 +1,44 @@
<template>
<div class="form-group">
<label for="message-content" class="form-label">Contenu du message</label>
<textarea
id="message-content"
v-model="localValue"
class="form-control"
placeholder="Entrez votre message"
rows="6"
@blur="handleBlur"
></textarea>
</div>
</template>
<script setup>
import { ref, watch } from "vue";
import { defineProps, defineEmits } from "vue";
const props = defineProps({
modelValue: {
type: String,
default: "",
},
});
const emit = defineEmits(["update:modelValue", "blur"]);
const localValue = ref(props.modelValue);
watch(
() => props.modelValue,
(newValue) => {
localValue.value = newValue;
}
);
watch(localValue, (newValue) => {
emit("update:modelValue", newValue);
});
const handleBlur = () => {
emit("blur");
};
</script>

View File

@ -0,0 +1,55 @@
<template>
<div class="form-group">
<label for="message-type" class="form-label">Type de message</label>
<select
id="message-type"
v-model="localValue"
class="form-select"
@change="handleChange"
>
<option value="">-- Sélectionner un type --</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>
</select>
</div>
</template>
<script setup>
import { ref, watch } from "vue";
import { defineProps, defineEmits } from "vue";
const props = defineProps({
modelValue: {
type: String,
default: "",
},
});
const emit = defineEmits(["update:modelValue"]);
const localValue = ref(props.modelValue);
watch(
() => props.modelValue,
(newValue) => {
localValue.value = newValue;
}
);
const handleChange = () => {
emit("update:modelValue", localValue.value);
};
</script>

View File

@ -0,0 +1,47 @@
<template>
<div class="form-group">
<label for="recipient" class="form-label">Destinataire</label>
<select
id="recipient"
v-model="localValue"
class="form-select"
@change="handleChange"
>
<option value="">-- Sélectionner un utilisateur --</option>
<option v-for="user in users" :key="user.id" :value="user.id">
{{ user.name }} ({{ user.role }})
</option>
</select>
</div>
</template>
<script setup>
import { defineProps, defineEmits } from "vue";
import { ref, watch } from "vue";
const props = defineProps({
modelValue: {
type: String,
default: "",
},
users: {
type: Array,
default: () => [],
},
});
const emit = defineEmits(["update:modelValue"]);
const localValue = ref(props.modelValue);
watch(
() => props.modelValue,
(newValue) => {
localValue.value = newValue;
}
);
const handleChange = () => {
emit("update:modelValue", localValue.value);
};
</script>

View File

@ -0,0 +1,162 @@
<template>
<div class="date-range-filter">
<div class="row g-3">
<div class="col-md-3">
<soft-input
v-model="localStartDate"
type="date"
placeholder="Date de début"
icon="fas fa-calendar-alt"
icon-dir="left"
@blur="handleDateChange"
/>
</div>
<div class="col-md-3">
<soft-input
v-model="localEndDate"
type="date"
placeholder="Date de fin"
icon="fas fa-calendar-alt"
icon-dir="left"
@blur="handleDateChange"
/>
</div>
<div class="col-md-3">
<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>
<option value="month">Ce mois</option>
<option value="quarter">Ce trimestre</option>
<option value="year">Cette année</option>
</select>
</div>
<div class="col-md-3">
<soft-button color="primary" @click="applyFilter">
<i class="fas fa-filter"></i> Appliquer
</soft-button>
</div>
</div>
</div>
</template>
<script setup>
import { defineProps, defineEmits } from "vue";
import { ref, watch } from "vue";
import SoftInput from "@/components/SoftInput.vue";
import SoftButton from "@/components/SoftButton.vue";
const props = defineProps({
startDate: {
type: String,
default: "",
},
endDate: {
type: String,
default: "",
},
period: {
type: String,
default: "",
},
});
const emit = defineEmits(["filter-applied"]);
const localStartDate = ref(props.startDate);
const localEndDate = ref(props.endDate);
const localPeriod = ref(props.period);
watch(
() => props.startDate,
(newValue) => {
localStartDate.value = newValue;
}
);
watch(
() => props.endDate,
(newValue) => {
localEndDate.value = newValue;
}
);
watch(
() => props.period,
(newValue) => {
localPeriod.value = newValue;
}
);
const handleDateChange = () => {
// Date changed manually
};
const handlePeriodChange = () => {
// Update dates based on period
const today = new Date();
switch (localPeriod.value) {
case "today": {
localStartDate.value = formatDate(today);
localEndDate.value = formatDate(today);
break;
}
case "week": {
const weekStart = new Date(today);
weekStart.setDate(today.getDate() - today.getDay());
localStartDate.value = formatDate(weekStart);
localEndDate.value = formatDate(today);
break;
}
case "month": {
const monthStart = new Date(today.getFullYear(), today.getMonth(), 1);
localStartDate.value = formatDate(monthStart);
localEndDate.value = formatDate(today);
break;
}
case "quarter": {
const quarter = Math.floor(today.getMonth() / 3);
const quarterStart = new Date(today.getFullYear(), quarter * 3, 1);
localStartDate.value = formatDate(quarterStart);
localEndDate.value = formatDate(today);
break;
}
case "year": {
const yearStart = new Date(today.getFullYear(), 0, 1);
localStartDate.value = formatDate(yearStart);
localEndDate.value = formatDate(today);
break;
}
}
};
const formatDate = (date) => {
return date.toISOString().split("T")[0];
};
const applyFilter = () => {
emit("filter-applied", {
startDate: localStartDate.value,
endDate: localEndDate.value,
period: localPeriod.value,
});
};
</script>
<style scoped>
.date-range-filter {
background-color: white;
padding: 1.5rem;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
margin-bottom: 2rem;
}
.g-3 {
gap: 1rem;
}
.col-md-3 {
flex: 0 0 calc(25% - 0.75rem);
}
</style>

View File

@ -0,0 +1,133 @@
<template>
<div class="stat-card" :class="cardClass">
<div class="stat-icon">
<i :class="icon"></i>
</div>
<div class="stat-content">
<h6 class="stat-label">{{ label }}</h6>
<h3 class="stat-value">{{ value }}</h3>
<small :class="trendClass">
<i :class="trendIcon"></i>
{{ trend }}
</small>
</div>
</div>
</template>
<script setup>
import { computed } from "vue";
import { defineProps } from "vue";
const props = defineProps({
label: {
type: String,
required: true,
},
value: {
type: [String, Number],
required: true,
},
icon: {
type: String,
default: "fas fa-chart-line",
},
color: {
type: String,
default: "primary",
},
trend: {
type: String,
default: "+5% cette semaine",
},
trendPositive: {
type: Boolean,
default: true,
},
});
const cardClass = computed(() => {
return `stat-card-${props.color}`;
});
const trendClass = computed(() => {
return props.trendPositive ? "text-success" : "text-danger";
});
const trendIcon = computed(() => {
return props.trendPositive
? "fas fa-arrow-up"
: "fas fa-arrow-down";
});
</script>
<style scoped>
.stat-card {
background: white;
padding: 1.5rem;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
display: flex;
align-items: center;
gap: 1.5rem;
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.stat-card:hover {
transform: translateY(-5px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.stat-icon {
width: 60px;
height: 60px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
color: white;
}
.stat-card-primary .stat-icon {
background: #0d6efd;
}
.stat-card-success .stat-icon {
background: #198754;
}
.stat-card-warning .stat-icon {
background: #ffc107;
}
.stat-card-danger .stat-icon {
background: #dc3545;
}
.stat-card-info .stat-icon {
background: #0dcaf0;
}
.stat-content {
flex: 1;
}
.stat-label {
color: #6c757d;
margin-bottom: 0.5rem;
text-transform: uppercase;
font-size: 0.85rem;
font-weight: 600;
}
.stat-value {
font-size: 2rem;
font-weight: 700;
color: #212529;
margin: 0;
}
.stat-card small {
font-size: 0.875rem;
}
</style>

View File

@ -0,0 +1,37 @@
<template>
<div class="form-group">
<label :for="id" class="form-label">
<i class="fas fa-paperclip"></i> Pièces jointes
</label>
<input
:id="id"
type="file"
class="form-control"
multiple
@change="handleFileChange"
/>
<small class="text-muted">Formats acceptés: PDF, DOC, DOCX, XLS, XLSX, JPG, PNG</small>
</div>
</template>
<script setup>
import { defineProps, defineEmits } from "vue";
const props = defineProps({
id: {
type: String,
default: "webmailing-attachments",
},
});
const emit = defineEmits(["files-selected"]);
const handleFileChange = (event) => {
const files = event.target.files;
const fileList = Array.from(files).map((file) => ({
name: file.name,
size: file.size,
type: file.type,
}));
emit("files-selected", fileList);
};
</script>

View File

@ -0,0 +1,64 @@
<template>
<div class="form-group">
<textarea
:id="id"
v-model="localValue"
class="form-control"
:class="getClasses(error, success)"
placeholder="Contenu du message"
rows="8"
@blur="handleBlur"
></textarea>
</div>
</template>
<script setup>
import { ref, watch, defineEmits, defineProps } from "vue";
const props = defineProps({
modelValue: {
type: String,
default: "",
},
id: {
type: String,
default: "webmailing-body",
},
error: {
type: Boolean,
default: false,
},
success: {
type: Boolean,
default: false,
},
});
const emit = defineEmits(["update:modelValue", "blur"]);
const localValue = ref(props.modelValue);
watch(
() => props.modelValue,
(newValue) => {
localValue.value = newValue;
}
);
watch(localValue, (newValue) => {
emit("update:modelValue", newValue);
});
const getClasses = (error, success) => {
if (error) {
return "is-invalid";
} else if (success) {
return "is-valid";
}
return "";
};
const handleBlur = () => {
emit("blur");
};
</script>

View File

@ -0,0 +1,55 @@
<template>
<soft-input
:id="id"
v-model="localValue"
type="text"
placeholder="Sujet du message"
:error="error"
:success="success"
@blur="handleBlur"
/>
</template>
<script setup>
import { defineEmits, defineProps } from "vue";
import { ref, watch } from "vue";
import SoftInput from "@/components/SoftInput.vue";
const props = defineProps({
modelValue: {
type: String,
default: "",
},
id: {
type: String,
default: "webmailing-subject",
},
error: {
type: Boolean,
default: false,
},
success: {
type: Boolean,
default: false,
},
});
const emit = defineEmits(["update:modelValue", "blur"]);
const localValue = ref(props.modelValue);
watch(
() => props.modelValue,
(newValue) => {
localValue.value = newValue;
}
);
watch(localValue, (newValue) => {
emit("update:modelValue", newValue);
});
const handleBlur = () => {
emit("blur");
};
</script>

View File

@ -0,0 +1,36 @@
<template>
<div class="row mb-4">
<div class="col-12">
<h3 class="mb-1">
<strong>{{ avoirNumber }}</strong>
</h3>
<p class="text-muted mb-0">
Créé le {{ formatDate(date) }}
</p>
</div>
</div>
</template>
<script setup>
import { defineProps } from "vue";
defineProps({
avoirNumber: {
type: String,
required: true,
},
date: {
type: [String, Date],
required: true,
},
});
const formatDate = (date) => {
if (!date) return "-";
return new Date(date).toLocaleDateString("fr-FR", {
year: "numeric",
month: "long",
day: "numeric",
});
};
</script>

View File

@ -0,0 +1,47 @@
<template>
<div class="row">
<div class="col-12">
<h6 class="mb-3">Détail des lignes</h6>
<div class="table-responsive">
<table class="table">
<thead>
<tr>
<th>Désignation</th>
<th class="text-end">Quantité</th>
<th class="text-end">Prix HT</th>
<th class="text-end">Total HT</th>
</tr>
</thead>
<tbody>
<tr v-for="line in lines" :key="line.id">
<td>{{ line.description }}</td>
<td class="text-end">{{ line.quantity }}</td>
<td class="text-end">{{ formatCurrency(line.price_ht) }}</td>
<td class="text-end">
<strong>{{ formatCurrency(line.total_ht) }}</strong>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</template>
<script setup>
import { defineProps } from "vue";
defineProps({
lines: {
type: Array,
required: true,
},
});
const formatCurrency = (value) => {
return new Intl.NumberFormat("fr-FR", {
style: "currency",
currency: "EUR",
}).format(value);
};
</script>

View File

@ -0,0 +1,95 @@
<template>
<div class="d-sm-flex justify-content-between">
<div>
<soft-button color="success" variant="gradient" @click="$emit('create')">
Nouveau Avoir
</soft-button>
</div>
<div class="d-flex">
<div class="dropdown d-inline">
<soft-button
id="navbarDropdownMenuLink2"
color="dark"
variant="outline"
class="dropdown-toggle"
data-bs-toggle="dropdown"
aria-expanded="false"
>
Filtrer
</soft-button>
<ul
class="dropdown-menu dropdown-menu-lg-start px-2 py-3"
aria-labelledby="navbarDropdownMenuLink2"
>
<li>
<a
class="dropdown-item border-radius-md"
href="javascript:;"
@click="$emit('filter', 'emis')"
>
Statut: Émis
</a>
</li>
<li>
<a
class="dropdown-item border-radius-md"
href="javascript:;"
@click="$emit('filter', 'applique')"
>
Statut: Appliqué
</a>
</li>
<li>
<a
class="dropdown-item border-radius-md"
href="javascript:;"
@click="$emit('filter', 'brouillon')"
>
Statut: Brouillon
</a>
</li>
<li>
<a
class="dropdown-item border-radius-md"
href="javascript:;"
@click="$emit('filter', 'annule')"
>
Statut: Annulé
</a>
</li>
<li>
<hr class="horizontal dark my-2" />
</li>
<li>
<a
class="dropdown-item border-radius-md text-danger"
href="javascript:;"
@click="$emit('filter', null)"
>
Retirer Filtres
</a>
</li>
</ul>
</div>
<soft-button
class="btn-icon ms-2 export"
color="dark"
variant="outline"
data-type="csv"
@click="$emit('export')"
>
<span class="btn-inner--icon">
<i class="ni ni-archive-2"></i>
</span>
<span class="btn-inner--text">Export CSV</span>
</soft-button>
</div>
</div>
</template>
<script setup>
import { defineEmits } from "vue";
import SoftButton from "@/components/SoftButton.vue";
const emit = defineEmits(["create", "filter", "export"]);
</script>

View File

@ -0,0 +1,49 @@
<template>
<div class="row">
<div class="col-12">
<div class="alert alert-light">
<div class="d-flex justify-content-end">
<div class="text-end">
<p class="mb-2">
<strong>Total HT :</strong>
<span>{{ formatCurrency(ht) }}</span>
</p>
<p class="mb-2">
<strong>TVA :</strong>
<span>{{ formatCurrency(tva) }}</span>
</p>
<h5 class="mb-0 text-danger">
<strong>Total TTC : {{ formatCurrency(ttc) }}</strong>
</h5>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { defineProps } from "vue";
defineProps({
ht: {
type: [Number, String],
required: true,
},
tva: {
type: [Number, String],
default: 0,
},
ttc: {
type: [Number, String],
required: true,
},
});
const formatCurrency = (value) => {
return new Intl.NumberFormat("fr-FR", {
style: "currency",
currency: "EUR",
}).format(parseFloat(value));
};
</script>

View File

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

View File

@ -0,0 +1,30 @@
<template>
<div class="row align-items-center">
<div class="col-md-8">
<h4 class="mb-0">Commande {{ commandeNumber }}</h4>
</div>
<div class="col-md-4 text-end">
<span class="text-secondary text-sm">
<i class="fas fa-calendar me-2"></i>{{ formatDate(date) }}
</span>
</div>
</div>
</template>
<script setup>
import { defineProps } from "vue";
defineProps({
commandeNumber: String,
date: [String, Date],
});
const formatDate = (dateString) => {
if (!dateString) return "-";
return new Date(dateString).toLocaleDateString("fr-FR", {
day: "numeric",
month: "long",
year: "numeric",
});
};
</script>

View File

@ -0,0 +1,45 @@
<template>
<div>
<h6 class="mb-3 text-sm">Articles commandés</h6>
<div class="table-responsive">
<table class="table table-sm table-borderless">
<thead class="thead-light">
<tr>
<th>Désignation</th>
<th class="text-center">Quantité</th>
<th class="text-end">Prix HT</th>
<th class="text-end">Total HT</th>
</tr>
</thead>
<tbody>
<tr v-for="line in lines" :key="line.id">
<td class="text-sm">{{ line.designation }}</td>
<td class="text-center text-sm">{{ line.quantity }}</td>
<td class="text-end text-sm">{{ formatCurrency(line.price_ht) }}</td>
<td class="text-end text-sm font-weight-bold">
{{ formatCurrency(line.total_ht) }}
</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
<script setup>
import { defineProps } from "vue";
defineProps({
lines: {
type: Array,
required: true,
},
});
const formatCurrency = (value) => {
return new Intl.NumberFormat("fr-FR", {
style: "currency",
currency: "EUR",
}).format(value);
};
</script>

View File

@ -0,0 +1,51 @@
<template>
<div>
<div class="d-flex justify-content-between mb-2">
<p class="text-sm mb-1">
<strong>Total HT :</strong>
</p>
<p class="text-sm mb-1">{{ formatCurrency(ht) }}</p>
</div>
<div class="d-flex justify-content-between mb-2">
<p class="text-sm mb-1">
<strong>TVA (20%) :</strong>
</p>
<p class="text-sm mb-1">{{ formatCurrency(tva) }}</p>
</div>
<hr class="my-2" />
<div class="d-flex justify-content-between">
<h6 class="mb-0">
<strong>Total TTC :</strong>
</h6>
<h6 class="mb-0 text-success">
<strong>{{ formatCurrency(ttc) }}</strong>
</h6>
</div>
</div>
</template>
<script setup>
import { defineProps } from "vue";
defineProps({
ht: {
type: Number,
required: true,
},
tva: {
type: Number,
required: true,
},
ttc: {
type: Number,
required: true,
},
});
const formatCurrency = (value) => {
return new Intl.NumberFormat("fr-FR", {
style: "currency",
currency: "EUR",
}).format(value);
};
</script>

View File

@ -0,0 +1,315 @@
<template>
<form @submit.prevent="submitForm">
<!-- Row 1: N° Commande, Fournisseur, Date -->
<div class="row g-3 mb-4">
<div class="col-md-4">
<label class="form-label">N° Commande</label>
<soft-input
v-model="formData.number"
type="text"
placeholder="Auto-généré"
:disabled="true"
/>
</div>
<div class="col-md-4">
<label class="form-label">Fournisseur *</label>
<select v-model="formData.supplierId" class="form-select" @change="updateSupplierInfo" required>
<option value="">-- Sélectionner un fournisseur --</option>
<option v-for="supplier in suppliers" :key="supplier.id" :value="supplier.id">
{{ supplier.name }}
</option>
</select>
</div>
<div class="col-md-4">
<label class="form-label">Date commande *</label>
<soft-input
v-model="formData.date"
type="date"
required
/>
</div>
</div>
<!-- Row 2: Statut, Adresse fournisseur -->
<div class="row g-3 mb-4">
<div class="col-md-6">
<label class="form-label">Statut</label>
<select v-model="formData.status" class="form-select">
<option value="brouillon">Brouillon</option>
<option value="confirmee">Confirmée</option>
<option value="livree">Livrée</option>
<option value="facturee">Facturée</option>
<option value="annulee">Annulée</option>
</select>
</div>
<div class="col-md-6">
<label class="form-label">Adresse livraison</label>
<soft-input
v-model="formData.deliveryAddress"
type="text"
placeholder="Adresse de livraison"
/>
</div>
</div>
<!-- Notes -->
<div class="mb-4">
<label class="form-label">Notes de commande</label>
<textarea
v-model="formData.notes"
class="form-control"
placeholder="Notes importantes..."
rows="2"
></textarea>
</div>
<!-- Articles Section -->
<div class="mb-4">
<div class="d-flex justify-content-between align-items-center mb-3">
<h6 class="mb-0">Articles</h6>
<soft-button
type="button"
color="primary"
size="sm"
@click="addLine"
>
<i class="fas fa-plus me-1"></i> Ajouter ligne
</soft-button>
</div>
<div class="space-y-3">
<div
v-for="(line, index) in formData.lines"
:key="index"
class="row g-2 align-items-end bg-light p-3 rounded"
>
<div class="col-md-4">
<label class="form-label text-xs mb-2">Article *</label>
<select
v-model="line.productId"
class="form-select form-select-sm"
@change="updateProductInfo(index)"
required
>
<option value="">-- Choisir un article --</option>
<option v-for="product in products" :key="product.id" :value="product.id">
{{ product.name }}
</option>
</select>
</div>
<div class="col-md-2">
<label class="form-label text-xs mb-2">Désignation *</label>
<soft-input
v-model="line.designation"
type="text"
placeholder="Nom article"
required
/>
</div>
<div class="col-md-2">
<label class="form-label text-xs mb-2">Quantité *</label>
<soft-input
v-model.number="line.quantity"
type="number"
placeholder="Qté"
:min="1"
required
/>
</div>
<div class="col-md-2">
<label class="form-label text-xs mb-2">Prix HT *</label>
<soft-input
v-model.number="line.priceHt"
type="number"
placeholder="Prix"
step="0.01"
required
/>
</div>
<div class="col-md-1 text-end">
<button
type="button"
class="btn btn-sm btn-danger"
@click="removeLine(index)"
:disabled="formData.lines.length === 1"
>
<i class="fas fa-trash"></i>
</button>
</div>
<div class="col-md-1 text-end">
<span class="text-sm font-weight-bold">
{{ formatCurrency(line.quantity * line.priceHt) }}
</span>
</div>
</div>
</div>
</div>
<!-- Totaux -->
<div class="row mb-4">
<div class="col-12">
<div class="alert alert-info">
<div class="row text-end">
<div class="col-12">
<p class="mb-2">
<strong>Total HT :</strong>
<span>{{ formatCurrency(calculateTotalHt()) }}</span>
</p>
<p class="mb-2">
<strong>TVA (20%) :</strong>
<span>{{ formatCurrency(calculateTotalTva()) }}</span>
</p>
<h5 class="mb-0 text-info">
<strong>Total TTC : {{ formatCurrency(calculateTotalTtc()) }}</strong>
</h5>
</div>
</div>
</div>
</div>
</div>
<!-- Boutons d'action -->
<div class="d-flex justify-content-end gap-3">
<soft-button
type="button"
color="secondary"
variant="outline"
@click="cancelForm"
>
Annuler
</soft-button>
<soft-button
type="submit"
color="success"
>
Créer la commande
</soft-button>
</div>
</form>
</template>
<script setup>
import { ref, defineEmits } from "vue";
import SoftInput from "@/components/SoftInput.vue";
import SoftButton from "@/components/SoftButton.vue";
const emit = defineEmits(["submit"]);
const suppliers = [
{ id: "1", name: "Produits Funéraires Pro" },
{ id: "2", name: "Thanatos Supply" },
{ id: "3", name: "ISOFROID" },
{ id: "4", name: "EEP Co EUROPE" },
{ id: "5", name: "NEXTECH MEDICAL" },
{ id: "6", name: "ACTION" },
{ id: "7", name: "E.LECLERC" },
];
const products = [
{ id: "1", name: "Fluide artériel Premium 5L", price: 450.0 },
{ id: "2", name: "Aiguilles de suture serpentine 10,80cm", price: 125.0 },
{ id: "3", name: "Trocar professionnel", price: 85.0 },
{ id: "4", name: "Pince à dissection 250mm", price: 65.0 },
{ id: "5", name: "Crème visage reconstructive", price: 32.0 },
{ id: "6", name: "Fluide cavité 2L", price: 210.0 },
];
const formData = ref({
number: "CMD-" + Date.now(),
supplierId: "",
supplierName: "",
supplierAddress: "",
date: new Date().toISOString().split("T")[0],
status: "brouillon",
deliveryAddress: "",
notes: "",
lines: [
{
productId: "",
designation: "",
quantity: 1,
priceHt: 0,
},
],
});
const updateSupplierInfo = () => {
const supplier = suppliers.find(s => s.id === formData.value.supplierId);
if (supplier) {
formData.value.supplierName = supplier.name;
formData.value.supplierAddress = "À déterminer";
}
};
const updateProductInfo = (index) => {
const product = products.find(p => p.id === formData.value.lines[index].productId);
if (product) {
formData.value.lines[index].designation = product.name;
formData.value.lines[index].priceHt = product.price;
}
};
const formatCurrency = (value) => {
return new Intl.NumberFormat("fr-FR", {
style: "currency",
currency: "EUR",
}).format(value);
};
const calculateTotalHt = () => {
return formData.value.lines.reduce((sum, line) => {
return sum + line.quantity * line.priceHt;
}, 0);
};
const calculateTotalTva = () => {
return calculateTotalHt() * 0.2;
};
const calculateTotalTtc = () => {
return calculateTotalHt() + calculateTotalTva();
};
const addLine = () => {
formData.value.lines.push({
productId: "",
designation: "",
quantity: 1,
priceHt: 0,
});
};
const removeLine = (index) => {
if (formData.value.lines.length > 1) {
formData.value.lines.splice(index, 1);
}
};
const submitForm = () => {
if (!formData.value.supplierId) {
alert("Veuillez sélectionner un fournisseur");
return;
}
emit("submit", formData.value);
};
const cancelForm = () => {
// Navigate back or close form
};
</script>
<style scoped>
.space-y-3 > div + div {
margin-top: 0.75rem;
}
.form-label {
font-size: 0.875rem;
font-weight: 500;
margin-bottom: 0.5rem;
}
.text-xs {
font-size: 0.75rem;
}
</style>

View File

@ -0,0 +1,104 @@
<template>
<div class="d-sm-flex justify-content-between">
<div>
<soft-button color="success" variant="gradient" @click="$emit('create')">
Nouvelle Commande
</soft-button>
</div>
<div class="d-flex">
<div class="dropdown d-inline">
<soft-button
id="navbarDropdownMenuLink3"
color="dark"
variant="outline"
class="dropdown-toggle"
data-bs-toggle="dropdown"
aria-expanded="false"
>
Filtrer
</soft-button>
<ul
class="dropdown-menu dropdown-menu-lg-start px-2 py-3"
aria-labelledby="navbarDropdownMenuLink3"
>
<li>
<a
class="dropdown-item border-radius-md"
href="javascript:;"
@click="$emit('filter', 'brouillon')"
>
Statut: Brouillon
</a>
</li>
<li>
<a
class="dropdown-item border-radius-md"
href="javascript:;"
@click="$emit('filter', 'confirmee')"
>
Statut: Confirmée
</a>
</li>
<li>
<a
class="dropdown-item border-radius-md"
href="javascript:;"
@click="$emit('filter', 'livree')"
>
Statut: Livrée
</a>
</li>
<li>
<a
class="dropdown-item border-radius-md"
href="javascript:;"
@click="$emit('filter', 'facturee')"
>
Statut: Facturée
</a>
</li>
<li>
<a
class="dropdown-item border-radius-md"
href="javascript:;"
@click="$emit('filter', 'annulee')"
>
Statut: Annulée
</a>
</li>
<li>
<hr class="horizontal dark my-2" />
</li>
<li>
<a
class="dropdown-item border-radius-md text-danger"
href="javascript:;"
@click="$emit('filter', null)"
>
Retirer Filtres
</a>
</li>
</ul>
</div>
<soft-button
class="btn-icon ms-2 export"
color="dark"
variant="outline"
data-type="csv"
@click="$emit('export')"
>
<span class="btn-inner--icon">
<i class="ni ni-archive-2"></i>
</span>
<span class="btn-inner--text">Export CSV</span>
</soft-button>
</div>
</div>
</template>
<script setup>
import { defineEmits } from "vue";
import SoftButton from "@/components/SoftButton.vue";
const emit = defineEmits(["create", "filter", "export"]);
</script>

View File

@ -0,0 +1,145 @@
<template>
<div class="message-form">
<div class="form-section mb-4">
<message-type-select
v-model="formData.type"
@update:model-value="emitFormData"
/>
</div>
<div class="form-section mb-4">
<recipient-select
v-model="formData.recipientId"
:users="users"
@update:model-value="emitFormData"
/>
</div>
<div class="form-section mb-4">
<div class="form-group">
<label for="message-priority" class="form-label">Priorité</label>
<select
id="message-priority"
v-model="formData.priority"
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>
</select>
</div>
</div>
<div v-if="formData.type === 'phone'" class="form-section mb-4">
<soft-input
v-model="formData.phoneNumber"
type="tel"
placeholder="Numéro de téléphone"
icon="fas fa-phone"
icon-dir="left"
@blur="emitFormData"
/>
</div>
<div v-if="formData.type === 'meeting'" class="form-section mb-4">
<soft-input
v-model="formData.meetingDate"
type="datetime-local"
placeholder="Date et heure de la réunion"
icon="fas fa-calendar"
icon-dir="left"
@blur="emitFormData"
/>
<soft-input
v-model="formData.meetingLocation"
type="text"
placeholder="Lieu de la réunion"
icon="fas fa-map-marker-alt"
icon-dir="left"
class="mt-3"
@blur="emitFormData"
/>
</div>
<div class="form-section mb-4">
<message-content
v-model="formData.content"
@blur="emitFormData"
/>
</div>
<div class="form-section">
<div class="form-check">
<input
id="mark-urgent"
v-model="formData.markAsUrgent"
type="checkbox"
class="form-check-input"
@change="emitFormData"
/>
<label class="form-check-label" for="mark-urgent">
Marquer comme urgent
</label>
</div>
</div>
</div>
</template>
<script setup>
import { defineProps, defineEmits } from "vue";
import { ref } from "vue";
import SoftInput from "@/components/SoftInput.vue";
import MessageTypeSelect from "@/components/atoms/InternalMessages/MessageTypeSelect.vue";
import MessageContent from "@/components/atoms/InternalMessages/MessageContent.vue";
import RecipientSelect from "@/components/atoms/InternalMessages/RecipientSelect.vue";
const props = defineProps({
initialData: {
type: Object,
default: () => ({}),
},
users: {
type: Array,
default: () => [],
},
});
const emit = defineEmits(["form-data-change"]);
const formData = ref({
type: props.initialData.type || "",
recipientId: props.initialData.recipientId || "",
priority: props.initialData.priority || "normal",
phoneNumber: props.initialData.phoneNumber || "",
meetingDate: props.initialData.meetingDate || "",
meetingLocation: props.initialData.meetingLocation || "",
content: props.initialData.content || "",
markAsUrgent: props.initialData.markAsUrgent || false,
});
const emitFormData = () => {
emit("form-data-change", formData.value);
};
</script>
<style scoped>
.message-form {
background-color: #f8f9fa;
padding: 20px;
border-radius: 8px;
}
.form-section {
background-color: white;
padding: 15px;
border-radius: 6px;
border-left: 4px solid #0d6efd;
}
</style>

View File

@ -0,0 +1,168 @@
<template>
<div class="message-list">
<div v-if="messages.length === 0" class="alert alert-info">
<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 class="message-header">
<div class="d-flex justify-content-between align-items-start">
<div>
<h6 class="mb-1">
<strong>{{ message.senderName }}</strong>
<span :class="getTypeClass(message.type)" class="ms-2">
{{ getTypeLabel(message.type) }}
</span>
</h6>
<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"
>
<i class="fas fa-exclamation-circle"></i> Haute priorité
</span>
<span
v-if="message.isUrgent"
class="badge bg-warning"
>
<i class="fas fa-fire"></i> Urgent
</span>
<span :class="getStatusClass(message.read)">
{{ message.read ? "Lu" : "Non lu" }}
</span>
</div>
</div>
</div>
<div class="message-body p-3 bg-light rounded">
<p class="mb-0">{{ message.content }}</p>
<div v-if="message.type === 'phone'" class="mt-2">
<small>
<i class="fas fa-phone"></i>
<strong>{{ message.phoneNumber }}</strong>
</small>
</div>
<div v-if="message.type === 'meeting'" class="mt-2">
<small>
<div>
<i class="fas fa-calendar"></i>
<strong>{{ formatDate(message.meetingDate) }}</strong>
</div>
<div>
<i class="fas fa-map-marker-alt"></i>
<strong>{{ message.meetingLocation }}</strong>
</div>
</small>
</div>
</div>
<div class="message-actions mt-2">
<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)">
<i class="fas fa-reply"></i> Répondre
</button>
<button class="btn btn-sm btn-outline-danger ms-2" @click="deleteMessage(message.id)">
<i class="fas fa-trash"></i> Supprimer
</button>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { defineProps, defineEmits } from "vue";
const props = defineProps({
messages: {
type: Array,
default: () => [],
},
});
const emit = defineEmits(["mark-as-read", "reply", "delete"]);
const formatDate = (date) => {
if (!date) return "-";
return new Date(date).toLocaleDateString("fr-FR", {
year: "numeric",
month: "short",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
});
};
const getTypeClass = (type) => {
const typeClasses = {
text: "badge bg-info",
phone: "badge bg-success",
email: "badge bg-secondary",
meeting: "badge bg-warning text-dark",
note: "badge bg-light text-dark",
};
return typeClasses[type] || "badge bg-secondary";
};
const getTypeLabel = (type) => {
const typeLabels = {
text: "Texte",
phone: "Appel",
email: "Email",
meeting: "Réunion",
note: "Note",
};
return typeLabels[type] || "Inconnu";
};
const getStatusClass = (read) => {
return read ? "badge bg-success" : "badge bg-warning";
};
const markAsRead = (id) => {
emit("mark-as-read", id);
};
const replyMessage = (id) => {
emit("reply", id);
};
const deleteMessage = (id) => {
emit("delete", id);
};
</script>
<style scoped>
.message-list {
background-color: white;
padding: 20px;
border-radius: 8px;
}
.message-item {
border: 1px solid #e0e0e0;
border-radius: 6px;
overflow: hidden;
transition: all 0.3s ease;
}
.message-item:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.message-header {
background-color: #f8f9fa;
padding: 15px;
border-bottom: 1px solid #e0e0e0;
}
.gap-2 {
gap: 8px;
}
.message-actions {
padding: 10px 15px;
}
</style>

View File

@ -0,0 +1,135 @@
<template>
<div class="intervention-chart">
<h5 class="mb-3">Interventions par mois</h5>
<div class="chart-placeholder">
<div class="chart-bars">
<div v-for="(month, index) in monthData" :key="index" class="bar-group">
<div class="bar-container">
<div
class="bar"
:style="{ height: (month.completed / 100) * 250 + 'px' }"
style="background-color: #198754"
></div>
<div
class="bar"
:style="{ height: (month.pending / 100) * 250 + 'px' }"
style="background-color: #ffc107"
></div>
</div>
<small class="month-label">{{ month.month }}</small>
</div>
</div>
<div class="chart-legend">
<div class="legend-item">
<div class="legend-color" style="background-color: #198754"></div>
<span>Complétées</span>
</div>
<div class="legend-item">
<div class="legend-color" style="background-color: #ffc107"></div>
<span>En attente</span>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from "vue";
const monthData = ref([
{ month: "Jan", completed: 85, pending: 15 },
{ month: "Fév", completed: 90, pending: 10 },
{ month: "Mar", completed: 88, pending: 12 },
{ month: "Avr", completed: 92, pending: 8 },
{ month: "Mai", completed: 87, pending: 13 },
{ month: "Juin", completed: 95, pending: 5 },
{ month: "Juil", completed: 89, pending: 11 },
{ month: "Août", completed: 93, pending: 7 },
{ month: "Sep", completed: 91, pending: 9 },
{ month: "Oct", completed: 88, pending: 12 },
{ month: "Nov", completed: 94, pending: 6 },
{ month: "Déc", completed: 96, pending: 4 },
]);
</script>
<style scoped>
.intervention-chart {
background-color: white;
padding: 1.5rem;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
margin-bottom: 2rem;
}
.chart-placeholder {
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
padding: 2rem;
border-radius: 6px;
min-height: 300px;
display: flex;
flex-direction: column;
justify-content: flex-end;
}
.chart-bars {
display: flex;
align-items: flex-end;
gap: 1rem;
margin-bottom: 1.5rem;
height: 250px;
}
.bar-group {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
flex: 1;
}
.bar-container {
display: flex;
flex-direction: column-reverse;
height: 250px;
width: 100%;
border-radius: 4px 4px 0 0;
overflow: hidden;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.bar {
width: 100%;
transition: all 0.3s ease;
}
.bar:hover {
opacity: 0.8;
}
.month-label {
font-size: 0.75rem;
font-weight: 600;
color: #212529;
}
.chart-legend {
display: flex;
gap: 2rem;
justify-content: center;
}
.legend-item {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.875rem;
font-weight: 500;
color: #212529;
}
.legend-color {
width: 16px;
height: 16px;
border-radius: 2px;
}
</style>

View File

@ -0,0 +1,74 @@
<template>
<div class="statistics-overview">
<div class="row g-4">
<div class="col-md-6 col-lg-3">
<stat-card
label="Total interventions"
value="486"
icon="fas fa-briefcase"
color="primary"
trend="+12% ce mois"
:trend-positive="true"
/>
</div>
<div class="col-md-6 col-lg-3">
<stat-card
label="Interventions complétées"
value="412"
icon="fas fa-check-circle"
color="success"
trend="+8% ce mois"
:trend-positive="true"
/>
</div>
<div class="col-md-6 col-lg-3">
<stat-card
label="En cours"
value="74"
icon="fas fa-hourglass-half"
color="warning"
trend="-2% ce mois"
:trend-positive="false"
/>
</div>
<div class="col-md-6 col-lg-3">
<stat-card
label="Taux de satisfaction"
value="94%"
icon="fas fa-smile"
color="info"
trend="+3% ce mois"
:trend-positive="true"
/>
</div>
</div>
</div>
</template>
<script setup>
import StatCard from "@/components/atoms/Statistics/StatCard.vue";
</script>
<style scoped>
.statistics-overview {
margin-bottom: 2rem;
}
.g-4 {
gap: 1.5rem;
}
.col-md-6 {
flex: 0 0 50%;
}
.col-lg-3 {
flex: 0 0 25%;
}
@media (max-width: 768px) {
.col-md-6 {
flex: 0 0 100%;
}
}
</style>

View File

@ -0,0 +1,164 @@
<template>
<div class="performance-table">
<h5 class="mb-3">Performance des thanatopractiens</h5>
<div class="table-responsive">
<table class="table table-hover">
<thead class="table-light">
<tr>
<th>Nom</th>
<th>Interventions</th>
<th>Complétées</th>
<th>Taux de complétion</th>
<th>Satisfaction</th>
<th>Statut</th>
</tr>
</thead>
<tbody>
<tr v-for="practitioner in practitioners" :key="practitioner.id">
<td>
<strong>{{ practitioner.name }}</strong>
</td>
<td>{{ practitioner.totalInterventions }}</td>
<td>{{ practitioner.completedInterventions }}</td>
<td>
<div class="progress" style="height: 20px">
<div
class="progress-bar"
:style="{ width: practitioner.completionRate + '%' }"
:class="getProgressClass(practitioner.completionRate)"
>
{{ practitioner.completionRate }}%
</div>
</div>
</td>
<td>
<div class="satisfaction-stars">
<span v-for="i in 5" :key="i" class="star" :class="getStarClass(i, practitioner.satisfaction)">
<i class="fas fa-star"></i>
</span>
</div>
</td>
<td>
<span :class="getStatusClass(practitioner.status)">
{{ getStatusLabel(practitioner.status) }}
</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
<script setup>
import { defineProps } from "vue";
const props = defineProps({
practitioners: {
type: Array,
default: () => [
{
id: 1,
name: "Alice Dupont",
totalInterventions: 98,
completedInterventions: 94,
completionRate: 96,
satisfaction: 4.8,
status: "active",
},
{
id: 2,
name: "Bob Martin",
totalInterventions: 87,
completedInterventions: 78,
completionRate: 90,
satisfaction: 4.5,
status: "active",
},
{
id: 3,
name: "Catherine Leclerc",
totalInterventions: 105,
completedInterventions: 103,
completionRate: 98,
satisfaction: 4.9,
status: "active",
},
{
id: 4,
name: "David Bernard",
totalInterventions: 92,
completedInterventions: 88,
completionRate: 96,
satisfaction: 4.7,
status: "active",
},
{
id: 5,
name: "Emma Wilson",
totalInterventions: 78,
completedInterventions: 70,
completionRate: 90,
satisfaction: 4.4,
status: "inactive",
},
],
},
});
const getProgressClass = (rate) => {
if (rate >= 95) return "bg-success";
if (rate >= 85) return "bg-info";
if (rate >= 75) return "bg-warning";
return "bg-danger";
};
const getStarClass = (index, satisfaction) => {
return index <= Math.round(satisfaction) ? "text-warning" : "text-muted";
};
const getStatusClass = (status) => {
return status === "active" ? "badge bg-success" : "badge bg-secondary";
};
const getStatusLabel = (status) => {
return status === "active" ? "Actif" : "Inactif";
};
</script>
<style scoped>
.performance-table {
background-color: white;
padding: 1.5rem;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
margin-bottom: 2rem;
}
.table {
margin-bottom: 0;
}
.table thead {
position: sticky;
top: 0;
z-index: 10;
}
.satisfaction-stars {
display: flex;
gap: 0.25rem;
}
.star {
font-size: 0.875rem;
}
.star.text-warning {
color: #ffc107;
}
.star.text-muted {
color: #ccc;
}
</style>

View File

@ -0,0 +1,264 @@
<template>
<div class="card mt-4">
<div class="table-responsive">
<table id="avoir-list" class="table table-flush">
<thead class="thead-light">
<tr>
<th>N° Avoir</th>
<th>Date</th>
<th>Statut</th>
<th>Client</th>
<th>Facture d'origine</th>
<th>Montant</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr v-for="avoir in data" :key="avoir.id">
<!-- N° Avoir (Reference) -->
<td>
<div class="d-flex align-items-center">
<soft-checkbox class="me-2" />
<p class="text-xs font-weight-bold ms-2 mb-0">
{{ avoir.number }}
</p>
</div>
</td>
<!-- Date -->
<td class="font-weight-bold">
<span class="my-2 text-xs">{{
formatDate(avoir.date)
}}</span>
</td>
<!-- Status -->
<td class="text-xs font-weight-bold">
<div class="d-flex align-items-center">
<soft-button
:color="getStatusColor(avoir.status)"
variant="outline"
class="btn-icon-only btn-rounded mb-0 me-2 btn-sm d-flex align-items-center justify-content-center"
>
<i
:class="getStatusIcon(avoir.status)"
aria-hidden="true"
></i>
</soft-button>
<span>{{ getStatusLabel(avoir.status) }}</span>
</div>
</td>
<!-- Client -->
<td class="text-xs font-weight-bold">
<div class="d-flex align-items-center">
<soft-avatar
:img="getRandomAvatar()"
class="me-2"
size="xs"
alt="user image"
circular
/>
<span>{{
avoir.clientName || "Client Inconnu"
}}</span>
</div>
</td>
<!-- Invoice Reference -->
<td class="text-xs font-weight-bold">
<span class="my-2 text-xs">
{{ avoir.invoiceNumber }}
</span>
</td>
<!-- Amount (Total) -->
<td class="text-xs font-weight-bold">
<span class="my-2 text-xs">{{
formatCurrency(avoir.amount)
}}</span>
</td>
<!-- Actions -->
<td class="text-xs font-weight-bold">
<div class="d-flex align-items-center">
<button
class="btn btn-link text-secondary mb-0 px-2"
:data-id="avoir.id"
data-action="view"
title="Voir l'avoir"
>
<i class="fas fa-eye text-xs" aria-hidden="true"></i>
</button>
<button
class="btn btn-link text-danger mb-0 px-2"
:data-id="avoir.id"
data-action="delete"
title="Supprimer l'avoir"
>
<i class="fas fa-trash text-xs" aria-hidden="true"></i>
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
<script setup>
import {
ref,
onMounted,
watch,
onUnmounted,
defineProps,
defineEmits,
} from "vue";
import { DataTable } from "simple-datatables";
import SoftButton from "@/components/SoftButton.vue";
import SoftAvatar from "@/components/SoftAvatar.vue";
import SoftCheckbox from "@/components/SoftCheckbox.vue";
// Sample avatar images
import img1 from "@/assets/img/team-2.jpg";
import img2 from "@/assets/img/team-1.jpg";
import img3 from "@/assets/img/team-3.jpg";
import img4 from "@/assets/img/team-4.jpg";
import img5 from "@/assets/img/team-5.jpg";
import img6 from "@/assets/img/ivana-squares.jpg";
const avatarImages = [img1, img2, img3, img4, img5, img6];
const emit = defineEmits(["view", "delete"]);
const props = defineProps({
data: {
type: Array,
default: () => [],
},
loading: {
type: Boolean,
default: false,
},
});
const dataTableInstance = ref(null);
const getRandomAvatar = () => {
const randomIndex = Math.floor(Math.random() * avatarImages.length);
return avatarImages[randomIndex];
};
const formatDate = (dateString) => {
if (!dateString) return "-";
const options = {
day: "numeric",
month: "short",
year: "numeric",
};
return new Date(dateString).toLocaleDateString("fr-FR", options);
};
const formatCurrency = (value) => {
return new Intl.NumberFormat("fr-FR", {
style: "currency",
currency: "EUR",
}).format(value);
};
// Map status to colors and icons
const getStatusColor = (status) => {
const map = {
brouillon: "secondary",
emis: "info",
applique: "success",
annule: "danger",
};
return map[status] || "secondary";
};
const getStatusIcon = (status) => {
const map = {
brouillon: "fas fa-pen",
emis: "fas fa-paper-plane",
applique: "fas fa-check",
annule: "fas fa-ban",
};
return map[status] || "fas fa-info";
};
const getStatusLabel = (status) => {
const labels = {
brouillon: "Brouillon",
emis: "Émis",
applique: "Appliqué",
annule: "Annulé",
};
return labels[status] || status;
};
const initializeDataTable = () => {
if (dataTableInstance.value) {
dataTableInstance.value.destroy();
dataTableInstance.value = null;
}
const dataTableEl = document.getElementById("avoir-list");
if (dataTableEl) {
dataTableInstance.value = new DataTable(dataTableEl, {
searchable: true,
fixedHeight: false,
perPageSelect: false,
});
}
};
onMounted(() => {
if (!props.loading && props.data.length > 0) {
initializeDataTable();
}
// Event delegation
const table = document.getElementById("avoir-list");
if (table) {
table.addEventListener("click", (event) => {
// Check if the click is on a button or an icon inside a button
const btn = event.target.closest("button");
if (!btn) return;
const id = btn.getAttribute("data-id");
const action = btn.getAttribute("data-action");
if (id && action) {
if (action === "view") {
console.log("Delegated View click for id:", id);
emit("view", parseInt(id));
} else if (action === "delete") {
console.log("Delegated Delete click for id:", id);
emit("delete", parseInt(id));
}
}
});
}
});
watch(
() => props.data,
() => {
if (!props.loading) {
setTimeout(() => {
initializeDataTable();
}, 100);
}
},
{ deep: true }
);
onUnmounted(() => {
if (dataTableInstance.value) {
dataTableInstance.value.destroy();
}
});
</script>

View File

@ -0,0 +1,262 @@
<template>
<div class="card mt-4">
<div class="table-responsive">
<table id="commande-list" class="table table-flush">
<thead class="thead-light">
<tr>
<th>N° Commande</th>
<th>Date</th>
<th>Fournisseur</th>
<th>Statut</th>
<th>Montant TTC</th>
<th>Articles</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr v-for="commande in data" :key="commande.id">
<!-- N° Commande -->
<td>
<div class="d-flex align-items-center">
<soft-checkbox class="me-2" />
<p class="text-xs font-weight-bold ms-2 mb-0">
{{ commande.number }}
</p>
</div>
</td>
<!-- Date -->
<td class="font-weight-bold">
<span class="my-2 text-xs">{{
formatDate(commande.date)
}}</span>
</td>
<!-- Supplier -->
<td class="text-xs font-weight-bold">
<div class="d-flex align-items-center">
<soft-avatar
:img="getRandomAvatar()"
class="me-2"
size="xs"
alt="supplier image"
circular
/>
<span>{{ commande.supplier }}</span>
</div>
</td>
<!-- Status -->
<td class="text-xs font-weight-bold">
<div class="d-flex align-items-center">
<soft-button
:color="getStatusColor(commande.status)"
variant="outline"
class="btn-icon-only btn-rounded mb-0 me-2 btn-sm d-flex align-items-center justify-content-center"
>
<i
:class="getStatusIcon(commande.status)"
aria-hidden="true"
></i>
</soft-button>
<span>{{ getStatusLabel(commande.status) }}</span>
</div>
</td>
<!-- Amount -->
<td class="text-xs font-weight-bold">
<span class="my-2 text-xs">{{
formatCurrency(commande.amount)
}}</span>
</td>
<!-- Items Count -->
<td class="text-xs font-weight-bold">
<span class="badge bg-secondary">{{ commande.items_count }}</span>
</td>
<!-- Actions -->
<td class="text-xs font-weight-bold">
<div class="d-flex align-items-center">
<button
class="btn btn-link text-secondary mb-0 px-2"
:data-id="commande.id"
data-action="view"
title="Voir la commande"
>
<i class="fas fa-eye text-xs" aria-hidden="true"></i>
</button>
<button
class="btn btn-link text-danger mb-0 px-2"
:data-id="commande.id"
data-action="delete"
title="Supprimer la commande"
>
<i class="fas fa-trash text-xs" aria-hidden="true"></i>
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
<script setup>
import {
ref,
onMounted,
watch,
onUnmounted,
defineProps,
defineEmits,
} from "vue";
import { DataTable } from "simple-datatables";
import SoftButton from "@/components/SoftButton.vue";
import SoftAvatar from "@/components/SoftAvatar.vue";
import SoftCheckbox from "@/components/SoftCheckbox.vue";
// Sample avatar images
import img1 from "@/assets/img/team-2.jpg";
import img2 from "@/assets/img/team-1.jpg";
import img3 from "@/assets/img/team-3.jpg";
import img4 from "@/assets/img/team-4.jpg";
import img5 from "@/assets/img/team-5.jpg";
import img6 from "@/assets/img/ivana-squares.jpg";
const avatarImages = [img1, img2, img3, img4, img5, img6];
const emit = defineEmits(["view", "delete"]);
const props = defineProps({
data: {
type: Array,
default: () => [],
},
loading: {
type: Boolean,
default: false,
},
});
const dataTableInstance = ref(null);
const getRandomAvatar = () => {
const randomIndex = Math.floor(Math.random() * avatarImages.length);
return avatarImages[randomIndex];
};
const formatDate = (dateString) => {
if (!dateString) return "-";
const options = {
day: "numeric",
month: "short",
year: "numeric",
};
return new Date(dateString).toLocaleDateString("fr-FR", options);
};
const formatCurrency = (value) => {
return new Intl.NumberFormat("fr-FR", {
style: "currency",
currency: "EUR",
}).format(value);
};
// Map status to colors and icons
const getStatusColor = (status) => {
const map = {
brouillon: "secondary",
confirmee: "info",
livree: "success",
facturee: "warning",
annulee: "danger",
};
return map[status] || "secondary";
};
const getStatusIcon = (status) => {
const map = {
brouillon: "fas fa-pen",
confirmee: "fas fa-check-circle",
livree: "fas fa-truck",
facturee: "fas fa-file-invoice-dollar",
annulee: "fas fa-ban",
};
return map[status] || "fas fa-info";
};
const getStatusLabel = (status) => {
const labels = {
brouillon: "Brouillon",
confirmee: "Confirmée",
livree: "Livrée",
facturee: "Facturée",
annulee: "Annulée",
};
return labels[status] || status;
};
const initializeDataTable = () => {
if (dataTableInstance.value) {
dataTableInstance.value.destroy();
dataTableInstance.value = null;
}
const dataTableEl = document.getElementById("commande-list");
if (dataTableEl) {
dataTableInstance.value = new DataTable(dataTableEl, {
searchable: true,
fixedHeight: false,
perPageSelect: false,
});
}
};
onMounted(() => {
if (!props.loading && props.data.length > 0) {
initializeDataTable();
}
// Event delegation
const table = document.getElementById("commande-list");
if (table) {
table.addEventListener("click", (event) => {
const btn = event.target.closest("button");
if (!btn) return;
const id = btn.getAttribute("data-id");
const action = btn.getAttribute("data-action");
if (id && action) {
if (action === "view") {
console.log("View commande:", id);
emit("view", parseInt(id));
} else if (action === "delete") {
console.log("Delete commande:", id);
emit("delete", parseInt(id));
}
}
});
}
});
watch(
() => props.data,
() => {
if (!props.loading) {
setTimeout(() => {
initializeDataTable();
}, 100);
}
},
{ deep: true }
);
onUnmounted(() => {
if (dataTableInstance.value) {
dataTableInstance.value.destroy();
}
});
</script>

View File

@ -0,0 +1,149 @@
<template>
<div class="webmailing-form">
<div class="form-section mb-4">
<h5 class="mb-3">Destinataires</h5>
<soft-input
v-model="formData.recipients"
type="email"
placeholder="Entrez les adresses email (séparées par des virgules)"
icon="fas fa-envelope"
icon-dir="left"
/>
</div>
<div class="form-section mb-4">
<h5 class="mb-3">Sujet</h5>
<webmailing-subject-input
v-model="formData.subject"
@blur="validateSubject"
/>
</div>
<div class="form-section mb-4">
<h5 class="mb-3">Contenu du message</h5>
<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"
/>
<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">
<span class="badge bg-info">{{ file.name }}</span>
<small class="ms-2 text-muted">({{ formatFileSize(file.size) }})</small>
</li>
</ul>
</div>
</div>
<div class="form-section">
<h5 class="mb-3">Options</h5>
<div class="form-check mb-2">
<input
id="send-copy"
v-model="formData.sendCopy"
type="checkbox"
class="form-check-input"
/>
<label class="form-check-label" for="send-copy">
M'envoyer une copie
</label>
</div>
<div class="form-check">
<input
id="send-scheduled"
v-model="formData.scheduled"
type="checkbox"
class="form-check-input"
/>
<label class="form-check-label" for="send-scheduled">
Programmer l'envoi
</label>
</div>
<div v-if="formData.scheduled" class="mt-3">
<soft-input
v-model="formData.scheduledDate"
type="datetime-local"
placeholder="Date et heure d'envoi"
icon="fas fa-calendar"
icon-dir="left"
/>
</div>
</div>
</div>
</template>
<script setup>
import { defineEmits, defineProps } from "vue";
import { ref } from "vue";
import SoftInput from "@/components/SoftInput.vue";
import WebmailingSubjectInput from "@/components/atoms/Webmailing/WebmailingSubjectInput.vue";
import WebmailingBodyInput from "@/components/atoms/Webmailing/WebmailingBodyInput.vue";
import WebmailingAttachment from "@/components/atoms/Webmailing/WebmailingAttachment.vue";
const props = defineProps({
initialData: {
type: Object,
default: () => ({}),
},
});
const emit = defineEmits(["form-data-change"]);
const formData = ref({
recipients: props.initialData.recipients || "",
subject: props.initialData.subject || "",
body: props.initialData.body || "",
attachments: props.initialData.attachments || [],
sendCopy: props.initialData.sendCopy || false,
scheduled: props.initialData.scheduled || false,
scheduledDate: props.initialData.scheduledDate || "",
});
const validateSubject = () => {
// Add validation logic if needed
};
const validateBody = () => {
// Add validation logic if needed
};
const handleFilesSelected = (files) => {
formData.value.attachments = files;
emitFormData();
};
const emitFormData = () => {
emit("form-data-change", formData.value);
};
const formatFileSize = (bytes) => {
if (bytes === 0) return "0 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];
};
</script>
<style scoped>
.webmailing-form {
background-color: #f8f9fa;
padding: 20px;
border-radius: 8px;
}
.form-section {
background-color: white;
padding: 15px;
border-radius: 6px;
border-left: 4px solid #17a2b8;
}
</style>

View File

@ -0,0 +1,106 @@
<template>
<div class="webmailing-list">
<div v-if="emails.length === 0" class="alert alert-info">
<i class="fas fa-info-circle"></i> Aucun email envoyé
</div>
<div v-else class="table-responsive">
<table class="table table-hover">
<thead class="table-light">
<tr>
<th>Destinataires</th>
<th>Sujet</th>
<th>Date d'envoi</th>
<th>Statut</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr v-for="email in emails" :key="email.id">
<td>
<small>{{ email.recipients }}</small>
</td>
<td>
<strong>{{ email.subject }}</strong>
</td>
<td>
<small>{{ formatDate(email.sentDate) }}</small>
</td>
<td>
<span :class="getStatusClass(email.status)">
{{ getStatusLabel(email.status) }}
</span>
</td>
<td>
<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)">
<i class="fas fa-trash"></i>
</button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
<script setup>
import { defineProps, defineEmits } from "vue";
const props = defineProps({
emails: {
type: Array,
default: () => [],
},
});
const emit = defineEmits(["view-email", "delete-email"]);
const formatDate = (date) => {
if (!date) return "-";
return new Date(date).toLocaleDateString("fr-FR", {
year: "numeric",
month: "short",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
});
};
const getStatusClass = (status) => {
const statusClasses = {
sent: "badge bg-success",
pending: "badge bg-warning",
failed: "badge bg-danger",
scheduled: "badge bg-info",
};
return statusClasses[status] || "badge bg-secondary";
};
const getStatusLabel = (status) => {
const statusLabels = {
sent: "Envoyé",
pending: "En attente",
failed: "Échoué",
scheduled: "Programmé",
};
return statusLabels[status] || "Inconnu";
};
const viewEmail = (id) => {
emit("view-email", id);
};
const deleteEmail = (id) => {
emit("delete-email", id);
};
</script>
<style scoped>
.webmailing-list {
background-color: white;
padding: 20px;
border-radius: 8px;
}
</style>

View File

@ -52,6 +52,13 @@
:badge="childrenCount > 0 ? childrenCount : null"
@click="$emit('change-tab', 'children')"
/>
<TabNavigationItem
icon="fas fa-file-invoice-dollar"
label="Avoirs"
:is-active="activeTab === 'avoirs'"
:badge="avoirsCount > 0 ? avoirsCount : null"
@click="$emit('change-tab', 'avoirs')"
/>
</ul>
</template>
@ -76,6 +83,10 @@ defineProps({
type: Number,
default: 0,
},
avoirsCount: {
type: Number,
default: 0,
},
isParent: {
type: Boolean,
default: false,

View File

@ -0,0 +1,46 @@
<template>
<div class="container-fluid py-4">
<div class="row">
<div class="col-lg-8 mx-auto">
<div class="card mb-4">
<slot name="header"></slot>
<div class="card-body p-3 pt-0">
<hr class="horizontal dark mt-0 mb-4" />
<!-- Product Lines Section -->
<div class="row">
<div class="col-12">
<slot name="lines"></slot>
</div>
</div>
<hr class="horizontal dark mt-4 mb-4" />
<div class="row">
<!-- Tracking/Timeline Section -->
<div class="col-lg-3 col-md-6 col-12">
<slot name="timeline"></slot>
</div>
<!-- Billing Info Section -->
<div class="col-lg-5 col-md-6 col-12">
<slot name="billing"></slot>
</div>
<!-- Summary Section -->
<div class="col-lg-3 col-12 ms-auto">
<slot name="summary"></slot>
</div>
</div>
</div>
<div class="card-footer p-3">
<slot name="actions"></slot>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup></script>

View File

@ -0,0 +1,46 @@
<template>
<div class="container-fluid py-4">
<div class="row">
<div class="col-lg-8 mx-auto">
<div class="card mb-4">
<slot name="header"></slot>
<div class="card-body p-3 pt-0">
<hr class="horizontal dark mt-0 mb-4" />
<!-- Product Lines Section -->
<div class="row">
<div class="col-12">
<slot name="lines"></slot>
</div>
</div>
<hr class="horizontal dark mt-4 mb-4" />
<div class="row">
<!-- Tracking/Timeline Section -->
<div class="col-lg-3 col-md-6 col-12">
<slot name="timeline"></slot>
</div>
<!-- Supplier Info Section -->
<div class="col-lg-5 col-md-6 col-12">
<slot name="billing"></slot>
</div>
<!-- Summary Section -->
<div class="col-lg-3 col-12 ms-auto">
<slot name="summary"></slot>
</div>
</div>
</div>
<div class="card-footer p-3">
<slot name="actions"></slot>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup></script>

View File

@ -0,0 +1,25 @@
<template>
<div class="container-fluid py-4">
<slot name="header"></slot>
<div class="card shadow-lg">
<div class="card-body">
<slot name="tabs"></slot>
<slot name="content"></slot>
</div>
</div>
</div>
</template>
<script></script>
<style scoped>
.card {
border: none;
border-radius: 12px;
}
.card-body {
padding: 2rem;
}
</style>

View File

@ -0,0 +1,48 @@
<template>
<div class="container-fluid py-4">
<slot name="header"></slot>
<div class="card shadow-lg mb-4">
<div class="card-body">
<slot name="filter"></slot>
</div>
</div>
<slot name="overview"></slot>
<div class="row">
<div class="col-lg-12">
<slot name="chart"></slot>
</div>
</div>
<div class="row">
<div class="col-lg-12">
<slot name="performance"></slot>
</div>
</div>
<div class="row">
<div class="col-lg-12">
<slot name="export"></slot>
</div>
</div>
</div>
</template>
<script></script>
<style scoped>
.card {
border: none;
border-radius: 12px;
}
.card-body {
padding: 2rem;
}
.row {
margin-bottom: 0;
}
</style>

View File

@ -0,0 +1,25 @@
<template>
<div class="container-fluid py-4">
<slot name="webmailing-header"></slot>
<div class="card shadow-lg">
<div class="card-body">
<slot name="webmailing-tabs"></slot>
<slot name="webmailing-content"></slot>
</div>
</div>
</div>
</template>
<script></script>
<style scoped>
.card {
border: none;
border-radius: 12px;
}
.card-body {
padding: 2rem;
}
</style>

View File

@ -125,7 +125,7 @@ export default {
text: "Courriel",
icon: "Office",
miniIcon: "C",
route: { name: "Courriel" },
route: { name: "Webmailing" },
},
{
id: "contacts",
@ -272,6 +272,12 @@ export default {
miniIcon: "D",
text: "Devis",
},
{
id: "avoirs",
route: { name: "Liste Avoirs" },
miniIcon: "A",
text: "Avoirs",
},
{
id: "factures-ventes",
route: { name: "Liste Factures" },

View File

@ -407,6 +407,24 @@ const routes = [
name: "Courriel",
component: () => import("@/views/pages/Courriel.vue"),
},
// Webmailing
{
path: "/webmailing",
name: "Webmailing",
component: () => import("@/views/pages/Webmailing.vue"),
},
// Messages internes
{
path: "/messages",
name: "Messages internes",
component: () => import("@/views/pages/InternalMessages.vue"),
},
// Statistiques Thanatopracteurs
{
path: "/statistiques/thanatopracteurs",
name: "Statistiques Thanatopracteurs",
component: () => import("@/views/pages/StatistiquesThanatopracteurs.vue"),
},
// Clients - Statistiques
{
path: "/clients/statistiques",
@ -440,6 +458,16 @@ const routes = [
name: "Commandes fournisseurs",
component: () => import("@/views/pages/Fournisseurs/Commandes.vue"),
},
{
path: "/fournisseurs/commandes/new",
name: "Nouvelle Commande",
component: () => import("@/views/pages/Fournisseurs/NewCommande.vue"),
},
{
path: "/fournisseurs/commandes/:id",
name: "Commande Details",
component: () => import("@/views/pages/Fournisseurs/CommandeDetail.vue"),
},
{
path: "/fournisseurs/factures",
name: "Factures fournisseurs",
@ -508,6 +536,22 @@ const routes = [
name: "Invoice Details",
component: () => import("@/views/pages/Ventes/InvoiceDetail.vue"),
},
// Avoirs
{
path: "/avoirs",
name: "Liste Avoirs",
component: () => import("@/views/pages/Avoirs/AvoirList.vue"),
},
{
path: "/avoirs/new",
name: "Nouvel Avoir",
component: () => import("@/views/pages/Avoirs/NewAvoir.vue"),
},
{
path: "/avoirs/:id",
name: "Avoir Details",
component: () => import("@/views/pages/Avoirs/AvoirDetail.vue"),
},
// Client Groups
{
path: "/clients/groups",

View File

@ -0,0 +1,12 @@
<template>
<avoir-detail-presentation :avoir-id="avoirId" />
</template>
<script setup>
import { computed } from "vue";
import { useRoute } from "vue-router";
import AvoirDetailPresentation from "@/components/Organism/Avoir/AvoirDetailPresentation.vue";
const route = useRoute();
const avoirId = computed(() => route.params.id);
</script>

View File

@ -0,0 +1,7 @@
<template>
<avoir-list-presentation />
</template>
<script setup>
import AvoirListPresentation from "@/components/Organism/Avoir/AvoirListPresentation.vue";
</script>

View File

@ -0,0 +1,7 @@
<template>
<new-avoir-presentation />
</template>
<script setup>
import NewAvoirPresentation from "@/components/Organism/Avoir/NewAvoirPresentation.vue";
</script>

View File

@ -0,0 +1,12 @@
<template>
<commande-detail-presentation :commande-id="commandeId" />
</template>
<script setup>
import { computed } from "vue";
import { useRoute } from "vue-router";
import CommandeDetailPresentation from "@/components/Organism/Commande/CommandeDetailPresentation.vue";
const route = useRoute();
const commandeId = computed(() => route.params.id);
</script>

View File

@ -1,11 +1,8 @@
<template>
<div>
<h1>Commandes fournisseurs</h1>
</div>
<commande-list-presentation />
</template>
<script>
export default {
name: "CommandesFournisseurs",
};
<script setup>
import CommandeListPresentation from "@/components/Organism/Commande/CommandeListPresentation.vue";
</script>

View File

@ -0,0 +1,7 @@
<template>
<new-commande-presentation />
</template>
<script setup>
import NewCommandePresentation from "@/components/Organism/Commande/NewCommandePresentation.vue";
</script>

View File

@ -0,0 +1,9 @@
<template>
<div>
<internal-message-presentation />
</div>
</template>
<script setup>
import InternalMessagePresentation from "@/components/Organism/InternalMessages/InternalMessagePresentation.vue";
</script>

View File

@ -0,0 +1,9 @@
<template>
<div>
<statistics-presentation />
</div>
</template>
<script setup>
import StatisticsPresentation from "@/components/Organism/Statistics/StatisticsPresentation.vue";
</script>

View File

@ -0,0 +1,9 @@
<template>
<div>
<webmailing-presentation />
</div>
</template>
<script setup>
import WebmailingPresentation from "@/components/Organism/Webmailing/WebmailingPresentation.vue";
</script>