Compare commits

..

No commits in common. "ae7574045eb6e5393c2f74349a69816f28226a65" and "34875321ac1df96447b2fbe1aadf3b2088e802ea" have entirely different histories.

7 changed files with 379 additions and 456 deletions

View File

@ -86,28 +86,9 @@ class EmployeeRepository extends BaseRepository implements EmployeeRepositoryInt
/** /**
* Get employees with pagination. * Get employees with pagination.
*/ */
public function getPaginated(int $perPage = 10, array $filters = []): array public function getPaginated(int $perPage = 10): array
{ {
$query = $this->model->newQuery()->with(['thanatopractitioner', 'user']); $paginator = $this->model->newQuery()->with(['thanatopractitioner', 'user'])->paginate($perPage);
if (!empty($filters['search'])) {
$query->search($filters['search']);
}
if (array_key_exists('active', $filters)) {
if (filter_var($filters['active'], FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE)) {
$query->active();
} else {
$query->inactive();
}
}
$sortField = $filters['sort_by'] ?? 'last_name';
$sortDirection = $filters['sort_direction'] ?? 'asc';
$paginator = $query
->orderBy($sortField, $sortDirection)
->paginate($perPage);
return [ return [
'employees' => $paginator->getCollection(), 'employees' => $paginator->getCollection(),
@ -116,8 +97,6 @@ class EmployeeRepository extends BaseRepository implements EmployeeRepositoryInt
'last_page' => $paginator->lastPage(), 'last_page' => $paginator->lastPage(),
'per_page' => $paginator->perPage(), 'per_page' => $paginator->perPage(),
'total' => $paginator->total(), 'total' => $paginator->total(),
'from' => $paginator->firstItem(),
'to' => $paginator->lastItem(),
], ],
]; ];
} }

View File

@ -62,10 +62,9 @@ interface EmployeeRepositoryInterface
* Get employees with pagination. * Get employees with pagination.
* *
* @param int $perPage * @param int $perPage
* @param array<string, mixed> $filters
* @return array{employees: Collection<int, Employee>, pagination: array} * @return array{employees: Collection<int, Employee>, pagination: array}
*/ */
public function getPaginated(int $perPage = 10, array $filters = []): array; public function getPaginated(int $perPage = 10): array;
/** /**
* Get employees with their thanatopractitioner data. * Get employees with their thanatopractitioner data.

View File

@ -14,11 +14,9 @@
:data="employeeData" :data="employeeData"
:loading="loadingData" :loading="loadingData"
:pagination="pagination" :pagination="pagination"
:search="search"
@view="goToDetails" @view="goToDetails"
@delete="deleteEmployee" @delete="deleteEmployee"
@page-change="handleChangePage" @changePage="handleChangePage"
@search-change="handleSearchChange"
/> />
</template> </template>
</employee-template> </employee-template>
@ -34,12 +32,7 @@ import { useRouter } from "vue-router";
const router = useRouter(); const router = useRouter();
const emit = defineEmits([ const emit = defineEmits(["pushDetails", "deleteEmployee", "changePage"]);
"pushDetails",
"deleteEmployee",
"changePage",
"searchChange",
]);
const props = defineProps({ const props = defineProps({
employeeData: { employeeData: {
@ -54,10 +47,6 @@ const props = defineProps({
type: Object, type: Object,
default: null, default: null,
}, },
search: {
type: String,
default: "",
},
}); });
const goToEmployee = () => { const goToEmployee = () => {
@ -88,8 +77,8 @@ const handleChangePage = (page) => {
emit("changePage", page); emit("changePage", page);
} }
}; };
const handleSearchChange = (query) => {
emit("searchChange", query);
};
</script> </script>
<style scoped>
/* Component-specific styles */
</style>

View File

@ -66,7 +66,7 @@
<strong>1.</strong> Définir nom et email. <strong>1.</strong> Définir nom et email.
</p> </p>
<p class="text-sm mb-2"> <p class="text-sm mb-2">
<strong>2.</strong> Le mot de passe initial est optionnel. <strong>2.</strong> Saisir un mot de passe initial.
</p> </p>
<p class="text-sm mb-0"> <p class="text-sm mb-0">
<strong>3.</strong> Assigner le rôle adapté au périmètre <strong>3.</strong> Assigner le rôle adapté au périmètre

