Laravel

Un framework qui rend heureux

Voir cette catégorie
Vers le bas
Voir cette série
Shop : les clients
Dimanche 2 février 2025 17:54

Dans notre précédent article, nous avons codé la gestion des commandes. On a ainsi créé un tableau avec les éléments principaux pour chaque commande. On a ensuite donné la possibilité d'obtenir le détail de chaque commande sur une page dédiée avec la possibilité, sur cette page, de générer la facture ou changer l'état. À présent, nous allons faire quelque chose de similaire pour la gestion des clients et de leurs adresses..

Vous pouvez trouver le code dans ce dépôt Github.

Conception du tableau de gestion des clients

Le tableau

Pour faciliter la gestion des clients, nous allons créer un tableau intuitif et complet. Ce tableau rassemblera les informations clés de chaque client en un coup d'œil, permettant aux administrateurs de gérer efficacement le contenu. Voici les éléments que nous inclurons dans notre tableau :

  • Nom
  • Prénom
  • Email
  • Date d'inscription
  • Statut pour la lettre d'information
  • Facture

Fonctionnalités avancées

En plus de ces informations de base, nous allons envisager d'ajouter des fonctionnalités supplémentaires pour améliorer l'expérience utilisateur :

  • Envoie d'un email au client
  • Suppression du client

Un composant pour le tableau

Il nous faut maintenant un composant Volt pour afficher le tableau :

php artisan make:volt admin/customers/index --class

Avec ce code :

<?php

use Livewire\Volt\Component;
use Livewire\Attributes\{Layout, Title};
use App\Models\User;
use Mary\Traits\Toast;
use Livewire\WithPagination;
use Illuminate\Database\Eloquent\Builder;

new 
#[Title('Customers')] 
#[Layout('components.layouts.admin')] 
class extends Component
{
    use Toast, WithPagination;

    public int $perPage = 10;
    public string $search = '';
    public bool $deleteButton = true;

    public array $sortBy = [
        'column' => 'created_at',
        'direction' => 'desc',
    ];

    public function headers(): array
    {
        return [
            ['key' => 'name', 'label' => __('Name')], 
            ['key' => 'firstname', 'label' => __('Firstname')],            
            ['key' => 'email', 'label' => __('Email')],
            ['key' => 'created_at', 'label' => __('Registration')],
            ['key' => 'newsletter', 'label' => __('Newsletter')],
        ];
    }

    public function deleteUser(User $user): void
    {
        $user->delete();
        $this->success(__('User deleted successfully.'));
    }

    public function with(): array
	{
		return [
            'users' => User::orderBy(...array_values($this->sortBy))
                ->when($this->search, function (Builder $query)
                {
                    $query->where('name', 'like', "%{$this->search}%");
                })
                ->paginate($this->perPage),			
			'headers' => $this->headers(),
		];
	}
   
}; ?>

<div>
    <x-header title="{{ __('Customers') }}" separator progress-indicator >
        <x-slot:actions>
            <x-input 
                placeholder="{{ __('Search a name...') }}" 
                wire:model.live.debounce="search" 
                clearable
                icon="o-magnifying-glass" 
            />
            <x-button 
                icon="s-building-office-2" 
                label="{{ __('Dashboard') }}" 
                class="btn-outline lg:hidden" 
                link="{{ route('admin') }}" 
            />
        </x-slot:actions>
    </x-header>

    <x-card>
        <x-table 
            striped 
            :headers="$headers" 
            :rows="$users" 
            :sort-by="$sortBy" 
            per-page="perPage"
            with-pagination
            link="/admin/customers/{id}"
        >
            @scope('cell_newsletter', $user)
                @if ($user->newsletter)
                    <x-icon name="o-check-circle" />
                @endif
            @endscope
            @scope('cell_created_at', $user)
                <span class="whitespace-nowrap">
                    {{ $user->created_at->isoFormat('LL') }}
                    @if(!$user->created_at->isSameDay($user->updated_at))
                        <p>@lang('Change') : {{ $user->updated_at->isoFormat('LL') }}</p>
                    @endif
                </span>
            @endscope
            @scope('actions', $user, $deleteButton)
                <div class="flex">
                    <x-popover>
                        <x-slot:trigger>
                            <x-button icon="o-envelope" link="mailto:{{ $user->email }}" no-wire-navigate spinner
                                class="text-blue-500 btn-ghost btn-sm" />
                        </x-slot:trigger>
                        <x-slot:content class="pop-small">
                            @lang('Send an email')
                        </x-slot:content>
                    </x-popover>
                    @if($deleteButton)
                        <x-popover>
                            <x-slot:trigger>
                                <x-button 
                                    icon="o-trash" 
                                    wire:click="deleteUser({{ $user->id }})" 
                                    wire:confirm="{{ __('Are you sure you want to delete this user?') }}" 
                                    spinner 
                                    class="text-red-500 btn-ghost btn-sm" 
                                />
                            </x-slot:trigger>
                            <x-slot:content class="pop-small">
                                @lang('Delete')
                            </x-slot:content>
                        </x-popover>
                    @endif
                </div>
            @endscope
        </x-table>
    </x-card>

