CRM: refonte clients, fournisseurs et groupes clients

This commit is contained in:
nyavokevin 2026-03-02 15:46:08 +03:00
parent 083f78673e
commit ecfe25d3ca
21 changed files with 704 additions and 358 deletions

View File

@ -36,7 +36,7 @@ import { storeToRefs } from "pinia";
const router = useRouter(); const router = useRouter();
const clientStore = useClientStore(); const clientStore = useClientStore();
// Use storeToRefs to keep reactivity for pagination if it was passed as a prop, // Use storeToRefs to keep reactivity for pagination if it was passed as a prop,
// but since we are using the store directly for actions, we can also extract it here if needed. // but since we are using the store directly for actions, we can also extract it here if needed.
// However, the common pattern is that the parent view passes the data. // However, the common pattern is that the parent view passes the data.
// Let's check where clientData comes from. It comes from props. // Let's check where clientData comes from. It comes from props.
@ -54,9 +54,9 @@ const props = defineProps({
}, },
// We need to accept pagination as a prop if it is passed from the view // We need to accept pagination as a prop if it is passed from the view
pagination: { pagination: {
type: Object, type: Object,
default: () => ({}) default: () => ({}),
} },
}); });
const goToClient = () => { const goToClient = () => {
@ -74,14 +74,18 @@ const deleteClient = (client) => {
}; };
const onPageChange = (page) => { const onPageChange = (page) => {
clientStore.fetchClients({ page: page, per_page: props.pagination.per_page }); clientStore.fetchClients({ page: page, per_page: props.pagination.per_page });
}; };
const onPerPageChange = (perPage) => { const onPerPageChange = (perPage) => {
clientStore.fetchClients({ page: 1, per_page: perPage }); clientStore.fetchClients({ page: 1, per_page: perPage });
}; };
const onSearch = (query) => { const onSearch = (query) => {
clientStore.fetchClients({ page: 1, per_page: props.pagination.per_page, search: query }); clientStore.fetchClients({
page: 1,
per_page: props.pagination.per_page,
search: query,
});
}; };
</script> </script>

View File

@ -1,5 +1,61 @@
<template> <template>
<fournisseur-detail-template> <fournisseur-detail-template>
<template #header-right>
<span class="badge bg-white text-primary px-3 py-2">
{{ fournisseur.type_label || "Fournisseur" }}
</span>
<span
class="badge px-3 py-2"
:class="
fournisseur.is_active
? 'bg-white text-success'
: 'bg-white text-danger'
"
>
{{ fournisseur.is_active ? "Actif" : "Inactif" }}
</span>
</template>
<template #summary-cards>
<div class="col-12 col-md-6 col-xl-3">
<div class="card stat-card border-0">
<div class="card-body py-3">
<p class="text-sm text-secondary mb-1">Contacts</p>
<h5 class="mb-0">{{ filteredContactsCount }}</h5>
</div>
</div>
</div>
<div class="col-12 col-md-6 col-xl-3">
<div class="card stat-card border-0">
<div class="card-body py-3">
<p class="text-sm text-secondary mb-1">Localisations</p>
<h5 class="mb-0">{{ locations.length }}</h5>
</div>
</div>
</div>
<div class="col-12 col-md-6 col-xl-3">
<div class="card stat-card border-0">
<div class="card-body py-3">
<p class="text-sm text-secondary mb-1">Email</p>
<h6
class="mb-0 text-truncate"
:title="fournisseur.email || 'Non renseigné'"
>
{{ fournisseur.email || "Non renseigné" }}
</h6>
</div>
</div>
</div>
<div class="col-12 col-md-6 col-xl-3">
<div class="card stat-card border-0">
<div class="card-body py-3">
<p class="text-sm text-secondary mb-1">Téléphone</p>
<h6 class="mb-0">{{ fournisseur.phone || "Non renseigné" }}</h6>
</div>
</div>
</div>
</template>
<template #button-return> <template #button-return>
<div class="col-12"> <div class="col-12">
<router-link <router-link
@ -204,3 +260,9 @@ const triggerFileInput = () => {
} }
}; };
</script> </script>
<style scoped>
.stat-card {
box-shadow: 0 6px 20px rgba(15, 23, 42, 0.06);
}
</style>

View File

