
Dans notre précédent article, nous avons établi les fondations de notre tableau de bord d'administration. Cette étape cruciale a impliqué la création d'un nouveau layout et d'une barre de navigation latérale pour une navigation fluide. Soucieux de l'ergonomie et de la sécurité de notre code, nous avons également mis en place un middleware spécifique pour les administrateurs, garantissant ainsi un accès approprié aux différentes fonctionnalités.
Maintenant que cette infrastructure est solidement en place, nous pouvons nous concentrer sur l'ajout de fonctionnalités essentielles à notre boutique. Au cœur de notre système se trouvent les commandes, et leur gestion efficace est primordiale pour le bon fonctionnement de notre plateforme.
Vous pouvez trouver le code dans ce dépôt Github.
Conception du tableau de gestion des commandes
Le tableau
Pour faciliter la gestion des commandes, nous allons créer un tableau intuitif et complet. Ce tableau rassemblera les informations clés de chaque commande en un coup d'œil, permettant aux administrateurs de gérer efficacement le contenu. Voici les éléments que nous inclurons dans notre tableau :
- Référence
- Nom du client (ou entité)
- Prix total
- Date de création
- État
- 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 :
- Recherche rapide : intégrer un contrôle de recherche pour trouver rapidement une commande spécifique
- Pagination : pour gérer efficacement un grand nombre de commandes
Une vue partielle pour le tableau
Dans l'idée d'afficher aussi un tableau des commandes allégé au niveau du tableau de bord, on va créer la partie tableau avec une vue partielle :
Avec ce code :
<x-card>
<x-table
striped
:headers="$headersOrders"
:rows="$orders"
:sort-by="$sortBy"
per-page="perPage"
:with-pagination="$paginationOrders"
link="/admin/orders/{id}"
>
@scope('cell_user', $order)
{{ $order->addresses->first()->company? $order->addresses->first()->company : $order->user->name . ' ' . $order->user->firstname }}
@endscope
@scope('cell_created_at', $order)
<span class="whitespace-nowrap">
{{ $order->created_at->isoFormat('LL') }}
@if(!$order->created_at->isSameDay($order->updated_at))
<p>@lang('Change') : {{ $order->updated_at->isoFormat('LL') }}</p>
@endif
</span>
@endscope
@scope('cell_total', $order)
<span class="whitespace-nowrap">
{{ $order->total }} €
</span>
@endscope
@scope('cell_state', $order)
<x-badge value="{{ $order->state->name }}" class="p-3 bg-{{ $order->state->color }}-400 whitespace-nowrap" />
@endscope
@scope('cell_reference', $order)
<strong>{{ $order->reference }}</strong>
@endscope
@scope('cell_invoice_id', $order)
@if ($order->invoice_id)
<x-icon name="o-check-circle" />
@endif
@endscope
</x-table>
</x-card>
Un trait
Pour la même raison de partage du code, on va aussi ajouter un trait :
Avec ce code :
<?php
namespace App\Traits;
trait ManageOrders
{
public array $sortBy = [
'column' => 'created_at',
'direction' => 'desc',
];
public function headersOrders(): array
{
return [
['key' => 'id', 'label' => __('Id')],
['key' => 'reference', 'label' => __('Reference')],
['key' => 'user', 'label' => __('Customer'), 'sortable' => false],
['key' => 'total', 'label' => __('Total price')],
['key' => 'created_at', 'label' => __('Date')],
['key' => 'state', 'label' => __('Etat'), 'sortable' => false],
['key' => 'invoice_id', 'label' => __('Invoice')],
];
}
}
Avec ces traductions :
"Customer": "Client",
"Total price": "Prix total",
"Invoice": "Facture",
Un composant pour le tableau
Il nous faut maintenant un composant Volt pour afficher le tableau :
php artisan make:volt admin/orders/index --class
Avec ce code :
<?php
use Livewire\Volt\Component;
use Livewire\Attributes\{Layout, Title};
use App\Models\Order;
use Mary\Traits\Toast;
use Livewire\WithPagination;
use Illuminate\Database\Eloquent\Builder;
use App\Traits\ManageOrders;
new
#[Title('Orders')]
#[Layout('components.layouts.admin')]
class extends Component
{
use Toast, WithPagination, ManageOrders;
public int $perPage = 10;
public string $search = '';
public bool $paginationOrders = true;
public function deleteOrder(Order $order): void
{
$order->delete();
$this->success(__('Order deleted successfully.'));
}
public function with(): array
{
return [
'orders' => Order::with('user', 'state', 'addresses')
->orderBy(...array_values($this->sortBy))
->when($this->search, function (Builder $q)
{
$q->where('reference', 'like', "%{$this->search}%")
->orWhereRelation('addresses', 'company', 'like', "%{$this->search}%")
->orWhereRelation('state', 'name', 'like', "%{$this->search}%");
})
->paginate($this->perPage),
'headersOrders' => $this->headersOrders(),
];
}
}; ?>
<div>
<x-header title="{{ __('Orders') }}" 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>
@include('livewire.admin.orders.table')
</div>
On ajoute cette traduction :
"Search...": "Rechercher...",
La route
On ajoute une route pour l'atteindre :
Route::middleware('auth')->group(function () {
...
Route::middleware(IsAdmin::class)->prefix('admin')->group(function ()
{
...
Volt::route('/orders', 'admin.orders.index')->name('admin.orders.index');
});
La navigation
On ajoute un lien dans la barre latérale de l'administration :
<x-menu-item title="{{ __('Orders') }}" icon="s-shopping-bag" link="{{ route('admin.orders.index') }}" />
Et on a enfin notre tableau :
Avec la pagination en partie inférieure :
Vérifiez que cette pagination fonctionne, ainsi que la recherche qui s'effectue sur les références, les adresses et les états. On peut aussi trier par référence, par pris et par date.
Le détail d'une commande
Lorsqu'on clique sur la ligne d'une commande dans le tableau, on doit ouvrir une page avec le détail de la commande concernée. Il nous faut un composant Volt pour afficher ce détail :
php artisan make:volt admin/orders/show --class
Avec un code un peu fourni :
<?php
use Livewire\Volt\Component;
use Livewire\Attributes\{Layout, Title};
use App\Models\{ State, Order };
use Mary\Traits\Toast;
use Illuminate\Support\Collection;
use App\Services\Invoice;
new
#[Title('Order')]
#[Layout('components.layouts.admin')]
class extends Component
{
use Toast;
public Order $order;
public bool $paid = false;
public int $annule_indice = 0;
public Collection $states;
public ?string $purchase_order = null;
public string $state = '';
public function mount(Order $order): void
{
$this->order = $order;
$this->purchase_order = $order->purchase_order?? null;
$this->state = $order->state_id;
$this->order->load('addresses', 'state', 'products', 'payment_infos', 'user', 'user.orders');
$this->paid = $order->state->indice > 3;
// Cas du mandat administratif
$this->annule_indice = State::whereSlug('annule')->first()->indice;
$this->states = $order->payment === 'mandat' && !$order->purchase_order ?
State::where('indice', '<=', $this->annule_indice)
->where('indice', '>', 0)
->get() :
State::where('indice', '>=', $order->state->indice)->get();
}
public function generateInvoice(Invoice $invoice): void
{
$response = $invoice->create($this->order, true);
if($response->successful()) {
$data = json_decode($response->body());
$this->order->invoice_id = $data->id;
$this->order->invoice_number = $data->number;
$this->order->save();
$this->success(__('Invoice generated successfully.'));
}
}
public function savePurchaseOrder(Invoice $invoice): void
{
$this->order->purchase_order = $this->purchase_order == '' ? null : $this->purchase_order;
$this->order->save();
$this->success(__('Purchase order numberupdated successfully.'));
}
public function updatedState($value): void
{
$this->order->state_id = $value;
$state = $this->states->find($value);
if($state->indice === 1) {
$this->order->payment = $state->slug;
}
$this->order->save();
$this->order->refresh();
$this->success(__('State updated successfully.'));
}
}; ?>
<div>
<x-header title="{!! __('Order management') !!}" 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="{{ __('References') }}"
shadow
>
<div class="space-y-4">
<div>
<strong>@lang('Number') :</strong>
<x-badge value="{{ $order->id }}" class="badge-info" />
</div>
<div>
<strong>@lang('Reference') :</strong>
<x-badge value="{{ $order->reference }}" class="badge-info" />
</div>
</div>
</x-card>
<br>
<x-card
title="{{ __('Mode of payment') }}"
shadow
>
<div class="space-y-4">
<div>
{{ $order->payment_text }}
</div>
@if($order->payment_infos)
<div>
<strong>@lang('Payment ID') :</strong>
<x-badge value="{{ $order->payment_infos->payment_id }}" class="badge-info" />
</div>
@endif
</div>
</x-card>
<br>
@if($shop->invoice && ($order->invoice_id || $order->state->indice > $annule_indice))
<x-card
title="{{ __('Invoice') }}"
shadow
>
@if($order->invoice_id)
<p>@lang('The invoice was generated with id') <strong>{{ $order->invoice_id }}</strong> @lang('and the number') <strong>{{ $order->invoice_number }}</strong>.</p>
@else
<div class="flex flex-row justify-between">
<x-checkbox label="{{ __('Payment has been made') }}" wire:model="paid" hint="{!! __('Tick this box if the invoice has actually been paid.') !!}" />
<x-button label="{{ __('Generate invoice') }}" wire:click="generateInvoice" class="btn-primary" />
</div>
@endif
</x-card>
<br>
@endif
@if($order->payment === 'mandat')
<x-card
title="{{ __('Order form') }}"
shadow
progress-indicator
>
<x-form wire:submit="savePurchaseOrder">
<x-input label="{{ __('Purchase order number') }}" wire:model="purchase_order" type="text" />
<x-slot:actions>
<x-button label="{{ __('Update purchase order number') }}" icon="o-paper-airplane" spinner="save" type="submit" class="btn-primary" />
</x-slot:actions>
</x-form>
</x-card>
<br>
@endif
<x-card
title="{{ __('State') }}"
shadow
>
<x-slot:menu>
<x-badge value="{{ $order->state->name }}" class="bg-{{ $order->state->color }}-400 mb-4 p-3" />
</x-slot:menu>
<x-select label="{!! __('Change state') !!}" :options="$states" wire:model="state" wire:change="$refresh" />
</x-card>
<br>
<x-card
title="{{ __('Products') }}"
shadow
>
<x-details
:content="$order->products"
:shipping="$order->shipping"
:tax="$order->tax"
:total="$order->total"
:pick="$order->pick"
/>
</x-card>
<br>
<x-card
title="{{ __('Customer') }}"
shadow
>
<div class="space-y-4">
<div class="flex items-center">
<x-icon name="o-user" class="mr-3 w-6 h-6 text-primary" />
<span>
<strong>@lang('Name') :</strong>
{{ $order->user->name }} {{ $order->user->firstname }}
</span>
</div>
<div class="flex items-center">
<x-button icon="o-envelope" link="mailto:{{ $order->user->email }}" no-wire-navigate spinner
class="mr-3 w-6 h-6 text-primary btn-ghost btn-sm" tooltip="{{ __('Send an email') }}" />
<span>
<strong>@lang('Email') :</strong>
{{ $order->user->email }}
</span>
</div>
<div class="flex items-center">
<x-icon name="o-calendar" class="mr-3 w-6 h-6 text-primary" />
<span>
<strong>@lang('Date of registration') :</strong>
{{ $order->user->created_at->isoFormat('LL') }}
</span>
</div>
<div class="flex items-center">
<x-icon name="o-shopping-cart" class="mr-3 w-6 h-6 text-primary" />
<span>
<strong>@lang('Validated orders') :</strong>
<x-badge value="{{ $order->user->orders->where('state_id', '>', 5)->count() }}" class="badge-success" />
</span>
</div>
</div>
<x-slot:actions>
<x-button label="{{ __('View customer details') }}" class="btn-primary" link="/" />
</x-slot:actions>
</x-card>
<br>
<x-card
title="{{ __('Addresses') }}"
shadow
>
<x-card title="{{ __('Billing address') }} {{ $this->order->addresses->count() === 1 && !$this->order->pick ? __('and delivery') : '' }}" shadow >
<x-address :address="$this->order->addresses->first()" />
</x-card>
@if($this->order->pick)
<x-alert title="{!! __('The customer has chosen to collect the order on site.') !!}" icon="o-exclamation-triangle" class="alert-info" />
@else
@if($this->order->addresses->count() === 2)
<x-card class="w-full sm:min-w-[50vw]" title="{{ __('Delivery address') }}" shadow separator >
<x-address :address="$this->order->addresses->get(1)" />
</x-card>
@endif
@endif
</x-card>
</div>
On a besoin de nouvelles traductions :
"Order management": "Gestion d'une commande",
"Number": "N°",
"Payment ID": "ID paiement",
"The invoice was generated with id": "La facture a été générée avec l'id",
"and the number": "et le n°",
"Payment has been made": "Le paiement a été effectué",
"Generate invoice": "Générer la facture",
"Invoice generated successfully.": "Facture génerée avec succès.",
"Order form": "Bon de commande",
"Purchase order number": "Numéro du bon de commande",
"Update purchase order number": "Mettre à jour le numéro du bon de commande",
"Purchase order numberupdated successfully.": "Numéro de bon de commande mis à jour avec succès.",
"Change state": "Changement de l'état",
"Update state": "Mise à jour de l'état",
"The customer has chosen to pick up the order on site.": "Le client a choisi de venir chercher sa commande sur place.",
"Normal Colissimo delivery.": "Livraison Colissimo standard.",
"Tick this box if the invoice has actually been paid.": "Cochez cette case si la facture a effectivement été payée.",
"View customer details": "Voir les détails du client",
"Date of registration": "Date d'inscription",
"The customer has chosen to collect the order on site.": "Le client a choisi de venir chercher sa commande sur place.",
On ajoute la route :
Volt::route('/orders/{order}', 'admin.orders.show')->name('admin.orders.show');
Dans la partie supérieure, on a les références :
Puis le moyen de paiement utilisé :
Ensuite une zone optionnelle selon la situation de la commande pour la facture :
Si le paiement a été effectivement effectué, on peut générer la facture correspondante. Si la facture existe déjà, on obtient sa référence dans cette zone :
On a ensuite l'état de paiement, que l'on peut évidemment changer avec les valeurs possibles :
Vient ensuite le détail de la commande :
Ensuite les données principales du client :
Pour le moment le bouton pour voir les autres données ne fonctionne pas, on le complètera plus tard.
Et on a finalement la zone des adresses :
Cette page constitue sans doute la plus importante dans l'administration.
Les dernières commandes dans le tableau de bord
Il semble judicieux d'afficher le tableau des dernières commandes passées sur le tableau de bord alors, on va compléter le composant admin.index (on en profite pour ajouter le lien vers les commandes) :
<?php
use App\Models\{ Order, Product, User };
use Livewire\Attributes\{Layout, Title};
use Livewire\Volt\Component;
use App\Traits\ManageOrders;
new
#[Title('Dashboard')]
#[Layout('components.layouts.admin')]
class extends Component
{
use ManageOrders;
public bool $openGlance = true;
public bool $openOrders = true;
public bool $paginationOrders = false;
public function with(): array
{
return [
'productsCount' => Product::count(),
'ordersCount' => Order::whereRelation('state', 'indice', '>', 3)
->whereRelation('state', 'indice', '<', 6)
->count(),
'usersCount' => User::count(),
'orders' => Order::with('user', 'state', 'addresses')
->orderBy(...array_values($this->sortBy))
->take(6)
->get(),
'headersOrders' => $this->headersOrders(),
];
}
}; ?>
<div>
<x-collapse wire:model="openGlance" class="shadow-md">
<x-slot:heading>
@lang('In a glance')
</x-slot:heading>
<x-slot:content class="flex flex-wrap gap-4">
<a href="/" class="flex-grow">
<x-stat
title="{{ __('Active products') }}"
description=""
value="{{ $productsCount }}"
icon="s-shopping-bag"
class="shadow-hover" />
</a>
<a href="{{ route('admin.orders.index') }}" class="flex-grow">
<x-stat
title="{{ __('Successful orders') }}"
description=""
value="{{ $ordersCount }}"
icon="s-shopping-cart"
class="shadow-hover" />
</a>
<a href="/" class="flex-grow">
<x-stat
title="{{ __('Customers') }}"
description=""
value="{{ $usersCount }}"
icon="s-user"
class="shadow-hover" />
</a>
</x-slot:content>
</x-collapse>
<br>
<x-collapse wire:model="openOrders" class="shadow-md">
<x-slot:heading>
@lang('Latest orders')
</x-slot:heading>
<x-slot:content>
<x-card class="mt-6" title="" shadow separator >
@include('livewire.admin.orders.table')
<x-slot:actions>
<x-button
label="{{ __('See all orders') }}"
class="btn-primary"
link="{{ route('admin.orders.index') }}" />
</x-slot:actions>
</x-card>
</x-slot:content>
</x-collapse>
</div>
Il nous manque deux traductions :
"Latest orders": "Dernières commandes",
"See all orders": "Voir toutes les commandes",
Maintenant, on a les dernières commandes passées directement sur le tableau de bord :
Et évidemment, un clic sur la ligne d'une commande conduit directement sur le détail correspondant.
Conclusion
Cet article est un peu chargé, mais je voulais terminer la gestion des commandes. Dans la prochaine étape, on codera la gestion des clients.
Par bestmomo
Aucun commentaire