</div>

On ajoute ces traductions :

"Firstname": "Prénom",
"Registration": "Inscription",
"Send an email": "Envoyer un email",
"Search a name...": "Rechercher un nom...",

La route

On ajoute une route pour l'atteindre :

Volt::route('/customers', 'admin.customers.index')->name('admin.customers.index');

La navigation

On ajoute un lien dans la barre latérale de l'administration (on prévoit un sous-menu parce qu'on ajoutera plus loin les adresses) :

<x-menu-sub title="{{ __('Customers') }}" icon="s-users">
    <x-menu-item title="{{ __('Datas') }}" icon="s-list-bullet" link="{{ route('admin.customers.index') }}" />
</x-menu-sub>

Une petite traduction :

"Datas": "Données",

Et on a notre tableau :

Avec la pagination en partie inférieure :

J'avais oublié dans les précédents article de compléter le fichier resources.css pour avoir une bonne présentation de la pagination :

/* Table pagination: active page highlight */
.mary-table-pagination span[aria-current="page"] > span {
    @apply bg-primary text-base-100
}

/* Table pagination: for dark mode*/
.mary-table-pagination span[aria-disabled="true"] span {
    @apply bg-inherit
}

/* Table pagination: for dark mode */
.mary-table-pagination button {
    @apply bg-base-100
}

Vérifiez que cette pagination fonctionne, ainsi que la recherche qui s'effectue sur les noms. On peut aussi trier par nom, prénom, email, date d'inscription et statut pour la lettre d'information. 

On va aussi en profiter pour renseigner le lien dans le dashboard :

<a href="{{ route('admin.customers.index') }}" class="flex-grow">

Ainsi, on ira directement au tableau des clients en cliquant sur l'élément statistique :

Le détail d'un client

Lorsqu'on clique sur la ligne d'un client dans le tableau, on doit ouvrir une page avec le détail du client concerné. Il nous faut un composant Volt pour afficher ce détail :

php artisan make:volt admin/customers/show --class

Avec ce code plus léger que pour les commandes :

<?php

use Livewire\Volt\Component;
use Livewire\Attributes\{Layout, Title};
use App\Models\User;
use Mary\Traits\Toast;

new 
#[Title('Customer')] 
#[Layout('components.layouts.admin')] 
class extends Component
{
    use Toast;
    
    public User $user;

    public function headers(): array
    {
        return [
            ['key' => 'reference', 'label' => __('Reference')], 
            ['key' => 'created_at', 'label' => __('Date')],            
            ['key' => 'total', 'label' => __('Total price')],
            ['key' => 'state', 'label' => __('State')],
        ];
    }

    public function mount(User $user): void
    {
        $this->user = $user;
        $this->user->load('addresses', 'orders', 'orders.state');
    }

    public function with(): array
	{
		return [	
			'headers' => $this->headers(),
		];
	}
   
}; ?>

