add page fournisseur
This commit is contained in:
parent
425d2d510c
commit
e924c4f819
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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">
|
||||
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
@ -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>
|
||||
@ -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,
|
||||
},
|
||||
|
||||
@ -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({
|
||||
|
||||
11
thanasoft-front/src/views/pages/Agenda.vue
Normal file
11
thanasoft-front/src/views/pages/Agenda.vue
Normal file
@ -0,0 +1,11 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1>Agenda</h1>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "Agenda",
|
||||
};
|
||||
</script>
|
||||
11
thanasoft-front/src/views/pages/Clients/Statistiques.vue
Normal file
11
thanasoft-front/src/views/pages/Clients/Statistiques.vue
Normal file
@ -0,0 +1,11 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1>Statistiques clients</h1>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "StatistiquesClients",
|
||||
};
|
||||
</script>
|
||||
11
thanasoft-front/src/views/pages/Courriel.vue
Normal file
11
thanasoft-front/src/views/pages/Courriel.vue
Normal file
@ -0,0 +1,11 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1>Courriel</h1>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "Courriel",
|
||||
};
|
||||
</script>
|
||||
11
thanasoft-front/src/views/pages/Employes/Absences.vue
Normal file
11
thanasoft-front/src/views/pages/Employes/Absences.vue
Normal file
@ -0,0 +1,11 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1>Absences</h1>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "Absences",
|
||||
};
|
||||
</script>
|
||||
11
thanasoft-front/src/views/pages/Employes/Employes.vue
Normal file
11
thanasoft-front/src/views/pages/Employes/Employes.vue
Normal file
@ -0,0 +1,11 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1>Gestion employes</h1>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "GestionEmployes",
|
||||
};
|
||||
</script>
|
||||
11
thanasoft-front/src/views/pages/Employes/NDF.vue
Normal file
11
thanasoft-front/src/views/pages/Employes/NDF.vue
Normal file
@ -0,0 +1,11 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1>NDF</h1>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "NDF",
|
||||
};
|
||||
</script>
|
||||
11
thanasoft-front/src/views/pages/Employes/Vehicules.vue
Normal file
11
thanasoft-front/src/views/pages/Employes/Vehicules.vue
Normal file
@ -0,0 +1,11 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1>Vehicules</h1>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "Vehicules",
|
||||
};
|
||||
</script>
|
||||
11
thanasoft-front/src/views/pages/Fournisseurs/Commandes.vue
Normal file
11
thanasoft-front/src/views/pages/Fournisseurs/Commandes.vue
Normal file
@ -0,0 +1,11 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1>Commandes fournisseurs</h1>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "CommandesFournisseurs",
|
||||
};
|
||||
</script>
|
||||
11
thanasoft-front/src/views/pages/Fournisseurs/Contacts.vue
Normal file
11
thanasoft-front/src/views/pages/Fournisseurs/Contacts.vue
Normal file
@ -0,0 +1,11 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1>Contacts fournisseurs</h1>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "ContactsFournisseurs",
|
||||
};
|
||||
</script>
|
||||
11
thanasoft-front/src/views/pages/Fournisseurs/Factures.vue
Normal file
11
thanasoft-front/src/views/pages/Fournisseurs/Factures.vue
Normal file
@ -0,0 +1,11 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1>Factures fournisseurs</h1>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "FacturesFournisseurs",
|
||||
};
|
||||
</script>
|
||||
@ -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>
|
||||
104
thanasoft-front/src/views/pages/Fournisseurs/Fournisseurs.vue
Normal file
104
thanasoft-front/src/views/pages/Fournisseurs/Fournisseurs.vue
Normal 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>
|
||||
@ -0,0 +1,11 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1>Statistiques fournisseurs</h1>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "StatistiquesFournisseurs",
|
||||
};
|
||||
</script>
|
||||
11
thanasoft-front/src/views/pages/Parametrage/Droits.vue
Normal file
11
thanasoft-front/src/views/pages/Parametrage/Droits.vue
Normal file
@ -0,0 +1,11 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1>Gestion des droits</h1>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "GestionDroits",
|
||||
};
|
||||
</script>
|
||||
11
thanasoft-front/src/views/pages/Parametrage/Emails.vue
Normal file
11
thanasoft-front/src/views/pages/Parametrage/Emails.vue
Normal file
@ -0,0 +1,11 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1>Gestion des emails</h1>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "GestionEmails",
|
||||
};
|
||||
</script>
|
||||
11
thanasoft-front/src/views/pages/Parametrage/Modeles.vue
Normal file
11
thanasoft-front/src/views/pages/Parametrage/Modeles.vue
Normal file
@ -0,0 +1,11 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1>Gestion des modeles</h1>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "GestionModeles",
|
||||
};
|
||||
</script>
|
||||
11
thanasoft-front/src/views/pages/SousTraitants/Commandes.vue
Normal file
11
thanasoft-front/src/views/pages/SousTraitants/Commandes.vue
Normal file
@ -0,0 +1,11 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1>Commandes sous-traitants</h1>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "CommandesSousTraitants",
|
||||
};
|
||||
</script>
|
||||
11
thanasoft-front/src/views/pages/SousTraitants/Contacts.vue
Normal file
11
thanasoft-front/src/views/pages/SousTraitants/Contacts.vue
Normal file
@ -0,0 +1,11 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1>Contacts sous-traitants</h1>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "ContactsSousTraitants",
|
||||
};
|
||||
</script>
|
||||
11
thanasoft-front/src/views/pages/SousTraitants/Factures.vue
Normal file
11
thanasoft-front/src/views/pages/SousTraitants/Factures.vue
Normal file
@ -0,0 +1,11 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1>Factures sous-traitants</h1>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "FacturesSousTraitants",
|
||||
};
|
||||
</script>
|
||||
@ -0,0 +1,11 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1>Sous-Traitants</h1>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "SousTraitants",
|
||||
};
|
||||
</script>
|
||||
@ -0,0 +1,11 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1>Statistiques sous-traitants</h1>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "StatistiquesSousTraitants",
|
||||
};
|
||||
</script>
|
||||
11
thanasoft-front/src/views/pages/Stock/Reception.vue
Normal file
11
thanasoft-front/src/views/pages/Stock/Reception.vue
Normal file
@ -0,0 +1,11 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1>Reception stock</h1>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "ReceptionStock",
|
||||
};
|
||||
</script>
|
||||
11
thanasoft-front/src/views/pages/Stock/Stock.vue
Normal file
11
thanasoft-front/src/views/pages/Stock/Stock.vue
Normal file
@ -0,0 +1,11 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1>Gestion stock</h1>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "GestionStock",
|
||||
};
|
||||
</script>
|
||||
11
thanasoft-front/src/views/pages/Ventes/Devis.vue
Normal file
11
thanasoft-front/src/views/pages/Ventes/Devis.vue
Normal file
@ -0,0 +1,11 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1>Devis</h1>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "Devis",
|
||||
};
|
||||
</script>
|
||||
11
thanasoft-front/src/views/pages/Ventes/Factures.vue
Normal file
11
thanasoft-front/src/views/pages/Ventes/Factures.vue
Normal file
@ -0,0 +1,11 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1>Factures ventes</h1>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "FacturesVentes",
|
||||
};
|
||||
</script>
|
||||
11
thanasoft-front/src/views/pages/Ventes/Statistiques.vue
Normal file
11
thanasoft-front/src/views/pages/Ventes/Statistiques.vue
Normal file
@ -0,0 +1,11 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1>Statistiques ventes</h1>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "StatistiquesVentes",
|
||||
};
|
||||
</script>
|
||||
Loading…
x
Reference in New Issue
Block a user