View File

@ -1,60 +1,72 @@
<template> <template>
<div class="table-container"> <div class="table-container">
<!-- Loading State -->
<div v-if="loading" class="loading-container"> <div v-if="loading" class="loading-container">
<div class="loading-spinner"> <div class="loading-spinner">
<div <div class="spinner-border text-primary" role="status">
class="spinner-border text-success loading-spinner-circle"
role="status"
>
<span class="visually-hidden">Chargement...</span> <span class="visually-hidden">Chargement...</span>
</div> </div>
</div> </div>
<div class="loading-content"> <div class="loading-content">
<!-- Skeleton Rows -->
<div class="table-responsive"> <div class="table-responsive">
<table class="table table-flush"> <table class="table table-flush">
<thead class="thead-light"> <thead class="thead-light">
<tr> <tr>
<th>Employé</th> <th>ID</th>
<th>Référence</th> <th>Nom & Prénom</th>
<th>Email</th>
<th>Téléphone</th>
<th>Poste</th> <th>Poste</th>
<th>Coordonnées</th>
<th>Date d'embauche</th> <th>Date d'embauche</th>
<th>Compte lié</th>
<th>Status</th> <th>Status</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr v-for="i in skeletonRows" :key="i" class="skeleton-row"> <tr v-for="i in skeletonRows" :key="i" class="skeleton-row">
<!-- ID Column Skeleton -->
<td>
<div class="d-flex align-items-center">
<div class="skeleton-checkbox"></div>
<div class="skeleton-text short ms-2"></div>
</div>
</td>
<!-- Name Column Skeleton -->
<td> <td>
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
<div class="skeleton-avatar"></div> <div class="skeleton-avatar"></div>
<div class="skeleton-text medium ms-2"></div> <div class="skeleton-text medium ms-2"></div>
</div> </div>
</td> </td>
<!-- Email Column Skeleton -->
<td> <td>
<div class="skeleton-text short"></div> <div class="skeleton-text long"></div>
</td> </td>
<!-- Phone Column Skeleton -->
<td>
<div class="skeleton-text medium"></div>
</td>
<!-- Position Column Skeleton -->
<td> <td>
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
<div class="skeleton-icon"></div> <div class="skeleton-icon"></div>
<div class="skeleton-text medium ms-2"></div> <div class="skeleton-text medium ms-2"></div>
</div> </div>
</td> </td>
<td>
<div class="contact-info"> <!-- Hire Date Column Skeleton -->
<div class="skeleton-text long mb-1"></div>
<div class="skeleton-text medium"></div>
</div>
</td>
<td>
<div class="skeleton-text medium"></div>
</td>
<td> <td>
<div class="skeleton-text medium"></div> <div class="skeleton-text medium"></div>
</td> </td>
<!-- Status Column Skeleton -->
<td> <td>
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
<div class="skeleton-icon"></div> <div class="skeleton-icon small"></div>
<div class="skeleton-text short ms-2"></div> <div class="skeleton-text short ms-2"></div>
</div> </div>
</td> </td>
@ -65,201 +77,196 @@
</div> </div>
</div> </div>
<div v-else class="table-responsive"> <!-- Data State -->
<table id="employee-list" class="table table-flush"> <div v-else>
<thead class="thead-light"> <div class="table-responsive">
<tr> <table id="employee-list" class="table table-flush">
<th>Employé</th> <thead class="thead-light">
<th>Référence</th> <tr>
<th>Poste</th> <th>ID</th>
<th>Coordonnées</th> <th>Nom & Prénom</th>
<th>Date d'embauche</th> <th>Email</th>
<th>Compte lié</th> <th>Téléphone</th>
<th>Status</th> <th>Poste</th>
<th>Action</th> <th>Date d'embauche</th>
</tr> <th>Status</th>
</thead> <th>Action</th>
<tbody> </tr>
<tr v-for="employee in data" :key="employee.id"> </thead>
<td class="font-weight-bold"> <tbody>
<div class="d-flex align-items-center"> <tr v-for="employee in data" :key="employee.id">
<soft-avatar <!-- ID Column -->
:img="getRandomAvatar()" <td>
size="xs" <div class="d-flex align-items-center">
class="me-2" <soft-checkbox />
alt="employee image" <p class="text-xs font-weight-bold ms-2 mb-0">
circular {{ employee.id }}
/> </p>
<div> </div>
<span </td>
>{{ employee.last_name }} {{ employee.first_name }}</span
> <!-- Name Column -->
<div <td class="font-weight-bold">
v-if="employee.thanatopractitioner" <div class="d-flex align-items-center">
class="text-xs text-info" <soft-avatar
> :img="getRandomAvatar()"
Thanatopractitioner size="xs"
class="me-2"
alt="user image"
circular
/>
<div>
<span
>{{ employee.last_name }} {{ employee.first_name }}</span
>
<div
v-if="employee.thanatopractitioner"
class="text-xs text-info"
>
Thanatopractitioner
</div>
</div> </div>
</div> </div>
</div> </td>
</td>
<td class="text-xs font-weight-bold"> <!-- Email Column -->
<span class="my-2 text-xs">EMP-{{ employee.id }}</span> <td class="text-xs font-weight-bold">
</td> <span class="text-xs">{{ employee.email || "N/A" }}</span>
</td>
<td class="text-xs font-weight-bold"> <!-- Phone Column -->
<div class="d-flex align-items-center"> <td class="text-xs font-weight-bold">
<soft-button <span class="text-xs">{{ employee.phone || "N/A" }}</span>
:color="getPositionColor(employee.job_title)" </td>
variant="outline"
class="btn-icon-only btn-rounded mb-0 me-2 btn-sm d-flex align-items-center justify-content-center"
>
<i
:class="getPositionIcon(employee.job_title)"
aria-hidden="true"
></i>
</soft-button>
<span>{{ employee.job_title || "Non renseigné" }}</span>
</div>
</td>
<td class="text-xs font-weight-bold"> <!-- Position Column -->
<div class="contact-info"> <td class="text-xs font-weight-bold">
<div class="text-xs text-secondary"> <div class="d-flex align-items-center">
{{ employee.email || "N/A" }} <soft-button
:color="getPositionColor(employee.job_title)"
variant="outline"
class="btn-icon-only btn-rounded mb-0 me-2 btn-sm d-flex align-items-center justify-content-center"
>
<i
:class="getPositionIcon(employee.job_title)"
aria-hidden="true"
></i>
</soft-button>
<span>{{ employee.job_title || "N/A" }}</span>
</div> </div>
<div class="text-xs">{{ employee.phone || "N/A" }}</div> </td>
</div>
</td>
<td class="text-xs font-weight-bold"> <!-- Hire Date Column -->
{{ formatDate(employee.hire_date) }} <td class="text-xs font-weight-bold">
</td> <span class="text-xs">{{
formatDate(employee.hire_date)
}}</span>
</td>
<td class="text-xs font-weight-bold"> <!-- Status Column -->
<div class="d-flex flex-column"> <td class="text-xs font-weight-bold">
<span>{{ employee.user?.name || "Aucun compte" }}</span> <div class="d-flex align-items-center">
<span class="text-xs text-muted"> <soft-button
{{ employee.user?.email || "Non lié" }} :color="employee.active ? 'success' : 'danger'"
</span> variant="outline"
</div> class="btn-icon-only btn-rounded mb-0 me-2 btn-sm d-flex align-items-center justify-content-center"
</td> >
<i
:class="employee.active ? 'fas fa-check' : 'fas fa-times'"
aria-hidden="true"
></i>
</soft-button>
<span>{{ employee.active ? "Actif" : "Inactif" }}</span>
</div>
</td>
<td class="text-xs font-weight-bold"> <td>
<div class="d-flex flex-column"> <div class="d-flex align-items-center gap-2">
<soft-button <!-- View Button -->
v-if="employee.active" <soft-button
color="success" color="info"
variant="outline" variant="outline"
class="btn-sm" title="Voir l'employé"
> :data-employee-id="employee.id"
<i class="fas fa-check me-1"></i> class="btn-icon-only btn-rounded mb-0 btn-sm d-flex align-items-center justify-content-center"
Actif @click="handleView(employee.id)"
</soft-button> >
<soft-button <i class="fas fa-eye" aria-hidden="true"></i>
v-else </soft-button>
color="danger"
variant="outline"
class="btn-sm"
>
<i class="fas fa-times me-1"></i>
Inactif
</soft-button>
</div>
</td>
<td> <!-- Delete Button -->
<div class="d-flex align-items-center gap-2"> <soft-button
<soft-button color="danger"
color="info" variant="outline"
variant="outline" title="Supprimer l'employé"
title="Voir l'employé" :data-employee-id="employee.id"
:data-employee-id="employee.id" class="btn-icon-only btn-rounded mb-0 btn-sm d-flex align-items-center justify-content-center"
class="btn-icon-only btn-rounded mb-0 btn-sm d-flex align-items-center justify-content-center" @click="handleDelete(employee.id)"
> >
<i class="fas fa-eye" aria-hidden="true"></i> <i class="fas fa-trash" aria-hidden="true"></i>
</soft-button> </soft-button>
</div>
<soft-button </td>
color="danger" </tr>
variant="outline" </tbody>
title="Supprimer l'employé" </table>
:data-employee-id="employee.id"
class="btn-icon-only btn-rounded mb-0 btn-sm d-flex align-items-center justify-content-center"
>
<i class="fas fa-trash" aria-hidden="true"></i>
</soft-button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<div
v-if="!loading && data.length > 0 && (pagination?.last_page || 1) > 1"
class="d-flex justify-content-between align-items-center mt-3 px-3 flex-wrap gap-3"
>
<div class="text-xs text-secondary font-weight-bold">
Affichage de {{ safeFrom }} à {{ safeTo }} sur
{{ pagination.total || data.length }} employés
</div> </div>
<nav aria-label="Pagination employés"> <!-- Custom Pagination Controls -->
<ul class="pagination pagination-sm pagination-success mb-0"> <div
<li v-if="pagination.total > pagination.per_page"
class="page-item" class="d-flex justify-content-between align-items-center mt-3"
:class="{ disabled: (pagination.current_page || 1) === 1 }" >
<div class="text-sm text-muted">
Affichage de {{ pagination.from }} à {{ pagination.to }} sur
{{ pagination.total }} employés
</div>
<div class="d-flex align-items-center gap-2">
<!-- Previous Button -->
<soft-button
color="outline"
variant="outline"
class="btn-sm"
:disabled="pagination.current_page === 1 || loading"
@click="changePage(pagination.current_page - 1)"
> >
<a <i class="fas fa-chevron-left me-1"></i>
class="page-link" Précédent
href="#" </soft-button>
aria-label="Previous"
@click.prevent="changePage((pagination.current_page || 1) - 1)"
>
<span aria-hidden="true">
<i class="fa fa-angle-left" aria-hidden="true"></i>
</span>
</a>
</li>
<li <!-- Page Numbers -->
v-for="page in displayedPages" <div class="d-flex gap-1">
:key="`page-${page}`" <soft-button
class="page-item" v-for="page in getVisiblePages()"
:class="{ :key="page"
active: (pagination.current_page || 1) === page, :color="page === pagination.current_page ? 'primary' : 'outline'"
disabled: page === '...', variant="outline"
}" class="btn-sm"
> :disabled="loading"
<a class="page-link" href="#" @click.prevent="changePage(page)"> @click="changePage(page)"
>
{{ page }} {{ page }}
</a> </soft-button>
</li> </div>
<li <!-- Next Button -->
class="page-item" <soft-button
:class="{ color="outline"
disabled: variant="outline"
(pagination.current_page || 1) === (pagination.last_page || 1), class="btn-sm"
}" :disabled="
pagination.current_page === pagination.last_page || loading
"
@click="changePage(pagination.current_page + 1)"
> >
<a Suivant
class="page-link" <i class="fas fa-chevron-right ms-1"></i>
href="#" </soft-button>
aria-label="Next" </div>
@click.prevent="changePage((pagination.current_page || 1) + 1)" </div>
>
<span aria-hidden="true">
<i class="fa fa-angle-right" aria-hidden="true"></i>
</span>
</a>
</li>
</ul>
</nav>
</div> </div>
<!-- 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">
<i class="fas fa-users fa-3x text-muted"></i> <i class="fas fa-users fa-3x text-muted"></i>
@ -273,21 +280,16 @@
</template> </template>
<script setup> <script setup>
import { import { ref, onMounted, watch, onUnmounted } from "vue";
computed, // import { DataTable } from "simple-datatables"; // Disabled to avoid interference
defineEmits, import SoftCheckbox from "@/components/SoftCheckbox.vue";
defineProps,
onMounted,
onUnmounted,
ref,
watch,
} from "vue";
import { DataTable } from "simple-datatables";
import SoftButton from "@/components/SoftButton.vue"; import SoftButton from "@/components/SoftButton.vue";
import SoftAvatar from "@/components/SoftAvatar.vue"; import SoftAvatar from "@/components/SoftAvatar.vue";
import { defineProps, defineEmits } from "vue";
const emit = defineEmits(["view", "delete", "page-change", "search-change"]); const emit = defineEmits(["view", "delete", "changePage"]);
// Sample avatar images
import img1 from "@/assets/img/team-2.jpg"; import img1 from "@/assets/img/team-2.jpg";
import img2 from "@/assets/img/team-1.jpg"; import img2 from "@/assets/img/team-1.jpg";
import img3 from "@/assets/img/team-3.jpg"; import img3 from "@/assets/img/team-3.jpg";
@ -297,7 +299,8 @@ import img6 from "@/assets/img/ivana-squares.jpg";
const avatarImages = [img1, img2, img3, img4, img5, img6]; const avatarImages = [img1, img2, img3, img4, img5, img6];
const dataTableInstance = ref(null); // Reactive data - DataTable disabled
// const dataTableInstance = ref(null);
const props = defineProps({ const props = defineProps({
data: { data: {
@ -323,85 +326,9 @@ const props = defineProps({
to: 0, to: 0,
}), }),
}, },
search: {
type: String,
default: "",
},
});
let searchInputHandler = null;
const displayedPages = computed(() => {
const total = Number(props.pagination?.last_page) || 1;
const current = Number(props.pagination?.current_page) || 1;
if (total <= 1) {
return [1];
}
const delta = 2;
const range = [];
for (
let page = Math.max(2, current - delta);
page <= Math.min(total - 1, current + delta);
page++
) {
range.push(page);
}
if (current - delta > 2) {
range.unshift("...");
}
if (current + delta < total - 1) {
range.push("...");
}
range.unshift(1);
if (total > 1) {
range.push(total);
}
return range.filter(
(value, index, self) =>
value !== "..." || (value === "..." && self[index - 1] !== "...")
);
});
const safeFrom = computed(() => {
if (props.pagination?.from) {
return props.pagination.from;
}
if (!props.pagination?.total || props.data.length === 0) {
return 0;
}
return (
((Number(props.pagination.current_page) || 1) - 1) *
(Number(props.pagination.per_page) || 10) +
1
);
});
const safeTo = computed(() => {
if (props.pagination?.to) {
return props.pagination.to;
}
if (!props.pagination?.total || props.data.length === 0) {
return 0;
}
return Math.min(
(Number(props.pagination.current_page) || 1) *
(Number(props.pagination.per_page) || 10),
Number(props.pagination.total) || 0
);
}); });
// Methods
const getRandomAvatar = () => { const getRandomAvatar = () => {
const randomIndex = Math.floor(Math.random() * avatarImages.length); const randomIndex = Math.floor(Math.random() * avatarImages.length);
return avatarImages[randomIndex]; return avatarImages[randomIndex];
@ -435,27 +362,69 @@ const getPositionIcon = (position) => {
return icons[position] || "fas fa-user"; return icons[position] || "fas fa-user";
}; };
const handleTableClick = (event) => { // Direct button handlers
const button = event.target.closest("button"); const handleView = (employeeId) => {
if (!button) return; console.log("Direct view button clicked for ID:", employeeId);
emit("view", employeeId);
};
const employeeId = button.getAttribute("data-employee-id"); const handleDelete = (employeeId) => {
if (!employeeId) return; console.log("Direct delete button clicked for ID:", employeeId);
emit("delete", employeeId);
};
if ( // Pagination methods
button.title === "Supprimer l'employé" || const changePage = (page) => {
button.querySelector(".fa-trash") console.log("changePage called in EmployeeTable with page:", page);
) { if (page >= 1 && page <= props.pagination.last_page) {
emit("delete", Number(employeeId)); console.log("Emitting changePage event from EmployeeTable:", page);
} else if ( emit("changePage", page);
button.title === "Voir l'employé" ||
button.querySelector(".fa-eye")
) {
emit("view", Number(employeeId));
} }
}; };
const getVisiblePages = () => {
const current = props.pagination.current_page;
const last = props.pagination.last_page;
const pages = [];
if (last <= 7) {
// Show all pages if 7 or fewer
for (let i = 1; i <= last; i++) {
pages.push(i);
}
} else {
// Show first page, current range, and last page
pages.push(1);
if (current > 3) {
pages.push("...");
}
const start = Math.max(2, current - 1);
const end = Math.min(last - 1, current + 1);
for (let i = start; i <= end; i++) {
if (!pages.includes(i)) {
pages.push(i);
}
}
if (current < last - 2) {
pages.push("...");
}
if (!pages.includes(last)) {
pages.push(last);
}
}
return pages;
};
// Commented out DataTable initialization
/*
const initializeDataTable = () => { const initializeDataTable = () => {
// Destroy existing instance if it exists
if (dataTableInstance.value) { if (dataTableInstance.value) {
dataTableInstance.value.destroy(); dataTableInstance.value.destroy();
dataTableInstance.value = null; dataTableInstance.value = null;
@ -463,84 +432,75 @@ const initializeDataTable = () => {
const dataTableEl = document.getElementById("employee-list"); const dataTableEl = document.getElementById("employee-list");
if (dataTableEl) { if (dataTableEl) {
// Initialize DataTable with search and default pagination
dataTableInstance.value = new DataTable(dataTableEl, { dataTableInstance.value = new DataTable(dataTableEl, {
searchable: true, searchable: true,
fixedHeight: true, fixedHeight: true,
paging: false, perPage: 10, // Default to 10 entries per page
perPage: Number(props.pagination?.per_page) || 10, perPageSelect: false, // Disable per-page selector since we handle it server-side
perPageSelect: false, pager: false, // Disable DataTable pagination since we handle it server-side
}); });
// Add click listener for action buttons
dataTableEl.addEventListener("click", handleTableClick); dataTableEl.addEventListener("click", handleTableClick);
const searchInput =
document.querySelector("#employee-list .dataTable-input") ||
document.querySelector(".dataTable-input");
if (searchInput instanceof HTMLInputElement) {
searchInput.placeholder = "Rechercher un employé par nom";
searchInput.value = props.search || "";
searchInputHandler = (event) => {
emit("search-change", event.target.value);
};
searchInput.removeEventListener("input", searchInputHandler);
searchInput.addEventListener("input", searchInputHandler);
}
} }
}; };
const changePage = (page) => { const handleTableClick = (event) => {
console.log("Table click detected:", event.target);
const button = event.target.closest("button");
if (!button) return;
const employeeId = button.getAttribute("data-employee-id");
console.log("Employee ID:", employeeId);
console.log("Button title:", button.title);
if ( if (
page !== "..." && button.title === "Supprimer l'employé" ||
page >= 1 && button.querySelector(".fa-trash")
page <= (props.pagination?.last_page || 1) &&
page !== Number(props.pagination?.current_page)
) { ) {
emit("page-change", page); console.log("Delete button clicked!");
emit("delete", employeeId);
} else if (
button.title === "Voir l'employé" ||
button.querySelector(".fa-eye")
) {
console.log("View button clicked!");
emit("view", employeeId);
} }
}; };
*/
watch( // Watch for data changes
() => props.search,
(value) => {
const searchInput = document.querySelector(".dataTable-input");
if (
searchInput instanceof HTMLInputElement &&
searchInput.value !== value
) {
searchInput.value = value || "";
}
}
);
watch( watch(
() => props.data, () => props.data,
() => { () => {
if (!props.loading) { if (!props.loading) {
setTimeout(() => { console.log("EmployeeTable: Data changed");
initializeDataTable();
}, 100);
} }
}, },
{ deep: true } { deep: true }
); );
onUnmounted(() => { onUnmounted(() => {
const dataTableEl = document.getElementById("employee-list"); // Clean up any event listeners if needed
if (dataTableEl) { // const dataTableEl = document.getElementById("employee-list");
dataTableEl.removeEventListener("click", handleTableClick); // if (dataTableEl) {
} // dataTableEl.removeEventListener("click", handleTableClick);
// }
if (dataTableInstance.value) { // if (dataTableInstance.value) {
dataTableInstance.value.destroy(); // dataTableInstance.value.destroy();
} // }
}); });
// Initialize data
onMounted(() => { onMounted(() => {
if (!props.loading && props.data.length > 0) { if (!props.loading && props.data.length > 0) {
initializeDataTable(); console.log(
"EmployeeTable: Component mounted with",
props.data.length,
"employees"
);
} }
}); });
</script> </script>
@ -553,30 +513,31 @@ onMounted(() => {
.loading-container { .loading-container {
position: relative; position: relative;
min-height: 260px;
} }
.loading-spinner { .loading-spinner {
position: absolute; position: absolute;
top: 50%; top: 20px;
left: 50%; right: 20px;
transform: translate(-50%, -50%);
z-index: 10; z-index: 10;
} }
.loading-spinner-circle {
width: 2.25rem;
height: 2.25rem;
border-width: 0.28em;
}
.loading-content { .loading-content {
opacity: 0.55; opacity: 0.7;
pointer-events: none; pointer-events: none;
} }
.skeleton-row { .skeleton-row {
animation: none; animation: pulse 1.5s ease-in-out infinite;
}
.skeleton-checkbox {
width: 18px;
height: 18px;
border-radius: 3px;
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: shimmer 2s infinite;
} }
.skeleton-avatar { .skeleton-avatar {
@ -597,6 +558,15 @@ onMounted(() => {
animation: shimmer 2s infinite; animation: shimmer 2s infinite;
} }
.skeleton-icon.small {
width: 24px;
height: 24px;
border-radius: 50%;
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: shimmer 2s infinite;
}
.skeleton-text { .skeleton-text {
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%); background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%; background-size: 200% 100%;
@ -637,13 +607,21 @@ onMounted(() => {
margin: 0 auto; margin: 0 auto;
} }
.address-info,
.text-xs { .text-xs {
font-size: 0.75rem; font-size: 0.75rem;
} }
.contact-info { /* Animations */
line-height: 1.2; @keyframes pulse {
0% {
opacity: 1;
}
50% {
opacity: 0.7;
}
100% {
opacity: 1;
}
} }
@keyframes shimmer { @keyframes shimmer {
@ -655,7 +633,13 @@ onMounted(() => {
} }
} }
/* Responsive adjustments */
@media (max-width: 768px) { @media (max-width: 768px) {
.loading-spinner {
top: 10px;
right: 10px;
}
.skeleton-text.long { .skeleton-text.long {
width: 80px; width: 80px;
} }

View File

@ -3,11 +3,9 @@
:employee-data="employeeStore.employees" :employee-data="employeeStore.employees"
:loading-data="employeeStore.loading" :loading-data="employeeStore.loading"
:pagination="employeeStore.getPagination" :pagination="employeeStore.getPagination"
:search="search"
@push-details="goDetails" @push-details="goDetails"
@delete-employee="confirmDeleteEmployee" @delete-employee="confirmDeleteEmployee"
@change-page="changePage" @change-page="changePage"
@search-change="updateSearch"
/> />
<!-- Confirm Delete Modal --> <!-- Confirm Delete Modal -->
@ -27,7 +25,7 @@
</template> </template>
<script setup> <script setup>
import { reactive, onMounted, ref } from "vue"; import { reactive, onMounted } from "vue";
import EmployeePresentation from "@/components/Organism/Employee/EmployeePresentation.vue"; import EmployeePresentation from "@/components/Organism/Employee/EmployeePresentation.vue";
import ConfirmModal from "@/components/molecules/common/ConfirmModal.vue"; import ConfirmModal from "@/components/molecules/common/ConfirmModal.vue";
import { useEmployeeStore } from "@/stores/employeeStore"; import { useEmployeeStore } from "@/stores/employeeStore";
@ -37,9 +35,6 @@ import { useRouter } from "vue-router";
const employeeStore = useEmployeeStore(); const employeeStore = useEmployeeStore();
const notificationStore = useNotificationStore(); const notificationStore = useNotificationStore();
const router = useRouter(); const router = useRouter();
const search = ref("");
let searchDebounceTimeout = null;
const DEFAULT_PER_PAGE = 10;
// Confirm modal state // Confirm modal state
const confirmModal = reactive({ const confirmModal = reactive({
@ -57,11 +52,7 @@ const confirmModal = reactive({
}); });
onMounted(async () => { onMounted(async () => {
await employeeStore.fetchEmployees({ await employeeStore.fetchEmployees();
page: 1,
per_page: DEFAULT_PER_PAGE,
search: search.value.trim(),
});
}); });
const goDetails = (id) => { const goDetails = (id) => {
@ -152,12 +143,10 @@ const closeConfirmModal = () => {
}; };
const changePage = async (page) => { const changePage = async (page) => {
console.log("changePage called in Employees.vue with page:", page);
try { try {
await employeeStore.fetchEmployees({ console.log("Fetching employees with page:", page);
page, await employeeStore.fetchEmployees({ page, per_page: 10 });
per_page: employeeStore.getPagination.per_page || DEFAULT_PER_PAGE,
search: search.value.trim(),
});
} catch (error) { } catch (error) {
console.error("Error changing page:", error); console.error("Error changing page:", error);
notificationStore.error( notificationStore.error(
@ -166,28 +155,4 @@ const changePage = async (page) => {
); );
} }
}; };
const updateSearch = (value) => {
search.value = value;
if (searchDebounceTimeout) {
window.clearTimeout(searchDebounceTimeout);
}
searchDebounceTimeout = window.setTimeout(async () => {
try {
await employeeStore.fetchEmployees({
page: 1,
per_page: employeeStore.getPagination.per_page || DEFAULT_PER_PAGE,
search: search.value.trim(),
});
} catch (error) {
console.error("Error searching employees:", error);
notificationStore.error(
"Erreur de recherche",
"Une erreur est survenue lors de la recherche des employés."
);
}
}, 300);
};
</script> </script>

View File

@ -15,15 +15,14 @@
<script setup> <script setup>
import { computed, onMounted, reactive } from "vue"; import { computed, onMounted, reactive } from "vue";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import Swal from "sweetalert2";
import UserCreatePresentation from "@/components/Organism/Parametrage/Users/UserCreatePresentation.vue"; import UserCreatePresentation from "@/components/Organism/Parametrage/Users/UserCreatePresentation.vue";
import { useAccessControlStore } from "@/stores/accessControlStore"; import { useAccessControlStore } from "@/stores/accessControlStore";
import { useNotificationStore } from "@/stores/notification";
import { useUserStore } from "@/stores/userStore"; import { useUserStore } from "@/stores/userStore";
const router = useRouter(); const router = useRouter();
const userStore = useUserStore(); const userStore = useUserStore();
const accessControlStore = useAccessControlStore(); const accessControlStore = useAccessControlStore();
const notificationStore = useNotificationStore();
const form = reactive({ const form = reactive({
id: null, id: null,
@ -40,7 +39,8 @@ const submitDisabled = computed(
userStore.isLoading || userStore.isLoading ||
accessControlStore.isLoading || accessControlStore.isLoading ||
!form.name.trim() || !form.name.trim() ||
!form.email.trim() !form.email.trim() ||
!form.password.trim()
); );
onMounted(async () => { onMounted(async () => {
@ -80,19 +80,26 @@ const submitUser = async () => {
const user = await userStore.createUser({ const user = await userStore.createUser({
name: form.name.trim(), name: form.name.trim(),
email: form.email.trim(), email: form.email.trim(),
password: form.password.trim() || null, password: form.password.trim(),
roles: form.roles, roles: form.roles,
permissions: form.permissions, permissions: form.permissions,
}); });
notificationStore.created("L'utilisateur"); await Swal.fire({
icon: "success",
title: "Succès",
text: "L'utilisateur a été créé avec succès.",
confirmButtonText: "Voir le détail",
});
router.push(`/parametrage/utilisateurs/${user.id}`); router.push(`/parametrage/utilisateurs/${user.id}`);
} catch { } catch {
notificationStore.error( await Swal.fire({
"Erreur de création", icon: "error",
userStore.error || "Impossible de créer l'utilisateur." title: "Erreur",
); text: userStore.error || "Impossible de créer l'utilisateur.",
confirmButtonText: "Fermer",
});
} }
}; };
</script> </script>