Fix Client Desig
This commit is contained in:
parent
d8d2b68421
commit
ce61b79080
48
thanasoft-back/database/seeders/ClientSeeder.php
Normal file
48
thanasoft-back/database/seeders/ClientSeeder.php
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Seeders;
|
||||||
|
|
||||||
|
use Illuminate\Database\Seeder;
|
||||||
|
use App\Models\Client;
|
||||||
|
use App\Models\User;
|
||||||
|
use Faker\Factory as Faker;
|
||||||
|
|
||||||
|
class ClientSeeder extends Seeder
|
||||||
|
{
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
$faker = Faker::create('fr_FR');
|
||||||
|
|
||||||
|
// Récupérer des users existants (optionnel)
|
||||||
|
$users = User::pluck('id')->toArray();
|
||||||
|
|
||||||
|
for ($i = 0; $i < 20; $i++) {
|
||||||
|
Client::create([
|
||||||
|
'name' => $faker->company,
|
||||||
|
'vat_number' => 'FR' . $faker->numberBetween(10000000000, 99999999999),
|
||||||
|
'siret' => $faker->numberBetween(10000000000000, 99999999999999),
|
||||||
|
'email' => $faker->unique()->companyEmail,
|
||||||
|
'phone' => $faker->phoneNumber,
|
||||||
|
|
||||||
|
'billing_address_line1' => $faker->streetAddress,
|
||||||
|
'billing_address_line2' => $faker->optional()->secondaryAddress,
|
||||||
|
'billing_postal_code' => $faker->postcode,
|
||||||
|
'billing_city' => $faker->city,
|
||||||
|
'billing_country_code' => 'FR',
|
||||||
|
|
||||||
|
'group_id' => null, // ou random si tu veux
|
||||||
|
'notes' => $faker->optional()->sentence,
|
||||||
|
|
||||||
|
'is_active' => $faker->boolean(90),
|
||||||
|
'is_parent' => false,
|
||||||
|
'parent_id' => null,
|
||||||
|
|
||||||
|
'client_category_id' => $faker->numberBetween(1, 5),
|
||||||
|
|
||||||
|
'user_id' => 1,
|
||||||
|
|
||||||
|
'avatar' => null,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -24,5 +24,6 @@ class DatabaseSeeder extends Seeder
|
|||||||
$this->call(ProductCategorySeeder::class);
|
$this->call(ProductCategorySeeder::class);
|
||||||
$this->call(EmployeeSeeder::class);
|
$this->call(EmployeeSeeder::class);
|
||||||
$this->call(ThanatopractitionerSeeder::class);
|
$this->call(ThanatopractitionerSeeder::class);
|
||||||
|
$this->call(ClientSeeder::class);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -36,12 +36,7 @@ import { storeToRefs } from "pinia";
|
|||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const clientStore = useClientStore();
|
const clientStore = useClientStore();
|
||||||
// Use storeToRefs to keep reactivity for pagination if it was passed as a prop,
|
const emit = defineEmits(["pushDetails", "deleteClient"]);
|
||||||
// but since we are using the store directly for actions, we can also extract it here if needed.
|
|
||||||
// However, the common pattern is that the parent view passes the data.
|
|
||||||
// Let's check where clientData comes from. It comes from props.
|
|
||||||
|
|
||||||
const emit = defineEmits(["pushDetails"]);
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
clientData: {
|
clientData: {
|
||||||
@ -74,7 +69,10 @@ const deleteClient = (client) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onPageChange = (page) => {
|
const onPageChange = (page) => {
|
||||||
clientStore.fetchClients({ page: page, per_page: props.pagination.per_page });
|
clientStore.fetchClients({
|
||||||
|
page,
|
||||||
|
per_page: props.pagination.per_page,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const onPerPageChange = (perPage) => {
|
const onPerPageChange = (perPage) => {
|
||||||
@ -84,7 +82,6 @@ const onPerPageChange = (perPage) => {
|
|||||||
const onSearch = (query) => {
|
const onSearch = (query) => {
|
||||||
clientStore.fetchClients({
|
clientStore.fetchClients({
|
||||||
page: 1,
|
page: 1,
|
||||||
per_page: props.pagination.per_page,
|
|
||||||
search: query,
|
search: query,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,101 +1,54 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="table-container">
|
<div class="table-container">
|
||||||
<!-- Top Controls (Search & Per Page) -->
|
|
||||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
||||||
<div class="d-flex align-items-center">
|
|
||||||
<select
|
|
||||||
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>
|
|
||||||
|
|
||||||
<!-- 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>Commercial</th>
|
|
||||||
<th>Client</th>
|
<th>Client</th>
|
||||||
<th>Address</th>
|
<th>Référence</th>
|
||||||
<th>Categories</th>
|
<th>Catégorie</th>
|
||||||
|
<th>Commercial</th>
|
||||||
|
<th>Adresse</th>
|
||||||
<th>Contact</th>
|
<th>Contact</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">
|
||||||
<!-- Commercial 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>
|
|
||||||
|
|
||||||
<!-- Client 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>
|
||||||
|
|
||||||
<!-- Address Column Skeleton -->
|
|
||||||
<td>
|
<td>
|
||||||
<div class="skeleton-text long"></div>
|
<div class="skeleton-text short"></div>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<!-- Categories 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>
|
||||||
<!-- Contact Column Skeleton -->
|
<div class="skeleton-text long"></div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="skeleton-text long"></div>
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="contact-info">
|
<div class="contact-info">
|
||||||
<div class="skeleton-text long mb-1"></div>
|
<div class="skeleton-text long mb-1"></div>
|
||||||
<div class="skeleton-text medium"></div>
|
<div class="skeleton-text medium"></div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<!-- Status Column Skeleton -->
|
|
||||||
<td>
|
<td>
|
||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
<div class="skeleton-icon"></div>
|
<div class="skeleton-icon"></div>
|
||||||
@ -109,15 +62,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Data State -->
|
|
||||||
<div v-else class="table-responsive">
|
<div v-else class="table-responsive">
|
||||||
<table id="contact-list" class="table table-flush">
|
<table id="client-list" class="table table-flush">
|
||||||
<thead class="thead-light">
|
<thead class="thead-light">
|
||||||
<tr>
|
<tr>
|
||||||
<th>Commercial</th>
|
|
||||||
<th>Client</th>
|
<th>Client</th>
|
||||||
<th>Address</th>
|
<th>Référence</th>
|
||||||
<th>Categories</th>
|
<th>Catégorie</th>
|
||||||
|
<th>Commercial</th>
|
||||||
|
<th>Adresse</th>
|
||||||
<th>Contact</th>
|
<th>Contact</th>
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
<th>Action</th>
|
<th>Action</th>
|
||||||
@ -125,38 +78,23 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr v-for="client in data" :key="client.id">
|
<tr v-for="client in data" :key="client.id">
|
||||||
<!-- Commercial Column -->
|
|
||||||
<td>
|
|
||||||
<div class="d-flex align-items-center">
|
|
||||||
<soft-checkbox />
|
|
||||||
<p class="text-xs font-weight-bold ms-2 mb-0">
|
|
||||||
{{ client.commercial }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<!-- Client 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="client.avatar_url || getRandomAvatar()"
|
||||||
size="xs"
|
size="xs"
|
||||||
class="me-2"
|
class="me-2"
|
||||||
alt="user image"
|
alt="client image"
|
||||||
circular
|
circular
|
||||||
/>
|
/>
|
||||||
<span>{{ client.name }}</span>
|
<span>{{ client.name }}</span>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<!-- Address Column (Shortened) -->
|
|
||||||
<td class="text-xs font-weight-bold">
|
<td class="text-xs font-weight-bold">
|
||||||
<span class="my-2 text-xs">{{
|
<span class="my-2 text-xs">{{ getClientReference(client) }}</span>
|
||||||
getShortAddress(client.billing_address)
|
|
||||||
}}</span>
|
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<!-- Categories 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
|
||||||
@ -169,58 +107,71 @@
|
|||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
></i>
|
></i>
|
||||||
</soft-button>
|
</soft-button>
|
||||||
<span>{{ client.type_label }}</span>
|
<span>{{ client.type_label || "Non renseigné" }}</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td class="text-xs font-weight-bold">
|
||||||
|
{{ client.commercial || "N/A" }}
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td class="text-xs font-weight-bold">
|
||||||
|
<div class="address-info">
|
||||||
|
<div>{{ getAddressLine(client.billing_address) }}</div>
|
||||||
|
<div class="text-xs text-muted">
|
||||||
|
{{ getShortAddress(client.billing_address) }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
|
|
||||||
<!-- Contact Column -->
|
|
||||||
<td class="text-xs font-weight-bold">
|
<td class="text-xs font-weight-bold">
|
||||||
<div class="contact-info">
|
<div class="contact-info">
|
||||||
<div class="text-xs text-secondary">{{ client.email }}</div>
|
<div class="text-xs text-secondary">{{ client.email || "N/A" }}</div>
|
||||||
<div class="text-xs">{{ client.phone }}</div>
|
<div class="text-xs">{{ client.phone || "N/A" }}</div>
|
||||||
</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">
|
<div class="d-flex flex-column">
|
||||||
<soft-button
|
<soft-button
|
||||||
:color="client.is_active ? 'success' : 'danger'"
|
v-if="client.is_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="client.is_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>{{ client.is_active ? "Active" : "Inactive" }}</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="View Client"
|
title="Voir le client"
|
||||||
:data-client-id="client.id"
|
:data-client-id="client.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', client.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="Delete Client"
|
title="Supprimer le client"
|
||||||
:data-client-id="client.id"
|
:data-client-id="client.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', client.id)"
|
|
||||||
>
|
>
|
||||||
<i class="fas fa-trash" aria-hidden="true"></i>
|
<i class="fas fa-trash" aria-hidden="true"></i>
|
||||||
</soft-button>
|
</soft-button>
|
||||||
@ -231,72 +182,72 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Pagination Footer -->
|
|
||||||
<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-3 px-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 {{ pagination.from }} à {{ pagination.to }} sur
|
Affichage de {{ safeFrom }} à {{ safeTo }} sur
|
||||||
{{ pagination.total }} clients
|
{{ pagination.total || data.length }} clients
|
||||||
</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>
|
||||||
|
|
||||||
<li
|
<li
|
||||||
v-for="page in displayedPages"
|
v-for="page in displayedPages"
|
||||||
:key="page"
|
:key="`page-${page}`"
|
||||||
class="page-item"
|
class="page-item"
|
||||||
:class="{ active: pagination.current_page === page }"
|
:class="{
|
||||||
|
active: (pagination.current_page || 1) === 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>
|
||||||
|
|
||||||
<!-- 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>
|
||||||
</div>
|
</div>
|
||||||
"
|
|
||||||
<h5 class="empty-title">Aucun client trouvé</h5>
|
<h5 class="empty-title">Aucun client trouvé</h5>
|
||||||
<p class="empty-text text-muted">
|
<p class="empty-text text-muted">
|
||||||
Aucun client à afficher pour le moment.
|
Aucun client à afficher pour le moment.
|
||||||
@ -306,22 +257,14 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from "vue";
|
import { ref, onMounted, watch, onUnmounted, computed } from "vue";
|
||||||
import SoftCheckbox from "@/components/SoftCheckbox.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";
|
import { defineProps, defineEmits } from "vue";
|
||||||
import debounce from "lodash/debounce";
|
|
||||||
|
|
||||||
const emit = defineEmits([
|
const emit = defineEmits(["view", "delete", "page-change", "per-page-change"]);
|
||||||
"view",
|
|
||||||
"delete",
|
|
||||||
"page-change",
|
|
||||||
"per-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";
|
||||||
@ -331,6 +274,8 @@ import img6 from "@/assets/img/ivana-squares.jpg";
|
|||||||
|
|
||||||
const avatarImages = [img1, img2, img3, img4, img5, img6];
|
const avatarImages = [img1, img2, img3, img4, img5, img6];
|
||||||
|
|
||||||
|
const dataTableInstance = ref(null);
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
data: {
|
data: {
|
||||||
type: Array,
|
type: Array,
|
||||||
@ -357,64 +302,104 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Calculate displayed page numbers
|
|
||||||
const displayedPages = computed(() => {
|
const displayedPages = computed(() => {
|
||||||
const total = props.pagination.last_page;
|
const total = Number(props.pagination?.last_page) || 1;
|
||||||
const current = props.pagination.current_page;
|
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] !== "...")
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Methods
|
const safeFrom = computed(() => {
|
||||||
const changePage = (page) => {
|
if (props.pagination?.from) {
|
||||||
if (page !== "..." && page >= 1 && page <= props.pagination.last_page) {
|
return props.pagination.from;
|
||||||
emit("page-change", page);
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const onPerPageChange = (event) => {
|
if (!props.pagination?.total || props.data.length === 0) {
|
||||||
const newPerPage = parseInt(event.target.value);
|
return 0;
|
||||||
emit("per-page-change", newPerPage);
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const onSearch = debounce((event) => {
|
return (
|
||||||
emit("search-change", event.target.value);
|
((Number(props.pagination.current_page) || 1) - 1) *
|
||||||
}, 300);
|
(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
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
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];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getClientReference = (client) => {
|
||||||
|
return client.vat_number || client.siret || "N/A";
|
||||||
|
};
|
||||||
|
|
||||||
|
const getAddressLine = (address) => {
|
||||||
|
if (!address) return "Adresse indisponible";
|
||||||
|
|
||||||
|
return (
|
||||||
|
address.line1 ||
|
||||||
|
address.line2 ||
|
||||||
|
address.full_address ||
|
||||||
|
"Adresse indisponible"
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const getShortAddress = (address) => {
|
const getShortAddress = (address) => {
|
||||||
if (!address) return "N/A";
|
if (!address) return "N/A";
|
||||||
// Return just city and postal code for brevity
|
|
||||||
return `${address.postal_code} ${address.city}`;
|
const parts = [address.postal_code, address.city, address.country_code].filter(
|
||||||
|
Boolean
|
||||||
|
);
|
||||||
|
return parts.length > 0 ? parts.join(" ") : "N/A";
|
||||||
};
|
};
|
||||||
|
|
||||||
const getCategoryColor = (type) => {
|
const getCategoryColor = (type) => {
|
||||||
@ -432,8 +417,88 @@ const getCategoryIcon = (type) => {
|
|||||||
Particulier: "fas fa-user",
|
Particulier: "fas fa-user",
|
||||||
Association: "fas fa-users",
|
Association: "fas fa-users",
|
||||||
};
|
};
|
||||||
return icons[type] || "fas fa-circle";
|
return icons[type] || "fas fa-tag";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleTableClick = (event) => {
|
||||||
|
const button = event.target.closest("button");
|
||||||
|
if (!button) return;
|
||||||
|
|
||||||
|
const clientId = button.getAttribute("data-client-id");
|
||||||
|
if (!clientId) return;
|
||||||
|
|
||||||
|
if (
|
||||||
|
button.title === "Supprimer le client" ||
|
||||||
|
button.querySelector(".fa-trash")
|
||||||
|
) {
|
||||||
|
emit("delete", clientId);
|
||||||
|
} else if (
|
||||||
|
button.title === "Voir le client" ||
|
||||||
|
button.querySelector(".fa-eye")
|
||||||
|
) {
|
||||||
|
emit("view", clientId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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) => {
|
||||||
|
if (
|
||||||
|
page !== "..." &&
|
||||||
|
page >= 1 &&
|
||||||
|
page <= (Number(props.pagination?.last_page) || 1) &&
|
||||||
|
page !== Number(props.pagination?.current_page)
|
||||||
|
) {
|
||||||
|
emit("page-change", page);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.data,
|
||||||
|
() => {
|
||||||
|
if (!props.loading) {
|
||||||
|
setTimeout(() => {
|
||||||
|
initializeDataTable();
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
const dataTableEl = document.getElementById("client-list");
|
||||||
|
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>
|
<style scoped>
|
||||||
@ -444,31 +509,30 @@ const getCategoryIcon = (type) => {
|
|||||||
|
|
||||||
.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 {
|
||||||
@ -529,6 +593,7 @@ const getCategoryIcon = (type) => {
|
|||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.address-info,
|
||||||
.contact-info {
|
.contact-info {
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
}
|
}
|
||||||
@ -537,19 +602,6 @@ const getCategoryIcon = (type) => {
|
|||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Animations */
|
|
||||||
@keyframes pulse {
|
|
||||||
0% {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
opacity: 0.7;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes shimmer {
|
@keyframes shimmer {
|
||||||
0% {
|
0% {
|
||||||
background-position: -200% 0;
|
background-position: -200% 0;
|
||||||
@ -559,13 +611,7 @@ const getCategoryIcon = (type) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 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;
|
||||||
}
|
}
|
||||||
@ -574,162 +620,4 @@ const getCategoryIcon = (type) => {
|
|||||||
width: 60px;
|
width: 60px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.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;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.table-container {
|
|
||||||
position: relative;
|
|
||||||
min-height: 200px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-container {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-spinner {
|
|
||||||
position: absolute;
|
|
||||||
top: 20px;
|
|
||||||
right: 20px;
|
|
||||||
z-index: 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
.loading-content {
|
|
||||||
opacity: 0.7;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.skeleton-row {
|
|
||||||
animation: pulse 1.5s ease-in-out infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.skeleton-checkbox {
|
|
||||||
width: 18px;
|
|
||||||
height: 18px;
|
|
||||||
border-radius: 3px;
|
|
||||||
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
|
|
||||||
background-size: 200% 100%;
|
|
||||||
animation: shimmer 2s infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Animations */
|
|
||||||
@keyframes pulse {
|
|
||||||
0% {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
opacity: 0.7;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes shimmer {
|
|
||||||
0% {
|
|
||||||
background-position: -200% 0;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
background-position: 200% 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Responsive adjustments */
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.loading-spinner {
|
|
||||||
top: 10px;
|
|
||||||
right: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.skeleton-text.long {
|
|
||||||
width: 80px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.skeleton-text.medium {
|
|
||||||
width: 60px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.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;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -244,7 +244,7 @@
|
|||||||
maxlength="255"
|
maxlength="255"
|
||||||
/>
|
/>
|
||||||
<p v-else class="form-control-static text-sm">
|
<p v-else class="form-control-static text-sm">
|
||||||
{{ client.billing_address_line1 || "-" }}
|
{{ client.billing_address?.line1 || "-" }}
|
||||||
</p>
|
</p>
|
||||||
<div
|
<div
|
||||||
v-if="errors.billing_address_line1"
|
v-if="errors.billing_address_line1"
|
||||||
@ -265,7 +265,7 @@
|
|||||||
maxlength="255"
|
maxlength="255"
|
||||||
/>
|
/>
|
||||||
<p v-else class="form-control-static text-sm">
|
<p v-else class="form-control-static text-sm">
|
||||||
{{ client.billing_address_line2 || "-" }}
|
{{ client.billing_address?.line2 || "-" }}
|
||||||
</p>
|
</p>
|
||||||
<div
|
<div
|
||||||
v-if="errors.billing_address_line2"
|
v-if="errors.billing_address_line2"
|
||||||
@ -286,7 +286,7 @@
|
|||||||
maxlength="20"
|
maxlength="20"
|
||||||
/>
|
/>
|
||||||
<p v-else class="form-control-static text-sm">
|
<p v-else class="form-control-static text-sm">
|
||||||
{{ client.billing_postal_code || "-" }}
|
{{ client.billing_address?.postal_code || "-" }}
|
||||||
</p>
|
</p>
|
||||||
<div
|
<div
|
||||||
v-if="errors.billing_postal_code"
|
v-if="errors.billing_postal_code"
|
||||||
@ -307,7 +307,7 @@
|
|||||||
maxlength="191"
|
maxlength="191"
|
||||||
/>
|
/>
|
||||||
<p v-else class="form-control-static text-sm">
|
<p v-else class="form-control-static text-sm">
|
||||||
{{ client.billing_city || "-" }}
|
{{ client.billing_address?.city || "-" }}
|
||||||
</p>
|
</p>
|
||||||
<div v-if="errors.billing_city" class="invalid-feedback d-block">
|
<div v-if="errors.billing_city" class="invalid-feedback d-block">
|
||||||
{{ errors.billing_city }}
|
{{ errors.billing_city }}
|
||||||
@ -325,7 +325,7 @@
|
|||||||
maxlength="2"
|
maxlength="2"
|
||||||
/>
|
/>
|
||||||
<p v-else class="form-control-static text-sm">
|
<p v-else class="form-control-static text-sm">
|
||||||
{{ client.billing_country_code || "-" }}
|
{{ client.billing_address?.country_code || "-" }}
|
||||||
</p>
|
</p>
|
||||||
<div
|
<div
|
||||||
v-if="errors.billing_country_code"
|
v-if="errors.billing_country_code"
|
||||||
@ -412,6 +412,8 @@ const formData = reactive({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const startEdit = () => {
|
const startEdit = () => {
|
||||||
|
const billingAddress = props.client.billing_address || {};
|
||||||
|
|
||||||
isEditing.value = true;
|
isEditing.value = true;
|
||||||
Object.assign(formData, {
|
Object.assign(formData, {
|
||||||
name: props.client.name || "",
|
name: props.client.name || "",
|
||||||
@ -419,11 +421,11 @@ const startEdit = () => {
|
|||||||
siret: props.client.siret || "",
|
siret: props.client.siret || "",
|
||||||
email: props.client.email || "",
|
email: props.client.email || "",
|
||||||
phone: props.client.phone || "",
|
phone: props.client.phone || "",
|
||||||
billing_address_line1: props.client.billing_address_line1 || "",
|
billing_address_line1: billingAddress.line1 || "",
|
||||||
billing_address_line2: props.client.billing_address_line2 || "",
|
billing_address_line2: billingAddress.line2 || "",
|
||||||
billing_postal_code: props.client.billing_postal_code || "",
|
billing_postal_code: billingAddress.postal_code || "",
|
||||||
billing_city: props.client.billing_city || "",
|
billing_city: billingAddress.city || "",
|
||||||
billing_country_code: props.client.billing_country_code || "FR", // Valeur par défaut
|
billing_country_code: billingAddress.country_code || "FR",
|
||||||
group_id: props.client.group_id || null,
|
group_id: props.client.group_id || null,
|
||||||
notes: props.client.notes || "",
|
notes: props.client.notes || "",
|
||||||
is_active:
|
is_active:
|
||||||
@ -606,7 +608,7 @@ const saveChanges = async () => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
isEditing.value = false;
|
isEditing.value = false;
|
||||||
emit("client-updated", formData);
|
emit("client-updated", prepareFormData());
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Erreur lors de la mise à jour:", error);
|
console.error("Erreur lors de la mise à jour:", error);
|
||||||
if (error.response && error.response.data && error.response.data.errors) {
|
if (error.response && error.response.data && error.response.data.errors) {
|
||||||
|
|||||||
@ -6,7 +6,6 @@ import type {
|
|||||||
Client,
|
Client,
|
||||||
CreateClientPayload,
|
CreateClientPayload,
|
||||||
UpdateClientPayload,
|
UpdateClientPayload,
|
||||||
ClientListResponse,
|
|
||||||
} from "@/services/client";
|
} from "@/services/client";
|
||||||
import { Contact } from "@/services/contact";
|
import { Contact } from "@/services/contact";
|
||||||
|
|
||||||
@ -18,6 +17,16 @@ export const useClientStore = defineStore("client", () => {
|
|||||||
const error = ref<string | null>(null);
|
const error = ref<string | null>(null);
|
||||||
const searchResults = ref<Client[]>([]);
|
const searchResults = ref<Client[]>([]);
|
||||||
const contacts_client = ref<Contact[]>([]);
|
const contacts_client = ref<Contact[]>([]);
|
||||||
|
const filters = ref<{
|
||||||
|
page: number;
|
||||||
|
per_page: number;
|
||||||
|
search?: string;
|
||||||
|
is_active?: boolean;
|
||||||
|
group_id?: number;
|
||||||
|
}>({
|
||||||
|
page: 1,
|
||||||
|
per_page: 10,
|
||||||
|
});
|
||||||
|
|
||||||
// Pagination state
|
// Pagination state
|
||||||
const pagination = ref({
|
const pagination = ref({
|
||||||
@ -25,6 +34,8 @@ export const useClientStore = defineStore("client", () => {
|
|||||||
last_page: 1,
|
last_page: 1,
|
||||||
per_page: 10,
|
per_page: 10,
|
||||||
total: 0,
|
total: 0,
|
||||||
|
from: 0,
|
||||||
|
to: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Getters
|
// Getters
|
||||||
@ -80,10 +91,33 @@ export const useClientStore = defineStore("client", () => {
|
|||||||
last_page: Number(getValue(meta.last_page)) || 1,
|
last_page: Number(getValue(meta.last_page)) || 1,
|
||||||
per_page: Number(getValue(meta.per_page)) || 10,
|
per_page: Number(getValue(meta.per_page)) || 10,
|
||||||
total: Number(getValue(meta.total)) || 0,
|
total: Number(getValue(meta.total)) || 0,
|
||||||
|
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;
|
||||||
|
is_active?: boolean;
|
||||||
|
group_id?: number;
|
||||||
|
}) => {
|
||||||
|
filters.value = {
|
||||||
|
...filters.value,
|
||||||
|
...params,
|
||||||
|
page: params?.page ?? filters.value.page ?? 1,
|
||||||
|
per_page: params?.per_page ?? filters.value.per_page ?? 10,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch all clients with optional pagination and filters
|
* Fetch all clients with optional pagination and filters
|
||||||
*/
|
*/
|
||||||
@ -98,7 +132,21 @@ export const useClientStore = defineStore("client", () => {
|
|||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await ClientService.getAllClients(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;
|
||||||
|
is_active?: boolean;
|
||||||
|
group_id?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await ClientService.getAllClients(requestParams);
|
||||||
setClients(response.data);
|
setClients(response.data);
|
||||||
if (response.meta) {
|
if (response.meta) {
|
||||||
setPagination(response.meta);
|
setPagination(response.meta);
|
||||||
@ -166,7 +214,6 @@ export const useClientStore = defineStore("client", () => {
|
|||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
||||||
const response = await ClientService.updateClient(payload);
|
const response = await ClientService.updateClient(payload);
|
||||||
const updatedClient = response.data;
|
const updatedClient = response.data;
|
||||||
|
|
||||||
@ -227,7 +274,6 @@ export const useClientStore = defineStore("client", () => {
|
|||||||
* Search clients
|
* Search clients
|
||||||
*/
|
*/
|
||||||
const searchClients = async (query: string, exactMatch: boolean = false) => {
|
const searchClients = async (query: string, exactMatch: boolean = false) => {
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
error.value = null;
|
error.value = null;
|
||||||
|
|
||||||
@ -375,6 +421,12 @@ export const useClientStore = defineStore("client", () => {
|
|||||||
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,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -394,6 +446,7 @@ export const useClientStore = defineStore("client", () => {
|
|||||||
getError,
|
getError,
|
||||||
getClientById,
|
getClientById,
|
||||||
getPagination,
|
getPagination,
|
||||||
|
filters,
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
fetchClients,
|
fetchClients,
|
||||||
|
|||||||
@ -4,16 +4,19 @@
|
|||||||
:loading-data="clientStore.loading"
|
:loading-data="clientStore.loading"
|
||||||
:pagination="clientStore.getPagination"
|
:pagination="clientStore.getPagination"
|
||||||
@push-details="goDetails"
|
@push-details="goDetails"
|
||||||
|
@delete-client="handleDeleteClient"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import ClientPresentation from "@/components/Organism/CRM/ClientPresentation.vue";
|
import ClientPresentation from "@/components/Organism/CRM/ClientPresentation.vue";
|
||||||
import { useClientStore } from "@/stores/clientStore";
|
import { useClientStore } from "@/stores/clientStore";
|
||||||
|
import { useNotificationStore } from "@/stores/notification";
|
||||||
import { onMounted } from "vue";
|
import { onMounted } from "vue";
|
||||||
import { useRouter } from "vue-router";
|
import { useRouter } from "vue-router";
|
||||||
|
|
||||||
const clientStore = useClientStore();
|
const clientStore = useClientStore();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const notificationStore = useNotificationStore();
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await clientStore.fetchClients();
|
await clientStore.fetchClients();
|
||||||
@ -27,4 +30,15 @@ const goDetails = (id) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDeleteClient = async (clientId) => {
|
||||||
|
try {
|
||||||
|
await clientStore.deleteClient(Number(clientId));
|
||||||
|
await clientStore.fetchClients();
|
||||||
|
notificationStore.deleted("Client");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error deleting client:", error);
|
||||||
|
notificationStore.error("Erreur", "Impossible de supprimer le client");
|
||||||
|
}
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user