@ -1,5 +1,34 @@
<template> <template>
<fournisseur-template> <fournisseur-template>
<template #summary-cards>
<div class="col-12 col-md-6 col-xl-3">
<div class="card stat-card border-0">
<div class="card-body py-3">
<p class="text-sm text-secondary mb-1">Fournisseurs</p>
<h5 class="mb-0">{{ totalFournisseurs }}</h5>
</div>
</div>
</div>
<div class="col-12 col-md-6 col-xl-3">
<div class="card stat-card border-0">
<div class="card-body py-3">
<p class="text-sm text-secondary mb-1">Actifs</p>
<h5 class="mb-0 text-success">{{ activeFournisseurs }}</h5>
</div>
</div>
</div>
<div class="col-12 col-md-6 col-xl-3">
<div class="card stat-card border-0">
<div class="card-body py-3">
<p class="text-sm text-secondary mb-1">Inactifs</p>
<h5 class="mb-0 text-danger">{{ inactiveFournisseurs }}</h5>
</div>
</div>
</div>
</template>
<template #fournisseur-new-action> <template #fournisseur-new-action>
<add-button text="Ajouter" @click="goToFournisseur" /> <add-button text="Ajouter" @click="goToFournisseur" />
</template> </template>
@ -25,14 +54,14 @@ import FournisseurTable from "@/components/molecules/Tables/CRM/FournisseurTable
import addButton from "@/components/molecules/new-button/addButton.vue"; import addButton from "@/components/molecules/new-button/addButton.vue";
import FilterTable from "@/components/molecules/Tables/FilterTable.vue"; import FilterTable from "@/components/molecules/Tables/FilterTable.vue";
import TableAction from "@/components/molecules/Tables/TableAction.vue"; import TableAction from "@/components/molecules/Tables/TableAction.vue";
import { defineProps, defineEmits } from "vue"; import { computed, defineProps, defineEmits } from "vue";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
const router = useRouter(); const router = useRouter();
const emit = defineEmits(["pushDetails", "deleteFournisseur"]); const emit = defineEmits(["pushDetails", "deleteFournisseur"]);
defineProps({ const props = defineProps({
fournisseurData: { fournisseurData: {
type: Array, type: Array,
default: [], default: [],
@ -43,6 +72,17 @@ defineProps({
}, },
}); });
const totalFournisseurs = computed(() => props.fournisseurData?.length || 0);
const activeFournisseurs = computed(
() => props.fournisseurData?.filter((f) => f?.is_active).length || 0
);
const inactiveFournisseurs = computed(
() => totalFournisseurs.value - activeFournisseurs.value
);
const fournisseursWithEmail = computed(
() => props.fournisseurData?.filter((f) => !!f?.email).length || 0
);
const goToFournisseur = () => { const goToFournisseur = () => {
router.push({ router.push({
name: "Creation fournisseur", name: "Creation fournisseur",
@ -62,3 +102,9 @@ const deleteFournisseur = (fournisseur) => {
emit("deleteFournisseur", fournisseur); emit("deleteFournisseur", fournisseur);
}; };
</script> </script>
<style scoped>
.stat-card {
box-shadow: 0 6px 20px rgba(15, 23, 42, 0.06);
}
</style>

View File

@ -11,53 +11,76 @@
<div class="modal-dialog modal-dialog-centered" role="document"> <div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title" id="addChildModalLabel">Ajouter un sous-client</h5> <h5 id="addChildModalLabel" class="modal-title">
Ajouter un sous-client
</h5>
<button <button
type="button" type="button"
class="btn-close text-dark" class="btn-close text-dark"
@click="closeModal"
aria-label="Close" aria-label="Close"
@click="closeModal"
> >
<span aria-hidden="true">&times;</span> <span aria-hidden="true">&times;</span>
</button> </button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div class="mb-3"> <div class="mb-3">
<label class="form-label">Rechercher un client</label> <label class="form-label">Rechercher un client</label>
<div v-if="selectedClient" class="d-flex align-items-center justify-content-between p-2 border rounded mb-3 bg-light"> <div
<div class="d-flex align-items-center"> v-if="selectedClient"
<soft-avatar class="d-flex align-items-center justify-content-between p-2 border rounded mb-3 bg-light"
size="sm" >
border-radius="md" <div class="d-flex align-items-center">
class="me-3" <soft-avatar
alt="selected client" size="sm"
/> border-radius="md"
<div class="d-flex flex-column"> class="me-3"
<span class="font-weight-bold">{{ selectedClient.name }}</span> alt="selected client"
<span class="text-xs text-muted">{{ selectedClient.email }}</span>
</div>
</div>
<button class="btn btn-link text-danger mb-0" @click="selectedClient = null">
<i class="fas fa-times"></i>
</button>
</div>
<ClientSearchInput
v-else
:exclude-ids="excludeIds"
@select="handleSelect"
/> />
</div> <div class="d-flex flex-column">
<span class="font-weight-bold">{{
selectedClient.name
}}</span>
<span class="text-xs text-muted">{{
selectedClient.email
}}</span>
</div>
</div>
<button
class="btn btn-link text-danger mb-0"
@click="selectedClient = null"
>
<i class="fas fa-times"></i>
</button>
</div>
<ClientSearchInput
v-else
:exclude-ids="excludeIds"
@select="handleSelect"
/>
</div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-secondary btn-sm" @click="closeModal">Annuler</button> <button
<button type="button"
type="button" class="btn btn-secondary btn-sm"
class="btn btn-success btn-sm" @click="closeModal"
>
Annuler
</button>
<button
type="button"
class="btn btn-success btn-sm"
:disabled="!selectedClient || loading" :disabled="!selectedClient || loading"
@click="confirmAdd" @click="confirmAdd"
> >
<span v-if="loading" class="spinner-border spinner-border-sm me-1" role="status" aria-hidden="true"></span> <span
v-if="loading"
class="spinner-border spinner-border-sm me-1"
role="status"
aria-hidden="true"
></span>
Ajouter Ajouter
</button> </button>
</div> </div>
@ -80,7 +103,7 @@ const props = defineProps({
excludeIds: { excludeIds: {
type: Array, type: Array,
default: () => [], default: () => [],
} },
}); });
const emit = defineEmits(["close", "add"]); const emit = defineEmits(["close", "add"]);
@ -93,18 +116,18 @@ const handleSelect = (client) => {
}; };
const closeModal = () => { const closeModal = () => {
selectedClient.value = null; selectedClient.value = null;
emit("close"); emit("close");
}; };
const confirmAdd = async () => { const confirmAdd = async () => {
if (!selectedClient.value) return; if (!selectedClient.value) return;
loading.value = true; loading.value = true;
emit("add", selectedClient.value); emit("add", selectedClient.value);
// Loading state is handled by parent usually, but here we emit and wait? // Loading state is handled by parent usually, but here we emit and wait?
// Ideally parent handles the async and closes modal. // Ideally parent handles the async and closes modal.
// For now simple emit. // For now simple emit.
loading.value = false; loading.value = false;
closeModal(); closeModal();
}; };
</script> </script>

View File

@ -23,7 +23,11 @@
> >
Retour Retour
</soft-button> </soft-button>
<soft-button color="info" variant="gradient" @click="handleEdit"> <soft-button
color="info"
variant="gradient"
@click="handleEdit"
>
Modifier Modifier
</soft-button> </soft-button>
</div> </div>
@ -42,7 +46,9 @@
</p> </p>
</div> </div>
<div class="col-md-6 mb-3"> <div class="col-md-6 mb-3">
<h6 class="text-sm text-uppercase text-muted">Date de création</h6> <h6 class="text-sm text-uppercase text-muted">
Date de création
</h6>
<p class="text-sm">{{ formatDate(clientGroup.created_at) }}</p> <p class="text-sm">{{ formatDate(clientGroup.created_at) }}</p>
</div> </div>
<div class="col-md-6 mb-3"> <div class="col-md-6 mb-3">

View File

@ -1,7 +1,9 @@
<template> <template>
<div class="card"> <div class="card">
<div class="card-body"> <div class="card-body">
<h5 class="mb-4">{{ isEdit ? "Modifier le groupe" : "Nouveau groupe" }}</h5> <h5 class="mb-4">
{{ isEdit ? "Modifier le groupe" : "Nouveau groupe" }}
</h5>
<form @submit.prevent="handleSubmit"> <form @submit.prevent="handleSubmit">
<div class="row"> <div class="row">
<div class="col-md-12"> <div class="col-md-12">
@ -39,8 +41,19 @@
> >
Annuler Annuler
</soft-button> </soft-button>
<soft-button type="submit" color="success" variant="gradient" :disabled="loading"> <soft-button
{{ loading ? "Enregistrement..." : isEdit ? "Mettre à jour" : "Créer" }} type="submit"
color="success"
variant="gradient"
:disabled="loading"
>
{{
loading
? "Enregistrement..."
: isEdit
? "Mettre à jour"
: "Créer"
}}
</soft-button> </soft-button>
</div> </div>
</div> </div>

View File

@ -15,19 +15,23 @@
<option :value="20">20</option> <option :value="20">20</option>
<option :value="50">50</option> <option :value="50">50</option>
</select> </select>
<span class="text-secondary text-xs font-weight-bold">éléments par page</span> <span class="text-secondary text-xs font-weight-bold"
>éléments par page</span
>
</div> </div>
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
<div class="input-group"> <div class="input-group">
<span class="input-group-text text-body"><i class="fas fa-search" aria-hidden="true"></i></span> <span class="input-group-text text-body"
<input ><i class="fas fa-search" aria-hidden="true"></i
type="text" ></span>
class="form-control form-control-sm" <input
placeholder="Rechercher..." type="text"
@input="onSearch" class="form-control form-control-sm"
> placeholder="Rechercher..."
</div> @input="onSearch"
/>
</div>
</div> </div>
</div> </div>
@ -227,39 +231,66 @@
</table> </table>
</div> </div>
<!-- Pagination Footer --> <!-- Pagination Footer -->
<div v-if="!loading && data.length > 0" class="d-flex justify-content-between align-items-center mt-3 px-3"> <div
v-if="!loading && data.length > 0"
class="d-flex justify-content-between align-items-center mt-3 px-3"
>
<div class="text-xs text-secondary font-weight-bold"> <div class="text-xs text-secondary font-weight-bold">
Affichage de {{ pagination.from }} à {{ pagination.to }} sur {{ pagination.total }} clients Affichage de {{ pagination.from }} à {{ pagination.to }} sur
{{ pagination.total }} clients
</div> </div>
<nav aria-label="Page navigation"> <nav aria-label="Page navigation">
<ul class="pagination pagination-sm pagination-success mb-0"> <ul class="pagination pagination-sm pagination-success mb-0">
<li class="page-item" :class="{ disabled: pagination.current_page === 1 }"> <li
<a class="page-link" href="#" aria-label="Previous" @click.prevent="changePage(pagination.current_page - 1)"> class="page-item"
<span aria-hidden="true"><i class="fa fa-angle-left" aria-hidden="true"></i></span> :class="{ disabled: pagination.current_page === 1 }"
>
<a
class="page-link"
href="#"
aria-label="Previous"
@click.prevent="changePage(pagination.current_page - 1)"
>
<span aria-hidden="true"
><i class="fa fa-angle-left" aria-hidden="true"></i
></span>
</a> </a>
</li> </li>
<li <li
v-for="page in displayedPages" v-for="page in displayedPages"
:key="page" :key="page"
class="page-item" class="page-item"
:class="{ active: pagination.current_page === page }" :class="{ active: pagination.current_page === page }"
> >
<a class="page-link" href="#" @click.prevent="changePage(page)">{{ page }}</a> <a class="page-link" href="#" @click.prevent="changePage(page)">{{
page
}}</a>
</li> </li>
<li class="page-item" :class="{ disabled: pagination.current_page === pagination.last_page }"> <li
<a class="page-link" href="#" aria-label="Next" @click.prevent="changePage(pagination.current_page + 1)"> class="page-item"
<span aria-hidden="true"><i class="fa fa-angle-right" aria-hidden="true"></i></span> :class="{
disabled: pagination.current_page === pagination.last_page,
}"
>
<a
class="page-link"
href="#"
aria-label="Next"
@click.prevent="changePage(pagination.current_page + 1)"
>
<span aria-hidden="true"
><i class="fa fa-angle-right" aria-hidden="true"></i
></span>
</a> </a>
</li> </li>
</ul> </ul>
</nav> </nav>
</div> </div>
<!-- Empty State --> <!-- Empty State -->
<div v-if="!loading && data.length === 0" class="empty-state"> <div v-if="!loading && data.length === 0" class="empty-state">
<div class="empty-icon"> <div class="empty-icon">
@ -282,7 +313,13 @@ import SoftAvatar from "@/components/SoftAvatar.vue";
import { defineProps, defineEmits } from "vue"; import { defineProps, defineEmits } from "vue";
import debounce from "lodash/debounce"; import debounce from "lodash/debounce";
const emit = defineEmits(["view", "delete", "page-change", "per-page-change", "search-change"]); const emit = defineEmits([
"view",
"delete",
"page-change",
"per-page-change",
"search-change",
]);
// Sample avatar images // Sample avatar images
import img1 from "@/assets/img/team-2.jpg"; import img1 from "@/assets/img/team-2.jpg";
@ -326,24 +363,31 @@ const displayedPages = computed(() => {
const current = props.pagination.current_page; const current = props.pagination.current_page;
const delta = 2; const delta = 2;
const range = []; const range = [];
for (let i = Math.max(2, current - delta); i <= Math.min(total - 1, current + delta); i++) { for (
let i = Math.max(2, current - delta);
i <= Math.min(total - 1, current + delta);
i++
) {
range.push(i); range.push(i);
} }
if (current - delta > 2) { if (current - delta > 2) {
range.unshift("..."); range.unshift("...");
} }
if (current + delta < total - 1) { if (current + delta < total - 1) {
range.push("..."); range.push("...");
} }
range.unshift(1); range.unshift(1);
if (total > 1) { if (total > 1) {
range.push(total); range.push(total);
} }
return range.filter((val, index, self) => val !== "..." || (val === "..." && self[index - 1] !== "...")); return range.filter(
(val, index, self) =>
val !== "..." || (val === "..." && self[index - 1] !== "...")
);
}); });
// Methods // Methods
@ -359,7 +403,7 @@ const onPerPageChange = (event) => {
}; };
const onSearch = debounce((event) => { const onSearch = debounce((event) => {
emit("search-change", event.target.value); emit("search-change", event.target.value);
}, 300); }, 300);
const getRandomAvatar = () => { const getRandomAvatar = () => {

View File

@ -31,7 +31,9 @@
<!-- Created At --> <!-- Created At -->
<td class="text-sm font-weight-bold"> <td class="text-sm font-weight-bold">
<span class="my-2 text-xs">{{ formatDate(group.created_at) }}</span> <span class="my-2 text-xs">{{
formatDate(group.created_at)
}}</span>
</td> </td>
<!-- Actions --> <!-- Actions -->
@ -71,7 +73,14 @@
</template> </template>
<script setup> <script setup>
import { ref, onMounted, watch, onUnmounted, defineProps, defineEmits } from "vue"; import {
ref,
onMounted,
watch,
onUnmounted,
defineProps,
defineEmits,
} from "vue";
import { DataTable } from "simple-datatables"; import { DataTable } from "simple-datatables";
import SoftCheckbox from "@/components/SoftCheckbox.vue"; import SoftCheckbox from "@/components/SoftCheckbox.vue";

View File

@ -158,21 +158,31 @@ watch(
} }
); );
const filteredActivities = computed(() => { const filteredActivities = computed(() => {
if (activeFilter.value === "all") { if (activeFilter.value === "all") {
return activities.value; return activities.value;
} }
// Filter locally based on event_type mapping to filter categories // Filter locally based on event_type mapping to filter categories
return activities.value.filter((a) => { return activities.value.filter((a) => {
const type = a.event_type; const type = a.event_type;
switch (activeFilter.value) { switch (activeFilter.value) {
case 'call': return type === 'call'; case "call":
case 'email': return ['email_sent', 'email_received'].includes(type); return type === "call";
case 'invoice': return ['invoice_created', 'invoice_sent', 'invoice_paid'].includes(type); case "email":
case 'file': return ['file_uploaded', 'attachment_sent', 'attachment_received'].includes(type); return ["email_sent", "email_received"].includes(type);
default: return false; case "invoice":
} return ["invoice_created", "invoice_sent", "invoice_paid"].includes(
type
);
case "file":
return [
"file_uploaded",
"attachment_sent",
"attachment_received",
].includes(type);
default:
return false;
}
}); });
}); });
</script> </script>

