Feat, design, liste avoir, details avoirs, creation avoir, liste commande fournisseur, details commande fournisseur, creation commande fournisseur, webmail
This commit is contained in:
parent
86472e0de9
commit
f62a2db36e
@ -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}"
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
133
thanasoft-front/src/components/atoms/Statistics/StatCard.vue
Normal file
133
thanasoft-front/src/components/atoms/Statistics/StatCard.vue
Normal 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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
348
thanasoft-front/src/components/molecules/Avoir/NewAvoirForm.vue
Normal file
348
thanasoft-front/src/components/molecules/Avoir/NewAvoirForm.vue
Normal 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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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,
|
||||
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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" },
|
||||
|
||||
@ -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",
|
||||
|
||||
12
thanasoft-front/src/views/pages/Avoirs/AvoirDetail.vue
Normal file
12
thanasoft-front/src/views/pages/Avoirs/AvoirDetail.vue
Normal 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>
|
||||
7
thanasoft-front/src/views/pages/Avoirs/AvoirList.vue
Normal file
7
thanasoft-front/src/views/pages/Avoirs/AvoirList.vue
Normal file
@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<avoir-list-presentation />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import AvoirListPresentation from "@/components/Organism/Avoir/AvoirListPresentation.vue";
|
||||
</script>
|
||||
7
thanasoft-front/src/views/pages/Avoirs/NewAvoir.vue
Normal file
7
thanasoft-front/src/views/pages/Avoirs/NewAvoir.vue
Normal file
@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<new-avoir-presentation />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import NewAvoirPresentation from "@/components/Organism/Avoir/NewAvoirPresentation.vue";
|
||||
</script>
|
||||
@ -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>
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<new-commande-presentation />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import NewCommandePresentation from "@/components/Organism/Commande/NewCommandePresentation.vue";
|
||||
</script>
|
||||
9
thanasoft-front/src/views/pages/InternalMessages.vue
Normal file
9
thanasoft-front/src/views/pages/InternalMessages.vue
Normal file
@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<div>
|
||||
<internal-message-presentation />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import InternalMessagePresentation from "@/components/Organism/InternalMessages/InternalMessagePresentation.vue";
|
||||
</script>
|
||||
@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<div>
|
||||
<statistics-presentation />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import StatisticsPresentation from "@/components/Organism/Statistics/StatisticsPresentation.vue";
|
||||
</script>
|
||||
9
thanasoft-front/src/views/pages/Webmailing.vue
Normal file
9
thanasoft-front/src/views/pages/Webmailing.vue
Normal file
@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<div>
|
||||
<webmailing-presentation />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import WebmailingPresentation from "@/components/Organism/Webmailing/WebmailingPresentation.vue";
|
||||
</script>
|
||||
Loading…
x
Reference in New Issue
Block a user