add page fournisseur

This commit is contained in:
Nyavokevin 2025-10-28 15:25:04 +03:00
parent 425d2d510c
commit e924c4f819
46 changed files with 2521 additions and 602 deletions

View File

@ -0,0 +1,192 @@
<template>
<fournisseur-detail-template>
<template #button-return>
<div class="col-12">
<router-link
to="/fournisseurs"
class="btn btn-outline-secondary btn-sm mb-3"
>
<i class="fas fa-arrow-left me-2"></i>Retour aux fournisseurs
</router-link>
</div>
</template>
<template #loading-state>
<div v-if="isLoading" class="text-center p-5">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Chargement...</span>
</div>
</div>
</template>
<template #fournisseur-detail-sidebar>
<FournisseurDetailSidebar
:avatar-url="fournisseurAvatar"
:initials="getInitials(fournisseur.name)"
:fournisseur-name="fournisseur.name"
:fournisseur-type="fournisseur.type_label || 'Fournisseur'"
:contacts-count="contacts.length"
:locations-count="locations.length"
:is-active="fournisseur.is_active"
:active-tab="activeTab"
@edit-avatar="triggerFileInput"
@change-tab="activeTab = $event"
/>
</template>
<template #file-input>
<input
:ref="fileInput"
type="file"
class="d-none"
accept="image/*"
@change="handleAvatarUpload"
/>
</template>
<template #fournisseur-detail-content>
<FournisseurDetailContent
:active-tab="activeTab"
:fournisseur="fournisseur"
:contacts="contacts"
:locations="locations"
:formatted-address="formatAddress(fournisseur)"
:fournisseur-id="fournisseur.id"
:contact-is-loading="contactLoading"
:location-is-loading="locationLoading"
@change-tab="activeTab = $event"
@updating-fournisseur="handleUpdateFournisseur"
@create-contact="handleAddContact"
@updating-contact="handleModifiedContact"
@create-location="handleAddLocation"
@modify-location="handleModifyLocation"
@remove-location="handleRemoveLocation"
/>
</template>
</fournisseur-detail-template>
</template>
<script setup>
import { defineProps, defineEmits, ref } from "vue";
import FournisseurDetailTemplate from "@/components/templates/CRM/FournisseurDetailTemplate.vue";
import FournisseurDetailSidebar from "./fournisseur/FournisseurDetailSidebar.vue";
import FournisseurDetailContent from "./fournisseur/FournisseurDetailContent.vue";
import { RouterLink } from "vue-router";
const props = defineProps({
fournisseur: {
type: Object,
required: true,
},
contacts: {
type: Array,
required: false,
default: () => [],
},
locations: {
type: Array,
required: false,
default: () => [],
},
isLoading: {
type: Boolean,
default: false,
},
fournisseurAvatar: {
type: String,
default: "",
},
activeTab: {
type: String,
default: "overview",
},
fileInput: {
type: Object,
required: true,
},
contactLoading: {
type: Boolean,
default: false,
},
locationLoading: {
type: Boolean,
default: false,
},
});
const localAvatar = ref(props.fournisseurAvatar);
const emit = defineEmits([
"updateTheFournisseur",
"handleFileInput",
"add-new-contact",
"updating-contact",
"add-new-location",
"modify-location",
"remove-location",
]);
const handleAvatarUpload = (event) => {
const file = event.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
localAvatar.value = e.target.result;
// TODO: Upload to server
console.log("Upload avatar to server");
};
reader.readAsDataURL(file);
}
};
const handleUpdateFournisseur = (updateData) => {
emit("updateTheFournisseur", updateData);
};
const inputFile = () => {
emit("handleFileInput");
};
const handleAddContact = (data) => {
emit("add-new-contact", data);
};
const handleModifiedContact = (modifiedContact) => {
emit("updating-contact", modifiedContact);
};
const handleAddLocation = (data) => {
emit("add-new-location", data);
};
const handleModifyLocation = (location) => {
emit("modify-location", location);
};
const handleRemoveLocation = (locationId) => {
emit("remove-location", locationId);
};
const getInitials = (name) => {
if (!name) return "?";
return name
.split(" ")
.map((word) => word[0])
.join("")
.toUpperCase()
.substring(0, 2);
};
const formatAddress = (fournisseur) => {
const parts = [
fournisseur.billing_address.line1,
fournisseur.billing_address.line2,
fournisseur.billing_address.postal_code,
fournisseur.billing_address.city,
fournisseur.billing_address.country_code,
].filter(Boolean);
return parts.length > 0 ? parts.join(", ") : "Aucune adresse renseignée";
};
const triggerFileInput = () => {
if (props.fileInput) {
props.fileInput.click();
}
};
</script>

View File

@ -0,0 +1,63 @@
<template>
<fournisseur-template>
<template #fournisseur-new-action>
<add-button text="Ajouter" @click="goToFournisseur" />
</template>
<template #select-filter>
<filter-table />
</template>
<template #fournisseur-other-action>
<table-action />
</template>
<template #fournisseur-table>
<fournisseur-table
:data="fournisseurData"
:loading="loadingData"
@view="goToDetails"
@delete="deleteFournisseur"
/>
</template>
</fournisseur-template>
</template>
<script setup>
import FournisseurTemplate from "@/components/templates/CRM/FournisseurTemplate.vue";
import FournisseurTable from "@/components/molecules/Tables/CRM/FournisseurTable.vue";
import addButton from "@/components/molecules/new-button/addButton.vue";
import FilterTable from "@/components/molecules/Tables/FilterTable.vue";
import TableAction from "@/components/molecules/Tables/TableAction.vue";
import { defineProps, defineEmits } from "vue";
import { useRouter } from "vue-router";
const router = useRouter();
const emit = defineEmits(["pushDetails", "deleteFournisseur"]);
defineProps({
fournisseurData: {
type: Array,
default: [],
},
loadingData: {
type: Boolean,
default: false,
},
});
const goToFournisseur = () => {
// Navigate to create fournisseur page when implemented
console.log("Navigate to create fournisseur");
};
const goToDetails = (fournisseurId) => {
router.push({
name: "Fournisseur details",
params: {
id: fournisseurId,
},
});
};
const deleteFournisseur = (fournisseur) => {
emit("deleteFournisseur", fournisseur);
};
</script>

View File

@ -0,0 +1,139 @@
<template>
<div>
<!-- Overview Tab -->
<div v-show="activeTab === 'overview'">
<FournisseurOverview
:fournisseur="fournisseur"
:contacts="contacts"
:formatted-address="formattedAddress"
:fournisseur-id="fournisseurId"
@view-all-contacts="$emit('change-tab', 'contacts')"
/>
</div>
<!-- Information Tab -->
<div v-show="activeTab === 'info'">
<FournisseurInfoTab
:fournisseur="fournisseur"
@fournisseur-updated="updateFournisseur"
/>
</div>
<div v-show="activeTab === 'activity'">
<FournisseurActivityTab />
</div>
<!-- Contacts Tab -->
<div v-show="activeTab === 'contacts'">
<FournisseurContactsTab
:contacts="contacts"
:fournisseur-id="fournisseur.id"
:is-loading="contactIsLoading"
@contact-created="handleCreateContact"
@contact-modified="handleModifiedContact"
/>
</div>
<!-- Address Tab -->
<div v-show="activeTab === 'address'">
<FournisseurAddressTab :fournisseur="fournisseur" />
</div>
<!-- Locations Tab -->
<div v-show="activeTab === 'locations'">
<FournisseurLocationsTab
:locations="locations"
:fournisseur-id="fournisseur.id"
:is-loading="locationIsLoading"
@location-created="handleCreateLocation"
@location-modified="handleModifyLocation"
@location-removed="handleRemoveLocation"
/>
</div>
<!-- Notes Tab -->
<div v-show="activeTab === 'notes'">
<FournisseurNotesTab :notes="fournisseur.notes" />
</div>
</div>
</template>
<script setup>
import FournisseurOverview from "@/components/molecules/fournisseur/FournisseurOverview.vue";
import FournisseurInfoTab from "@/components/molecules/fournisseur/FournisseurInfoTab.vue";
import FournisseurContactsTab from "@/components/molecules/fournisseur/FournisseurContactsTab.vue";
import FournisseurAddressTab from "@/components/molecules/fournisseur/FournisseurAddressTab.vue";
import FournisseurLocationsTab from "@/components/molecules/fournisseur/FournisseurLocationsTab.vue";
import FournisseurNotesTab from "@/components/molecules/fournisseur/FournisseurNotesTab.vue";
import FournisseurActivityTab from "@/components/molecules/fournisseur/FournisseurActivityTab.vue";
import { defineProps, defineEmits } from "vue";
defineProps({
activeTab: {
type: String,
required: true,
},
fournisseur: {
type: Object,
required: true,
},
contacts: {
type: Array,
default: () => [],
},
locations: {
type: Array,
default: () => [],
},
formattedAddress: {
type: String,
default: "Aucune adresse renseignée",
},
fournisseurId: {
type: [Number, String],
required: true,
},
contactIsLoading: {
type: Boolean,
default: false,
},
locationIsLoading: {
type: Boolean,
default: false,
},
});
const emit = defineEmits([
"change-tab",
"create-contact",
"updatingFournisseur",
"create-location",
"modify-location",
"remove-location",
"updating-contact",
]);
const updateFournisseur = (updatedFournisseur) => {
emit("updatingFournisseur", updatedFournisseur);
};
const handleCreateContact = (newContact) => {
emit("create-contact", newContact);
};
const handleModifiedContact = (modifiedContact) => {
emit("updating-contact", modifiedContact);
};
const handleCreateLocation = (newLocation) => {
emit("create-location", newLocation);
};
const handleModifyLocation = (location) => {
emit("modify-location", location);
};
const handleRemoveLocation = (locationId) => {
emit("remove-location", locationId);
};
</script>

