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.
|
* 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 [
|
return [
|
||||||
'employees' => $paginator->getCollection(),
|
'employees' => $paginator->getCollection(),
|
||||||
@ -97,6 +116,8 @@ 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(),
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -62,9 +62,10 @@ 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;
|
public function getPaginated(int $perPage = 10, array $filters = []): array;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get employees with their thanatopractitioner data.
|
* Get employees with their thanatopractitioner data.
|
||||||
|
|||||||
@ -14,9 +14,11 @@
|
|||||||
:data="employeeData"
|
:data="employeeData"
|
||||||
:loading="loadingData"
|
:loading="loadingData"
|
||||||
:pagination="pagination"
|
:pagination="pagination"
|
||||||
|
:search="search"
|
||||||
@view="goToDetails"
|
@view="goToDetails"
|
||||||
@delete="deleteEmployee"
|
@delete="deleteEmployee"
|
||||||
@changePage="handleChangePage"
|
@page-change="handleChangePage"
|
||||||
|
@search-change="handleSearchChange"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</employee-template>
|
</employee-template>
|
||||||
@ -32,7 +34,12 @@ import { useRouter } from "vue-router";
|
|||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const emit = defineEmits(["pushDetails", "deleteEmployee", "changePage"]);
|
const emit = defineEmits([
|
||||||
|
"pushDetails",
|
||||||
|
"deleteEmployee",
|
||||||
|
"changePage",
|
||||||
|
"searchChange",
|
||||||
|
]);
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
employeeData: {
|
employeeData: {
|
||||||
@ -47,6 +54,10 @@ const props = defineProps({
|
|||||||
type: Object,
|
type: Object,
|
||||||
default: null,
|
default: null,
|
||||||
},
|
},
|
||||||
|
search: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const goToEmployee = () => {
|
const goToEmployee = () => {
|
||||||
@ -77,8 +88,8 @@ const handleChangePage = (page) => {
|
|||||||
emit("changePage", page);
|
emit("changePage", page);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
const handleSearchChange = (query) => {
|
||||||
/* Component-specific styles */
|
emit("searchChange", query);
|
||||||
</style>
|
};
|
||||||
|
</script>
|
||||||
|
|||||||
@ -1,72 +1,60 @@
|
|||||||
<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 class="spinner-border text-primary" role="status">
|
<div
|
||||||
|
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>ID</th>
|
<th>Employé</th>
|
||||||
<th>Nom & Prénom</th>
|
<th>Référence</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 long"></div>
|
<div class="skeleton-text short"></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>
|
||||||
<!-- Hire Date Column Skeleton -->
|
<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>
|
<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 small"></div>
|
<div class="skeleton-icon"></div>
|
||||||
<div class="skeleton-text short ms-2"></div>
|
<div class="skeleton-text short ms-2"></div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
@ -77,42 +65,29 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Data State -->
|
<div v-else class="table-responsive">
|
||||||
<div v-else>
|
|
||||||
<div class="table-responsive">
|
|
||||||
<table id="employee-list" class="table table-flush">
|
<table id="employee-list" class="table table-flush">
|
||||||
<thead class="thead-light">
|
<thead class="thead-light">
|
||||||
<tr>
|
<tr>
|
||||||
<th>ID</th>
|
<th>Employé</th>
|
||||||
<th>Nom & Prénom</th>
|
<th>Référence</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>
|
||||||
<th>Action</th>
|
<th>Action</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="employee in data" :key="employee.id">
|
<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">
|
<td class="font-weight-bold">
|
||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
<soft-avatar
|
<soft-avatar
|
||||||
:img="getRandomAvatar()"
|
:img="getRandomAvatar()"
|
||||||
size="xs"
|
size="xs"
|
||||||
class="me-2"
|
class="me-2"
|
||||||
alt="user image"
|
alt="employee image"
|
||||||
circular
|
circular
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
@ -129,17 +104,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<!-- Email Column -->
|
|
||||||
<td class="text-xs font-weight-bold">
|
<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>
|
</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">
|
<td class="text-xs font-weight-bold">
|
||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
<soft-button
|
<soft-button
|
||||||
@ -152,56 +120,73 @@
|
|||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
></i>
|
></i>
|
||||||
</soft-button>
|
</soft-button>
|
||||||
<span>{{ employee.job_title || "N/A" }}</span>
|
<span>{{ employee.job_title || "Non renseigné" }}</span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<!-- Hire Date Column -->
|
|
||||||
<td class="text-xs font-weight-bold">
|
<td class="text-xs font-weight-bold">
|
||||||
<span class="text-xs">{{
|
<div class="contact-info">
|
||||||
formatDate(employee.hire_date)
|
<div class="text-xs text-secondary">
|
||||||
}}</span>
|
{{ employee.email || "N/A" }}
|
||||||
|
</div>
|
||||||
|
<div class="text-xs">{{ employee.phone || "N/A" }}</div>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<!-- Status Column -->
|
|
||||||
<td class="text-xs font-weight-bold">
|
<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
|
<soft-button
|
||||||
:color="employee.active ? 'success' : 'danger'"
|
v-if="employee.active"
|
||||||
|
color="success"
|
||||||
variant="outline"
|
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
|
<i class="fas fa-check me-1"></i>
|
||||||
:class="employee.active ? 'fas fa-check' : 'fas fa-times'"
|
Actif
|
||||||
aria-hidden="true"
|
</soft-button>
|
||||||
></i>
|
<soft-button
|
||||||
|
v-else
|
||||||
|
color="danger"
|
||||||
|
variant="outline"
|
||||||
|
class="btn-sm"
|
||||||
|
>
|
||||||
|
<i class="fas fa-times me-1"></i>
|
||||||
|
Inactif
|
||||||
</soft-button>
|
</soft-button>
|
||||||
<span>{{ employee.active ? "Actif" : "Inactif" }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td>
|
<td>
|
||||||
<div class="d-flex align-items-center gap-2">
|
<div class="d-flex align-items-center gap-2">
|
||||||
<!-- View Button -->
|
|
||||||
<soft-button
|
<soft-button
|
||||||
color="info"
|
color="info"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
title="Voir 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="handleView(employee.id)"
|
|
||||||
>
|
>
|
||||||
<i class="fas fa-eye" aria-hidden="true"></i>
|
<i class="fas fa-eye" aria-hidden="true"></i>
|
||||||
</soft-button>
|
</soft-button>
|
||||||
|
|
||||||
<!-- Delete Button -->
|
|
||||||
<soft-button
|
<soft-button
|
||||||
color="danger"
|
color="danger"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
title="Supprimer l'employé"
|
title="Supprimer 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-trash" aria-hidden="true"></i>
|
<i class="fas fa-trash" aria-hidden="true"></i>
|
||||||
</soft-button>
|
</soft-button>
|
||||||
@ -212,61 +197,69 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Custom Pagination Controls -->
|
|
||||||
<div
|
<div
|
||||||
v-if="pagination.total > pagination.per_page"
|
v-if="!loading && data.length > 0 && (pagination?.last_page || 1) > 1"
|
||||||
class="d-flex justify-content-between align-items-center mt-3"
|
class="d-flex justify-content-between align-items-center mt-3 px-3 flex-wrap gap-3"
|
||||||
>
|
>
|
||||||
<div class="text-sm text-muted">
|
<div class="text-xs text-secondary font-weight-bold">
|
||||||
Affichage de {{ pagination.from }} à {{ pagination.to }} sur
|
Affichage de {{ safeFrom }} à {{ safeTo }} sur
|
||||||
{{ pagination.total }} employés
|
{{ pagination.total || data.length }} employés
|
||||||
</div>
|
</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 -->
|
<nav aria-label="Pagination employés">
|
||||||
<div class="d-flex gap-1">
|
<ul class="pagination pagination-sm pagination-success mb-0">
|
||||||
<soft-button
|
<li
|
||||||
v-for="page in getVisiblePages()"
|
class="page-item"
|
||||||
:key="page"
|
:class="{ disabled: (pagination.current_page || 1) === 1 }"
|
||||||
:color="page === pagination.current_page ? 'primary' : 'outline'"
|
|
||||||
variant="outline"
|
|
||||||
class="btn-sm"
|
|
||||||
:disabled="loading"
|
|
||||||
@click="changePage(page)"
|
|
||||||
>
|
>
|
||||||
|
<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 }}
|
{{ page }}
|
||||||
</soft-button>
|
</a>
|
||||||
</div>
|
</li>
|
||||||
|
|
||||||
<!-- Next Button -->
|
<li
|
||||||
<soft-button
|
class="page-item"
|
||||||
color="outline"
|
:class="{
|
||||||
variant="outline"
|
disabled:
|
||||||
class="btn-sm"
|
(pagination.current_page || 1) === (pagination.last_page || 1),
|
||||||
:disabled="
|
}"
|
||||||
pagination.current_page === pagination.last_page || loading
|
|
||||||
"
|
|
||||||
@click="changePage(pagination.current_page + 1)"
|
|
||||||
>
|
>
|
||||||
Suivant
|
<a
|
||||||
<i class="fas fa-chevron-right ms-1"></i>
|
class="page-link"
|
||||||
</soft-button>
|
href="#"
|
||||||
</div>
|
aria-label="Next"
|
||||||
</div>
|
@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>
|
</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>
|
||||||
@ -280,16 +273,21 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, watch, onUnmounted } from "vue";
|
import {
|
||||||
// import { DataTable } from "simple-datatables"; // Disabled to avoid interference
|
computed,
|
||||||
import SoftCheckbox from "@/components/SoftCheckbox.vue";
|
defineEmits,
|
||||||
|
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", "changePage"]);
|
const emit = defineEmits(["view", "delete", "page-change", "search-change"]);
|
||||||
|
|
||||||
// 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";
|
||||||
@ -299,8 +297,7 @@ import img6 from "@/assets/img/ivana-squares.jpg";
|
|||||||
|
|
||||||
const avatarImages = [img1, img2, img3, img4, img5, img6];
|
const avatarImages = [img1, img2, img3, img4, img5, img6];
|
||||||
|
|
||||||
// Reactive data - DataTable disabled
|
const dataTableInstance = ref(null);
|
||||||
// const dataTableInstance = ref(null);
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
data: {
|
data: {
|
||||||
@ -326,9 +323,85 @@ 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];
|
||||||
@ -362,69 +435,27 @@ const getPositionIcon = (position) => {
|
|||||||
return icons[position] || "fas fa-user";
|
return icons[position] || "fas fa-user";
|
||||||
};
|
};
|
||||||
|
|
||||||
// Direct button handlers
|
const handleTableClick = (event) => {
|
||||||
const handleView = (employeeId) => {
|
const button = event.target.closest("button");
|
||||||
console.log("Direct view button clicked for ID:", employeeId);
|
if (!button) return;
|
||||||
emit("view", employeeId);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDelete = (employeeId) => {
|
const employeeId = button.getAttribute("data-employee-id");
|
||||||
console.log("Direct delete button clicked for ID:", employeeId);
|
if (!employeeId) return;
|
||||||
emit("delete", employeeId);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Pagination methods
|
if (
|
||||||
const changePage = (page) => {
|
button.title === "Supprimer l'employé" ||
|
||||||
console.log("changePage called in EmployeeTable with page:", page);
|
button.querySelector(".fa-trash")
|
||||||
if (page >= 1 && page <= props.pagination.last_page) {
|
) {
|
||||||
console.log("Emitting changePage event from EmployeeTable:", page);
|
emit("delete", Number(employeeId));
|
||||||
emit("changePage", page);
|
} 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 = () => {
|
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;
|
||||||
@ -432,75 +463,84 @@ 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,
|
||||||
perPage: 10, // Default to 10 entries per page
|
paging: false,
|
||||||
perPageSelect: false, // Disable per-page selector since we handle it server-side
|
perPage: Number(props.pagination?.per_page) || 10,
|
||||||
pager: false, // Disable DataTable pagination since we handle it server-side
|
perPageSelect: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 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 handleTableClick = (event) => {
|
const changePage = (page) => {
|
||||||
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 (
|
||||||
button.title === "Supprimer l'employé" ||
|
page !== "..." &&
|
||||||
button.querySelector(".fa-trash")
|
page >= 1 &&
|
||||||
|
page <= (props.pagination?.last_page || 1) &&
|
||||||
|
page !== Number(props.pagination?.current_page)
|
||||||
) {
|
) {
|
||||||
console.log("Delete button clicked!");
|
emit("page-change", page);
|
||||||
emit("delete", employeeId);
|
|
||||||
} else if (
|
|
||||||
button.title === "Voir l'employé" ||
|
|
||||||
button.querySelector(".fa-eye")
|
|
||||||
) {
|
|
||||||
console.log("View button clicked!");
|
|
||||||
emit("view", employeeId);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
*/
|
|
||||||
|
|
||||||
// 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(
|
watch(
|
||||||
() => props.data,
|
() => props.data,
|
||||||
() => {
|
() => {
|
||||||
if (!props.loading) {
|
if (!props.loading) {
|
||||||
console.log("EmployeeTable: Data changed");
|
setTimeout(() => {
|
||||||
|
initializeDataTable();
|
||||||
|
}, 100);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ deep: true }
|
{ deep: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
// Clean up any event listeners if needed
|
const dataTableEl = document.getElementById("employee-list");
|
||||||
// const dataTableEl = document.getElementById("employee-list");
|
if (dataTableEl) {
|
||||||
// if (dataTableEl) {
|
dataTableEl.removeEventListener("click", handleTableClick);
|
||||||
// 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) {
|
||||||
console.log(
|
initializeDataTable();
|
||||||
"EmployeeTable: Component mounted with",
|
|
||||||
props.data.length,
|
|
||||||
"employees"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@ -513,31 +553,30 @@ onMounted(() => {
|
|||||||
|
|
||||||
.loading-container {
|
.loading-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
min-height: 260px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.loading-spinner {
|
.loading-spinner {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 20px;
|
top: 50%;
|
||||||
right: 20px;
|
left: 50%;
|
||||||
|
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.7;
|
opacity: 0.55;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.skeleton-row {
|
.skeleton-row {
|
||||||
animation: pulse 1.5s ease-in-out infinite;
|
animation: none;
|
||||||
}
|
|
||||||
|
|
||||||
.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 {
|
||||||
@ -558,15 +597,6 @@ 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%;
|
||||||
@ -607,21 +637,13 @@ onMounted(() => {
|
|||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.address-info,
|
||||||
.text-xs {
|
.text-xs {
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Animations */
|
.contact-info {
|
||||||
@keyframes pulse {
|
line-height: 1.2;
|
||||||
0% {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
opacity: 0.7;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes shimmer {
|
@keyframes shimmer {
|
||||||
@ -633,13 +655,7 @@ 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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,9 +3,11 @@
|
|||||||
: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 -->
|
||||||
@ -25,7 +27,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { reactive, onMounted } from "vue";
|
import { reactive, onMounted, ref } 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";
|
||||||
@ -35,6 +37,9 @@ 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({
|
||||||
@ -52,7 +57,11 @@ 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) => {
|
||||||
@ -143,10 +152,12 @@ const closeConfirmModal = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const changePage = async (page) => {
|
const changePage = async (page) => {
|
||||||
console.log("changePage called in Employees.vue with page:", page);
|
|
||||||
try {
|
try {
|
||||||
console.log("Fetching employees with page:", page);
|
await employeeStore.fetchEmployees({
|
||||||
await employeeStore.fetchEmployees({ page, per_page: 10 });
|
page,
|
||||||
|
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(
|
||||||
@ -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>
|
</script>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user