Fix all table design
This commit is contained in:
parent
ce61b79080
commit
d5916d96a2
@ -13,6 +13,7 @@ use App\Models\Client;
|
|||||||
use App\Repositories\ClientGroupRepositoryInterface;
|
use App\Repositories\ClientGroupRepositoryInterface;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
|
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Arr;
|
use Illuminate\Support\Arr;
|
||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
@ -27,10 +28,18 @@ class ClientGroupController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Display a listing of client groups.
|
* Display a listing of client groups.
|
||||||
*/
|
*/
|
||||||
public function index(): AnonymousResourceCollection|JsonResponse
|
public function index(Request $request): AnonymousResourceCollection|JsonResponse
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$clientGroups = $this->clientGroupRepository->all();
|
$perPage = (int) $request->get('per_page', 5);
|
||||||
|
$filters = array_filter([
|
||||||
|
'search' => $request->get('search'),
|
||||||
|
'sort_by' => $request->get('sort_by', 'created_at'),
|
||||||
|
'sort_direction' => $request->get('sort_direction', 'desc'),
|
||||||
|
], static fn ($value) => $value !== null && $value !== '');
|
||||||
|
|
||||||
|
$clientGroups = $this->clientGroupRepository->paginate($perPage, $filters);
|
||||||
|
|
||||||
return ClientGroupResource::collection($clientGroups);
|
return ClientGroupResource::collection($clientGroups);
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
Log::error('Error fetching client groups: ' . $e->getMessage(), [
|
Log::error('Error fetching client groups: ' . $e->getMessage(), [
|
||||||
|
|||||||
@ -5,6 +5,7 @@ declare(strict_types=1);
|
|||||||
namespace App\Repositories;
|
namespace App\Repositories;
|
||||||
|
|
||||||
use App\Models\ClientGroup;
|
use App\Models\ClientGroup;
|
||||||
|
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||||
|
|
||||||
class ClientGroupRepository extends BaseRepository implements ClientGroupRepositoryInterface
|
class ClientGroupRepository extends BaseRepository implements ClientGroupRepositoryInterface
|
||||||
{
|
{
|
||||||
@ -12,4 +13,22 @@ class ClientGroupRepository extends BaseRepository implements ClientGroupReposit
|
|||||||
{
|
{
|
||||||
parent::__construct($model);
|
parent::__construct($model);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function paginate(int $perPage = 15, array $filters = []): LengthAwarePaginator
|
||||||
|
{
|
||||||
|
$query = $this->model->newQuery()->withCount('clients');
|
||||||
|
|
||||||
|
if (!empty($filters['search'])) {
|
||||||
|
$query->where(function ($builder) use ($filters) {
|
||||||
|
$builder
|
||||||
|
->where('name', 'like', '%' . $filters['search'] . '%')
|
||||||
|
->orWhere('description', 'like', '%' . $filters['search'] . '%');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$sortField = $filters['sort_by'] ?? 'created_at';
|
||||||
|
$sortDirection = $filters['sort_direction'] ?? 'desc';
|
||||||
|
|
||||||
|
return $query->orderBy($sortField, $sortDirection)->paginate($perPage);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,7 +4,9 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Repositories;
|
namespace App\Repositories;
|
||||||
|
|
||||||
|
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||||
|
|
||||||
interface ClientGroupRepositoryInterface extends BaseRepositoryInterface
|
interface ClientGroupRepositoryInterface extends BaseRepositoryInterface
|
||||||
{
|
{
|
||||||
// Add ClientGroup-specific methods here later if needed
|
public function paginate(int $perPage = 15, array $filters = []): LengthAwarePaginator;
|
||||||
}
|
}
|
||||||
|
|||||||
34
thanasoft-back/database/seeders/ClientGroupSeeder.php
Normal file
34
thanasoft-back/database/seeders/ClientGroupSeeder.php
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Seeders;
|
||||||
|
|
||||||
|
use Illuminate\Database\Seeder;
|
||||||
|
use App\Models\ClientGroup;
|
||||||
|
use Faker\Factory as Faker;
|
||||||
|
|
||||||
|
class ClientGroupSeeder extends Seeder
|
||||||
|
{
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
$faker = Faker::create('fr_FR');
|
||||||
|
|
||||||
|
$groups = [
|
||||||
|
'Clients Premium',
|
||||||
|
'Clients Standard',
|
||||||
|
'Clients VIP',
|
||||||
|
'Revendeurs',
|
||||||
|
'Partenaires',
|
||||||
|
'Grands Comptes',
|
||||||
|
'PME',
|
||||||
|
'Startups',
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($groups as $group) {
|
||||||
|
ClientGroup::create([
|
||||||
|
'name' => $group,
|
||||||
|
'description' => $faker->sentence,
|
||||||
|
'price_list_id' => null, // volontairement null
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -13,8 +13,7 @@
|
|||||||
<location-table
|
<location-table
|
||||||
:data="locationData"
|
:data="locationData"
|
||||||
:loading="isLoading"
|
:loading="isLoading"
|
||||||
:current-page="currentPage"
|
:pagination="pagination"
|
||||||
:per-page="perPage"
|
|
||||||
@view="handleViewLocation"
|
@view="handleViewLocation"
|
||||||
@delete="handleDeleteLocation"
|
@delete="handleDeleteLocation"
|
||||||
@page-change="handlePageChange"
|
@page-change="handlePageChange"
|
||||||
@ -41,13 +40,16 @@ defineProps({
|
|||||||
type: Array,
|
type: Array,
|
||||||
default: () => [],
|
default: () => [],
|
||||||
},
|
},
|
||||||
currentPage: {
|
pagination: {
|
||||||
type: Number,
|
type: Object,
|
||||||
default: 1,
|
default: () => ({
|
||||||
},
|
current_page: 1,
|
||||||
perPage: {
|
last_page: 1,
|
||||||
type: Number,
|
per_page: 10,
|
||||||
default: 10,
|
total: 0,
|
||||||
|
from: 0,
|
||||||
|
to: 0,
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,18 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="container-fluid py-4">
|
<client-template>
|
||||||
<client-group-list-controls @create="openCreateModal" />
|
<template #client-new-action>
|
||||||
<div class="row">
|
<add-button text="Ajouter" @click="openCreateModal" />
|
||||||
<div class="col-12">
|
</template>
|
||||||
|
|
||||||
|
<template #select-filter>
|
||||||
|
<filter-table />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #client-other-action>
|
||||||
|
<table-action />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #client-table>
|
||||||
<client-group-table
|
<client-group-table
|
||||||
:data="clientGroups"
|
:data="clientGroups"
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
@ -14,16 +24,18 @@
|
|||||||
@per-page-change="onPerPageChange"
|
@per-page-change="onPerPageChange"
|
||||||
@search-change="onSearch"
|
@search-change="onSearch"
|
||||||
/>
|
/>
|
||||||
</div>
|
</template>
|
||||||
</div>
|
</client-template>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { onMounted } from "vue";
|
import { onMounted } from "vue";
|
||||||
import { storeToRefs } from "pinia";
|
import { storeToRefs } from "pinia";
|
||||||
import { useRouter } from "vue-router";
|
import { useRouter } from "vue-router";
|
||||||
import ClientGroupListControls from "@/components/molecules/ClientGroup/ClientGroupListControls.vue";
|
import ClientTemplate from "@/components/templates/CRM/ClientTemplate.vue";
|
||||||
|
import addButton from "@/components/molecules/new-button/addButton.vue";
|
||||||
|
import FilterTable from "@/components/molecules/Tables/FilterTable.vue";
|
||||||
|
import TableAction from "@/components/molecules/Tables/TableAction.vue";
|
||||||
import ClientGroupTable from "@/components/molecules/Tables/ClientGroup/ClientGroupTable.vue";
|
import ClientGroupTable from "@/components/molecules/Tables/ClientGroup/ClientGroupTable.vue";
|
||||||
import { useClientGroupStore } from "@/stores/clientGroupStore";
|
import { useClientGroupStore } from "@/stores/clientGroupStore";
|
||||||
import { useNotificationStore } from "@/stores/notification";
|
import { useNotificationStore } from "@/stores/notification";
|
||||||
@ -38,12 +50,10 @@ const openCreateModal = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleView = (id) => {
|
const handleView = (id) => {
|
||||||
console.log("handleView called with id:", id);
|
|
||||||
router.push(`/clients/groups/${id}`);
|
router.push(`/clients/groups/${id}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEdit = (id) => {
|
const handleEdit = (id) => {
|
||||||
console.log("handleEdit called with id:", id);
|
|
||||||
router.push(`/clients/groups/${id}/edit`);
|
router.push(`/clients/groups/${id}/edit`);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -89,6 +99,9 @@ const onSearch = (query) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
clientGroupStore.fetchClientGroups();
|
clientGroupStore.fetchClientGroups({
|
||||||
|
page: 1,
|
||||||
|
per_page: pagination.value.per_page,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -1,87 +1,102 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="table-container card mt-4">
|
<div class="table-container">
|
||||||
<div class="card-body pb-0">
|
<div v-if="loading" class="loading-container">
|
||||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
<div class="loading-spinner">
|
||||||
<div class="d-flex align-items-center">
|
<div class="spinner-border text-success loading-spinner-circle" role="status">
|
||||||
<select
|
<span class="visually-hidden">Chargement...</span>
|
||||||
class="form-select form-select-sm me-2"
|
|
||||||
style="width: 80px"
|
|
||||||
:value="pagination.per_page"
|
|
||||||
@change="onPerPageChange"
|
|
||||||
>
|
|
||||||
<option :value="5">5</option>
|
|
||||||
<option :value="10">10</option>
|
|
||||||
<option :value="15">15</option>
|
|
||||||
<option :value="20">20</option>
|
|
||||||
<option :value="50">50</option>
|
|
||||||
</select>
|
|
||||||
<span class="text-secondary text-xs font-weight-bold"
|
|
||||||
>éléments par page</span
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="d-flex align-items-center">
|
|
||||||
<div class="input-group">
|
|
||||||
<span class="input-group-text text-body"
|
|
||||||
><i class="fas fa-search" aria-hidden="true"></i
|
|
||||||
></span>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="form-control form-control-sm"
|
|
||||||
placeholder="Rechercher..."
|
|
||||||
@input="onSearch"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="loading-content">
|
||||||
</div>
|
<div class="table-responsive">
|
||||||
|
<table class="table table-flush">
|
||||||
<div class="table-responsive px-3 pb-3">
|
|
||||||
<table class="table table-flush mb-0">
|
|
||||||
<thead class="thead-light">
|
<thead class="thead-light">
|
||||||
<tr>
|
<tr>
|
||||||
<th>Nom</th>
|
<th>Groupe</th>
|
||||||
<th>Description</th>
|
<th>Description</th>
|
||||||
<th>Date de création</th>
|
<th>Date de création</th>
|
||||||
<th>Actions</th>
|
<th>Statut</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-if="loading">
|
<tr v-for="i in skeletonRows" :key="i" class="skeleton-row">
|
||||||
<td colspan="4" class="text-center py-4">
|
|
||||||
<div class="spinner-border text-primary" role="status">
|
|
||||||
<span class="visually-hidden">Chargement...</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
<tr v-else-if="data.length === 0">
|
|
||||||
<td colspan="4" class="text-center py-4 text-secondary text-sm">
|
|
||||||
Aucun groupe trouvé.
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
|
|
||||||
<template v-else>
|
|
||||||
<tr v-for="group in data" :key="group.id">
|
|
||||||
<td>
|
<td>
|
||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
<soft-checkbox class="me-2" />
|
<div class="skeleton-avatar"></div>
|
||||||
<p class="text-sm font-weight-bold ms-2 mb-0">
|
<div class="skeleton-text medium ms-2"></div>
|
||||||
{{ group.name }}
|
</div>
|
||||||
</p>
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="skeleton-text long"></div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="skeleton-text short"></div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<div class="skeleton-icon"></div>
|
||||||
|
<div class="skeleton-text short ms-2"></div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="table-responsive">
|
||||||
|
<table id="client-list" class="table table-flush">
|
||||||
|
<thead class="thead-light">
|
||||||
|
<tr>
|
||||||
|
<th>Groupe</th>
|
||||||
|
<th>Description</th>
|
||||||
|
<th>Date de création</th>
|
||||||
|
<th>Statut</th>
|
||||||
|
<th>Action</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="group in data" :key="group.id">
|
||||||
|
<td class="font-weight-bold">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<soft-button
|
||||||
|
color="info"
|
||||||
|
variant="outline"
|
||||||
|
class="btn-icon-only btn-rounded mb-0 me-2 btn-sm d-flex align-items-center justify-content-center"
|
||||||
|
>
|
||||||
|
<i class="fas fa-layer-group" aria-hidden="true"></i>
|
||||||
|
</soft-button>
|
||||||
|
<div>
|
||||||
|
<div>{{ group.name }}</div>
|
||||||
|
<div class="text-xs text-muted">ID #{{ group.id }}</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td class="text-sm">
|
<td class="text-xs font-weight-bold">
|
||||||
<span class="text-secondary">
|
<div class="contact-info">
|
||||||
{{ group.description || "Aucune description" }}
|
<div class="text-xs text-secondary">
|
||||||
</span>
|
{{ getDescriptionLine(group.description) }}
|
||||||
|
</div>
|
||||||
|
<div class="text-xs">{{ getDescriptionMeta(group.description) }}</div>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td class="text-sm font-weight-bold">
|
<td class="text-xs font-weight-bold">
|
||||||
<span class="my-2 text-xs">{{
|
{{ formatDate(group.created_at) }}
|
||||||
formatDate(group.created_at)
|
</td>
|
||||||
}}</span>
|
|
||||||
|
<td class="text-xs font-weight-bold">
|
||||||
|
<div class="d-flex flex-column">
|
||||||
|
<soft-button
|
||||||
|
color="success"
|
||||||
|
variant="outline"
|
||||||
|
class="btn-sm"
|
||||||
|
>
|
||||||
|
<i class="fas fa-check me-1"></i>
|
||||||
|
Actif
|
||||||
|
</soft-button>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<td>
|
<td>
|
||||||
@ -90,8 +105,8 @@
|
|||||||
color="info"
|
color="info"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
title="Voir le groupe"
|
title="Voir le groupe"
|
||||||
|
:data-group-id="group.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="emit('view', group.id)"
|
|
||||||
>
|
>
|
||||||
<i class="fas fa-eye" aria-hidden="true"></i>
|
<i class="fas fa-eye" aria-hidden="true"></i>
|
||||||
</soft-button>
|
</soft-button>
|
||||||
@ -100,8 +115,8 @@
|
|||||||
color="warning"
|
color="warning"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
title="Modifier le groupe"
|
title="Modifier le groupe"
|
||||||
|
:data-group-id="group.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="emit('edit', group.id)"
|
|
||||||
>
|
>
|
||||||
<i class="fas fa-edit" aria-hidden="true"></i>
|
<i class="fas fa-edit" aria-hidden="true"></i>
|
||||||
</soft-button>
|
</soft-button>
|
||||||
@ -110,42 +125,42 @@
|
|||||||
color="danger"
|
color="danger"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
title="Supprimer le groupe"
|
title="Supprimer le groupe"
|
||||||
|
:data-group-id="group.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="emit('delete', group.id)"
|
|
||||||
>
|
>
|
||||||
<i class="fas fa-trash" aria-hidden="true"></i>
|
<i class="fas fa-trash" aria-hidden="true"></i>
|
||||||
</soft-button>
|
</soft-button>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</template>
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="!loading && data.length > 0"
|
v-if="!loading && data.length > 0 && (pagination?.last_page || 1) > 1"
|
||||||
class="d-flex justify-content-between align-items-center mt-1 px-3 pb-3"
|
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">
|
<div class="text-xs text-secondary font-weight-bold">
|
||||||
Affichage de {{ from }} à {{ to }} sur {{ pagination.total }} groupes
|
Affichage de {{ safeFrom }} à {{ safeTo }} sur
|
||||||
|
{{ pagination.total || data.length }} groupes
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nav aria-label="Page navigation">
|
<nav aria-label="Pagination clients">
|
||||||
<ul class="pagination pagination-sm pagination-success mb-0">
|
<ul class="pagination pagination-sm pagination-success mb-0">
|
||||||
<li
|
<li
|
||||||
class="page-item"
|
class="page-item"
|
||||||
:class="{ disabled: pagination.current_page === 1 }"
|
:class="{ disabled: (pagination.current_page || 1) === 1 }"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
class="page-link"
|
class="page-link"
|
||||||
href="#"
|
href="#"
|
||||||
aria-label="Previous"
|
aria-label="Previous"
|
||||||
@click.prevent="changePage(pagination.current_page - 1)"
|
@click.prevent="changePage((pagination.current_page || 1) - 1)"
|
||||||
>
|
>
|
||||||
<span aria-hidden="true"
|
<span aria-hidden="true">
|
||||||
><i class="fa fa-angle-left" aria-hidden="true"></i
|
<i class="fa fa-angle-left" aria-hidden="true"></i>
|
||||||
></span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
@ -154,53 +169,71 @@
|
|||||||
:key="`page-${page}`"
|
:key="`page-${page}`"
|
||||||
class="page-item"
|
class="page-item"
|
||||||
:class="{
|
:class="{
|
||||||
active: pagination.current_page === page,
|
active: (pagination.current_page || 1) === page,
|
||||||
disabled: page === '...',
|
disabled: page === '...'
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<a class="page-link" href="#" @click.prevent="changePage(page)">{{
|
<a class="page-link" href="#" @click.prevent="changePage(page)">
|
||||||
page
|
{{ page }}
|
||||||
}}</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li
|
<li
|
||||||
class="page-item"
|
class="page-item"
|
||||||
:class="{
|
:class="{
|
||||||
disabled: pagination.current_page === pagination.last_page,
|
disabled: (pagination.current_page || 1) === (pagination.last_page || 1)
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
class="page-link"
|
class="page-link"
|
||||||
href="#"
|
href="#"
|
||||||
aria-label="Next"
|
aria-label="Next"
|
||||||
@click.prevent="changePage(pagination.current_page + 1)"
|
@click.prevent="changePage((pagination.current_page || 1) + 1)"
|
||||||
>
|
>
|
||||||
<span aria-hidden="true"
|
<span aria-hidden="true">
|
||||||
><i class="fa fa-angle-right" aria-hidden="true"></i
|
<i class="fa fa-angle-right" aria-hidden="true"></i>
|
||||||
></span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-if="!loading && data.length === 0" class="empty-state">
|
||||||
|
<div class="empty-icon">
|
||||||
|
<i class="fas fa-users fa-3x text-muted"></i>
|
||||||
|
</div>
|
||||||
|
<h5 class="empty-title">Aucun groupe trouvé</h5>
|
||||||
|
<p class="empty-text text-muted">
|
||||||
|
Aucun groupe à afficher pour le moment.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, defineProps, defineEmits } from "vue";
|
import { ref, onMounted, watch, onUnmounted, computed } from "vue";
|
||||||
import debounce from "lodash/debounce";
|
import { DataTable } from "simple-datatables";
|
||||||
import SoftCheckbox from "@/components/SoftCheckbox.vue";
|
|
||||||
import SoftButton from "@/components/SoftButton.vue";
|
import SoftButton from "@/components/SoftButton.vue";
|
||||||
|
import { defineProps, defineEmits } from "vue";
|
||||||
|
|
||||||
|
const emit = defineEmits(["view", "edit", "delete", "page-change"]);
|
||||||
|
|
||||||
|
const dataTableInstance = ref(null);
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
data: {
|
data: {
|
||||||
type: Array,
|
type: Array,
|
||||||
default: () => [],
|
default: [],
|
||||||
},
|
},
|
||||||
loading: {
|
loading: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
skeletonRows: {
|
||||||
|
type: Number,
|
||||||
|
default: 5,
|
||||||
|
},
|
||||||
pagination: {
|
pagination: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => ({
|
default: () => ({
|
||||||
@ -208,86 +241,313 @@ const props = defineProps({
|
|||||||
last_page: 1,
|
last_page: 1,
|
||||||
per_page: 10,
|
per_page: 10,
|
||||||
total: 0,
|
total: 0,
|
||||||
|
from: 0,
|
||||||
|
to: 0,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits([
|
|
||||||
"view",
|
|
||||||
"edit",
|
|
||||||
"delete",
|
|
||||||
"page-change",
|
|
||||||
"per-page-change",
|
|
||||||
"search-change",
|
|
||||||
]);
|
|
||||||
|
|
||||||
const formatDate = (dateString) => {
|
|
||||||
if (!dateString) return "-";
|
|
||||||
const options = {
|
|
||||||
day: "numeric",
|
|
||||||
month: "short",
|
|
||||||
year: "numeric",
|
|
||||||
};
|
|
||||||
return new Date(dateString).toLocaleDateString("fr-FR", options);
|
|
||||||
};
|
|
||||||
|
|
||||||
const displayedPages = computed(() => {
|
const displayedPages = computed(() => {
|
||||||
const total = props.pagination.last_page || 1;
|
const total = Number(props.pagination?.last_page) || 1;
|
||||||
const current = props.pagination.current_page || 1;
|
const current = Number(props.pagination?.current_page) || 1;
|
||||||
|
|
||||||
|
if (total <= 1) {
|
||||||
|
return [1];
|
||||||
|
}
|
||||||
|
|
||||||
const delta = 2;
|
const delta = 2;
|
||||||
const range = [];
|
const range = [];
|
||||||
|
|
||||||
for (
|
for (
|
||||||
let i = Math.max(2, current - delta);
|
let page = Math.max(2, current - delta);
|
||||||
i <= Math.min(total - 1, current + delta);
|
page <= Math.min(total - 1, current + delta);
|
||||||
i++
|
page++
|
||||||
) {
|
) {
|
||||||
range.push(i);
|
range.push(page);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (current - delta > 2) {
|
if (current - delta > 2) {
|
||||||
range.unshift("...");
|
range.unshift("...");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (current + delta < total - 1) {
|
if (current + delta < total - 1) {
|
||||||
range.push("...");
|
range.push("...");
|
||||||
}
|
}
|
||||||
|
|
||||||
range.unshift(1);
|
range.unshift(1);
|
||||||
|
|
||||||
if (total > 1) {
|
if (total > 1) {
|
||||||
range.push(total);
|
range.push(total);
|
||||||
}
|
}
|
||||||
|
|
||||||
return range.filter(
|
return range.filter(
|
||||||
(val, index, self) =>
|
(value, index, self) =>
|
||||||
val !== "..." || (val === "..." && self[index - 1] !== "...")
|
value !== "..." || (value === "..." && self[index - 1] !== "...")
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const from = computed(() => {
|
const safeFrom = computed(() => {
|
||||||
if (!props.pagination.total || props.data.length === 0) return 0;
|
if (props.pagination?.from) {
|
||||||
return (props.pagination.current_page - 1) * props.pagination.per_page + 1;
|
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 to = computed(() => {
|
const safeTo = computed(() => {
|
||||||
if (!props.pagination.total || props.data.length === 0) return 0;
|
if (props.pagination?.to) {
|
||||||
|
return props.pagination.to;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!props.pagination?.total || props.data.length === 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
return Math.min(
|
return Math.min(
|
||||||
props.pagination.current_page * props.pagination.per_page,
|
(Number(props.pagination.current_page) || 1) *
|
||||||
props.pagination.total
|
(Number(props.pagination.per_page) || 10),
|
||||||
|
Number(props.pagination.total) || 0
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const formatDate = (dateString) => {
|
||||||
|
if (!dateString) {
|
||||||
|
return "-";
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Date(dateString).toLocaleDateString("fr-FR", {
|
||||||
|
day: "numeric",
|
||||||
|
month: "short",
|
||||||
|
year: "numeric",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDescriptionLine = (description) => {
|
||||||
|
if (!description) {
|
||||||
|
return "Aucune description";
|
||||||
|
}
|
||||||
|
|
||||||
|
return description;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDescriptionMeta = (description) => {
|
||||||
|
if (!description) {
|
||||||
|
return "Description indisponible";
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${description.length} caractere${description.length > 1 ? "s" : ""}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTableClick = (event) => {
|
||||||
|
const button = event.target.closest("button");
|
||||||
|
if (!button) return;
|
||||||
|
|
||||||
|
const groupId = button.getAttribute("data-group-id");
|
||||||
|
if (!groupId) return;
|
||||||
|
|
||||||
|
if (
|
||||||
|
button.title === "Supprimer le groupe" ||
|
||||||
|
button.querySelector(".fa-trash")
|
||||||
|
) {
|
||||||
|
emit("delete", groupId);
|
||||||
|
} else if (
|
||||||
|
button.title === "Modifier le groupe" ||
|
||||||
|
button.querySelector(".fa-edit")
|
||||||
|
) {
|
||||||
|
emit("edit", groupId);
|
||||||
|
} else if (
|
||||||
|
button.title === "Voir le groupe" ||
|
||||||
|
button.querySelector(".fa-eye")
|
||||||
|
) {
|
||||||
|
emit("view", groupId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const initializeDataTable = () => {
|
||||||
|
if (dataTableInstance.value) {
|
||||||
|
dataTableInstance.value.destroy();
|
||||||
|
dataTableInstance.value = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dataTableEl = document.getElementById("client-list");
|
||||||
|
if (dataTableEl) {
|
||||||
|
dataTableInstance.value = new DataTable(dataTableEl, {
|
||||||
|
searchable: true,
|
||||||
|
fixedHeight: true,
|
||||||
|
paging: false,
|
||||||
|
perPage: Number(props.pagination?.per_page) || 10,
|
||||||
|
perPageSelect: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
dataTableEl.addEventListener("click", handleTableClick);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const changePage = (page) => {
|
const changePage = (page) => {
|
||||||
if (page !== "..." && page >= 1 && page <= props.pagination.last_page) {
|
if (
|
||||||
|
page !== "..." &&
|
||||||
|
page >= 1 &&
|
||||||
|
page <= (Number(props.pagination?.last_page) || 1) &&
|
||||||
|
page !== Number(props.pagination?.current_page)
|
||||||
|
) {
|
||||||
emit("page-change", page);
|
emit("page-change", page);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onPerPageChange = (event) => {
|
watch(
|
||||||
const newPerPage = parseInt(event.target.value, 10);
|
() => props.data,
|
||||||
emit("per-page-change", newPerPage);
|
() => {
|
||||||
};
|
if (!props.loading) {
|
||||||
|
setTimeout(() => {
|
||||||
|
initializeDataTable();
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
);
|
||||||
|
|
||||||
const onSearch = debounce((event) => {
|
onUnmounted(() => {
|
||||||
emit("search-change", event.target.value);
|
const dataTableEl = document.getElementById("client-list");
|
||||||
}, 300);
|
if (dataTableEl) {
|
||||||
|
dataTableEl.removeEventListener("click", handleTableClick);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dataTableInstance.value) {
|
||||||
|
dataTableInstance.value.destroy();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (!props.loading && props.data.length > 0) {
|
||||||
|
initializeDataTable();
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.table-container {
|
||||||
|
position: relative;
|
||||||
|
min-height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-container {
|
||||||
|
position: relative;
|
||||||
|
min-height: 260px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spinner {
|
||||||
|
position: absolute;
|
||||||
|
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.55;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-row {
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-avatar {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: shimmer 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-icon {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
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%;
|
||||||
|
animation: shimmer 2s infinite;
|
||||||
|
border-radius: 4px;
|
||||||
|
height: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-text.short {
|
||||||
|
width: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-text.medium {
|
||||||
|
width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-text.long {
|
||||||
|
width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-state {
|
||||||
|
text-align: center;
|
||||||
|
padding: 3rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-icon {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-title {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-text {
|
||||||
|
max-width: 300px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-info {
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-xs {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes shimmer {
|
||||||
|
0% {
|
||||||
|
background-position: -200% 0;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-position: 200% 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.skeleton-text.long {
|
||||||
|
width: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skeleton-text.medium {
|
||||||
|
width: 60px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@ -1,31 +1,30 @@
|
|||||||
<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>Nom</th>
|
<th>Lieu</th>
|
||||||
<th>Ville</th>
|
<th>Ville</th>
|
||||||
<th>Adresse</th>
|
<th>Adresse</th>
|
||||||
<th>Latitude GPS</th>
|
<th>Coordonnees GPS</th>
|
||||||
<th>Longitude GPS</th>
|
<th>Statut</th>
|
||||||
<th>Par défaut</th>
|
|
||||||
<th>Action</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">
|
||||||
<td>
|
<td>
|
||||||
<div class="skeleton-text medium"></div>
|
<div class="d-flex align-items-center">
|
||||||
|
<div class="skeleton-avatar"></div>
|
||||||
|
<div class="skeleton-text medium ms-2"></div>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="skeleton-text medium"></div>
|
<div class="skeleton-text medium"></div>
|
||||||
@ -34,20 +33,15 @@
|
|||||||
<div class="skeleton-text long"></div>
|
<div class="skeleton-text long"></div>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="skeleton-text short"></div>
|
<div class="contact-info">
|
||||||
</td>
|
<div class="skeleton-text long mb-1"></div>
|
||||||
<td>
|
<div class="skeleton-text medium"></div>
|
||||||
<div class="skeleton-text short"></div>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<div class="d-flex align-items-center">
|
|
||||||
<div class="skeleton-icon small"></div>
|
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="d-flex align-items-center gap-2">
|
<div class="d-flex align-items-center">
|
||||||
<div class="skeleton-icon small"></div>
|
<div class="skeleton-icon"></div>
|
||||||
<div class="skeleton-icon small"></div>
|
<div class="skeleton-text short ms-2"></div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@ -57,89 +51,83 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Data State -->
|
|
||||||
<div v-else class="table-responsive">
|
<div v-else class="table-responsive">
|
||||||
<table id="location-list" class="table table-flush">
|
<table id="location-list" class="table table-flush">
|
||||||
<thead class="thead-light">
|
<thead class="thead-light">
|
||||||
<tr>
|
<tr>
|
||||||
<th>Nom</th>
|
<th>Lieu</th>
|
||||||
<th>Ville</th>
|
<th>Ville</th>
|
||||||
<th>Adresse</th>
|
<th>Adresse</th>
|
||||||
<th>Latitude GPS</th>
|
<th>Coordonnees GPS</th>
|
||||||
<th>Longitude GPS</th>
|
<th>Statut</th>
|
||||||
<th>Par défaut</th>
|
|
||||||
<th>Action</th>
|
<th>Action</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="location in tableData" :key="location.id">
|
<tr v-for="location in tableData" :key="location.id">
|
||||||
<!-- Name Column -->
|
<td class="font-weight-bold">
|
||||||
<td class="text-xs font-weight-bold">
|
|
||||||
<span class="font-weight-bold">{{ location.name || "N/A" }}</span>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<!-- City Column -->
|
|
||||||
<td class="text-xs font-weight-bold">
|
|
||||||
<span>{{ location.address?.city || "N/A" }}</span>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<!-- Address Column -->
|
|
||||||
<td class="text-xs font-weight-bold">
|
|
||||||
<span>{{ location.address?.line1 || "N/A" }}</span>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<!-- GPS Latitude Column -->
|
|
||||||
<td class="text-xs font-weight-bold">
|
|
||||||
<span>{{
|
|
||||||
location.gps_lat ? location.gps_lat + "°" : "N/A"
|
|
||||||
}}</span>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<!-- GPS Longitude Column -->
|
|
||||||
<td class="text-xs font-weight-bold">
|
|
||||||
<span>{{
|
|
||||||
location.gps_lng ? location.gps_lng + "°" : "N/A"
|
|
||||||
}}</span>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<!-- Default Column -->
|
|
||||||
<td class="text-xs font-weight-bold">
|
|
||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
<soft-button
|
|
||||||
:color="location.is_default ? 'success' : 'secondary'"
|
|
||||||
variant="outline"
|
|
||||||
class="btn-icon-only btn-rounded mb-0 me-2 btn-sm d-flex align-items-center justify-content-center"
|
|
||||||
>
|
|
||||||
<i
|
|
||||||
:class="
|
|
||||||
location.is_default ? 'fas fa-check' : 'fas fa-times'
|
|
||||||
"
|
|
||||||
aria-hidden="true"
|
|
||||||
></i>
|
|
||||||
</soft-button>
|
|
||||||
<span>{{ location.is_default ? "Oui" : "Non" }}</span>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<!-- Actions Column -->
|
|
||||||
<td>
|
|
||||||
<div class="d-flex align-items-center gap-2">
|
|
||||||
<!-- View Button -->
|
|
||||||
<soft-button
|
<soft-button
|
||||||
color="info"
|
color="info"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
title="View Location"
|
class="btn-icon-only btn-rounded mb-0 me-2 btn-sm d-flex align-items-center justify-content-center"
|
||||||
|
>
|
||||||
|
<i class="fas fa-map-marker-alt" aria-hidden="true"></i>
|
||||||
|
</soft-button>
|
||||||
|
<div>
|
||||||
|
<div>{{ location.name || "N/A" }}</div>
|
||||||
|
<div class="text-xs text-muted">ID #{{ location.id }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td class="text-xs font-weight-bold">
|
||||||
|
{{ location.city || "N/A" }}
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td class="text-xs font-weight-bold">
|
||||||
|
<div class="contact-info">
|
||||||
|
<div class="text-xs text-secondary">{{ getAddressLine(location) }}</div>
|
||||||
|
<div class="text-xs">{{ getAddressMeta(location) }}</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td class="text-xs font-weight-bold">
|
||||||
|
<div class="contact-info">
|
||||||
|
<div class="text-xs text-secondary">{{ getLatitude(location) }}</div>
|
||||||
|
<div class="text-xs">{{ getLongitude(location) }}</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td class="text-xs font-weight-bold">
|
||||||
|
<div class="d-flex flex-column">
|
||||||
|
<soft-button
|
||||||
|
:color="location.is_default ? 'success' : 'secondary'"
|
||||||
|
variant="outline"
|
||||||
|
class="btn-sm"
|
||||||
|
>
|
||||||
|
<i :class="location.is_default ? 'fas fa-check me-1' : 'fas fa-times me-1'"></i>
|
||||||
|
{{ location.is_default ? "Par defaut" : "Secondaire" }}
|
||||||
|
</soft-button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
<div class="d-flex align-items-center gap-2">
|
||||||
|
<soft-button
|
||||||
|
color="info"
|
||||||
|
variant="outline"
|
||||||
|
title="Voir le lieu"
|
||||||
:data-location-id="location.id"
|
:data-location-id="location.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"
|
||||||
>
|
>
|
||||||
<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="Delete Location"
|
title="Supprimer le lieu"
|
||||||
:data-location-id="location.id"
|
:data-location-id="location.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"
|
||||||
>
|
>
|
||||||
@ -152,14 +140,75 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Empty State -->
|
<div
|
||||||
|
v-if="!loading && tableData.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 || tableData.length }} lieux
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav aria-label="Pagination lieux">
|
||||||
|
<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 }}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li
|
||||||
|
class="page-item"
|
||||||
|
:class="{
|
||||||
|
disabled: (pagination.current_page || 1) === (pagination.last_page || 1)
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
|
||||||
<div v-if="!loading && tableData.length === 0" class="empty-state">
|
<div v-if="!loading && tableData.length === 0" class="empty-state">
|
||||||
<div class="empty-icon">
|
<div class="empty-icon">
|
||||||
<i class="fas fa-map-marker-alt fa-3x text-muted"></i>
|
<i class="fas fa-map-marker-alt fa-3x text-muted"></i>
|
||||||
</div>
|
</div>
|
||||||
<h5 class="empty-title">Aucun emplacement trouvé</h5>
|
<h5 class="empty-title">Aucun emplacement trouve</h5>
|
||||||
<p class="empty-text text-muted">
|
<p class="empty-text text-muted">
|
||||||
Aucun emplacement à afficher pour le moment.
|
Aucun emplacement a afficher pour le moment.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -171,7 +220,7 @@ import { DataTable } from "simple-datatables";
|
|||||||
import SoftButton from "@/components/SoftButton.vue";
|
import SoftButton from "@/components/SoftButton.vue";
|
||||||
import { defineProps, defineEmits } from "vue";
|
import { defineProps, defineEmits } from "vue";
|
||||||
|
|
||||||
const emit = defineEmits(["view", "delete"]);
|
const emit = defineEmits(["view", "delete", "page-change"]);
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
data: {
|
data: {
|
||||||
@ -190,19 +239,114 @@ const props = defineProps({
|
|||||||
type: Number,
|
type: Number,
|
||||||
default: 5,
|
default: 5,
|
||||||
},
|
},
|
||||||
|
pagination: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({
|
||||||
|
current_page: 1,
|
||||||
|
last_page: 1,
|
||||||
|
per_page: 10,
|
||||||
|
total: 0,
|
||||||
|
from: 0,
|
||||||
|
to: 0,
|
||||||
|
}),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Use the provided data prop or fallback to locationData
|
|
||||||
const tableData = computed(() => {
|
const tableData = computed(() => {
|
||||||
return props.data && props.data.length > 0 ? props.data : props.locationData;
|
return props.data && props.data.length > 0 ? props.data : props.locationData;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Reactive data
|
|
||||||
const dataTableInstance = ref(null);
|
const dataTableInstance = ref(null);
|
||||||
|
|
||||||
// Methods
|
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 || tableData.value.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 || tableData.value.length === 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.min(
|
||||||
|
(Number(props.pagination.current_page) || 1) *
|
||||||
|
(Number(props.pagination.per_page) || 10),
|
||||||
|
Number(props.pagination.total) || 0
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const getAddressLine = (location) => {
|
||||||
|
return location.full_address || location.address_line1 || "Adresse indisponible";
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAddressMeta = (location) => {
|
||||||
|
const parts = [location.postal_code, location.city, location.country_code].filter(Boolean);
|
||||||
|
return parts.length > 0 ? parts.join(" ") : "N/A";
|
||||||
|
};
|
||||||
|
|
||||||
|
const getLatitude = (location) => {
|
||||||
|
return location.gps_lat ? `${location.gps_lat}°` : "Latitude N/A";
|
||||||
|
};
|
||||||
|
|
||||||
|
const getLongitude = (location) => {
|
||||||
|
return location.gps_lng ? `${location.gps_lng}°` : "Longitude N/A";
|
||||||
|
};
|
||||||
|
|
||||||
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;
|
||||||
@ -213,8 +357,9 @@ const initializeDataTable = () => {
|
|||||||
dataTableInstance.value = new DataTable(dataTableEl, {
|
dataTableInstance.value = new DataTable(dataTableEl, {
|
||||||
searchable: true,
|
searchable: true,
|
||||||
fixedHeight: true,
|
fixedHeight: true,
|
||||||
perPage: 10,
|
paging: false,
|
||||||
perPageSelect: [5, 10, 15, 20],
|
perPage: Number(props.pagination?.per_page) || 10,
|
||||||
|
perPageSelect: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
dataTableEl.addEventListener("click", handleTableClick);
|
dataTableEl.addEventListener("click", handleTableClick);
|
||||||
@ -226,22 +371,36 @@ const handleTableClick = (event) => {
|
|||||||
if (!button) return;
|
if (!button) return;
|
||||||
|
|
||||||
const locationId = button.getAttribute("data-location-id");
|
const locationId = button.getAttribute("data-location-id");
|
||||||
if (button.title === "Delete Location" || button.querySelector(".fa-trash")) {
|
if (!locationId) return;
|
||||||
|
|
||||||
|
if (
|
||||||
|
button.title === "Supprimer le lieu" ||
|
||||||
|
button.querySelector(".fa-trash")
|
||||||
|
) {
|
||||||
emit("delete", locationId);
|
emit("delete", locationId);
|
||||||
} else if (
|
} else if (
|
||||||
button.title === "View Location" ||
|
button.title === "Voir le lieu" ||
|
||||||
button.querySelector(".fa-eye")
|
button.querySelector(".fa-eye")
|
||||||
) {
|
) {
|
||||||
emit("view", locationId);
|
emit("view", locationId);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Watch for data changes to reinitialize datatable
|
const changePage = (page) => {
|
||||||
|
if (
|
||||||
|
page !== "..." &&
|
||||||
|
page >= 1 &&
|
||||||
|
page <= (Number(props.pagination?.last_page) || 1) &&
|
||||||
|
page !== Number(props.pagination?.current_page)
|
||||||
|
) {
|
||||||
|
emit("page-change", page);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => tableData.value,
|
() => tableData.value,
|
||||||
() => {
|
() => {
|
||||||
if (!props.loading && tableData.value.length > 0) {
|
if (!props.loading) {
|
||||||
// Small delay to ensure DOM is updated
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
initializeDataTable();
|
initializeDataTable();
|
||||||
}, 100);
|
}, 100);
|
||||||
@ -255,12 +414,12 @@ onUnmounted(() => {
|
|||||||
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 && tableData.value.length > 0) {
|
if (!props.loading && tableData.value.length > 0) {
|
||||||
initializeDataTable();
|
initializeDataTable();
|
||||||
@ -276,22 +435,39 @@ 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-avatar {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: shimmer 2s infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
.skeleton-icon {
|
.skeleton-icon {
|
||||||
@ -323,11 +499,6 @@ onMounted(() => {
|
|||||||
width: 120px;
|
width: 120px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.skeleton-icon.small {
|
|
||||||
width: 24px;
|
|
||||||
height: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-state {
|
.empty-state {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 3rem 1rem;
|
padding: 3rem 1rem;
|
||||||
@ -348,21 +519,12 @@ onMounted(() => {
|
|||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-xs {
|
.contact-info {
|
||||||
font-size: 0.75rem;
|
line-height: 1.2;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Animations */
|
.text-xs {
|
||||||
@keyframes pulse {
|
font-size: 0.75rem;
|
||||||
0% {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
opacity: 0.7;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes shimmer {
|
@keyframes shimmer {
|
||||||
@ -374,13 +536,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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -40,6 +40,8 @@ export interface ClientLocationListResponse {
|
|||||||
last_page: number;
|
last_page: number;
|
||||||
per_page: number;
|
per_page: number;
|
||||||
total: number;
|
total: number;
|
||||||
|
from?: number;
|
||||||
|
to?: number;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -12,13 +12,20 @@ export const useClientGroupStore = defineStore("clientGroup", () => {
|
|||||||
const currentClientGroup = ref<ClientGroup | null>(null);
|
const currentClientGroup = ref<ClientGroup | null>(null);
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const error = ref<string | null>(null);
|
const error = ref<string | null>(null);
|
||||||
|
const filters = ref({
|
||||||
|
page: 1,
|
||||||
|
per_page: 5,
|
||||||
|
search: undefined as string | undefined,
|
||||||
|
});
|
||||||
|
|
||||||
// Pagination state
|
// Pagination state
|
||||||
const pagination = ref({
|
const pagination = ref({
|
||||||
current_page: 1,
|
current_page: 1,
|
||||||
last_page: 1,
|
last_page: 1,
|
||||||
per_page: 10,
|
per_page: 5,
|
||||||
total: 0,
|
total: 0,
|
||||||
|
from: 0,
|
||||||
|
to: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Getters
|
// Getters
|
||||||
@ -50,15 +57,38 @@ export const useClientGroupStore = defineStore("clientGroup", () => {
|
|||||||
|
|
||||||
const setPagination = (meta: any) => {
|
const setPagination = (meta: any) => {
|
||||||
if (meta) {
|
if (meta) {
|
||||||
|
const getValue = (val: any) => (Array.isArray(val) ? val[0] : val);
|
||||||
|
|
||||||
pagination.value = {
|
pagination.value = {
|
||||||
current_page: meta.current_page || 1,
|
current_page: Number(getValue(meta.current_page)) || 1,
|
||||||
last_page: meta.last_page || 1,
|
last_page: Number(getValue(meta.last_page)) || 1,
|
||||||
per_page: meta.per_page || 10,
|
per_page: Number(getValue(meta.per_page)) || 5,
|
||||||
total: meta.total || 0,
|
total: Number(getValue(meta.total)) || 0,
|
||||||
|
from: Number(getValue(meta.from)) || 0,
|
||||||
|
to: Number(getValue(meta.to)) || 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
filters.value = {
|
||||||
|
...filters.value,
|
||||||
|
page: pagination.value.current_page,
|
||||||
|
per_page: pagination.value.per_page,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const setFilters = (params?: {
|
||||||
|
page?: number;
|
||||||
|
per_page?: number;
|
||||||
|
search?: string;
|
||||||
|
}) => {
|
||||||
|
filters.value = {
|
||||||
|
...filters.value,
|
||||||
|
...params,
|
||||||
|
page: params?.page ?? filters.value.page ?? 1,
|
||||||
|
per_page: params?.per_page ?? filters.value.per_page ?? 5,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch all client groups with optional pagination and filters
|
* Fetch all client groups with optional pagination and filters
|
||||||
*/
|
*/
|
||||||
@ -71,7 +101,19 @@ export const useClientGroupStore = defineStore("clientGroup", () => {
|
|||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await ClientGroupService.getAllClientGroups(params);
|
setFilters(params);
|
||||||
|
|
||||||
|
const requestParams = Object.fromEntries(
|
||||||
|
Object.entries(filters.value).filter(
|
||||||
|
([, value]) => value !== undefined && value !== null && value !== ""
|
||||||
|
)
|
||||||
|
) as {
|
||||||
|
page?: number;
|
||||||
|
per_page?: number;
|
||||||
|
search?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await ClientGroupService.getAllClientGroups(requestParams);
|
||||||
setClientGroups(response.data);
|
setClientGroups(response.data);
|
||||||
if (response.meta) {
|
if (response.meta) {
|
||||||
setPagination(response.meta);
|
setPagination(response.meta);
|
||||||
|
|||||||
@ -16,6 +16,16 @@ export const useClientLocationStore = defineStore("clientLocation", () => {
|
|||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const error = ref<string | null>(null);
|
const error = ref<string | null>(null);
|
||||||
const searchResults = ref<ClientLocation[]>([]);
|
const searchResults = ref<ClientLocation[]>([]);
|
||||||
|
const filters = ref<{
|
||||||
|
page: number;
|
||||||
|
per_page: number;
|
||||||
|
client_id?: number;
|
||||||
|
is_default?: boolean;
|
||||||
|
search?: string;
|
||||||
|
}>({
|
||||||
|
page: 1,
|
||||||
|
per_page: 10,
|
||||||
|
});
|
||||||
|
|
||||||
// Pagination state
|
// Pagination state
|
||||||
const pagination = ref({
|
const pagination = ref({
|
||||||
@ -23,6 +33,8 @@ export const useClientLocationStore = defineStore("clientLocation", () => {
|
|||||||
last_page: 1,
|
last_page: 1,
|
||||||
per_page: 10,
|
per_page: 10,
|
||||||
total: 0,
|
total: 0,
|
||||||
|
from: 0,
|
||||||
|
to: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Getters
|
// Getters
|
||||||
@ -74,15 +86,40 @@ export const useClientLocationStore = defineStore("clientLocation", () => {
|
|||||||
|
|
||||||
const setPagination = (meta: any) => {
|
const setPagination = (meta: any) => {
|
||||||
if (meta) {
|
if (meta) {
|
||||||
|
const getValue = (val: any) => (Array.isArray(val) ? val[0] : val);
|
||||||
|
|
||||||
pagination.value = {
|
pagination.value = {
|
||||||
current_page: meta.current_page || 1,
|
current_page: Number(getValue(meta.current_page)) || 1,
|
||||||
last_page: meta.last_page || 1,
|
last_page: Number(getValue(meta.last_page)) || 1,
|
||||||
per_page: meta.per_page || 10,
|
per_page: Number(getValue(meta.per_page)) || 10,
|
||||||
total: meta.total || 0,
|
total: Number(getValue(meta.total)) || 0,
|
||||||
|
from: Number(getValue(meta.from)) || 0,
|
||||||
|
to: Number(getValue(meta.to)) || 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
filters.value = {
|
||||||
|
...filters.value,
|
||||||
|
page: pagination.value.current_page,
|
||||||
|
per_page: pagination.value.per_page,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const setFilters = (params?: {
|
||||||
|
page?: number;
|
||||||
|
per_page?: number;
|
||||||
|
client_id?: number;
|
||||||
|
is_default?: boolean;
|
||||||
|
search?: string;
|
||||||
|
}) => {
|
||||||
|
filters.value = {
|
||||||
|
...filters.value,
|
||||||
|
...params,
|
||||||
|
page: params?.page ?? filters.value.page ?? 1,
|
||||||
|
per_page: params?.per_page ?? filters.value.per_page ?? 10,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch all client locations with optional pagination and filters
|
* Fetch all client locations with optional pagination and filters
|
||||||
*/
|
*/
|
||||||
@ -97,8 +134,22 @@ export const useClientLocationStore = defineStore("clientLocation", () => {
|
|||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
setFilters(params);
|
||||||
|
|
||||||
|
const requestParams = Object.fromEntries(
|
||||||
|
Object.entries(filters.value).filter(
|
||||||
|
([, value]) => value !== undefined && value !== null && value !== ""
|
||||||
|
)
|
||||||
|
) as {
|
||||||
|
page?: number;
|
||||||
|
per_page?: number;
|
||||||
|
client_id?: number;
|
||||||
|
is_default?: boolean;
|
||||||
|
search?: string;
|
||||||
|
};
|
||||||
|
|
||||||
const response = await ClientLocationService.getAllClientLocations(
|
const response = await ClientLocationService.getAllClientLocations(
|
||||||
params
|
requestParams
|
||||||
);
|
);
|
||||||
setClientLocations(response.data);
|
setClientLocations(response.data);
|
||||||
if (response.meta) {
|
if (response.meta) {
|
||||||
@ -411,6 +462,12 @@ export const useClientLocationStore = defineStore("clientLocation", () => {
|
|||||||
last_page: 1,
|
last_page: 1,
|
||||||
per_page: 10,
|
per_page: 10,
|
||||||
total: 0,
|
total: 0,
|
||||||
|
from: 0,
|
||||||
|
to: 0,
|
||||||
|
};
|
||||||
|
filters.value = {
|
||||||
|
page: 1,
|
||||||
|
per_page: 10,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -2,23 +2,28 @@
|
|||||||
<liste-lieux-presentation
|
<liste-lieux-presentation
|
||||||
:is-loading="clientLocationStore.isLoading"
|
:is-loading="clientLocationStore.isLoading"
|
||||||
:location-data="clientLocationStore.clientLocations"
|
:location-data="clientLocationStore.clientLocations"
|
||||||
|
:pagination="clientLocationStore.getPagination"
|
||||||
|
@page-change="handlePageChange"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import ListeLieuxPresentation from "@/components/Organism/CRM/lieux/ListeLieuxPresentation.vue";
|
import ListeLieuxPresentation from "@/components/Organism/CRM/lieux/ListeLieuxPresentation.vue";
|
||||||
import { useClientLocationStore } from "@/stores/clientLocation";
|
import { useClientLocationStore } from "@/stores/clientLocation";
|
||||||
import { onMounted, ref } from "vue";
|
import { onMounted } from "vue";
|
||||||
|
|
||||||
const clientLocationStore = useClientLocationStore();
|
const clientLocationStore = useClientLocationStore();
|
||||||
|
|
||||||
const filters = ref({
|
const handlePageChange = async (page) => {
|
||||||
page: 1,
|
await clientLocationStore.fetchClientLocations({
|
||||||
per_page: 10,
|
page,
|
||||||
search: "",
|
per_page: clientLocationStore.getPagination.per_page,
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await clientLocationStore.fetchClientLocations();
|
await clientLocationStore.fetchClientLocations({
|
||||||
console.log(clientLocationStore.clientLocations);
|
page: 1,
|
||||||
|
per_page: clientLocationStore.getPagination.per_page,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user