View File

@ -0,0 +1,79 @@
<template>
<div class="card position-sticky top-1">
<!-- Fournisseur Profile Card -->
<FournisseurProfileCard
:avatar-url="avatarUrl"
:initials="initials"
:fournisseur-name="fournisseurName"
:fournisseur-type="fournisseurType"
:contacts-count="contactsCount"
:is-active="isActive"
@edit-avatar="$emit('edit-avatar')"
/>
<hr class="horizontal dark my-3 mx-3" />
<!-- Tab Navigation -->
<div class="card-body pt-0">
<FournisseurTabNavigation
:active-tab="activeTab"
:contacts-count="contactsCount"
:locations-count="locationsCount"
@change-tab="$emit('change-tab', $event)"
/>
</div>
</div>
</template>
<script setup>
import FournisseurProfileCard from "@/components/molecules/fournisseur/FournisseurProfileCard.vue";
import FournisseurTabNavigation from "@/components/molecules/fournisseur/FournisseurTabNavigation.vue";
import { defineProps, defineEmits } from "vue";
defineProps({
avatarUrl: {
type: String,
default: null,
},
initials: {
type: String,
required: true,
},
fournisseurName: {
type: String,
required: true,
},
fournisseurType: {
type: String,
default: "Fournisseur",
},
contactsCount: {
type: Number,
default: 0,
},
locationsCount: {
type: Number,
default: 0,
},
isActive: {
type: Boolean,
default: true,
},
activeTab: {
type: String,
required: true,
},
});
defineEmits(["edit-avatar", "change-tab"]);
</script>
<style scoped>
.position-sticky {
top: 1rem;
}
.card {
border: 0;
box-shadow: 0 0 2rem 0 rgba(136, 152, 170, 0.15);
}
</style>

View File

@ -4,7 +4,7 @@
<div v-if="loading" class="loading-container">
<div class="loading-spinner">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
<span class="visually-hidden">Chargement...</span>
</div>
</div>
<div class="loading-content">

View File

@ -0,0 +1,489 @@
<template>
<div class="table-container">
<!-- Loading State -->
<div v-if="loading" class="loading-container">
<div class="loading-spinner">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Chargement...</span>
</div>
</div>
<div class="loading-content">
<!-- Skeleton Rows -->
<div class="table-responsive">
<table class="table table-flush">
<thead class="thead-light">
<tr>
<th>Commercial</th>
<th>Fournisseur</th>
<th>Address</th>
<th>Categories</th>
<th>Contact</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<tr v-for="i in skeletonRows" :key="i" class="skeleton-row">
<!-- Commercial Column Skeleton -->
<td>
<div class="d-flex align-items-center">
<div class="skeleton-checkbox"></div>
<div class="skeleton-text short ms-2"></div>
</div>
</td>
<!-- Fournisseur Name Column Skeleton -->
<td>
<div class="d-flex align-items-center">
<div class="skeleton-avatar"></div>
<div class="skeleton-text medium ms-2"></div>
</div>
</td>
<!-- Address Column Skeleton -->
<td>
<div class="skeleton-text long"></div>
</td>
<!-- Categories Column Skeleton -->
<td>
<div class="d-flex align-items-center">
<div class="skeleton-icon"></div>
<div class="skeleton-text medium ms-2"></div>
</div>
</td>
<!-- Contact Column Skeleton -->
<td>
<div class="contact-info">
<div class="skeleton-text long mb-1"></div>
<div class="skeleton-text medium"></div>
</div>
</td>
<!-- Status Column Skeleton -->
<td>
<div class="d-flex align-items-center">
<div class="skeleton-icon"></div>
<div class="skeleton-text short ms-2"></div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- Data State -->
<div v-else class="table-responsive">
<table id="fournisseur-list" class="table table-flush">
<thead class="thead-light">
<tr>
<th>Commercial</th>
<th>Fournisseur</th>
<th>Address</th>
<th>Categories</th>
<th>Contact</th>
<th>Status</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<tr v-for="fournisseur in data" :key="fournisseur.id">
<!-- Commercial Column -->
<td>
<div class="d-flex align-items-center">
<soft-checkbox />
<p class="text-xs font-weight-bold ms-2 mb-0">
{{ fournisseur.commercial }}
</p>
</div>
</td>
<!-- Fournisseur Name Column -->
<td class="font-weight-bold">
<div class="d-flex align-items-center">
<soft-avatar
:img="getRandomAvatar()"
size="xs"
class="me-2"
alt="user image"
circular
/>
<span>{{ fournisseur.name }}</span>
</div>
</td>
<!-- Address Column (Shortened) -->
<td class="text-xs font-weight-bold">
<span class="my-2 text-xs">{{
getShortAddress(fournisseur.billing_address)
}}</span>
</td>
<!-- Categories Column -->
<td class="text-xs font-weight-bold">
<div class="d-flex align-items-center">
<soft-button
:color="getCategoryColor(fournisseur.type_label)"
variant="outline"
class="btn-icon-only btn-rounded mb-0 me-2 btn-sm d-flex align-items-center justify-content-center"
>
<i
:class="getCategoryIcon(fournisseur.type_label)"
aria-hidden="true"
></i>
</soft-button>
<span>{{ fournisseur.type_label }}</span>
</div>
</td>
<!-- Contact Column -->
<td class="text-xs font-weight-bold">
<div class="contact-info">
<div class="text-xs text-secondary">
{{ fournisseur.email }}
</div>
<div class="text-xs">{{ fournisseur.phone }}</div>
</div>
</td>
<!-- Status Column -->
<td class="text-xs font-weight-bold">
<div class="d-flex align-items-center">
<soft-button
:color="fournisseur.is_active ? 'success' : 'danger'"
variant="outline"
class="btn-icon-only btn-rounded mb-0 me-2 btn-sm d-flex align-items-center justify-content-center"
>
<i
:class="
fournisseur.is_active ? 'fas fa-check' : 'fas fa-times'
"
aria-hidden="true"
></i>
</soft-button>
<span>{{ fournisseur.is_active ? "Active" : "Inactive" }}</span>
</div>
</td>
<td>
<div class="d-flex align-items-center gap-2">
<!-- View Button -->
<soft-button
color="info"
variant="outline"
title="View Fournisseur"
:data-fournisseur-id="fournisseur.id"
class="btn-icon-only btn-rounded mb-0 btn-sm d-flex align-items-center justify-content-center"
>
<i class="fas fa-eye" aria-hidden="true"></i>
</soft-button>
<!-- Delete Button -->
<soft-button
color="danger"
variant="outline"
title="Delete Fournisseur"
:data-fournisseur-id="fournisseur.id"
class="btn-icon-only btn-rounded mb-0 btn-sm d-flex align-items-center justify-content-center"
>
<i class="fas fa-trash" aria-hidden="true"></i>
</soft-button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!-- Empty State -->
<div v-if="!loading && data.length === 0" class="empty-state">
<div class="empty-icon">
<i class="fas fa-truck fa-3x text-muted"></i>
</div>
<h5 class="empty-title">Aucun fournisseur trouvé</h5>
<p class="empty-text text-muted">
Aucun fournisseur à afficher pour le moment.
</p>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, watch, onUnmounted } from "vue";
import { DataTable } from "simple-datatables";
import SoftCheckbox from "@/components/SoftCheckbox.vue";
import SoftButton from "@/components/SoftButton.vue";
import SoftAvatar from "@/components/SoftAvatar.vue";
import { defineProps, defineEmits } from "vue";
const emit = defineEmits(["view", "delete"]);
// 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];
// Reactive data
const dataTableInstance = ref(null);
const props = defineProps({
data: {
type: Array,
default: [],
},
loading: {
type: Boolean,
default: false,
},
skeletonRows: {
type: Number,
default: 5,
},
});
// Methods
const getRandomAvatar = () => {
const randomIndex = Math.floor(Math.random() * avatarImages.length);
return avatarImages[randomIndex];
};
const getShortAddress = (address) => {
if (!address) return "N/A";
// Return just city and postal code for brevity
return `${address.postal_code} ${address.city}`;
};
const getCategoryColor = (type) => {
const colors = {
Entreprise: "info",
Particulier: "success",
Association: "warning",
};
return colors[type] || "secondary";
};
const getCategoryIcon = (type) => {
const icons = {
Entreprise: "fas fa-building",
Particulier: "fas fa-user",
Association: "fas fa-users",
};
return icons[type] || "fas fa-circle";
};
const initializeDataTable = () => {
// Destroy existing instance if it exists
if (dataTableInstance.value) {
dataTableInstance.value.destroy();
dataTableInstance.value = null;
}
const dataTableEl = document.getElementById("fournisseur-list");
if (dataTableEl) {
dataTableInstance.value = new DataTable(dataTableEl, {
searchable: true,
fixedHeight: true,
perPage: 10,
perPageSelect: [5, 10, 15, 20],
});
dataTableEl.addEventListener("click", handleTableClick);
}
};
const handleTableClick = (event) => {
const button = event.target.closest("button");
if (!button) return;
const fournisseurId = button.getAttribute("data-fournisseur-id");
if (
button.title === "Delete Fournisseur" ||
button.querySelector(".fa-trash")
) {
emit("delete", fournisseurId);
} else if (
button.title === "View Fournisseur" ||
button.querySelector(".fa-eye")
) {
emit("view", fournisseurId);
}
};
// Watch for data changes to reinitialize datatable
watch(
() => props.data,
() => {
if (!props.loading) {
// Small delay to ensure DOM is updated
setTimeout(() => {
initializeDataTable();
}, 100);
}
},
{ deep: true }
);
onUnmounted(() => {
const dataTableEl = document.getElementById("fournisseur-list");
if (dataTableEl) {
dataTableEl.removeEventListener("click", handleTableClick);
}
if (dataTableInstance.value) {
dataTableInstance.value.destroy();
}
});
// Initialize data
onMounted(() => {
if (!props.loading && props.data.length > 0) {
initializeDataTable();
}
});
</script>
<style scoped>
.table-container {
position: relative;
min-height: 200px;
}
.loading-container {
position: relative;
}
.loading-spinner {
position: absolute;
top: 20px;
right: 20px;
z-index: 10;
}
.loading-content {
opacity: 0.7;
pointer-events: none;
}
.skeleton-row {
animation: pulse 1.5s ease-in-out infinite;
}
.skeleton-checkbox {
width: 18px;
height: 18px;
border-radius: 3px;
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: shimmer 2s infinite;
}
.skeleton-avatar {
width: 32px;
height: 32px;
border-radius: 50%;
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: shimmer 2s infinite;
}
.skeleton-icon {
width: 32px;
height: 32px;
border-radius: 50%;
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: shimmer 2s infinite;
}
.skeleton-text {
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: shimmer 2s infinite;
border-radius: 4px;
height: 12px;
}
.skeleton-text.short {
width: 40px;
}
.skeleton-text.medium {
width: 80px;
}
.skeleton-text.long {
width: 120px;
}
.empty-state {
text-align: center;
padding: 3rem 1rem;
}
.empty-icon {
margin-bottom: 1rem;
opacity: 0.5;
}
.empty-title {
margin-bottom: 0.5rem;
color: #6c757d;
}
.empty-text {
max-width: 300px;
margin: 0 auto;
}
.contact-info {
line-height: 1.2;
}
.text-xs {
font-size: 0.75rem;
}
/* Animations */
@keyframes pulse {
0% {
opacity: 1;
}
50% {
opacity: 0.7;
}
100% {
opacity: 1;
}
}
@keyframes shimmer {
0% {
background-position: -200% 0;
}
100% {
background-position: 200% 0;
}
}
/* Responsive adjustments */
@media (max-width: 768px) {
.loading-spinner {
top: 10px;
right: 10px;
}
.skeleton-text.long {
width: 80px;
}
.skeleton-text.medium {
width: 60px;
}
}
</style>