View File

@ -6,82 +6,95 @@
<h6 class="mb-0">Gestion des sous-comptes</h6> <h6 class="mb-0">Gestion des sous-comptes</h6>
</div> </div>
<div class="col-md-4 text-end"> <div class="col-md-4 text-end">
<!-- Toggle Is Parent --> <!-- Toggle Is Parent -->
<div class="form-check form-switch d-inline-block ms-auto"> <div class="form-check form-switch d-inline-block ms-auto">
<input <input
class="form-check-input" id="isParentToggle"
type="checkbox" class="form-check-input"
id="isParentToggle" type="checkbox"
:checked="client.is_parent" :checked="client.is_parent"
@change="toggleParentStatus" @change="toggleParentStatus"
/> />
<label class="form-check-label" for="isParentToggle">Compte Parent</label> <label class="form-check-label" for="isParentToggle"
</div> >Compte Parent</label
<!-- Add Child Button -->
<soft-button
v-if="client.is_parent"
size="sm"
variant="gradient"
color="success"
class="ms-3"
@click="showAddModal = true"
> >
<i class="fas fa-plus me-2"></i>Ajouter un sous-client </div>
</soft-button> <!-- Add Child Button -->
<soft-button
v-if="client.is_parent"
size="sm"
variant="gradient"
color="success"
class="ms-3"
@click="showAddModal = true"
>
<i class="fas fa-plus me-2"></i>Ajouter un sous-client
</soft-button>
</div> </div>
</div> </div>
</div> </div>
<div class="card-body p-3"> <div class="card-body p-3">
<div v-if="!client.is_parent" class="text-center py-4"> <div v-if="!client.is_parent" class="text-center py-4">
<p class="text-muted"> <p class="text-muted">
Ce client n'est pas défini comme compte parent. Activez l'option ci-dessus pour gérer des sous-comptes. Ce client n'est pas défini comme compte parent. Activez l'option
</p> ci-dessus pour gérer des sous-comptes.
</p>
</div>
<div v-else>
<div v-if="loading" class="text-center py-4">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Chargement...</span>
</div>
</div> </div>
<div v-else> <div v-else-if="children.length === 0" class="text-center py-4">
<div v-if="loading" class="text-center py-4"> <p class="text-muted">Aucun sous-compte associé.</p>
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Chargement...</span>
</div>
</div>
<div v-else-if="children.length === 0" class="text-center py-4">
<p class="text-muted">Aucun sous-compte associé.</p>
</div>
<ul v-else class="list-group">
<li v-for="child in children" :key="child.id" class="list-group-item border-0 d-flex justify-content-between ps-0 mb-2 border-radius-lg">
<div class="d-flex align-items-center">
<soft-avatar
:img="getAvatar(child.name)"
size="sm"
border-radius="md"
class="me-3"
alt="child client"
/>
<div class="d-flex flex-column">
<h6 class="mb-1 text-dark text-sm">{{ child.name }}</h6>
<span class="text-xs">{{ child.email }}</span>
</div>
</div>
<div class="d-flex align-items-center text-sm">
<button class="btn btn-link text-dark text-sm mb-0 px-0 ms-4" @click="goToClient(child.id)">
<i class="fas fa-eye text-lg me-1"></i> Voir
</button>
<button class="btn btn-link text-danger text-gradient px-3 mb-0" @click="confirmRemoveChild(child)">
<i class="far fa-trash-alt me-2"></i> Détacher
</button>
</div>
</li>
</ul>
</div> </div>
<ul v-else class="list-group">
<li
v-for="child in children"
:key="child.id"
class="list-group-item border-0 d-flex justify-content-between ps-0 mb-2 border-radius-lg"
>
<div class="d-flex align-items-center">
<soft-avatar
:img="getAvatar(child.name)"
size="sm"
border-radius="md"
class="me-3"
alt="child client"
/>
<div class="d-flex flex-column">
<h6 class="mb-1 text-dark text-sm">{{ child.name }}</h6>
<span class="text-xs">{{ child.email }}</span>
</div>
</div>
<div class="d-flex align-items-center text-sm">
<button
class="btn btn-link text-dark text-sm mb-0 px-0 ms-4"
@click="goToClient(child.id)"
>
<i class="fas fa-eye text-lg me-1"></i> Voir
</button>
<button
class="btn btn-link text-danger text-gradient px-3 mb-0"
@click="confirmRemoveChild(child)"
>
<i class="far fa-trash-alt me-2"></i> Détacher
</button>
</div>
</li>
</ul>
</div>
</div> </div>
<AddChildClientModal <AddChildClientModal
:show="showAddModal" :show="showAddModal"
:exclude-ids="[client.id, client.parent_id, ...children.map(c => c.id)]" :exclude-ids="[client.id, client.parent_id, ...children.map((c) => c.id)]"
@close="showAddModal = false" @close="showAddModal = false"
@add="handleAddChild" @add="handleAddChild"
/> />
</div> </div>
</template> </template>
@ -92,8 +105,8 @@ import SoftButton from "@/components/SoftButton.vue";
import SoftAvatar from "@/components/SoftAvatar.vue"; import SoftAvatar from "@/components/SoftAvatar.vue";
import AddChildClientModal from "@/components/Organism/CRM/client/AddChildClientModal.vue"; import AddChildClientModal from "@/components/Organism/CRM/client/AddChildClientModal.vue";
import { useClientStore } from "@/stores/clientStore"; import { useClientStore } from "@/stores/clientStore";
import { useRouter } from 'vue-router'; import { useRouter } from "vue-router";
import Swal from 'sweetalert2'; import Swal from "sweetalert2";
const props = defineProps({ const props = defineProps({
client: { client: {
@ -102,7 +115,7 @@ const props = defineProps({
}, },
}); });
const emit = defineEmits(['update-client']); const emit = defineEmits(["update-client"]);
const clientStore = useClientStore(); const clientStore = useClientStore();
const router = useRouter(); const router = useRouter();
const children = ref([]); const children = ref([]);
@ -110,89 +123,93 @@ const loading = ref(false);
const showAddModal = ref(false); const showAddModal = ref(false);
const fetchChildren = async () => { const fetchChildren = async () => {
if (!props.client.is_parent) return; if (!props.client.is_parent) return;
loading.value = true; loading.value = true;
try { try {
const res = await clientStore.fetchChildClients(props.client.id); const res = await clientStore.fetchChildClients(props.client.id);
children.value = res.data || res; // handle potential array vs response structure children.value = res.data || res; // handle potential array vs response structure
} catch (e) { } catch (e) {
console.error("Failed to fetch children", e); console.error("Failed to fetch children", e);
} finally { } finally {
loading.value = false; loading.value = false;
} }
}; };
onMounted(() => { onMounted(() => {
fetchChildren(); fetchChildren();
}); });
watch(() => props.client.is_parent, (newVal) => { watch(
() => props.client.is_parent,
(newVal) => {
if (newVal) fetchChildren(); if (newVal) fetchChildren();
}); }
);
/* eslint-disable require-atomic-updates */ /* eslint-disable require-atomic-updates */
const toggleParentStatus = async (e) => { const toggleParentStatus = async (e) => {
const isChecked = e.target.checked; const isChecked = e.target.checked;
// Optimistic update // Optimistic update
// emit('update-client', { ...props.client, is_parent: isChecked }); // emit('update-client', { ...props.client, is_parent: isChecked });
try { try {
// We'll update the client via store, passing just the id and field to update // We'll update the client via store, passing just the id and field to update
await clientStore.updateClient({ await clientStore.updateClient({
id: props.client.id, id: props.client.id,
name: props.client.name, name: props.client.name,
is_parent: isChecked is_parent: isChecked,
}); });
// The parent component should react to store changes if it watches it, or we emit updated // The parent component should react to store changes if it watches it, or we emit updated
} catch (err) { } catch (err) {
// Revert on error // Revert on error
e.target.checked = !isChecked; e.target.checked = !isChecked;
Swal.fire('Erreur', 'Impossible de mettre à jour le statut parent.', 'error'); Swal.fire(
} "Erreur",
"Impossible de mettre à jour le statut parent.",
"error"
);
}
}; };
const handleAddChild = async (selectedClient) => { const handleAddChild = async (selectedClient) => {
try { try {
await clientStore.addChildClient(props.client.id, selectedClient.id); await clientStore.addChildClient(props.client.id, selectedClient.id);
Swal.fire('Succès', 'Le client a été ajouté comme sous-compte.', 'success'); Swal.fire("Succès", "Le client a été ajouté comme sous-compte.", "success");
fetchChildren(); fetchChildren();
} catch (e) { } catch (e) {
Swal.fire('Erreur', "Impossible d'ajouter le sous-compte.", 'error'); Swal.fire("Erreur", "Impossible d'ajouter le sous-compte.", "error");
} }
}; };
const confirmRemoveChild = async (child) => { const confirmRemoveChild = async (child) => {
const result = await Swal.fire({ const result = await Swal.fire({
title: 'Confirmer le détachement', title: "Confirmer le détachement",
text: `Voulez-vous vraiment détacher ${child.name} ?`, text: `Voulez-vous vraiment détacher ${child.name} ?`,
icon: 'warning', icon: "warning",
showCancelButton: true, showCancelButton: true,
confirmButtonText: 'Oui, détacher', confirmButtonText: "Oui, détacher",
cancelButtonText: 'Annuler' cancelButtonText: "Annuler",
}); });
if (result.isConfirmed) { if (result.isConfirmed) {
try { try {
await clientStore.removeChildClient(props.client.id, child.id); await clientStore.removeChildClient(props.client.id, child.id);
Swal.fire('Détaché!', 'Le client a été détaché.', 'success'); Swal.fire("Détaché!", "Le client a été détaché.", "success");
children.value = children.value.filter(c => c.id !== child.id); children.value = children.value.filter((c) => c.id !== child.id);
} catch (e) { } catch (e) {
Swal.fire('Erreur', "Impossible de détacher le client.", 'error'); Swal.fire("Erreur", "Impossible de détacher le client.", "error");
}
} }
}
}; };
const goToClient = (id) => { const goToClient = (id) => {
router.push(`/crm/clients/${id}`); // Adjust route as needed router.push(`/crm/clients/${id}`); // Adjust route as needed
}; };
// Helper for initials/avatar // Helper for initials/avatar
const getAvatar = (name) => { const getAvatar = (name) => {
// placeholder logic, replace with actual avatar logic or component usage // placeholder logic, replace with actual avatar logic or component usage
return null; return null;
}; };
</script> </script>

View File

@ -9,28 +9,32 @@
placeholder="Rechercher un client (nom, email...)" placeholder="Rechercher un client (nom, email...)"
@input="handleInput" @input="handleInput"
/> />
<button <button
v-if="searchQuery" v-if="searchQuery"
class="btn btn-outline-secondary mb-0" class="btn btn-outline-secondary mb-0"
type="button" type="button"
@click="clearSearch" @click="clearSearch"
> >
<i class="fas fa-times"></i> <i class="fas fa-times"></i>
</button> </button>
</div> </div>
<!-- Loading State --> <!-- Loading State -->
<div v-if="loading" class="text-center py-2 position-absolute w-100 bg-white border rounded shadow-sm" style="z-index: 1000; top: 100%;"> <div
<div class="spinner-border spinner-border-sm text-primary" role="status"> v-if="loading"
<span class="visually-hidden">Chargement...</span> class="text-center py-2 position-absolute w-100 bg-white border rounded shadow-sm"
</div> style="z-index: 1000; top: 100%"
>
<div class="spinner-border spinner-border-sm text-primary" role="status">
<span class="visually-hidden">Chargement...</span>
</div>
</div> </div>
<!-- Results Dropdown --> <!-- Results Dropdown -->
<div <div
v-else-if="results.length > 0 && showResults" v-else-if="results.length > 0 && showResults"
class="list-group position-absolute w-100 mt-1 shadow-lg" class="list-group position-absolute w-100 mt-1 shadow-lg"
style="z-index: 1000; max-height: 300px; overflow-y: auto;" style="z-index: 1000; max-height: 300px; overflow-y: auto"
> >
<button <button
v-for="client in results" v-for="client in results"
@ -39,27 +43,29 @@
class="list-group-item list-group-item-action d-flex align-items-center p-2" class="list-group-item list-group-item-action d-flex align-items-center p-2"
@click="handleSelect(client)" @click="handleSelect(client)"
> >
<soft-avatar <soft-avatar
:img="getAvatar(client)" :img="getAvatar(client)"
size="sm" size="sm"
border-radius="md" border-radius="md"
class="me-3" class="me-3"
:alt="client.name" :alt="client.name"
/> />
<div class="d-flex flex-column"> <div class="d-flex flex-column">
<span class="font-weight-bold text-sm">{{ client.name }}</span> <span class="font-weight-bold text-sm">{{ client.name }}</span>
<span class="text-xs text-muted">{{ client.email || 'Pas d\'email' }}</span> <span class="text-xs text-muted">{{
client.email || "Pas d'email"
}}</span>
</div> </div>
</button> </button>
</div> </div>
<!-- No Results --> <!-- No Results -->
<div <div
v-else-if="searchQuery && !loading && showResults" v-else-if="searchQuery && !loading && showResults"
class="position-absolute w-100 mt-1 p-3 bg-white border rounded shadow-sm text-center text-sm text-muted" class="position-absolute w-100 mt-1 p-3 bg-white border rounded shadow-sm text-center text-sm text-muted"
style="z-index: 1000;" style="z-index: 1000"
> >
Aucun client trouvé. Aucun client trouvé.
</div> </div>
</div> </div>
</template> </template>
@ -70,10 +76,10 @@ import SoftAvatar from "@/components/SoftAvatar.vue";
import { useClientStore } from "@/stores/clientStore"; import { useClientStore } from "@/stores/clientStore";
const props = defineProps({ const props = defineProps({
excludeIds: { excludeIds: {
type: Array, type: Array,
default: () => [] default: () => [],
} },
}); });
const emit = defineEmits(["select"]); const emit = defineEmits(["select"]);
@ -86,46 +92,46 @@ const showResults = ref(false);
let debounceTimeout = null; let debounceTimeout = null;
const handleInput = () => { const handleInput = () => {
showResults.value = true; showResults.value = true;
if (debounceTimeout) clearTimeout(debounceTimeout); if (debounceTimeout) clearTimeout(debounceTimeout);
if (!searchQuery.value.trim()) {
results.value = [];
showResults.value = false;
return;
}
loading.value = true; if (!searchQuery.value.trim()) {
debounceTimeout = setTimeout(async () => { results.value = [];
try { showResults.value = false;
const res = await clientStore.searchClients(searchQuery.value); return;
// Filter out excluded IDs (e.g. self, parent) }
results.value = res.filter(c => !props.excludeIds.includes(c.id));
} catch (e) { loading.value = true;
console.error(e); debounceTimeout = setTimeout(async () => {
results.value = []; try {
} finally { const res = await clientStore.searchClients(searchQuery.value);
loading.value = false; // Filter out excluded IDs (e.g. self, parent)
} results.value = res.filter((c) => !props.excludeIds.includes(c.id));
}, 300); } catch (e) {
console.error(e);
results.value = [];
} finally {
loading.value = false;
}
}, 300);
}; };
const handleSelect = (client) => { const handleSelect = (client) => {
emit("select", client); emit("select", client);
searchQuery.value = ""; searchQuery.value = "";
results.value = []; results.value = [];
showResults.value = false; showResults.value = false;
}; };
const clearSearch = () => { const clearSearch = () => {
searchQuery.value = ""; searchQuery.value = "";
results.value = []; results.value = [];
showResults.value = false; showResults.value = false;
}; };
// Helper for avatar (placeholder logic) // Helper for avatar (placeholder logic)
const getAvatar = (client) => { const getAvatar = (client) => {
// If client has an avatar property use it, else return null/placeholder logic handled by SoftAvatar or similar // If client has an avatar property use it, else return null/placeholder logic handled by SoftAvatar or similar
return null; return null;
}; };
</script> </script>

View File

@ -22,10 +22,7 @@
{{ category.name }} {{ category.name }}
</option> </option>
</select> </select>
<div <div v-if="fieldErrors.client_category_id" class="invalid-feedback">
v-if="fieldErrors.client_category_id"
class="invalid-feedback"
>
{{ errorMessage("client_category_id") }} {{ errorMessage("client_category_id") }}
</div> </div>
</div> </div>

View File

@ -294,7 +294,7 @@ watch(
(newErrors) => { (newErrors) => {
fieldErrors.value = { ...newErrors }; fieldErrors.value = { ...newErrors };
}, },
{ deep: true }, { deep: true }
); );
// Watch for success from parent // Watch for success from parent
@ -304,7 +304,7 @@ watch(
if (newSuccess) { if (newSuccess) {
resetForm(); resetForm();
} }
}, }
); );
const submitForm = async () => { const submitForm = async () => {

View File

@ -1,16 +1,51 @@
<template> <template>
<div class="container-fluid py-4"> <div class="container-fluid py-4 fournisseur-detail-shell">
<div class="row mb-4"> <div class="card border-0 mb-4 hero-shell">
<div class="card-body p-4">
<div
class="d-flex flex-column flex-lg-row justify-content-between align-items-lg-center gap-3"
>
<div>
<h4 class="text-white mb-1">
<slot name="page-title">Détail fournisseur</slot>
</h4>
<p class="text-white-50 mb-0">
<slot name="page-subtitle"
>Consultez et gérez toutes les informations du
fournisseur.</slot
>
</p>
</div>
<div class="d-flex align-items-center flex-wrap gap-2">
<slot name="header-right" />
</div>
</div>
</div>
</div>
<div class="row g-3 mb-3">
<slot name="summary-cards" />
</div>
<div class="row mb-3">
<slot name="button-return" /> <slot name="button-return" />
</div> </div>
<div class="row">
<div class="col-lg-3"> <div class="row g-4">
<div class="col-xl-3 col-lg-4">
<slot name="fournisseur-detail-sidebar" /> <slot name="fournisseur-detail-sidebar" />
<slot name="file-input" /> <slot name="file-input" />
</div> </div>
<div class="col-lg-9 mt-lg-0 mt-4"> <div class="col-xl-9 col-lg-8 mt-lg-0 mt-2">
<slot name="fournisseur-detail-content" /> <slot name="fournisseur-detail-content" />
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<style scoped>
.hero-shell {
background: linear-gradient(135deg, #344767 0%, #5e72e4 100%);
box-shadow: 0 10px 30px rgba(52, 71, 103, 0.2);
}
</style>

View File

@ -1,19 +1,49 @@
<template> <template>
<div class="container-fluid py-4"> <div class="container-fluid py-4 fournisseur-shell">
<div class="d-sm-flex justify-content-between"> <div class="card border-0 mb-4 hero-shell">
<div> <div class="card-body p-4">
<slot name="fournisseur-new-action"></slot> <div
</div> class="d-flex flex-column flex-lg-row justify-content-between align-items-lg-center gap-3"
<div class="d-flex"> >
<div class="dropdown d-inline"> <div>
<slot name="select-filter"></slot> <h4 class="text-white mb-1">
<slot name="page-title">Gestion des fournisseurs</slot>
</h4>
<p class="text-white-50 mb-0">
<slot name="page-subtitle"
>Pilotez vos fournisseurs et centralisez leurs
informations.</slot
>
</p>
</div>
<div class="d-flex gap-2 flex-wrap">
<slot name="header-right" />
</div>
</div> </div>
<slot name="fournisseur-other-action"></slot>
</div> </div>
</div> </div>
<div class="row">
<div class="col-12"> <div class="row g-3 mb-3">
<div class="card mt-4"> <slot name="summary-cards" />
</div>
<div class="card border-0 content-shell">
<div class="card-body p-3 p-md-4">
<div
class="d-sm-flex justify-content-between align-items-center gap-2 action-row"
>
<div>
<slot name="fournisseur-new-action"></slot>
</div>
<div class="d-flex align-items-center flex-wrap gap-2">
<div class="dropdown d-inline">
<slot name="select-filter"></slot>
</div>
<slot name="fournisseur-other-action"></slot>
</div>
</div>
<div class="mt-3 table-shell">
<slot name="fournisseur-table"></slot> <slot name="fournisseur-table"></slot>
</div> </div>
</div> </div>
@ -21,3 +51,19 @@
</div> </div>
</template> </template>
<script></script> <script></script>
<style scoped>
.hero-shell {
background: linear-gradient(135deg, #344767 0%, #5e72e4 100%);
box-shadow: 0 10px 30px rgba(52, 71, 103, 0.2);
}
.content-shell {
box-shadow: 0 8px 24px rgba(15, 23, 42, 0.06);
}
.table-shell {
border-top: 1px solid rgba(148, 163, 184, 0.2);
padding-top: 1rem;
}
</style>

View File

@ -269,7 +269,7 @@ export const ClientService = {
return response; return response;
}, },
/** /**
* Get child clients for a parent * Get child clients for a parent
*/ */
async getChildClients(parentId: number): Promise<ClientListResponse> { async getChildClients(parentId: number): Promise<ClientListResponse> {

View File

@ -27,7 +27,8 @@ export interface CreateClientGroupPayload {
description?: string | null; description?: string | null;
} }
export interface UpdateClientGroupPayload extends Partial<CreateClientGroupPayload> { export interface UpdateClientGroupPayload
extends Partial<CreateClientGroupPayload> {
id: number; id: number;
} }
@ -64,7 +65,9 @@ export const ClientGroupService = {
/** /**
* Create a new client group * Create a new client group
*/ */
async createClientGroup(payload: CreateClientGroupPayload): Promise<ClientGroupResponse> { async createClientGroup(
payload: CreateClientGroupPayload
): Promise<ClientGroupResponse> {
const response = await request<ClientGroupResponse>({ const response = await request<ClientGroupResponse>({
url: "/api/client-groups", url: "/api/client-groups",
method: "post", method: "post",
@ -77,7 +80,9 @@ export const ClientGroupService = {
/** /**
* Update an existing client group * Update an existing client group
*/ */
async updateClientGroup(payload: UpdateClientGroupPayload): Promise<ClientGroupResponse> { async updateClientGroup(
payload: UpdateClientGroupPayload
): Promise<ClientGroupResponse> {
const { id, ...updateData } = payload; const { id, ...updateData } = payload;
const response = await request<ClientGroupResponse>({ const response = await request<ClientGroupResponse>({

View File

@ -79,7 +79,9 @@ export const useClientGroupStore = defineStore("clientGroup", () => {
return response; return response;
} catch (err: any) { } catch (err: any) {
const errorMessage = const errorMessage =
err.response?.data?.message || err.message || "Failed to fetch client groups"; err.response?.data?.message ||
err.message ||
"Failed to fetch client groups";
setError(errorMessage); setError(errorMessage);
throw err; throw err;
} finally { } finally {
@ -100,7 +102,9 @@ export const useClientGroupStore = defineStore("clientGroup", () => {
return response.data; return response.data;
} catch (err: any) { } catch (err: any) {
const errorMessage = const errorMessage =
err.response?.data?.message || err.message || "Failed to fetch client group"; err.response?.data?.message ||
err.message ||
"Failed to fetch client group";
setError(errorMessage); setError(errorMessage);
throw err; throw err;
} finally { } finally {
@ -123,7 +127,9 @@ export const useClientGroupStore = defineStore("clientGroup", () => {
return response.data; return response.data;
} catch (err: any) { } catch (err: any) {
const errorMessage = const errorMessage =
err.response?.data?.message || err.message || "Failed to create client group"; err.response?.data?.message ||
err.message ||
"Failed to create client group";
setError(errorMessage); setError(errorMessage);
throw err; throw err;
} finally { } finally {
@ -151,14 +157,19 @@ export const useClientGroupStore = defineStore("clientGroup", () => {
} }
// Update current group if it's the one being edited // Update current group if it's the one being edited
if (currentClientGroup.value && currentClientGroup.value.id === updatedGroup.id) { if (
currentClientGroup.value &&
currentClientGroup.value.id === updatedGroup.id
) {
setCurrentClientGroup(updatedGroup); setCurrentClientGroup(updatedGroup);
} }
return updatedGroup; return updatedGroup;
} catch (err: any) { } catch (err: any) {
const errorMessage = const errorMessage =
err.response?.data?.message || err.message || "Failed to update client group"; err.response?.data?.message ||
err.message ||
"Failed to update client group";
setError(errorMessage); setError(errorMessage);
throw err; throw err;
} finally { } finally {
@ -177,7 +188,9 @@ export const useClientGroupStore = defineStore("clientGroup", () => {
const response = await ClientGroupService.deleteClientGroup(id); const response = await ClientGroupService.deleteClientGroup(id);
// Remove from the groups list // Remove from the groups list
clientGroups.value = clientGroups.value.filter((group) => group.id !== id); clientGroups.value = clientGroups.value.filter(
(group) => group.id !== id
);
// Clear current group if it's the one being deleted // Clear current group if it's the one being deleted
if (currentClientGroup.value && currentClientGroup.value.id === id) { if (currentClientGroup.value && currentClientGroup.value.id === id) {
@ -187,7 +200,9 @@ export const useClientGroupStore = defineStore("clientGroup", () => {
return response; return response;
} catch (err: any) { } catch (err: any) {
const errorMessage = const errorMessage =
err.response?.data?.message || err.message || "Failed to delete client group"; err.response?.data?.message ||
err.message ||
"Failed to delete client group";
setError(errorMessage); setError(errorMessage);
throw err; throw err;
} finally { } finally {

View File

@ -99,7 +99,7 @@ export const useClientStore = defineStore("client", () => {
try { try {
const response = await ClientService.getAllClients(params); const response = await ClientService.getAllClients(params);
console.log('API Response:', response); console.log("API Response:", response);
setClients(response.data); setClients(response.data);
if (response.meta) { if (response.meta) {
setPagination(response.meta); setPagination(response.meta);
@ -300,13 +300,15 @@ export const useClientStore = defineStore("client", () => {
// Ideally backend returns the updated parent or child list. // Ideally backend returns the updated parent or child list.
// Assuming we need to refresh the current client if it is the parent // Assuming we need to refresh the current client if it is the parent
if (currentClient.value && currentClient.value.id === parentId) { if (currentClient.value && currentClient.value.id === parentId) {
// Optionally refetch or update manually if we knew the structure // Optionally refetch or update manually if we knew the structure
await fetchClient(parentId); await fetchClient(parentId);
} }
return response.data; return response.data;
} catch (err: any) { } catch (err: any) {
const errorMessage = const errorMessage =
err.response?.data?.message || err.message || "Failed to add child client"; err.response?.data?.message ||
err.message ||
"Failed to add child client";
setError(errorMessage); setError(errorMessage);
throw err; throw err;
} finally { } finally {
@ -314,7 +316,6 @@ export const useClientStore = defineStore("client", () => {
} }
}; };
/** /**
* Remove child client * Remove child client
*/ */
@ -324,12 +325,14 @@ export const useClientStore = defineStore("client", () => {
try { try {
await ClientService.removeChildClient(parentId, childId); await ClientService.removeChildClient(parentId, childId);
if (currentClient.value && currentClient.value.id === parentId) { if (currentClient.value && currentClient.value.id === parentId) {
await fetchClient(parentId); await fetchClient(parentId);
} }
} catch (err: any) { } catch (err: any) {
const errorMessage = const errorMessage =
err.response?.data?.message || err.message || "Failed to remove child client"; err.response?.data?.message ||
err.message ||
"Failed to remove child client";
setError(errorMessage); setError(errorMessage);
throw err; throw err;
} finally { } finally {
@ -337,23 +340,25 @@ export const useClientStore = defineStore("client", () => {
} }
}; };
/** /**
* Get children * Get children
*/ */
const fetchChildClients = async (parentId: number) => { const fetchChildClients = async (parentId: number) => {
setLoading(true); setLoading(true);
setError(null); setError(null);
try { try {
const response = await ClientService.getChildClients(parentId); const response = await ClientService.getChildClients(parentId);
return response.data; return response.data;
} catch (err: any) { } catch (err: any) {
const errorMessage = const errorMessage =
err.response?.data?.message || err.message || "Failed to fetch child clients"; err.response?.data?.message ||
setError(errorMessage); err.message ||
throw err; "Failed to fetch child clients";
} finally { setError(errorMessage);
setLoading(false); throw err;
} } finally {
setLoading(false);
}
}; };
/** /**

View File

@ -8,7 +8,7 @@ export const useClientTimelineStore = defineStore("clientTimeline", () => {
const activities = ref<TimelineActivity[]>([]); const activities = ref<TimelineActivity[]>([]);
const loading = ref(false); const loading = ref(false);
const error = ref<string | null>(null); const error = ref<string | null>(null);
const pagination = ref({ const pagination = ref({
current_page: 1, current_page: 1,
last_page: 1, last_page: 1,
@ -18,13 +18,13 @@ export const useClientTimelineStore = defineStore("clientTimeline", () => {
const setPagination = (meta: any) => { const setPagination = (meta: any) => {
if (meta) { if (meta) {
const getValue = (val: any) => (Array.isArray(val) ? val[0] : val); const getValue = (val: any) => (Array.isArray(val) ? val[0] : val);
pagination.value = { pagination.value = {
current_page: Number(getValue(meta.current_page)) || 1, current_page: Number(getValue(meta.current_page)) || 1,
last_page: Number(getValue(meta.last_page)) || 1, last_page: Number(getValue(meta.last_page)) || 1,
per_page: Number(getValue(meta.per_page)) || 10, per_page: Number(getValue(meta.per_page)) || 10,
total: Number(getValue(meta.total)) || 0, total: Number(getValue(meta.total)) || 0,
}; };
} }
}; };
@ -36,11 +36,14 @@ export const useClientTimelineStore = defineStore("clientTimeline", () => {
const queryParams = { const queryParams = {
page: pagination.value.current_page, page: pagination.value.current_page,
per_page: pagination.value.per_page, per_page: pagination.value.per_page,
...params ...params,
}; };
const response = await ClientTimelineService.getTimeline(clientId, queryParams); const response = await ClientTimelineService.getTimeline(
clientId,
queryParams
);
activities.value = response.data; activities.value = response.data;
if (response.meta) { if (response.meta) {
setPagination(response.meta); setPagination(response.meta);

View File

@ -53,7 +53,7 @@ onMounted(async () => {
client_id client_id
); );
locations_client.value = locationsResponse || []; locations_client.value = locationsResponse || [];
if (clientStore.currentClient.is_parent) { if (clientStore.currentClient.is_parent) {
children_client.value = await clientStore.fetchChildClients(client_id); children_client.value = await clientStore.fetchChildClients(client_id);
} }