
Notre administration commence à s'étoffer puisque nous avons déjà mis en place la gestion des commandes et des clients. Mais il nous reste encore bien des choses à coder ! Pour cet article, nous allons nous intéresser aux produits de la boutique. Il nous faut également une gestion complète : liste, suppression, modification, ajout... Nous allons continuer le codage dans le même esprit en conservant une homogénéité.
Vous pouvez trouver le code dans ce dépôt Github.
Avant de commencer
Ce projet est dynamique et appelé à subir des modifications au fil de l'eau. Le dépôt Github est ouvert à vos suggestions. Lionel de s'en est pas privé et a déjà apporté sa touche personnelle. En particulier, il a ajouté un helper bien pratique (dans app.helpers) :
// Translation Lower case first
if (!function_exists('transL')) {
function transL($key, $replace = [], $locale = null) {
$key = trans($key, $replace, $locale);
// return mb_strtolower($key, 'UTF-8');
return mb_substr(mb_strtolower($key, 'UTF-8'), 0, 1) . mb_substr($key, 1);
}
}
if (!function_exists('__L')) {
function __L($key, $replace = [], $locale = null) {
return transL($key, $replace, $locale);
}
}
Comme il a constaté que j'avais écrit des doublons dans le fichier de traduction, par exemple administration et Administration, pour tenir compte de la présence ou non de la majuscule au début du mot, il a ajouté cet helper transL qui donne la priorité à la minuscule en ne conservant dans le fichier de traduction que la version avec majuscule. Pour la suite de ces articles, j'utiliserai donc cet helper.
Pour qu'il soit pris en compte dans les fichiers Blade, on doit informer AppServiceProvider :
public function boot(): void {
...
Blade::directive(name: 'langL', handler: function ($expression) {
return "<?= transL({$expression}); ?>";
});
}
Il y a eu d'autres modifications, mais qui ne devrait pas impacter la progression exposée dans les précédents articles. Si jamais c'était le cas, vous pouvez allez jeter un coup d'œil dans le code du dépôt qui est toujours à jour.
Conception du tableau de gestion des produits
Le tableau
Pour faciliter la gestion des produits du catalogue, nous allons créer un tableau intuitif et complet. Ce tableau rassemblera les informations clés de chaque produit en un coup d'œil, permettant aux administrateurs de gérer efficacement le contenu. Voici les éléments que nous inclurons dans notre tableau :
- Image
- Nom
- Prix TTC
- État : actif ou pas actif
- Quantité disponible
Fonctionnalités avancées
En plus de ces informations de base, nous allons envisager d'ajouter une fonctionnalité supplémentaire pour améliorer l'expérience utilisateur :
- Suppression du produit
Un composant pour le tableau
Il nous faut maintenant un composant Volt pour afficher le tableau :
php artisan make:volt admin/products/index --class
Avec ce code :
<?php
use Livewire\Volt\Component;
use Livewire\Attributes\{Layout, Title};
use App\Models\Product;
use Mary\Traits\Toast;
use Livewire\WithPagination;
new
#[Title('Products')]
#[Layout('components.layouts.admin')]
class extends Component
{
use Toast, WithPagination;
public array $sortBy = [
'column' => 'name',
'direction' => 'asc',
];
public int $perPage = 10;
public function headers(): array
{
return [
['key' => 'image', 'label' => __(' ')],
['key' => 'name', 'label' => __('Name')],
['key' => 'price', 'label' => __('Price incl. VAT')],
['key' => 'active', 'label' => __('Active')],
['key' => 'quantity', 'label' => __('Quantity')],
];
}
public function deleteProduct(Product $product): void
{
$product->delete();
$this->success(__('Product deleted successfully.'));
}
public function with(): array
{
return [
'products' => Product::orderBy(...array_values($this->sortBy))->paginate($this->perPage),
'headers' => $this->headers(),
];
}
}; ?>
<div>
<x-header title="{{ __('Catalog') }}" separator progress-indicator>
<x-slot:actions>
<x-button
icon="s-building-office-2"
label="{{ __('Dashboard') }}"
class="btn-outline lg:hidden"
link="{{ route('admin') }}"
/>
<x-button
icon="o-plus"
label="{!! __('Create a new product') !!}"
link="/admin/products/create"
spinner
class="btn-primary"
/>
</x-slot:actions>
</x-header>
<x-card>
<x-table
striped
:headers="$headers"
:rows="$products"
:sort-by="$sortBy"
per-page="perPage"
with-pagination
link="/admin/products/{id}/edit"
>
@scope('cell_image', $product)
<img src="{{ asset('storage/photos/' . $product->image) }}" width="60" alt="">
@endscope
@scope('cell_active', $product)
@if ($product->active)
<x-icon name="o-check-circle" />
@endif
@endscope
@scope('cell_price', $product)
{{ $product->price }} €
@endscope
@scope('actions', $product)
<x-popover>
<x-slot:trigger>
<x-button
icon="o-trash"
wire:click="deleteProduct({{ $product->id }})"
wire:confirm="{{ __('Are you sure you want to delete this product?') }}"
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>
@endscope
</x-table>
</x-card>
</div>
On ajoute ces traductions :
"Price incl. VAT": "Prix TTC",
"Active": "Actif",
"Product deleted successfully.": "Produit supprimé avec succès.",
"Are you sure you want to delete this product?": "Voulez-vous vraiment supprimer ce produit ?",
"Create a new product": "Création d'un nouveau produit",
La route
On ajoute une route pour l'atteindre :
Volt::route('/products', 'admin.products.index')->name('admin.products.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-item icon="s-building-storefront" title="{{ __('Catalog') }}" link="{{ route('admin.products.index') }}" />
Une petite traduction :
"Catalog": "Catalogue",
Et on a notre tableau :
Un formulaire pour les adresses
On va avoir un formulaire commun pour la création et la modification d'un produit. On ajoute une vue partielle pour ce formulaire :
Avec ce formulaire :
<x-form wire:submit="save">
<x-input
label="{{ __('Name') }}"
wire:model="name"
required
placeholder="{!! __('Enter product name') !!}"
/>
<x-textarea
label="{{ __('Description') }}"
wire:model="description"
placeholder="{!! __('Enter product description') !!}"
rows="5"
required
/>
<x-input
label="{{ __('Weight in kg') }}"
wire:model="weight"
required
placeholder="{{ __('Enter product weight') }}"
/>
<x-input
label="{{ __('Price') }}"
wire:model="price"
required
placeholder="{{ __('Enter product price') }}"
/>
<x-input
label="{{ __('Quantity available') }}"
wire:model="quantity"
type="number"
required
placeholder="{{ __('Enter product quantity') }}"
/>
<x-input
label="{{ __('Quantity for stock alert') }}"
wire:model="quantity_alert"
type="number"
required
placeholder="{{ __('Enter product quantity') }}"
/>
<x-checkbox label="{{ __('Active product') }}" wire:model="active" />
<hr>
<x-file
wire:model="image"
label="{{ __('Image') }}"
hint="{!! __('Click on this image to modify it') !!}"
accept="image/png, image/jpeg">
<img src="{{ $image == '' ? asset('storage/ask.jpg') : asset('storage/photos/' . $image) }}" class="h-40" />
</x-file>
<x-slot:actions>
<x-button
label="{{ __('Save') }}"
icon="o-paper-airplane"
spinner="save"
type="submit"
class="btn-primary"
/>
</x-slot:actions>
</x-form>
On ajoute quelques traductions :
"Enter product name": "Entrez le nom du produit",
"Enter product description": "Entrez la description du produit",
"Enter product price": "Entrez le prix du produit",
"Enter product quantity": "Entrez la quantité du produit",
"Enter product weight": "Entrez le poids du produit",
"Quantity for stock alert": "Quantité pour alerte de stock",
"Click on this image to modify it": "Cliquez sur cette image pour la modifier",
"Active product": "Produit actif",
"Weight in kg": "Poids en kg",
"Quantity available": "Quantité disponible",
Pour l'image par défaut, vous pouvez aller la chercher dans le dépôt et la mettre ici :
Un trait pour les éléments communs
De la même manière, on va avoir des éléments PHP communs pour la création et la modification d'un produit, on crée un trait :
On va y placer les propriétés et les règles de validation :
<?php
namespace App\Traits;
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
trait ManageProduct
{
public string $name = '';
public TemporaryUploadedFile|string|null $image = null;
public string $description = '';
public float $price = 0;
public float $weight = 0;
public int $quantity = 0;
public int $quantity_alert = 0;
public bool $active = false;
protected function validateProductData(array $additionalData = []): array
{
$rules = [
'name' => 'required|max:255',
'image' => $this->image instanceof TemporaryUploadedFile ? 'image|mimes:jpeg,png,jpg,gif' : 'required',
'description' => 'required|string|max:65535',
'price' => 'required|numeric|min:0|regex:/^(\d+(?:[\.\,]\d{1,2})?)$/',
'weight' => 'required|numeric|min:0|regex:/^(\d+(?:[\.\,]\d{1,3})?)$/',
'quantity' => 'required|numeric|min:0',
'quantity_alert' => 'required|numeric|min:0|lte:quantity',
'active' => 'required|boolean',
];
return $this->validate(array_merge($rules, $additionalData));
}
}
Pour la validation, on a besoin de nouvelles traductions dans fr.validation :
'weight' => 'poids',
'quantity_alert' => 'alerte de quantité',
],
'custom' => [
'firstname' => [
'required_unless' => 'Le champ prénom est obligatoire sauf si c\'est une adresse professionnelle.',
],
'name' => [
'required_unless' => 'Le champ nom est obligatoire sauf si c\'est une adresse professionnelle.',
],
'company' => [
'required_unless' => 'Le champ raison sociale est obligatoire si c\'est une adresse professionnelle.',
],
],
J'en ai profité pouc combler quelques oublis précédents.
La création d'un produit
On crée le composant pour la création d'un produit :
php artisan make:volt admin/products/create --class
Avec la création précédemment de la vue pour le formulaire et le trait, le code devient bien plus léger dans ce composant :
<?php
use Livewire\Volt\Component;
use Livewire\WithFileUploads;
use Livewire\Attributes\{Layout, Title};
use App\Models\Product;
use Mary\Traits\Toast;
use App\Traits\ManageProduct;
new
#[Title('Product creation')]
#[Layout('components.layouts.admin')]
class extends Component
{
use Toast, ManageProduct, WithFileUploads;
public function save(): void
{
$data = $this->validateProductData();
$path = basename($this->image->store('photos', 'public'));
$data['image'] = $path;
Product::create($data);
$this->success(__('Product created successfully.'), redirectTo: '/admin/products');
}
}; ?>
<div>
<x-header title="{!! __('Catalog') !!}" 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="{!! __('Create a new product') !!}">
@include('livewire.admin.products.form')
</x-card>
</div>
On ajoute la route :
Volt::route('/products/create', 'admin.products.create')->name('admin.products.create');
On obtient ce gros formulaire :
Vérifiez que tout fonctionne correctement.
La modification d'un produit
On crée le composant pour la modification d'un produit :
php artisan make:volt admin/products/edit --class
Là aussi le code est léger :
<?php
use Livewire\Volt\Component;
use Livewire\Attributes\{Layout, Title};
use App\Models\Product;
use Mary\Traits\Toast;
use App\Traits\ManageProduct;
use Livewire\WithFileUploads;
new
#[Title('Product edition')]
#[Layout('components.layouts.admin')]
class extends Component
{
use Toast, ManageProduct, WithFileUploads;
public Product $product;
public function mount(Product $product): void
{
$this->product = $product;
$this->fill($this->product);
}
public function save(): void
{
$data = $this->validateProductData();
if ($this->image instanceof TemporaryUploadedFile) {
$path = basename($this->image->store('photos', 'public'));
$data['image'] = $path;
}
$this->product->update($data);
$this->success(__('Product updated successfully.'), redirectTo: '/admin/products');
}
}; ?>
<div>
<x-header title="{!! __('Catalog') !!}" 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="{!! __('Edit a product') !!}">
@include('livewire.admin.products.form')
</x-card>
</div>
On ajoute la route :
Volt::route('/products/{product}/edit', 'admin.products.edit')->name('admin.products.edit');
Des traductions :
"Product updated successfully.": "Produit mis à jour avec succès.",
"Edit a product": "Modification d'un produit",
On a le même formulaire que pour la création, mais renseigné avec les données du produit sélectionné :
Là aussi, vérifiez que tout fonctionne correctement.
Conclusion
On en a fini avec la gestion des produits. Dans la prochaine étape, on commencera à coder le paramétrage de la boutique.
Par bestmomo
Aucun commentaire