<div>
    <x-header title="{{ __('Customer details') }}" separator progress-indicator>
        <x-slot:actions>
            <x-button icon="s-building-office-2" label="{{ __('Dashboard') }}" class="btn-outline lg:hidden"
                link="{{ route('admin') }}" />
        </x-slot:actions>
    </x-header>

    <x-card 
        title="{{ __('Identity') }}" 
        shadow 
        class="h-full"
    >
        <div class="space-y-4">
            <div class="flex items-center">
                <x-icon name="o-user" class="w-6 h-6 mr-3 text-primary" />
                <span>
                    <strong>@lang('Name') :</strong> 
                    {{ $user->name }}
                </span>
            </div>
            <div class="flex items-center">
                <x-icon name="o-user" class="w-6 h-6 mr-3 text-primary" />
                <span>
                    <strong>@lang('Firstname') :</strong> 
                    {{ $user->firstname }}
                </span>
            </div>
            <div class="flex items-center">
                <x-button icon="o-envelope" link="mailto:{{ $user->email }}" no-wire-navigate spinner
                    class="w-6 h-6 mr-3 text-primary btn-ghost btn-sm" tooltip="{{ __('Send an email') }}" />
                <span>
                    <strong>@lang('Email') :</strong> 
                    {{ $user->email }}
                </span>
            </div>
            <div class="flex items-center">
                <x-icon name="o-calendar" class="w-6 h-6 mr-3 text-primary" />
                <span>
                    <strong>@lang('Date of registration') :</strong> 
                    {{ $user->created_at->isoFormat('LL') }}
                </span>
            </div>
            <div class="flex items-center">
                <x-icon name="o-calendar" class="w-6 h-6 mr-3 text-primary" />
                <span>
                    <strong>@lang('Last update') :</strong> 
                    {{ $user->updated_at->isoFormat('LL') }}
                </span>
            </div>
            <div class="flex items-center">
                <x-icon name="o-newspaper" class="w-6 h-6 mr-3 text-primary" />
                <span>
                    <strong>@lang('Newsletter') :</strong> 
                    {{ $user->newsletter ? __('Yes') : __('No') }}
                </span>
            </div>
        </div>
    </x-card>
    <br>
    
    <x-card 
        title="{{ __('Orders') }}" 
        shadow 
        class="h-full"
    >
        <x-table 
            :headers="$headers" 
            :rows="$user->orders" 
            link="/admin/orders/{id}"
        >
            @scope('cell_reference', $order)
                <strong>{{ $order->reference }}</strong>
            @endscope
            @scope('cell_created_at', $order)
                {{ $order->created_at->isoFormat('LL') }}
            @endscope
            @scope('cell_total', $order)
                {{ $order->total }} €
            @endscope
            @scope('cell_state', $order)
                <x-badge value="{{ $order->state->name }}" class="p-3 bg-{{ $order->state->color }}-400 self-start sm:self-center" />
            @endscope
        </x-table>
    </x-card><br>

    <x-card 
        title="{{ __('Adresses') }}" 
        shadow 
        class="h-full"
    >
        <div class="container mx-auto">
            <div class="grid gap-6 md:grid-cols-2 lg:grid-cols-4">
                @foreach ($user->addresses as $address)
                    <x-card
                        class="w-full shadow-md"
                        title="">
                        <x-address :address="$address" />
                    </x-card>
                @endforeach
            </div>
        </div>   
    </x-card>

</div>

On a besoin de nouvelles traductions :

"Customer details": "Détails du client",
"Identity": "Identité",
"Last update": "Dernière mise à jour",

On ajoute la route :

Volt::route('/customers/{user}', 'admin.customers.show')->name('admin.customers.show');

Dans la partie supérieure, on a les données de l'identité :

Puis les commandes (un clic sur une ligne envoie sur la page du détail de la commande) :

Et enfin les adresses :

Et évidemment, un clic sur la ligne d'une commande conduit directement sur le détail correspondant.

On en profite pour ajouter le lien vers le détail du client dans la page du détail de la commande à l'aide de ce bouton dont on avait pas encore renseigné le lien :

<x-slot:actions>
    <x-button label="{{ __('View customer details') }}" class="btn-primary" link="{{ route('admin.customers.show', $order->user) }}" />
</x-slot:actions>

Les adresses

Le tableau

