CRM: refonte clients, fournisseurs et groupes clients
This commit is contained in:
parent
083f78673e
commit
ecfe25d3ca
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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">×</span>
|
<span aria-hidden="true">×</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>
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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 = () => {
|
||||||
|
|||||||
@ -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";
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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 () => {
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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> {
|
||||||
|
|||||||
@ -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>({
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user