View File

@ -0,0 +1,12 @@
<template>
<div class="card">
<div class="card-header pb-0">
<h6 class="mb-0">Activités récentes</h6>
</div>
<div class="card-body">
<p class="text-sm">Historique des activités du fournisseur</p>
</div>
</div>
</template>
<script setup></script>

View File

@ -0,0 +1,20 @@
<template>
<div class="card">
<div class="card-header pb-0">
<h6 class="mb-0">Adresse du fournisseur</h6>
</div>
<div class="card-body">
<p class="text-sm">Adresse de facturation</p>
</div>
</div>
</template>
<script setup>
import { defineProps } from "vue";
defineProps({
fournisseur: {
type: Object,
required: true,
},
});
</script>

View File

@ -0,0 +1,28 @@
<template>
<div class="card">
<div class="card-header pb-0">
<h6 class="mb-0">Contacts du fournisseur</h6>
</div>
<div class="card-body">
<p class="text-sm">Liste des contacts</p>
</div>
</div>
</template>
<script setup>
import { defineProps } from "vue";
defineProps({
contacts: {
type: Array,
default: () => [],
},
fournisseurId: {
type: [Number, String],
required: true,
},
isLoading: {
type: Boolean,
default: false,
},
});
</script>

View File

@ -0,0 +1,21 @@
<template>
<div class="card">
<div class="card-header pb-0">
<h6 class="mb-0">Informations du fournisseur</h6>
</div>
<div class="card-body">
<p class="text-sm">Informations détaillées du fournisseur</p>
<p class="text-xs text-secondary">{{ fournisseur.name }}</p>
</div>
</div>
</template>
<script setup>
import { defineProps } from "vue";
defineProps({
fournisseur: {
type: Object,
required: true,
},
});
</script>

View File

@ -0,0 +1,28 @@
<template>
<div class="card">
<div class="card-header pb-0">
<h6 class="mb-0">Localisations du fournisseur</h6>
</div>
<div class="card-body">
<p class="text-sm">Liste des localisations</p>
</div>
</div>
</template>
<script setup>
import { defineProps } from "vue";
defineProps({
locations: {
type: Array,
default: () => [],
},
fournisseurId: {
type: [Number, String],
required: true,
},
isLoading: {
type: Boolean,
default: false,
},
});
</script>

View File

@ -0,0 +1,20 @@
<template>
<div class="card">
<div class="card-header pb-0">
<h6 class="mb-0">Notes</h6>
</div>
<div class="card-body">
<p class="text-sm">{{ notes || "Aucune note" }}</p>
</div>
</div>
</template>
<script setup>
import { defineProps } from "vue";
defineProps({
notes: {
type: String,
default: "",
},
});
</script>

View File

@ -0,0 +1,151 @@
<template>
<div class="card">
<div class="card-header pb-0">
<div class="d-flex align-items-center">
<h6 class="mb-0">Aperçu du fournisseur</h6>
</div>
</div>
<div class="card-body">
<div class="row">
<!-- Contact Info Card -->
<div class="col-md-6 mb-3">
<InfoCard title="Contact" icon="fas fa-phone text-primary">
<ul class="list-group list-group-flush">
<li class="list-group-item border-0 ps-0 pt-0 text-sm">
<strong class="text-dark">Email:</strong>
<a :href="`mailto:${fournisseur.email}`" class="ms-2">
{{ fournisseur.email || "-" }}
</a>
</li>
<li class="list-group-item border-0 ps-0 text-sm">
<strong class="text-dark">Téléphone:</strong>
<span class="ms-2">{{ fournisseur.phone || "-" }}</span>
</li>
</ul>
</InfoCard>
</div>
<!-- Business Info Card -->
<div class="col-md-6 mb-3">
<InfoCard title="Entreprise" icon="fas fa-building text-warning">
<ul class="list-group list-group-flush">
<li class="list-group-item border-0 ps-0 pt-0 text-sm">
<strong class="text-dark">SIRET:</strong>
<span class="ms-2">{{ fournisseur.siret || "-" }}</span>
</li>
<li class="list-group-item border-0 ps-0 text-sm">
<strong class="text-dark">TVA:</strong>
<span class="ms-2">{{ fournisseur.tva_number || "-" }}</span>
</li>
</ul>
</InfoCard>
</div>
<!-- Address Card (Full Width) -->
<div class="col-12 mb-3">
<InfoCard title="Adresse" icon="fas fa-map-marked-alt text-success">
<p class="text-sm mb-0">
{{ formattedAddress }}
</p>
</InfoCard>
</div>
<!-- Recent Contacts -->
<div class="col-12">
<InfoCard
title="Contacts récents"
icon="fas fa-address-book text-info"
>
<template v-if="contacts.length > 0">
<div class="d-flex align-items-center mb-3 justify-content-end">
<button
class="btn btn-sm btn-outline-primary"
@click="$emit('view-all-contacts')"
>
Voir tous
</button>
</div>
<div
v-for="contact in contacts.slice(0, 3)"
:key="contact.id"
class="d-flex align-items-center mb-2"
>
<div class="avatar avatar-sm me-3">
<div
class="avatar-placeholder bg-gradient-secondary text-white d-flex align-items-center justify-content-center rounded-circle"
>
{{
getInitials(contact.first_name + " " + contact.last_name)
}}
</div>
</div>
<div class="flex-grow-1">
<h6 class="mb-0 text-sm">
{{ contact.first_name }} {{ contact.last_name }}
</h6>
<p class="text-xs text-secondary mb-0">
{{ contact.position || "Contact" }}
</p>
</div>
<div>
<StatusBadge
v-if="contact.is_primary"
status="success"
label="Principal"
/>
</div>
</div>
</template>
<p v-else class="text-sm text-secondary mb-0">
Aucun contact enregistré
</p>
</InfoCard>
</div>
</div>
</div>
</div>
</template>
<script setup>
import InfoCard from "@/components/atoms/client/InfoCard.vue";
import StatusBadge from "@/components/atoms/client/StatusBadge.vue";
import { defineProps, defineEmits } from "vue";
defineProps({
fournisseur: {
type: Object,
required: true,
},
contacts: {
type: Array,
default: () => [],
},
formattedAddress: {
type: String,
default: "Aucune adresse renseignée",
},
fournisseurId: {
type: [Number, String],
required: true,
},
});
defineEmits(["view-all-contacts"]);
const getInitials = (name) => {
if (!name) return "?";
return name
.split(" ")
.map((word) => word[0])
.join("")
.toUpperCase()
.substring(0, 2);
};
</script>
<style scoped>
.avatar-sm .avatar-placeholder {
width: 36px;
height: 36px;
font-size: 0.875rem;
}
</style>

View File