Pour faciliter la gestion des adresses, nous allons créer aussi un tableau. Ce tableau rassemblera les informations clés de chaque adresse en un coup d'œil, permettant aux administrateurs de gérer efficacement le contenu. Voici les éléments que nous inclurons dans notre tableau :

  • Nom
  • Prénom
  • Raison sociale
  • Adresse
  • Code postal
  • Ville
  • Pays

Un composant pour le tableau

Il nous faut maintenant un composant Volt pour afficher le tableau :

php artisan make:volt admin/customers/addresses --class

Avec ce code :

<?php

use Livewire\Volt\Component;
use Livewire\Attributes\{Layout, Title};
use App\Models\Address;
use Mary\Traits\Toast;
use Livewire\WithPagination;
use Illuminate\Database\Eloquent\Builder;

new 
#[Title('Addresses')] 
#[Layout('components.layouts.admin')] 
class extends Component
{
    use Toast, WithPagination;

    public int $perPage = 10;
    public string $search = '';

    public array $sortBy = [
        'column' => 'name',
        'direction' => 'asc',
    ];

    public function headers(): array
    {
        return [
            ['key' => 'name', 'label' => __('Name')], 
            ['key' => 'firstname', 'label' => __('Firstname')],            
            ['key' => 'company', 'label' => __('Company name')],
            ['key' => 'address', 'label' => __('Address')],
            ['key' => 'postal', 'label' => __('Postcode')],
            ['key' => 'city', 'label' => __('City')],
            ['key' => 'country', 'label' => __('Country')],
        ];
    }

    public function with(): array
    {
        return [
            'addresses' => Address::with('country')
                ->when($this->sortBy['column'] === 'country', function (Builder $query) {
                    $query->join('countries', 'addresses.country_id', '=', 'countries.id')
                        ->orderBy('countries.name', $this->sortBy['direction']);
                }, function (Builder $query) {
                    $query->orderBy($this->sortBy['column'], $this->sortBy['direction']);
                })
                ->when($this->search, function (Builder $query) {
                    $query->where('addresses.name', 'like', "%{$this->search}%")
                        ->orWhere('company', 'like', "%{$this->search}%")
                        ->orWhere('address', 'like', "%{$this->search}%")
                        ->orWhere('city', 'like', "%{$this->search}%");
                })
                ->paginate($this->perPage),
            'headers' => $this->headers(),
        ];
    }
   
}; ?>

<div>
    <x-header title="{{ __('Addresses') }}" separator progress-indicator >
        <x-slot:actions>
            <x-input 
                placeholder="{{ __('Search...') }}" 
                wire:model.live.debounce="search" 
                clearable
                icon="o-magnifying-glass" 
            />
            <x-button 
                icon="s-building-office-2" 
                label="{{ __('Dashboard') }}" 
                class="btn-outline lg:hidden" 
                link="{{ route('admin') }}" 
            />
        </x-slot:actions>
    </x-header>

    <x-card>
        <x-table 
            striped 
            :headers="$headers" 
            :rows="$addresses" 
            :sort-by="$sortBy" 
            per-page="perPage"
            with-pagination
        >
            @scope('cell_country', $address)
                {{ $address->country->name }}
            @endscope
        </x-table>
    </x-card>
</div>

La route

On ajoute une route pour l'atteindre :

Volt::route('/addresses', 'admin.customers.addresses')->name('admin.addresses');

La navigation

On ajoute un lien dans la barre latérale de l'administration (on prévoit un sous-menu parce qu'on ajoutera plus loin les adresses) :

<x-menu-sub title="{{ __('Customers') }}" icon="s-users">
    <x-menu-item title="{{ __('Datas') }}" icon="s-list-bullet" link="{{ route('admin.customers.index') }}" />
    <x-menu-item title="{{ __('Addresses') }}" icon="c-map-pin" link="{{ route('admin.addresses') }}" />
</x-menu-sub>

Et voici notre nouveau tableau avec pagination, recherche et tri :

Conclusion

On en a fini avec la gestion des clients et de leurs adresses. Dans la prochaine étape, on codera le catalogue des produits.



Par bestmomo

Aucun commentaire

Article précédent : Shop : les commandes
Article suivant : Shop : le catalogue