Ajout pagination table employee
This commit is contained in:
parent
34875321ac
commit
5c8779cb6a
@ -86,9 +86,28 @@ class EmployeeRepository extends BaseRepository implements EmployeeRepositoryInt
|
||||
/**
|
||||
* Get employees with pagination.
|
||||
*/
|
||||
public function getPaginated(int $perPage = 10): array
|
||||
public function getPaginated(int $perPage = 10, array $filters = []): array
|
||||
{
|
||||
$paginator = $this->model->newQuery()->with(['thanatopractitioner', 'user'])->paginate($perPage);
|
||||
$query = $this->model->newQuery()->with(['thanatopractitioner', 'user']);
|
||||
|
||||
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 [
|
||||
'employees' => $paginator->getCollection(),
|
||||
@ -97,6 +116,8 @@ class EmployeeRepository extends BaseRepository implements EmployeeRepositoryInt
|
||||
'last_page' => $paginator->lastPage(),
|
||||
'per_page' => $paginator->perPage(),
|
||||
'total' => $paginator->total(),
|
||||
'from' => $paginator->firstItem(),
|
||||
'to' => $paginator->lastItem(),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
@ -62,9 +62,10 @@ interface EmployeeRepositoryInterface
|
||||
* Get employees with pagination.
|
||||
*
|
||||
* @param int $perPage
|
||||
* @param array<string, mixed> $filters
|
||||
* @return array{employees: Collection<int, Employee>, pagination: array}
|
||||
*/
|
||||
public function getPaginated(int $perPage = 10): array;
|
||||
public function getPaginated(int $perPage = 10, array $filters = []): array;
|
||||
|
||||
/**
|
||||
* Get employees with their thanatopractitioner data.
|
||||
|
||||
@ -14,9 +14,11 @@
|
||||
:data="employeeData"
|
||||
:loading="loadingData"
|
||||
:pagination="pagination"
|
||||
:search="search"
|
||||
@view="goToDetails"
|
||||
@delete="deleteEmployee"
|
||||
@changePage="handleChangePage"
|
||||
@page-change="handleChangePage"
|
||||
@search-change="handleSearchChange"
|
||||
/>
|
||||
</template>
|
||||
</employee-template>
|
||||
@ -32,7 +34,12 @@ import { useRouter } from "vue-router";
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const emit = defineEmits(["pushDetails", "deleteEmployee", "changePage"]);
|
||||
const emit = defineEmits([
|
||||
"pushDetails",
|
||||
"deleteEmployee",
|
||||
"changePage",
|
||||
"searchChange",
|
||||
]);
|
||||
|
||||
const props = defineProps({
|
||||
employeeData: {
|
||||
@ -47,6 +54,10 @@ const props = defineProps({
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
search: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
});
|
||||
|
||||
const goToEmployee = () => {
|
||||
@ -77,8 +88,8 @@ const handleChangePage = (page) => {
|
||||
emit("changePage", page);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* Component-specific styles */
|
||||
</style>
|
||||
const handleSearchChange = (query) => {
|
||||
emit("searchChange", query);
|
||||
};
|
||||
</script>
|
||||
|
||||
@ -1,72 +1,60 @@
|
||||
<template>
|
||||
<div class="table-container">
|
||||
<!-- Loading State -->
|
||||
<div v-if="loading" class="loading-container">
|
||||
<div class="loading-spinner">
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<div
|
||||
class="spinner-border text-success loading-spinner-circle"
|
||||
role="status"
|
||||
>
|
||||
<span class="visually-hidden">Chargement...</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="loading-content">
|
||||
<!-- Skeleton Rows -->
|
||||
<div class="table-responsive">
|
||||
<table class="table table-flush">
|
||||
<thead class="thead-light">
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Nom & Prénom</th>
|
||||
<th>Email</th>
|
||||
<th>Téléphone</th>
|
||||
<th>Employé</th>
|
||||
<th>Référence</th>
|
||||
<th>Poste</th>
|
||||
<th>Coordonnées</th>
|
||||
<th>Date d'embauche</th>
|
||||
<th>Compte lié</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<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>
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="skeleton-avatar"></div>
|
||||
<div class="skeleton-text medium ms-2"></div>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<!-- Email Column Skeleton -->
|
||||
<td>
|
||||
<div class="skeleton-text long"></div>
|
||||
<div class="skeleton-text short"></div>
|
||||
</td>
|
||||
|
||||
<!-- Phone Column Skeleton -->
|
||||
<td>
|
||||
<div class="skeleton-text medium"></div>
|
||||
</td>
|
||||
|
||||
<!-- Position Column Skeleton -->
|
||||
<td>
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="skeleton-icon"></div>
|
||||
<div class="skeleton-text medium ms-2"></div>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<!-- Hire Date Column Skeleton -->
|
||||
<td>
|
||||
<div class="contact-info">
|
||||
<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>
|
||||
<div class="skeleton-text medium"></div>
|
||||
</td>
|
||||
|
||||
<!-- Status Column Skeleton -->
|
||||
<td>
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="skeleton-icon small"></div>
|
||||
<div class="skeleton-icon"></div>
|
||||
<div class="skeleton-text short ms-2"></div>
|
||||
</div>
|
||||
</td>
|
||||
@ -77,42 +65,29 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Data State -->
|
||||
<div v-else>
|
||||
<div class="table-responsive">
|
||||
<div v-else class="table-responsive">
|
||||
<table id="employee-list" class="table table-flush">
|
||||
<thead class="thead-light">
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Nom & Prénom</th>
|
||||
<th>Email</th>
|
||||
<th>Téléphone</th>
|
||||
<th>Employé</th>
|
||||
<th>Référence</th>
|
||||
<th>Poste</th>
|
||||
<th>Coordonnées</th>
|
||||
<th>Date d'embauche</th>
|
||||
<th>Compte lié</th>
|
||||
<th>Status</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="employee in data" :key="employee.id">
|
||||
<!-- ID Column -->
|
||||
<td>
|
||||
<div class="d-flex align-items-center">
|
||||
<soft-checkbox />
|
||||
<p class="text-xs font-weight-bold ms-2 mb-0">
|
||||
{{ employee.id }}
|
||||
</p>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<!-- Name Column -->
|
||||
<td class="font-weight-bold">
|
||||
<div class="d-flex align-items-center">
|
||||
<soft-avatar
|
||||
:img="getRandomAvatar()"
|
||||
size="xs"
|
||||
class="me-2"
|
||||
alt="user image"
|
||||
alt="employee image"
|
||||
circular
|
||||
/>
|
||||
<div>
|
||||
@ -129,17 +104,10 @@
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<!-- Email Column -->
|
||||
<td class="text-xs font-weight-bold">
|
||||
<span class="text-xs">{{ employee.email || "N/A" }}</span>
|
||||
<span class="my-2 text-xs">EMP-{{ employee.id }}</span>
|
||||
</td>
|
||||
|
||||
<!-- Phone Column -->
|
||||
<td class="text-xs font-weight-bold">
|
||||
<span class="text-xs">{{ employee.phone || "N/A" }}</span>
|
||||
</td>
|
||||
|
||||
<!-- Position Column -->
|
||||
<td class="text-xs font-weight-bold">
|
||||
<div class="d-flex align-items-center">
|
||||
<soft-button
|
||||
@ -152,56 +120,73 @@
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
</soft-button>
|
||||
<span>{{ employee.job_title || "N/A" }}</span>
|
||||
<span>{{ employee.job_title || "Non renseigné" }}</span>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<!-- Hire Date Column -->
|
||||
<td class="text-xs font-weight-bold">
|
||||
<span class="text-xs">{{
|
||||
formatDate(employee.hire_date)
|
||||
}}</span>
|
||||
<div class="contact-info">
|
||||
<div class="text-xs text-secondary">
|
||||
{{ employee.email || "N/A" }}
|
||||
</div>
|
||||
<div class="text-xs">{{ employee.phone || "N/A" }}</div>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<!-- Status Column -->
|
||||
<td class="text-xs font-weight-bold">
|
||||
<div class="d-flex align-items-center">
|
||||
{{ formatDate(employee.hire_date) }}
|
||||
</td>
|
||||
|
||||
<td class="text-xs font-weight-bold">
|
||||
<div class="d-flex flex-column">
|
||||
<span>{{ employee.user?.name || "Aucun compte" }}</span>
|
||||
<span class="text-xs text-muted">
|
||||
{{ employee.user?.email || "Non lié" }}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td class="text-xs font-weight-bold">
|
||||
<div class="d-flex flex-column">
|
||||
<soft-button
|
||||
:color="employee.active ? 'success' : 'danger'"
|
||||
v-if="employee.active"
|
||||
color="success"
|
||||
variant="outline"
|
||||
class="btn-icon-only btn-rounded mb-0 me-2 btn-sm d-flex align-items-center justify-content-center"
|
||||
class="btn-sm"
|
||||
>
|
||||
<i
|
||||
:class="employee.active ? 'fas fa-check' : 'fas fa-times'"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<i class="fas fa-check me-1"></i>
|
||||
Actif
|
||||
</soft-button>
|
||||
<soft-button
|
||||
v-else
|
||||
color="danger"
|
||||
variant="outline"
|
||||
class="btn-sm"
|
||||
>
|
||||
<i class="fas fa-times me-1"></i>
|
||||
Inactif
|
||||
</soft-button>
|
||||
<span>{{ employee.active ? "Actif" : "Inactif" }}</span>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<!-- View Button -->
|
||||
<soft-button
|
||||
color="info"
|
||||
variant="outline"
|
||||
title="Voir l'employé"
|
||||
:data-employee-id="employee.id"
|
||||
class="btn-icon-only btn-rounded mb-0 btn-sm d-flex align-items-center justify-content-center"
|
||||
@click="handleView(employee.id)"
|
||||
>
|
||||
<i class="fas fa-eye" aria-hidden="true"></i>
|
||||
</soft-button>
|
||||
|
||||
<!-- Delete Button -->
|
||||
<soft-button
|
||||
color="danger"
|
||||
variant="outline"
|
||||
title="Supprimer l'employé"
|
||||
:data-employee-id="employee.id"
|
||||
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-trash" aria-hidden="true"></i>
|
||||
</soft-button>
|
||||
@ -212,61 +197,69 @@
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Custom Pagination Controls -->
|
||||
<div
|
||||
v-if="pagination.total > pagination.per_page"
|
||||
class="d-flex justify-content-between align-items-center mt-3"
|
||||
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-sm text-muted">
|
||||
Affichage de {{ pagination.from }} à {{ pagination.to }} sur
|
||||
{{ pagination.total }} employés
|
||||
<div class="text-xs text-secondary font-weight-bold">
|
||||
Affichage de {{ safeFrom }} à {{ safeTo }} sur
|
||||
{{ pagination.total || data.length }} 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)"
|
||||
>
|
||||
<i class="fas fa-chevron-left me-1"></i>
|
||||
Précédent
|
||||
</soft-button>
|
||||
|
||||
<!-- Page Numbers -->
|
||||
<div class="d-flex gap-1">
|
||||
<soft-button
|
||||
v-for="page in getVisiblePages()"
|
||||
:key="page"
|
||||
:color="page === pagination.current_page ? 'primary' : 'outline'"
|
||||
variant="outline"
|
||||
class="btn-sm"
|
||||
:disabled="loading"
|
||||
@click="changePage(page)"
|
||||
<nav aria-label="Pagination employés">
|
||||
<ul class="pagination pagination-sm pagination-success mb-0">
|
||||
<li
|
||||
class="page-item"
|
||||
:class="{ disabled: (pagination.current_page || 1) === 1 }"
|
||||
>
|
||||
<a
|
||||
class="page-link"
|
||||
href="#"
|
||||
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
|
||||
v-for="page in displayedPages"
|
||||
:key="`page-${page}`"
|
||||
class="page-item"
|
||||
:class="{
|
||||
active: (pagination.current_page || 1) === page,
|
||||
disabled: page === '...',
|
||||
}"
|
||||
>
|
||||
<a class="page-link" href="#" @click.prevent="changePage(page)">
|
||||
{{ page }}
|
||||
</soft-button>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
<!-- Next Button -->
|
||||
<soft-button
|
||||
color="outline"
|
||||
variant="outline"
|
||||
class="btn-sm"
|
||||
:disabled="
|
||||
pagination.current_page === pagination.last_page || loading
|
||||
"
|
||||
@click="changePage(pagination.current_page + 1)"
|
||||
<li
|
||||
class="page-item"
|
||||
:class="{
|
||||
disabled:
|
||||
(pagination.current_page || 1) === (pagination.last_page || 1),
|
||||
}"
|
||||
>
|
||||
Suivant
|
||||
<i class="fas fa-chevron-right ms-1"></i>
|
||||
</soft-button>
|
||||
</div>
|
||||
</div>
|
||||
<a
|
||||
class="page-link"
|
||||
href="#"
|
||||
aria-label="Next"
|
||||
@click.prevent="changePage((pagination.current_page || 1) + 1)"
|
||||
>
|
||||
<span aria-hidden="true">
|
||||
<i class="fa fa-angle-right" aria-hidden="true"></i>
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div v-if="!loading && data.length === 0" class="empty-state">
|
||||
<div class="empty-icon">
|
||||
<i class="fas fa-users fa-3x text-muted"></i>
|
||||
@ -280,16 +273,21 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, watch, onUnmounted } from "vue";
|
||||
// import { DataTable } from "simple-datatables"; // Disabled to avoid interference
|
||||
import SoftCheckbox from "@/components/SoftCheckbox.vue";
|
||||
import {
|
||||
computed,
|
||||
defineEmits,
|
||||
defineProps,
|
||||
onMounted,
|
||||
onUnmounted,
|
||||
ref,
|
||||
watch,
|
||||
} from "vue";
|
||||
import { DataTable } from "simple-datatables";
|
||||
import SoftButton from "@/components/SoftButton.vue";
|
||||
import SoftAvatar from "@/components/SoftAvatar.vue";
|
||||
import { defineProps, defineEmits } from "vue";
|
||||
|
||||
const emit = defineEmits(["view", "delete", "changePage"]);
|
||||
const emit = defineEmits(["view", "delete", "page-change", "search-change"]);
|
||||
|
||||
// Sample avatar images
|
||||
import img1 from "@/assets/img/team-2.jpg";
|
||||
import img2 from "@/assets/img/team-1.jpg";
|
||||
import img3 from "@/assets/img/team-3.jpg";
|
||||
@ -299,8 +297,7 @@ import img6 from "@/assets/img/ivana-squares.jpg";
|
||||
|
||||
const avatarImages = [img1, img2, img3, img4, img5, img6];
|
||||
|
||||
// Reactive data - DataTable disabled
|
||||
// const dataTableInstance = ref(null);
|
||||
const dataTableInstance = ref(null);
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
@ -326,9 +323,85 @@ const props = defineProps({
|
||||
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 randomIndex = Math.floor(Math.random() * avatarImages.length);
|
||||
return avatarImages[randomIndex];
|
||||
@ -362,69 +435,27 @@ const getPositionIcon = (position) => {
|
||||
return icons[position] || "fas fa-user";
|
||||
};
|
||||
|
||||
// Direct button handlers
|
||||
const handleView = (employeeId) => {
|
||||
console.log("Direct view button clicked for ID:", employeeId);
|
||||
emit("view", employeeId);
|
||||
};
|
||||
const handleTableClick = (event) => {
|
||||
const button = event.target.closest("button");
|
||||
if (!button) return;
|
||||
|
||||
const handleDelete = (employeeId) => {
|
||||
console.log("Direct delete button clicked for ID:", employeeId);
|
||||
emit("delete", employeeId);
|
||||
};
|
||||
const employeeId = button.getAttribute("data-employee-id");
|
||||
if (!employeeId) return;
|
||||
|
||||
// Pagination methods
|
||||
const changePage = (page) => {
|
||||
console.log("changePage called in EmployeeTable with page:", page);
|
||||
if (page >= 1 && page <= props.pagination.last_page) {
|
||||
console.log("Emitting changePage event from EmployeeTable:", page);
|
||||
emit("changePage", page);
|
||||
if (
|
||||
button.title === "Supprimer l'employé" ||
|
||||
button.querySelector(".fa-trash")
|
||||
) {
|
||||
emit("delete", Number(employeeId));
|
||||
} else if (
|
||||
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 = () => {
|
||||
// Destroy existing instance if it exists
|
||||
if (dataTableInstance.value) {
|
||||
dataTableInstance.value.destroy();
|
||||
dataTableInstance.value = null;
|
||||
@ -432,75 +463,84 @@ const initializeDataTable = () => {
|
||||
|
||||
const dataTableEl = document.getElementById("employee-list");
|
||||
if (dataTableEl) {
|
||||
// Initialize DataTable with search and default pagination
|
||||
dataTableInstance.value = new DataTable(dataTableEl, {
|
||||
searchable: true,
|
||||
fixedHeight: true,
|
||||
perPage: 10, // Default to 10 entries per page
|
||||
perPageSelect: false, // Disable per-page selector since we handle it server-side
|
||||
pager: false, // Disable DataTable pagination since we handle it server-side
|
||||
paging: false,
|
||||
perPage: Number(props.pagination?.per_page) || 10,
|
||||
perPageSelect: false,
|
||||
});
|
||||
|
||||
// Add click listener for action buttons
|
||||
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 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);
|
||||
|
||||
const changePage = (page) => {
|
||||
if (
|
||||
button.title === "Supprimer l'employé" ||
|
||||
button.querySelector(".fa-trash")
|
||||
page !== "..." &&
|
||||
page >= 1 &&
|
||||
page <= (props.pagination?.last_page || 1) &&
|
||||
page !== Number(props.pagination?.current_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);
|
||||
emit("page-change", page);
|
||||
}
|
||||
};
|
||||
*/
|
||||
|
||||
// Watch for data changes
|
||||
watch(
|
||||
() => props.search,
|
||||
(value) => {
|
||||
const searchInput = document.querySelector(".dataTable-input");
|
||||
if (
|
||||
searchInput instanceof HTMLInputElement &&
|
||||
searchInput.value !== value
|
||||
) {
|
||||
searchInput.value = value || "";
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.data,
|
||||
() => {
|
||||
if (!props.loading) {
|
||||
console.log("EmployeeTable: Data changed");
|
||||
setTimeout(() => {
|
||||
initializeDataTable();
|
||||
}, 100);
|
||||
}
|
||||
},
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
onUnmounted(() => {
|
||||
// Clean up any event listeners if needed
|
||||
// const dataTableEl = document.getElementById("employee-list");
|
||||
// if (dataTableEl) {
|
||||
// dataTableEl.removeEventListener("click", handleTableClick);
|
||||
// }
|
||||
// if (dataTableInstance.value) {
|
||||
// dataTableInstance.value.destroy();
|
||||
// }
|
||||
const dataTableEl = document.getElementById("employee-list");
|
||||
if (dataTableEl) {
|
||||
dataTableEl.removeEventListener("click", handleTableClick);
|
||||
}
|
||||
|
||||
if (dataTableInstance.value) {
|
||||
dataTableInstance.value.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize data
|
||||
onMounted(() => {
|
||||
if (!props.loading && props.data.length > 0) {
|
||||
console.log(
|
||||
"EmployeeTable: Component mounted with",
|
||||
props.data.length,
|
||||
"employees"
|
||||
);
|
||||
initializeDataTable();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@ -513,31 +553,30 @@ onMounted(() => {
|
||||
|
||||
.loading-container {
|
||||
position: relative;
|
||||
min-height: 260px;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.loading-spinner-circle {
|
||||
width: 2.25rem;
|
||||
height: 2.25rem;
|
||||
border-width: 0.28em;
|
||||
}
|
||||
|
||||
.loading-content {
|
||||
opacity: 0.7;
|
||||
opacity: 0.55;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.skeleton-row {
|
||||
animation: pulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.skeleton-checkbox {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 3px;
|
||||
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: shimmer 2s infinite;
|
||||
animation: none;
|
||||
}
|
||||
|
||||
.skeleton-avatar {
|
||||
@ -558,15 +597,6 @@ onMounted(() => {
|
||||
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 {
|
||||
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
|
||||
background-size: 200% 100%;
|
||||
@ -607,21 +637,13 @@ onMounted(() => {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.address-info,
|
||||
.text-xs {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.7;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
.contact-info {
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
@ -633,13 +655,7 @@ onMounted(() => {
|
||||
}
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.loading-spinner {
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
}
|
||||
|
||||
.skeleton-text.long {
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
@ -3,9 +3,11 @@
|
||||
:employee-data="employeeStore.employees"
|
||||
:loading-data="employeeStore.loading"
|
||||
:pagination="employeeStore.getPagination"
|
||||
:search="search"
|
||||
@push-details="goDetails"
|
||||
@delete-employee="confirmDeleteEmployee"
|
||||
@change-page="changePage"
|
||||
@search-change="updateSearch"
|
||||
/>
|
||||
|
||||
<!-- Confirm Delete Modal -->
|
||||
@ -25,7 +27,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { reactive, onMounted } from "vue";
|
||||
import { reactive, onMounted, ref } from "vue";
|
||||
import EmployeePresentation from "@/components/Organism/Employee/EmployeePresentation.vue";
|
||||
import ConfirmModal from "@/components/molecules/common/ConfirmModal.vue";
|
||||
import { useEmployeeStore } from "@/stores/employeeStore";
|
||||
@ -35,6 +37,9 @@ import { useRouter } from "vue-router";
|
||||
const employeeStore = useEmployeeStore();
|
||||
const notificationStore = useNotificationStore();
|
||||
const router = useRouter();
|
||||
const search = ref("");
|
||||
let searchDebounceTimeout = null;
|
||||
const DEFAULT_PER_PAGE = 10;
|
||||
|
||||
// Confirm modal state
|
||||
const confirmModal = reactive({
|
||||
@ -52,7 +57,11 @@ const confirmModal = reactive({
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
await employeeStore.fetchEmployees();
|
||||
await employeeStore.fetchEmployees({
|
||||
page: 1,
|
||||
per_page: DEFAULT_PER_PAGE,
|
||||
search: search.value.trim(),
|
||||
});
|
||||
});
|
||||
|
||||
const goDetails = (id) => {
|
||||
@ -143,10 +152,12 @@ const closeConfirmModal = () => {
|
||||
};
|
||||
|
||||
const changePage = async (page) => {
|
||||
console.log("changePage called in Employees.vue with page:", page);
|
||||
try {
|
||||
console.log("Fetching employees with page:", page);
|
||||
await employeeStore.fetchEmployees({ page, per_page: 10 });
|
||||
await employeeStore.fetchEmployees({
|
||||
page,
|
||||
per_page: employeeStore.getPagination.per_page || DEFAULT_PER_PAGE,
|
||||
search: search.value.trim(),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error changing page:", error);
|
||||
notificationStore.error(
|
||||
@ -155,4 +166,28 @@ 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>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user