@ -0,0 +1,84 @@
<template>
<div class="card-body text-center">
<!-- Fournisseur Avatar -->
<ClientAvatar
:avatar-url="avatarUrl"
:initials="initials"
:alt="fournisseurName"
:editable="true"
@edit="$emit('edit-avatar')"
/>
<!-- Fournisseur Name -->
<h5 class="font-weight-bolder mb-0">
{{ fournisseurName }}
</h5>
<p class="text-sm text-secondary mb-3">
{{ fournisseurType }}
</p>
<!-- Quick Stats -->
<div class="row text-center mt-3">
<div class="col-6 border-end">
<h6 class="text-sm font-weight-bolder mb-0">
{{ contactsCount }}
</h6>
<p class="text-xs text-secondary mb-0">Contacts</p>
</div>
<div class="col-6">
<h6 class="text-sm font-weight-bolder mb-0">
<i
class="fas"
:class="
isActive
? 'fa-check-circle text-success'
: 'fa-times-circle text-danger'
"
></i>
</h6>
<p class="text-xs text-secondary mb-0">Statut</p>
</div>
</div>
</div>
</template>
<script setup>
import ClientAvatar from "@/components/atoms/client/ClientAvatar.vue";
import { defineProps, defineEmits } from "vue";
defineProps({
avatarUrl: {
type: String,
default: null,
},
initials: {
type: String,
required: true,
},
fournisseurName: {
type: String,
required: true,
},
fournisseurType: {
type: String,
default: "Fournisseur",
},
contactsCount: {
type: Number,
default: 0,
},
isActive: {
type: Boolean,
default: true,
},
});
defineEmits(["edit-avatar"]);
</script>
<style scoped>
.avatar-sm .avatar-placeholder {
width: 36px;
height: 36px;
font-size: 0.875rem;
}
</style>

View File

@ -0,0 +1,71 @@
<template>
<ul class="nav nav-pills flex-column">
<TabNavigationItem
icon="fas fa-eye"
label="Aperçu"
:is-active="activeTab === 'overview'"
spacing=""
@click="$emit('change-tab', 'overview')"
/>
<TabNavigationItem
icon="fas fa-info-circle"
label="Informations"
:is-active="activeTab === 'info'"
@click="$emit('change-tab', 'info')"
/>
<TabNavigationItem
icon="fas fa-info-circle"
label="Activités recentes"
:is-active="activeTab === 'activity'"
@click="$emit('change-tab', 'activity')"
/>
<TabNavigationItem
icon="fas fa-users"
label="Contacts"
:is-active="activeTab === 'contacts'"
:badge="contactsCount > 0 ? contactsCount : null"
@click="$emit('change-tab', 'contacts')"
/>
<TabNavigationItem
icon="fas fa-map-marker-alt"
label="Adresse"
:is-active="activeTab === 'address'"
@click="$emit('change-tab', 'address')"
/>
<TabNavigationItem
icon="fas fa-map-marked-alt"
label="Localisations"
:is-active="activeTab === 'locations'"
:badge="locationsCount > 0 ? locationsCount : null"
@click="$emit('change-tab', 'locations')"
/>
<TabNavigationItem
icon="fas fa-sticky-note"
label="Notes"
:is-active="activeTab === 'notes'"
@click="$emit('change-tab', 'notes')"
/>
</ul>
</template>
<script setup>
import TabNavigationItem from "@/components/atoms/client/TabNavigationItem.vue";
import { defineProps, defineEmits } from "vue";
defineProps({
activeTab: {
type: String,
required: true,
},
contactsCount: {
type: Number,
default: 0,
},
locationsCount: {
type: Number,
default: 0,
},
});
defineEmits(["change-tab"]);
</script>

View File

@ -13,32 +13,38 @@
<table class="table table-flush">
<thead class="thead-light">
<tr>
<th v-for="column in visibleColumns" :key="column.key">
{{ column.label }}
</th>
<th v-if="showActions">Action</th>
<th>Nom</th>
<th>Ville</th>
<th>Adresse</th>
<th>Latitude GPS</th>
<th>Longitude GPS</th>
<th>Par défaut</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<tr v-for="i in skeletonRows" :key="i" class="skeleton-row">
<!-- Dynamic Skeleton Columns -->
<td v-for="column in visibleColumns" :key="column.key">
<td>
<div class="skeleton-text medium"></div>
</td>
<td>
<div class="skeleton-text medium"></div>
</td>
<td>
<div class="skeleton-text long"></div>
</td>
<td>
<div class="skeleton-text short"></div>
</td>
<td>
<div class="skeleton-text short"></div>
</td>
<td>
<div class="d-flex align-items-center">
<div
v-if="column.key === 'name'"
class="skeleton-avatar"
></div>
<div
v-if="column.key === 'is_default'"
class="skeleton-icon small"
></div>
<div
:class="['skeleton-text', getSkeletonWidth(column.key)]"
></div>
<div class="skeleton-icon small"></div>
</div>
</td>
<!-- Actions Skeleton -->
<td v-if="showActions">
<td>
<div class="d-flex align-items-center gap-2">
<div class="skeleton-icon small"></div>
<div class="skeleton-icon small"></div>
@ -52,217 +58,102 @@
</div>
<!-- Data State -->
<div v-else-if="data.length > 0" class="data-container">
<!-- Table Controls -->
<div class="table-controls mb-3">
<div class="row align-items-center">
<div class="col-md-6">
<div class="d-flex align-items-center gap-3">
<!-- Column Visibility Toggle -->
<div class="dropdown">
<button
class="btn btn-outline-secondary btn-sm dropdown-toggle"
type="button"
data-bs-toggle="dropdown"
aria-expanded="false"
<div v-else class="table-responsive">
<table id="location-list" class="table table-flush">
<thead class="thead-light">
<tr>
<th>Nom</th>
<th>Ville</th>
<th>Adresse</th>
<th>Latitude GPS</th>
<th>Longitude GPS</th>
<th>Par défaut</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<tr v-for="location in tableData" :key="location.id">
<!-- Name Column -->
<td class="text-xs font-weight-bold">
<span class="font-weight-bold">{{ location.name || "N/A" }}</span>
</td>
<!-- City Column -->
<td class="text-xs font-weight-bold">
<span>{{ location.address?.city || "N/A" }}</span>
</td>
<!-- Address Column -->
<td class="text-xs font-weight-bold">
<span>{{ location.address?.line1 || "N/A" }}</span>
</td>
<!-- GPS Latitude Column -->
<td class="text-xs font-weight-bold">
<span>{{
location.gps_lat ? location.gps_lat + "°" : "N/A"
}}</span>
</td>
<!-- GPS Longitude Column -->
<td class="text-xs font-weight-bold">
<span>{{
location.gps_lng ? location.gps_lng + "°" : "N/A"
}}</span>
</td>
<!-- Default Column -->
<td class="text-xs font-weight-bold">
<div class="d-flex align-items-center">
<soft-button
:color="location.is_default ? 'success' : 'secondary'"
variant="outline"
class="btn-icon-only btn-rounded mb-0 me-2 btn-sm d-flex align-items-center justify-content-center"
>
<i class="fas fa-columns me-2"></i>
Colonnes
</button>
<ul class="dropdown-menu">
<li v-for="column in allColumns" :key="column.key">
<div class="dropdown-item">
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
:id="`col-${column.key}`"
v-model="column.visible"
/>
<label
class="form-check-label"
:for="`col-${column.key}`"
>
{{ column.label }}
</label>
</div>
</div>
</li>
</ul>
<i
:class="
location.is_default ? 'fas fa-check' : 'fas fa-times'
"
aria-hidden="true"
></i>
</soft-button>
<span>{{ location.is_default ? "Oui" : "Non" }}</span>
</div>
</td>
<!-- Search -->
<div class="search-box">
<div class="input-group input-group-sm">
<span class="input-group-text">
<i class="fas fa-search"></i>
</span>
<input
type="text"
class="form-control"
placeholder="Rechercher..."
v-model="searchQuery"
@input="handleSearch"
/>
</div>
<!-- Actions Column -->
<td>
<div class="d-flex align-items-center gap-2">
<!-- View Button -->
<soft-button
color="info"
variant="outline"
title="View Location"
:data-location-id="location.id"
class="btn-icon-only btn-rounded mb-0 btn-sm d-flex align-items-center justify-content-center"
>
<i class="fas fa-eye" aria-hidden="true"></i>
</soft-button>
<!-- Delete Button -->
<soft-button
color="danger"
variant="outline"
title="Delete Location"
:data-location-id="location.id"
class="btn-icon-only btn-rounded mb-0 btn-sm d-flex align-items-center justify-content-center"
>
<i class="fas fa-trash" aria-hidden="true"></i>
</soft-button>
</div>
</div>
</div>
<div class="col-md-6 text-end">
<div class="d-flex align-items-center justify-content-end gap-2">
<!-- Items per page -->
<div class="items-per-page">
<select
class="form-select form-select-sm"
v-model="perPage"
@change="handlePerPageChange"
style="width: auto"
>
<option value="5">5 par page</option>
<option value="10">10 par page</option>
<option value="15">15 par page</option>
<option value="20">20 par page</option>
<option value="50">50 par page</option>
</select>
</div>
</div>
</div>
</div>
</div>
<!-- Table -->
<div class="table-responsive">
<table class="table table-flush table-hover">
<thead class="thead-light">
<tr>
<th v-for="column in visibleColumns" :key="column.key">
<div
class="column-header"
@click="() => sortBy(column.key)"
:class="{ sortable: column.sortable }"
>
{{ column.label }}
<span v-if="sortColumn === column.key" class="sort-icon">
<i
:class="
sortDirection === 'asc'
? 'fas fa-sort-up'
: 'fas fa-sort-down'
"
></i>
</span>
</div>
</th>
<th v-if="showActions">Action</th>
</tr>
</thead>
<tbody>
<tr v-for="location in paginatedData" :key="location.id">
<!-- Dynamic Columns -->
<td v-for="column in visibleColumns" :key="column.key">
<!-- <component
:is="getColumnComponent(column.key)"
:location="location"
:column="column"
/> -->
{{ location[column.key] }}
</td>
<!-- Actions Column -->
<td v-if="showActions">
<div class="d-flex align-items-center gap-2">
<!-- View Button -->
<soft-button
color="info"
variant="outline"
title="View Location"
:data-location-id="location.id"
class="btn-icon-only btn-rounded mb-0 btn-sm d-flex align-items-center justify-content-center"
@click="emit('view', location.id)"
>
<i class="fas fa-eye" aria-hidden="true"></i>
</soft-button>
<!-- Delete Button -->
<soft-button
color="danger"
variant="outline"
title="Delete Location"
:data-location-id="location.id"
class="btn-icon-only btn-rounded mb-0 btn-sm d-flex align-items-center justify-content-center"
@click="emit('delete', location.id)"
>
<i class="fas fa-trash" aria-hidden="true"></i>
</soft-button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!-- Pagination -->
<div class="table-pagination mt-4">
<div class="row align-items-center">
<div class="col-md-6">
<div class="pagination-info text-muted">
Affichage de {{ pagination.from }} à {{ pagination.to }} sur
{{ pagination.total }} éléments
</div>
</div>
<div class="col-md-6">
<nav aria-label="Table pagination">
<ul class="pagination justify-content-end mb-0">
<!-- Previous Page -->
<li
class="page-item"
:class="{ disabled: pagination.currentPage === 1 }"
>
<button
class="page-link"
@click="changePage(pagination.currentPage - 1)"
:disabled="pagination.currentPage === 1"
>
<i class="fas fa-chevron-left"></i>
</button>
</li>
<!-- Page Numbers -->
<li
v-for="page in pagination.pages"
:key="page"
class="page-item"
:class="{ active: page === pagination.currentPage }"
>
<button class="page-link" @click="changePage(page)">
{{ page }}
</button>
</li>
<!-- Next Page -->
<li
class="page-item"
:class="{
disabled: pagination.currentPage === pagination.lastPage,
}"
>
<button
class="page-link"
@click="changePage(pagination.currentPage + 1)"
:disabled="pagination.currentPage === pagination.lastPage"
>
<i class="fas fa-chevron-right"></i>
</button>
</li>
</ul>
</nav>
</div>
</div>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<!-- Empty State -->
<div v-else class="empty-state">
<div v-if="!loading && tableData.length === 0" class="empty-state">
<div class="empty-icon">
<i class="fas fa-map-marker-alt fa-3x text-muted"></i>
</div>
@ -275,30 +166,18 @@
</template>
<script setup>
import { ref, computed, onMounted, watch } from "vue";
import { ref, computed, onMounted, watch, onUnmounted } from "vue";
import { DataTable } from "simple-datatables";
import SoftButton from "@/components/SoftButton.vue";
import SoftAvatar from "@/components/SoftAvatar.vue";
import { defineProps, defineEmits } from "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 emit = defineEmits(["view", "delete"]);
const avatarImages = [img1, img2, img3, img4, img5, img6];
const emit = defineEmits(["view", "delete", "page-change"]);
// Accept both `data` and `location-data` from parents
const props = defineProps({
data: {
type: Array,
default: () => [],
},
// support alternative prop name used elsewhere: `location-data` / `locationData`
locationData: {
type: Array,
default: () => [],
@ -311,289 +190,82 @@ const props = defineProps({
type: Number,
default: 5,
},
showActions: {
type: Boolean,
default: true,
},
currentPage: {
type: Number,
default: 1,
},
perPage: {
type: Number,
default: 10,
},
});
// Use the provided data prop or fallback to locationData
const tableData = computed(() => {
// Prefer explicit `data` when provided; otherwise use `locationData`
return (props.data && props.data.length) ||
!props.data ||
props.data.length === 0
? props.data.length
? props.data
: props.locationData
: props.locationData;
return props.data && props.data.length > 0 ? props.data : props.locationData;
});
// Reactive data
const searchQuery = ref("");
const sortColumn = ref("name");
const sortDirection = ref("asc");
const perPage = ref(props.perPage);
const currentPage = ref(props.currentPage);
// Column configuration
const allColumns = ref([
{ key: "name", label: "Nom", visible: true, sortable: true },
{ key: "city", label: "Ville", visible: true, sortable: true },
{ key: "address_line1", label: "Adresse", visible: true, sortable: true },
{ key: "gps_lat", label: "Latitude GPS", visible: true, sortable: true },
{ key: "gps_lng", label: "Longitude GPS", visible: true, sortable: true },
{ key: "is_default", label: "Par défaut", visible: true, sortable: true },
]);
// Computed
const visibleColumns = computed(() =>
allColumns.value.filter((col) => col.visible)
);
const getNestedValue = (obj, path) => {
return path.split(".").reduce((acc, part) => acc && acc[part], obj);
};
const filteredAndSortedData = computed(() => {
let filtered = [...tableData.value];
// Apply search filter
if (searchQuery.value) {
const query = searchQuery.value.toLowerCase();
filtered = filtered.filter(
(location) =>
location.name?.toLowerCase().includes(query) ||
location.address?.city?.toLowerCase().includes(query) ||
location.address?.line1?.toLowerCase().includes(query) ||
location.address?.postal_code?.toLowerCase().includes(query)
);
}
// Apply sorting
if (sortColumn.value) {
filtered.sort((a, b) => {
let aValue = getNestedValue(a, sortColumn.value);
let bValue = getNestedValue(b, sortColumn.value);
if (aValue == null) aValue = "";
if (bValue == null) bValue = "";
if (aValue < bValue) return sortDirection.value === "asc" ? -1 : 1;
if (aValue > bValue) return sortDirection.value === "asc" ? 1 : -1;
return 0;
});
}
return filtered;
});
const paginatedData = computed(() => {
const start = (currentPage.value - 1) * perPage.value;
const end = start + perPage.value;
return filteredAndSortedData.value.slice(start, end);
});
const pagination = computed(() => {
const total = filteredAndSortedData.value.length;
const lastPage = Math.ceil(total / perPage.value);
const from = total === 0 ? 0 : (currentPage.value - 1) * perPage.value + 1;
const to = Math.min(currentPage.value * perPage.value, total);
// Generate page numbers
const pages = [];
const maxVisiblePages = 5;
let startPage = Math.max(
1,
currentPage.value - Math.floor(maxVisiblePages / 2)
);
let endPage = Math.min(lastPage, startPage + maxVisiblePages - 1);
if (endPage - startPage + 1 < maxVisiblePages) {
startPage = Math.max(1, endPage - maxVisiblePages + 1);
}
for (let i = startPage; i <= endPage; i++) {
pages.push(i);
}
return {
currentPage: currentPage.value,
lastPage,
total,
from,
to,
pages,
};
});
const dataTableInstance = ref(null);
// Methods
const getRandomAvatar = () => {
const randomIndex = Math.floor(Math.random() * avatarImages.length);
return avatarImages[randomIndex];
};
const initializeDataTable = () => {
// Destroy existing instance if it exists
if (dataTableInstance.value) {
dataTableInstance.value.destroy();
dataTableInstance.value = null;
}
const getSkeletonWidth = (columnKey) => {
const widths = {
name: "medium",
client_name: "medium",
gps_lat: "short",
gps_lng: "short",
code_portail: "medium",
code_alarm: "medium",
code_funeraire: "medium",
is_default: "short",
};
return widths[columnKey] || "medium";
};
const dataTableEl = document.getElementById("location-list");
if (dataTableEl) {
dataTableInstance.value = new DataTable(dataTableEl, {
searchable: true,
fixedHeight: true,
perPage: 10,
perPageSelect: [5, 10, 15, 20],
});
const getColumnComponent = (columnKey) => {
return {
name: NameColumn,
city: CityColumn,
address_line1: AddressColumn,
gps_lat: GpsLatColumn,
gps_lng: GpsLngColumn,
is_default: DefaultColumn,
}[columnKey];
};
const sortBy = (columnKey) => {
const column = allColumns.value.find((col) => col.key === columnKey);
if (!column?.sortable) return;
if (sortColumn.value === columnKey) {
sortDirection.value = sortDirection.value === "asc" ? "desc" : "asc";
} else {
sortColumn.value = columnKey;
sortDirection.value = "asc";
dataTableEl.addEventListener("click", handleTableClick);
}
};
const handleSearch = () => {
currentPage.value = 1;
};
const handleTableClick = (event) => {
const button = event.target.closest("button");
if (!button) return;
const handlePerPageChange = () => {
currentPage.value = 1;
};
const changePage = (page) => {
if (page >= 1 && page <= pagination.value.lastPage) {
currentPage.value = page;
emit("page-change", page);
const locationId = button.getAttribute("data-location-id");
if (button.title === "Delete Location" || button.querySelector(".fa-trash")) {
emit("delete", locationId);
} else if (
button.title === "View Location" ||
button.querySelector(".fa-eye")
) {
emit("view", locationId);
}
};
// Column Components
const NameColumn = {
props: ["location"],
template: `
<div class="d-flex align-items-center">
<soft-avatar
:img="getRandomAvatar()"
size="xs"
class="me-2"
alt="location image"
circular
/>
<span class="font-weight-bold">{{ location.name || 'N/A' }}</span>
</div>
`,
components: { SoftAvatar },
methods: { getRandomAvatar },
};
const CityColumn = {
props: ["location"],
template: `
<span class="text-xs font-weight-bold">
{{ location.address?.city || 'N/A' }}
</span>
`,
};
const AddressColumn = {
props: ["location"],
template: `
<span class="text-xs font-weight-bold">
{{ location.address?.line1 || 'N/A' }}
</span>
`,
};
const GpsLatColumn = {
props: ["location"],
template: `
<span class="text-xs font-weight-bold">
{{ location.gps_lat ? location.gps_lat + '°' : 'N/A' }}
</span>
`,
};
const GpsLngColumn = {
props: ["location"],
template: `
<span class="text-xs font-weight-bold">
{{ location.gps_lng ? location.gps_lng + '°' : 'N/A' }}
</span>
`,
};
const DefaultColumn = {
props: ["location"],
template: `
<div class="d-flex align-items-center">
<soft-button
:color="location.is_default ? 'success' : 'secondary'"
variant="outline"
class="btn-icon-only btn-rounded mb-0 me-2 btn-sm d-flex align-items-center justify-content-center"
>
<i :class="location.is_default ? 'fas fa-check' : 'fas fa-times'" aria-hidden="true"></i>
</soft-button>
<span class="text-xs font-weight-bold">{{ location.is_default ? "Oui" : "Non" }}</span>
</div>
`,
components: { SoftButton },
};
// Watch for data changes (both prop names)
// Watch for data changes to reinitialize datatable
watch(
() => props.data,
() => tableData.value,
() => {
currentPage.value = 1;
}
if (!props.loading && tableData.value.length > 0) {
// Small delay to ensure DOM is updated
setTimeout(() => {
initializeDataTable();
}, 100);
}
},
{ deep: true }
);
watch(
() => props.locationData,
() => {
currentPage.value = 1;
onUnmounted(() => {
const dataTableEl = document.getElementById("location-list");
if (dataTableEl) {
dataTableEl.removeEventListener("click", handleTableClick);
}
);
if (dataTableInstance.value) {
dataTableInstance.value.destroy();
}
});
// Watch for prop changes
watch(
() => props.currentPage,
(newPage) => {
currentPage.value = newPage;
// Initialize data
onMounted(() => {
if (!props.loading && tableData.value.length > 0) {
initializeDataTable();
}
);
watch(
() => props.perPage,
(newPerPage) => {
perPage.value = newPerPage;
}
);
});
</script>
<style scoped>
@ -622,15 +294,6 @@ watch(
animation: pulse 1.5s ease-in-out infinite;
}
.skeleton-avatar {
width: 32px;
height: 32px;
border-radius: 50%;
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: shimmer 2s infinite;
}
.skeleton-icon {
width: 32px;
height: 32px;
@ -685,62 +348,10 @@ watch(
margin: 0 auto;
}
.contact-info {
line-height: 1.2;
}
.text-xs {
font-size: 0.75rem;
}
/* Table Controls */
.table-controls {
background: #f8f9fa;
padding: 1rem;
border-radius: 0.5rem;
}
.search-box {
min-width: 250px;
}
.column-header {
cursor: pointer;
user-select: none;
}
.column-header.sortable:hover {
background-color: #f8f9fa;
}
.sort-icon {
margin-left: 0.25rem;
color: #6c757d;
}
/* Pagination */
.table-pagination {
background: #f8f9fa;
padding: 1rem;
border-radius: 0.5rem;
}
.pagination .page-item.active .page-link {
background-color: #007bff;
border-color: #007bff;
}
.pagination .page-link {
color: #007bff;
border: 1px solid #dee2e6;
}
.pagination .page-link:hover {
color: #0056b3;
background-color: #e9ecef;
border-color: #dee2e6;
}
/* Animations */
@keyframes pulse {
0% {
@ -763,23 +374,19 @@ watch(
}
}
/* Responsive */
/* Responsive adjustments */
@media (max-width: 768px) {
.table-controls .row > div {
margin-bottom: 1rem;
.loading-spinner {
top: 10px;
right: 10px;
}
.search-box {
min-width: 200px;
.skeleton-text.long {
width: 80px;
}
.table-pagination .row > div {
text-align: center !important;
margin-bottom: 1rem;
}
.pagination {
justify-content: center !important;
.skeleton-text.medium {
width: 60px;
}
}
</style>

View File

@ -0,0 +1,16 @@
<template>
<div class="container-fluid py-4">
<div class="row mb-4">
<slot name="button-return" />
</div>
<div class="row">
<div class="col-lg-3">
<slot name="fournisseur-detail-sidebar" />
<slot name="file-input" />
</div>
<div class="col-lg-9 mt-lg-0 mt-4">
<slot name="fournisseur-detail-content" />
</div>
</div>
</div>
</template>

View File

@ -0,0 +1,23 @@
<template>
<div class="container-fluid py-4">
<div class="d-sm-flex justify-content-between">
<div>
<slot name="fournisseur-new-action"></slot>
</div>
<div class="d-flex">
<div class="dropdown d-inline">
<slot name="select-filter"></slot>
</div>
<slot name="fournisseur-other-action"></slot>
</div>
</div>
<div class="row">
<div class="col-12">
<div class="card mt-4">
<slot name="fournisseur-table"></slot>
</div>
</div>
</div>
</div>
</template>
<script></script>

View File

@ -4,6 +4,7 @@
class="w-auto h-auto collapse navbar-collapse max-height-vh-100 h-100"
>
<ul class="navbar-nav">
<!-- Dashboard -->
<li class="nav-item">
<sidenav-collapse
collapse-ref="dashboardsExamples"
@ -15,7 +16,6 @@
</template>
<template #list>
<ul class="nav ms-4 ps-3">
<!-- nav links -->
<sidenav-item
:to="{ name: 'Default' }"
mini-icon="D"
@ -26,49 +26,272 @@
</template>
</sidenav-collapse>
</li>
<!-- Agenda -->
<li class="nav-item">
<sidenav-item :to="{ name: 'Agenda' }" mini-icon="A" text="Agenda">
<template #icon>
<Office />
</template>
</sidenav-item>
</li>
<!-- Courriel -->
<li class="nav-item">
<sidenav-item :to="{ name: 'Courriel' }" mini-icon="C" text="Courriel">
<template #icon>
<Office />
</template>
</sidenav-item>
</li>
<!-- Contacts -->
<li class="nav-item">
<sidenav-item
:to="{ name: 'Gestion contacts' }"
mini-icon="C"
text="Contacts"
>
<template #icon>
<Office />
</template>
</sidenav-item>
</li>
<li class="mt-3 nav-item">
<h6
class="text-xs ps-4 text-uppercase font-weight-bolder opacity-6"
:class="isRTL ? 'me-4' : 'ms-2'"
>
CRM
GESTION
</h6>
</li>
<!-- Clients -->
<li class="nav-item">
<sidenav-collapse
collapse-ref="pagesExamples"
nav-text="Pages"
:class="getRoute() === 'pages' ? 'active' : ''"
collapse-ref="clientsMenu"
nav-text="Clients"
:class="getRoute() === 'clients' ? 'active' : ''"
>
<template #icon>
<Office />
</template>
<template #list>
<ul class="nav ms-4 ps-3">
<!-- nav links -->
<sidenav-collapse-item
refer="profileExample"
mini-icon="P"
text="Client"
>
<template #nav-child-item>
<sidenav-item
:to="{ name: 'Gestion contacts' }"
mini-icon="P"
text="Gestion Contact"
/>
<sidenav-item
:to="{ name: 'Gestion clients' }"
mini-icon="T"
text="Gestion Clients"
/>
<sidenav-item
:to="{ name: 'Localisation clients' }"
mini-icon="A"
text="Gestion des lieux"
/>
</template>
</sidenav-collapse-item>
<sidenav-item
:to="{ name: 'Gestion clients' }"
mini-icon="C"
text="Clients"
/>
<sidenav-item
:to="{ name: 'Localisation clients' }"
mini-icon="L"
text="Lieux"
/>
<sidenav-item
:to="{ name: 'Statistiques clients' }"
mini-icon="S"
text="Statistiques"
/>
</ul>
</template>
</sidenav-collapse>
</li>
<!-- Fournisseurs -->
<li class="nav-item">
<sidenav-collapse
collapse-ref="fournisseursMenu"
nav-text="Fournisseurs"
:class="getRoute() === 'fournisseurs' ? 'active' : ''"
>
<template #icon>
<Office />
</template>
<template #list>
<ul class="nav ms-4 ps-3">
<sidenav-item
:to="{ name: 'Gestion fournisseurs' }"
mini-icon="F"
text="Fournisseurs"
/>
<sidenav-item
:to="{ name: 'Commandes fournisseurs' }"
mini-icon="C"
text="Commandes"
/>
<sidenav-item
:to="{ name: 'Factures fournisseurs' }"
mini-icon="F"
text="Factures"
/>
<sidenav-item
:to="{ name: 'Statistiques fournisseurs' }"
mini-icon="S"
text="Statistiques"
/>
</ul>
</template>
</sidenav-collapse>
</li>
<!-- Sous-Traitants -->
<li class="nav-item">
<sidenav-collapse
collapse-ref="sousTraitantsMenu"
nav-text="Sous-Traitants"
:class="getRoute() === 'sous-traitants' ? 'active' : ''"
>
<template #icon>
<Office />
</template>
<template #list>
<ul class="nav ms-4 ps-3">
<sidenav-item
:to="{ name: 'Gestion sous-traitants' }"
mini-icon="S"
text="Sous-Traitants"
/>
<sidenav-item
:to="{ name: 'Commandes sous-traitants' }"
mini-icon="C"
text="Commandes"
/>
<sidenav-item
:to="{ name: 'Factures sous-traitants' }"
mini-icon="F"
text="Factures"
/>
<sidenav-item
:to="{ name: 'Statistiques sous-traitants' }"
mini-icon="S"
text="Statistiques"
/>
</ul>
</template>
</sidenav-collapse>
</li>
<!-- Ventes -->
<li class="nav-item">
<sidenav-collapse
collapse-ref="ventesMenu"
nav-text="Ventes"
:class="getRoute() === 'ventes' ? 'active' : ''"
>
<template #icon>
<Shop />
</template>
<template #list>
<ul class="nav ms-4 ps-3">
<sidenav-item
:to="{ name: 'Devis' }"
mini-icon="D"
text="Devis"
/>
<sidenav-item
:to="{ name: 'Factures ventes' }"
mini-icon="F"
text="Factures"
/>
<sidenav-item
:to="{ name: 'Statistiques ventes' }"
mini-icon="S"
text="Statistiques"
/>
</ul>
</template>
</sidenav-collapse>
</li>
<!-- Stock -->
<li class="nav-item">
<sidenav-collapse
collapse-ref="stockMenu"
nav-text="Stock"
:class="getRoute() === 'stock' ? 'active' : ''"
>
<template #icon>
<Office />
</template>
<template #list>
<ul class="nav ms-4 ps-3">
<sidenav-item
:to="{ name: 'Reception stock' }"
mini-icon="R"
text="Réception"
/>
<sidenav-item
:to="{ name: 'Gestion stock' }"
mini-icon="S"
text="Stock"
/>
</ul>
</template>
</sidenav-collapse>
</li>
<!-- Employés -->
<li class="nav-item">
<sidenav-collapse
collapse-ref="employesMenu"
nav-text="Employés"
:class="getRoute() === 'employes' ? 'active' : ''"
>
<template #icon>
<Office />
</template>
<template #list>
<ul class="nav ms-4 ps-3">
<sidenav-item
:to="{ name: 'Gestion employes' }"
mini-icon="E"
text="Employés"
/>
<sidenav-item :to="{ name: 'NDF' }" mini-icon="N" text="NDF" />
<sidenav-item
:to="{ name: 'Vehicules' }"
mini-icon="V"
text="Véhicules"
/>
<sidenav-item
:to="{ name: 'Absences' }"
mini-icon="A"
text="Absences"
/>
</ul>
</template>
</sidenav-collapse>
</li>
<!-- Paramétrage -->
<li class="nav-item">
<sidenav-collapse
collapse-ref="parametrageMenu"
nav-text="Paramétrage"
:class="getRoute() === 'parametrage' ? 'active' : ''"
>
<template #icon>
<Office />
</template>
<template #list>
<ul class="nav ms-4 ps-3">
<sidenav-item
:to="{ name: 'Gestion droits' }"
mini-icon="D"
text="Gestion des droits"
/>
<sidenav-item
:to="{ name: 'Gestion emails' }"
mini-icon="E"
text="Gestion des emails"
/>
<sidenav-item
:to="{ name: 'Gestion modeles' }"
mini-icon="M"
text="Gestion des modèles"
/>
</ul>
</template>
</sidenav-collapse>
@ -79,8 +302,7 @@
<script>
import SidenavItem from "./SidenavItem.vue";
import SidenavCollapse from "./SidenavCollapse.vue";
import SidenavCard from "./SidenavCard.vue";
import SidenavCollapseItem from "./SidenavCollapseItem.vue";
import Shop from "../../components/Icon/Shop.vue";
import Office from "../../components/Icon/Office.vue";
@ -90,7 +312,6 @@ export default {
components: {
SidenavItem,
SidenavCollapse,
SidenavCollapseItem,
Shop,
Office,
},

View File

@ -395,6 +395,146 @@ const routes = [
name: "Add Contact",
component: () => import("@/views/pages/CRM/AddContact.vue"),
},
// Agenda
{
path: "/agenda",
name: "Agenda",
component: () => import("@/views/pages/Agenda.vue"),
},
// Courriel
{
path: "/courriel",
name: "Courriel",
component: () => import("@/views/pages/Courriel.vue"),
},
// Clients - Statistiques
{
path: "/clients/statistiques",
name: "Statistiques clients",
component: () => import("@/views/pages/Clients/Statistiques.vue"),
},
// Fournisseurs
{
path: "/fournisseurs",
name: "Gestion fournisseurs",
component: () => import("@/views/pages/Fournisseurs/Fournisseurs.vue"),
},
{
path: "/fournisseurs/:id",
name: "Fournisseur details",
component: () =>
import("@/views/pages/Fournisseurs/FournisseurDetails.vue"),
},
{
path: "/fournisseurs/contacts",
name: "Contacts fournisseurs",
component: () => import("@/views/pages/Fournisseurs/Contacts.vue"),
},
{
path: "/fournisseurs/commandes",
name: "Commandes fournisseurs",
component: () => import("@/views/pages/Fournisseurs/Commandes.vue"),
},
{
path: "/fournisseurs/factures",
name: "Factures fournisseurs",
component: () => import("@/views/pages/Fournisseurs/Factures.vue"),
},
{
path: "/fournisseurs/statistiques",
name: "Statistiques fournisseurs",
component: () => import("@/views/pages/Fournisseurs/Statistiques.vue"),
},
// Sous-Traitants
{
path: "/sous-traitants",
name: "Gestion sous-traitants",
component: () => import("@/views/pages/SousTraitants/SousTraitants.vue"),
},
{
path: "/sous-traitants/contacts",
name: "Contacts sous-traitants",
component: () => import("@/views/pages/SousTraitants/Contacts.vue"),
},
{
path: "/sous-traitants/commandes",
name: "Commandes sous-traitants",
component: () => import("@/views/pages/SousTraitants/Commandes.vue"),
},
{
path: "/sous-traitants/factures",
name: "Factures sous-traitants",
component: () => import("@/views/pages/SousTraitants/Factures.vue"),
},
{
path: "/sous-traitants/statistiques",
name: "Statistiques sous-traitants",
component: () => import("@/views/pages/SousTraitants/Statistiques.vue"),
},
// Ventes
{
path: "/ventes/devis",
name: "Devis",
component: () => import("@/views/pages/Ventes/Devis.vue"),
},
{
path: "/ventes/factures",
name: "Factures ventes",
component: () => import("@/views/pages/Ventes/Factures.vue"),
},
{
path: "/ventes/statistiques",
name: "Statistiques ventes",
component: () => import("@/views/pages/Ventes/Statistiques.vue"),
},
// Stock
{
path: "/stock/reception",
name: "Reception stock",
component: () => import("@/views/pages/Stock/Reception.vue"),
},
{
path: "/stock",
name: "Gestion stock",
component: () => import("@/views/pages/Stock/Stock.vue"),
},
// Employés
{
path: "/employes",
name: "Gestion employes",
component: () => import("@/views/pages/Employes/Employes.vue"),
},
{
path: "/employes/ndf",
name: "NDF",
component: () => import("@/views/pages/Employes/NDF.vue"),
},
{
path: "/employes/vehicules",
name: "Vehicules",
component: () => import("@/views/pages/Employes/Vehicules.vue"),
},
{
path: "/employes/absences",
name: "Absences",
component: () => import("@/views/pages/Employes/Absences.vue"),
},
// Paramétrage
{
path: "/parametrage/droits",
name: "Gestion droits",
component: () => import("@/views/pages/Parametrage/Droits.vue"),
},
{
path: "/parametrage/emails",
name: "Gestion emails",
component: () => import("@/views/pages/Parametrage/Emails.vue"),
},
{
path: "/parametrage/modeles",
name: "Gestion modeles",
component: () => import("@/views/pages/Parametrage/Modeles.vue"),
},
];
const router = createRouter({

View File

@ -0,0 +1,11 @@
<template>
<div>
<h1>Agenda</h1>
</div>
</template>
<script>
export default {
name: "Agenda",
};
</script>

View File

@ -0,0 +1,11 @@
<template>
<div>
<h1>Statistiques clients</h1>
</div>
</template>
<script>
export default {
name: "StatistiquesClients",
};
</script>

View File

@ -0,0 +1,11 @@
<template>
<div>
<h1>Courriel</h1>
</div>
</template>
<script>
export default {
name: "Courriel",
};
</script>

View File

@ -0,0 +1,11 @@
<template>
<div>
<h1>Absences</h1>
</div>
</template>
<script>
export default {
name: "Absences",
};
</script>

View File

@ -0,0 +1,11 @@
<template>
<div>
<h1>Gestion employes</h1>
</div>
</template>
<script>
export default {
name: "GestionEmployes",
};
</script>

View File

@ -0,0 +1,11 @@
<template>
<div>
<h1>NDF</h1>
</div>
</template>
<script>
export default {
name: "NDF",
};
</script>

View File

@ -0,0 +1,11 @@
<template>
<div>
<h1>Vehicules</h1>
</div>
</template>
<script>
export default {
name: "Vehicules",
};
</script>

View File

@ -0,0 +1,11 @@
<template>
<div>
<h1>Commandes fournisseurs</h1>
</div>
</template>
<script>
export default {
name: "CommandesFournisseurs",
};
</script>

View File

@ -0,0 +1,11 @@
<template>
<div>
<h1>Contacts fournisseurs</h1>
</div>
</template>
<script>
export default {
name: "ContactsFournisseurs",
};
</script>

View File

@ -0,0 +1,11 @@
<template>
<div>
<h1>Factures fournisseurs</h1>
</div>
</template>
<script>
export default {
name: "FacturesFournisseurs",
};
</script>

View File

@ -0,0 +1,147 @@
<template>
<fournisseur-detail-presentation
v-if="currentFournisseur"
:fournisseur="currentFournisseur"
:contacts="contacts_fournisseur"
:locations="locations_fournisseur"
:is-loading="isLoading"
:fournisseur-avatar="fournisseurAvatar"
:active-tab="activeTab"
:file-input="fileInput"
:contact-loading="contactLoading"
:location-loading="locationLoading"
@update-the-fournisseur="updateFournisseur"
@add-new-contact="createNewContact"
@updating-contact="updateContact"
@add-new-location="createNewLocation"
@modify-location="modifyLocation"
@remove-location="removeLocation"
/>
</template>
<script setup>
import { ref, onMounted } from "vue";
import { useRoute } from "vue-router";
import FournisseurDetailPresentation from "@/components/Organism/CRM/FournisseurDetailPresentation.vue";
const route = useRoute();
// Ensure fournisseur_id is a number
const fournisseur_id = Number(route.params.id);
const contacts_fournisseur = ref([]);
const locations_fournisseur = ref([]);
const activeTab = ref("overview");
const fournisseurAvatar = ref(null);
const fileInput = ref(null);
const isLoading = ref(false);
const contactLoading = ref(false);
const locationLoading = ref(false);
// Dummy fournisseur data
const currentFournisseur = ref({
id: fournisseur_id,
name: "Fournisseur Alpha",
commercial: "Jean Dupont",
billing_address: {
line1: "123 Rue de la Paix",
line2: "Bâtiment A",
postal_code: "75001",
city: "Paris",
country_code: "FR",
},
type_label: "Entreprise",
email: "contact@alpha.fr",
phone: "+33 1 23 45 67 89",
is_active: true,
siret: "12345678901234",
tva_number: "FR12345678901",
payment_terms: "30 jours",
notes: "Fournisseur principal pour les matériaux de construction",
});
onMounted(async () => {
// Dummy contacts data
contacts_fournisseur.value = [
{
id: 1,
first_name: "Pierre",
last_name: "Martin",
email: "pierre.martin@alpha.fr",
phone: "+33 1 23 45 67 90",
position: "Responsable commercial",
is_primary: true,
},
{
id: 2,
first_name: "Sophie",
last_name: "Dubois",
email: "sophie.dubois@alpha.fr",
phone: "+33 1 23 45 67 91",
position: "Assistante",
is_primary: false,
},
];
// Dummy locations data
locations_fournisseur.value = [
{
id: 1,
name: "Siège social",
address: {
line1: "123 Rue de la Paix",
line2: "Bâtiment A",
postal_code: "75001",
city: "Paris",
country_code: "FR",
},
gps_lat: "48.8566",
gps_lng: "2.3522",
is_default: true,
},
{
id: 2,
name: "Entrepôt",
address: {
line1: "456 Avenue des Champs",
line2: null,
postal_code: "93100",
city: "Montreuil",
country_code: "FR",
},
gps_lat: "48.8634",
gps_lng: "2.4411",
is_default: false,
},
];
});
const updateFournisseur = async (data) => {
console.log("Update fournisseur:", data);
// TODO: Implement update logic with store
};
const createNewContact = async (data) => {
console.log("Create new contact:", data);
// TODO: Implement create contact logic
};
const updateContact = async (modifiedContact) => {
console.log("Update contact:", modifiedContact);
// TODO: Implement update contact logic
};
const createNewLocation = async (data) => {
console.log("Create new location:", data);
// TODO: Implement create location logic
};
const modifyLocation = async (location) => {
console.log("Modify location:", location);
// TODO: Implement modify location logic
};
const removeLocation = async (locationId) => {
console.log("Remove location:", locationId);
// TODO: Implement remove location logic
};
</script>

View File

@ -0,0 +1,104 @@
<template>
<fournisseur-presentation
:fournisseur-data="fournisseurs"
:loading-data="loading"
@push-details="goDetails"
/>
</template>
<script setup>
import FournisseurPresentation from "@/components/Organism/CRM/FournisseurPresentation.vue";
import { ref } from "vue";
import { useRouter } from "vue-router";
const router = useRouter();
const loading = ref(false);
// Dummy data for fournisseurs
const fournisseurs = ref([
{
id: 1,
name: "Fournisseur Alpha",
commercial: "Jean Dupont",
billing_address: {
line1: "123 Rue de la Paix",
postal_code: "75001",
city: "Paris",
country_code: "FR",
},
type_label: "Entreprise",
email: "contact@alpha.fr",
phone: "+33 1 23 45 67 89",
is_active: true,
},
{
id: 2,
name: "Fournisseur Beta",
commercial: "Marie Martin",
billing_address: {
line1: "456 Avenue des Champs",
postal_code: "69001",
city: "Lyon",
country_code: "FR",
},
type_label: "Entreprise",
email: "info@beta.fr",
phone: "+33 4 78 90 12 34",
is_active: true,
},
{
id: 3,
name: "Fournisseur Gamma",
commercial: "Pierre Dubois",
billing_address: {
line1: "789 Boulevard Victor Hugo",
postal_code: "13001",
city: "Marseille",
country_code: "FR",
},
type_label: "Association",
email: "contact@gamma.org",
phone: "+33 4 91 23 45 67",
is_active: false,
},
{
id: 4,
name: "Fournisseur Delta",
commercial: "Sophie Laurent",
billing_address: {
line1: "321 Rue de la République",
postal_code: "33000",
city: "Bordeaux",
country_code: "FR",
},
type_label: "Entreprise",
email: "hello@delta.fr",
phone: "+33 5 56 78 90 12",
is_active: true,
},
{
id: 5,
name: "Fournisseur Epsilon",
commercial: "Luc Bernard",
billing_address: {
line1: "654 Avenue de la Liberté",
postal_code: "59000",
city: "Lille",
country_code: "FR",
},
type_label: "Particulier",
email: "contact@epsilon.fr",
phone: "+33 3 20 12 34 56",
is_active: true,
},
]);
const goDetails = (id) => {
console.log("Navigate to fournisseur details:", id);
// router.push({
// name: "Fournisseur details",
// params: {
// id: id,
// },
// });
};
</script>

View File

@ -0,0 +1,11 @@
<template>
<div>
<h1>Statistiques fournisseurs</h1>
</div>
</template>
<script>
export default {
name: "StatistiquesFournisseurs",
};
</script>

View File

@ -0,0 +1,11 @@
<template>
<div>
<h1>Gestion des droits</h1>
</div>
</template>
<script>
export default {
name: "GestionDroits",
};
</script>

View File

@ -0,0 +1,11 @@
<template>
<div>
<h1>Gestion des emails</h1>
</div>
</template>
<script>
export default {
name: "GestionEmails",
};
</script>

View File

@ -0,0 +1,11 @@
<template>
<div>
<h1>Gestion des modeles</h1>
</div>
</template>
<script>
export default {
name: "GestionModeles",
};
</script>

View File

@ -0,0 +1,11 @@
<template>
<div>
<h1>Commandes sous-traitants</h1>
</div>
</template>
<script>
export default {
name: "CommandesSousTraitants",
};
</script>

View File

@ -0,0 +1,11 @@
<template>
<div>
<h1>Contacts sous-traitants</h1>
</div>
</template>
<script>
export default {
name: "ContactsSousTraitants",
};
</script>

View File

@ -0,0 +1,11 @@
<template>
<div>
<h1>Factures sous-traitants</h1>
</div>
</template>
<script>
export default {
name: "FacturesSousTraitants",
};
</script>

View File

@ -0,0 +1,11 @@
<template>
<div>
<h1>Sous-Traitants</h1>
</div>
</template>
<script>
export default {
name: "SousTraitants",
};
</script>

View File

@ -0,0 +1,11 @@
<template>
<div>
<h1>Statistiques sous-traitants</h1>
</div>
</template>
<script>
export default {
name: "StatistiquesSousTraitants",
};
</script>

View File

@ -0,0 +1,11 @@
<template>
<div>
<h1>Reception stock</h1>
</div>
</template>
<script>
export default {
name: "ReceptionStock",
};
</script>

View File

@ -0,0 +1,11 @@
<template>
<div>
<h1>Gestion stock</h1>
</div>
</template>
<script>
export default {
name: "GestionStock",
};
</script>

View File

@ -0,0 +1,11 @@
<template>
<div>
<h1>Devis</h1>
</div>
</template>
<script>
export default {
name: "Devis",
};
</script>

View File

@ -0,0 +1,11 @@
<template>
<div>
<h1>Factures ventes</h1>
</div>
</template>
<script>
export default {
name: "FacturesVentes",
};
</script>

View File

@ -0,0 +1,11 @@
<template>
<div>
<h1>Statistiques ventes</h1>
</div>
</template>
<script>
export default {
name: "StatistiquesVentes",
};
</script>