Logomark

LARAVEL

Un framework qui rend heureux
Voir cette catégorie
Vers le bas
Voir cette série
Shop V2 : le compte client 1/2
Mercredi 5 août 2026 17:35

Désormais, nos clients peuvent créer un compte, se connecter et même récupérer leur mot de passe. Cependant, il nous reste encore à développer les fonctionnalités permettant aux clients de mettre à jour leurs informations personnelles, de gérer leurs adresses, d'accéder aux détails de leurs commandes et de consulter toutes leurs données, afin de satisfaire les exigences du RGPD.

Le code complet à la fin de cet article peut être téléchargé ici.

Le profil

Lors de son inscription, un client donne un certain nombre de renseignements personnels. Il doit en conserver la maîtrise et pouvoir apporter des changements au besoin. En particulier, il doit pouvoir changer son nom, son prénom, son email, son mot de passe et son inscription à la lettre d'information. On va en conséquence créer un formulaire pour lui permettre d'effectuer ces changements.

On crée un nouveau composant Livewire :

php artisan make:livewire pages::account.profile

Avec ce code :

<?php

use App\Models\User;
use Illuminate\Validation\Rules\Password;
use Livewire\Features\SupportRedirects\Redirector;
use Illuminate\Support\Facades\{Auth, Hash};
use Illuminate\Support\Str;
use Illuminate\Validation\Rule;
use Livewire\Attributes\Title;
use Livewire\Component;
use Mary\Traits\Toast;

new #[Title('Profil')]
class extends Component {
    use Toast;

    public User $user;
    public string $firstname = '';
    public string $name = '';
    public string $email = '';
    public bool $newsletter = false;
    public string $password = '';
    public string $password_confirmation = '';
    public bool $rgpd = false;

    public function mount(): void
    {
        $this->user = Auth::user();
        $this->firstname = $this->user->firstname ?? '';
        $this->name = $this->user->name ?? '';
        $this->email = $this->user->email ?? '';
        $this->newsletter = (bool) ($this->user->newsletter ?? false);
    }

    public function generatePassword(): void
    {
        $generated = Str::password(14);
        $this->password = $generated;
        $this->password_confirmation = $generated;

        $this->info(__('Nouveau mot de passe généré.'), position: 'toast-bottom');
    }

    public function save(): void
    {
        $data = $this->validate([
            'firstname'  => ['required', 'string', 'max:255'],
            'name'       => ['required', 'string', 'max:255'],
            'email'      => ['required', 'email', Rule::unique('users')->ignore($this->user->id)],
            'newsletter' => ['boolean'],
            'rgpd'       => ['accepted'],
            'password' => [
                'nullable',
                'confirmed',
                Password::min(8)
                    ->mixedCase()   // Majuscules & Minuscules
                    ->numbers()     // Chiffres
                    ->symbols()     // Caractères spéciaux
                    ->uncompromised(), // Vérifie via "Have I Been Pwned" si le mot de passe a fuité sur le web !
            ],
        ], [
            'rgpd.accepted' => __('Vous devez accepter la politique de confidentialité pour enregistrer.')
        ]);

        if (empty($data['password'])) {
            unset($data['password']);
        } else {
            $data['password'] = Hash::make($data['password']);
        }

        unset($data['rgpd']);

        $this->user->update($data);

        $this->success(__('Profil mis à jour avec succès !'), redirectTo: route('home'));
    }

    public function cancel(): Redirector
    {
        return redirect()->route('home');
    }
}; ?>

<div class="min-h-screen py-8 bg-base-200/50">
    <div class="max-w-3xl mx-auto px-4">

        {{-- En-tête --}}
        <div class="text-center mb-8">
            <h1 class="text-3xl font-extrabold tracking-tight">@lang('Mon profil')</h1>
            <p class="text-sm text-base-content/70 mt-1">@lang('Gérez vos informations personnelles et vos préférences')</p>
        </div>

        {{-- Formulaire Carte --}}
        <x-card class="shadow-xl bg-base-100 border border-base-300">
            <x-form wire:submit="save" class="space-y-6">

                {{-- Section : Informations personnelles --}}
                <div class="space-y-4">
                    <div class="flex items-center gap-2 text-lg font-semibold border-b border-base-200 pb-2">
                        <x-icon name="o-user" class="w-5 h-5 text-primary" />
                        <span>@lang('Informations personnelles')</span>
                    </div>

                    <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
                        <x-input
                            label="{{ __('Prénom') }}"
                            wire:model="firstname"
                            icon="o-user"
                            required
                        />
                        <x-input
                            label="{{ __('Nom') }}"
                            wire:model="name"
                            icon="o-user"
                            required
                        />
                    </div>

                    <x-input
                        label="{{ __('Email') }}"
                        wire:model="email"
                        icon="o-envelope"
                        type="email"
                        required
                    />
                </div>

                {{-- Section : Mot de passe --}}
                <div class="space-y-4 pt-4">
                    <div class="flex items-center gap-2 text-lg font-semibold border-b border-base-200 pb-2">
                        <x-icon name="o-lock-closed" class="w-5 h-5 text-primary" />
                        <span>@lang('Sécurité')</span>
                    </div>

                    <p class="text-xs text-base-content/60">
                        @lang('Laissez vide si vous ne souhaitez pas modifier votre mot de passe.')
                    </p>

                    <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
                        <x-input
                            label="{{ __('Nouveau mot de passe') }}"
                            wire:model="password"
                            icon="o-key"
                            type="password"
                        />
                        <x-input
                            label="{{ __('Confirmer le mot de passe') }}"
                            wire:model="password_confirmation"
                            icon="o-key"
                            type="password"
                        />
                    </div>

                    <div class="flex items-center gap-3">
                        <x-button
                            label="{{ __('Générer un mot de passe sécurisé') }}"
                            wire:click="generatePassword"
                            icon="o-arrow-path"
                            class="btn-outline btn-sm btn-secondary"
                            type="button"
                        />
                    </div>

                    {{-- Affichage temporaire du MDP généré --}}
                    @if($password)
                        <div x-data="{ showPass: false }" class="p-3 bg-warning/10 border border-warning/30 rounded-lg space-y-2">
                            <div class="flex items-center justify-between">
                                <span class="font-mono text-sm font-semibold">
                                    <span x-show="!showPass">••••••••••••</span>
                                    <span x-show="showPass" x-cloak>{{ $password }}</span>
                                </span>
                                <div class="flex items-center gap-2">
                                    <button type="button" @click="showPass = !showPass" class="btn btn-ghost btn-xs">
                                        <x-icon name="o-eye" class="w-4 h-4" />
                                    </button>
                                    <button type="button" onclick="navigator.clipboard.writeText('{{ $password }}')" class="btn btn-warning btn-xs">
                                        <x-icon name="o-clipboard-document" class="w-4 h-4" />
                                        @lang('Copier')
                                    </button>
                                </div>
                            </div>
                            <p class="text-xs text-base-content/70">
                                @lang('Pensez à copier le mot de passe généré avant d\'enregistrer.')
                            </p>
                        </div>
                    @endif
                </div>

                {{-- Section : Préférences & RGPD --}}
                <div class="space-y-4 pt-4">
                    <div class="flex items-center gap-2 text-lg font-semibold border-b border-base-200 pb-2">
                        <x-icon name="o-cog-6-tooth" class="w-5 h-5 text-primary" />
                        <span>@lang('Préférences et confidentialité')</span>
                    </div>

                    <div class="space-y-3 bg-base-200/50 p-4 rounded-lg">
                        <x-checkbox
                            label="{{ __('Je souhaite recevoir la newsletter') }}"
                            wire:model="newsletter"
                        />
                        <x-checkbox
                            wire:model="rgpd"
                        >
                            <x-slot:label>
                                @lang('J\'accepte la')
                                <a href="{{ route('pages', ['page' => 'politique-de-confidentialite']) }}" target="_blank" class="link link-primary">
                                    @lang('politique de confidentialité')
                                </a>
                            </x-slot:label>
                        </x-checkbox>
                    </div>
                </div>

                {{-- Actions --}}
                <div class="flex flex-col-reverse sm:flex-row sm:items-center sm:justify-between gap-3 pt-6 border-t border-base-200">
                    <x-button
                        label="{{ __('Annuler') }}"
                        wire:click="cancel"
                        class="btn-ghost"
                        type="button"
                    />

                    <x-button
                        label="{{ __('Enregistrer les modifications') }}"
                        icon="o-check"
                        spinner="save"
                        type="submit"
                        class="btn-primary"
                        ::disabled="!$wire.rgpd"
                    />
                </div>

            </x-form>
        </x-card>
    </div>
</div>

La route dans un groupe d'utilisateurs authentifiés et dans un groupe spécifique pour le compte utilisateur :

Route::middleware('auth')->group(function () {
    Route::prefix('account')->group(function () {
        Route::livewire('/profile', 'pages::account.profile')->name('profile');
    });
});

On renseigne la barre de navigation :

<x-menu class="p-2 bg-base-100 text-base-content rounded-box shadow-2xl min-w-56 space-y-1">
    <x-menu-item
        title="{{ __('Mon profil') }}"
        link="{{ route('profile') }}"
        icon="o-user"
    />

Et la barre latérale :

<x-menu-item title="{{ __('Mon profil') }}" icon="o-user" link="{{ route('profile') }}" />

Et on a notre formulaire :

Comme dans le formulaire d'inscription, on prévoit une possibilité de générer un mot de passe sécurisé.

Les adresses

Le client doit aussi disposer d'une gestion complète de ses adresses : ajout, modification, suppression. La plupart des clients n'en auront qu'une qui servira pour l'expédition et la facturation. Mais ces clients professionnels peuvent vouloir les distinguer. D'autre part, on peut imaginer des expéditions à différentes adresses.

Un composant pour afficher une adresse

Comme il faudra afficher des adresses sur diverses pages, on prévoit un composant Blade :

php artisan make:component address --view

Avec ce code :

<div class="space-y-1 text-sm text-base-content/90 leading-relaxed">
    {{-- Nom & Prénom ou Raison Sociale --}}
    @if ($address->company)
        <p class="font-bold text-base text-base-content">
            {{ $address->company }}
        </p>
    @endif

    @if ($address->name || $address->firstname)
        <p class="font-semibold text-base-content">
            {{ trim("{$address->civility} {$address->name} {$address->firstname}") }}
        </p>
    @endif

    {{-- Voies & Adresses --}}
    <div class="pt-0.5 space-y-0.5">
        <p>{{ $address->address }}</p>

        @if ($address->addressbis)
            <p class="text-base-content/80">{{ $address->addressbis }}</p>
        @endif

        @if ($address->bp)
            <p class="text-base-content/80">{{ $address->bp }}</p>
        @endif

        <p class="font-medium">
            {{ trim("{$address->postal} {$address->city}") }}
        </p>

        @if ($address->country)
            <p class="font-bold uppercase text-xs tracking-wider text-base-content/70 pt-1">
                {{ $address->country->name }}
            </p>
        @endif
    </div>

    {{-- Numéro de téléphone --}}
    @if ($address->phone)
        <div class="flex items-center gap-1.5 pt-2 text-xs text-base-content/70 font-mono">
            <x-icon name="o-phone" class="w-3.5 h-3.5 text-primary shrink-0" />
            <span>{{ $address->phone }}</span>
        </div>
    @endif
</div>

Les routes

On ajoute les trois routes qui vont nous être utiles (affichage des adresses, création et modification) :

Route::middleware('auth')->group(function () {
    Route::prefix('account')->group(function () {
        ...
        Route::livewire('/addresses', 'pages::account.addresses.index')->name('addresses');
        Route::livewire('/addresses/create', 'pages::account.addresses.create')->name('addresses.create');
        Route::livewire('/addresses/{address}/edit', 'pages::account.addresses.edit')->name('addresses.edit');
    });
});

Et on renseigne la barre de navigation et la barre latérale avec le même code :

<x-menu-item
    title="{{ __('Mes adresses') }}"
    link="{{ route('addresses') }}"
    icon="o-map-pin"
/>

L'affichage des adresses

On va commencer par créer l'affichage de toutes les adresses présentes avec un nouveau composant :

php artisan make:livewire pages::account.addresses.index

Avec ce code :

<?php

use App\Models\Address;
use Livewire\Component;
use Illuminate\Support\Collection;
use Livewire\Attributes\Title;
use Illuminate\Support\Facades\Auth;

new #[Title('Mes adresses')]
class extends Component {

    public function deleteAddress(Address $address): void
    {
        // Sécurité : Vérifier que l'adresse appartient bien à l'utilisateur connecté
        if ($address->user_id !== Auth::id()) {
            abort(403);
        }

        $address->delete();

        $this->success(__('Adresse supprimée avec succès.'));
    }

    public function with(): array
    {
        return [
            'addresses' => Auth::user()->addresses()->with('country')->get(),
        ];
    }

}; ?>

<div class="max-w-5xl mx-auto py-8 px-4 space-y-6">
    {{-- En-tête de la page --}}
    <x-header title="{{ __('Mes adresses') }}" subtitle="{{ __('Gérez vos adresses de livraison et de facturation') }}" separator>
        <x-slot:actions>
            <x-button
                label="{{ __('Ajouter une adresse') }}"
                icon="o-plus"
                link="{{ route('addresses.create') }}"
                class="btn-primary btn-sm"
            />
        </x-slot:actions>
    </x-header>

    {{-- Liste des adresses --}}
    @if($addresses->isNotEmpty())
        <div class="grid gap-6 grid-cols-1 md:grid-cols-2">
            @foreach ($addresses as $address)
                <x-card
                    class="shadow-sm hover:shadow-md transition-all duration-200 border border-base-200 hover:border-primary/50 bg-base-100"
                >
                    {{-- Composant d'affichage de l'adresse --}}
                    <div class="pb-2">
                        <x-address :address="$address" />
                    </div>

                    <x-slot:actions class="pt-2 border-t border-base-200 justify-end gap-1">
                        <x-button
                            icon="o-pencil"
                            link="{{ route('addresses.edit', $address) }}"
                            class="btn-ghost btn-circle btn-xs text-base-content/70 hover:text-primary"
                            tooltip="{{ __('Modifier') }}"
                        />

                        <x-button
                            icon="o-trash"
                            wire:click="deleteAddress({{ $address->id }})"
                            wire:confirm="{{ __('Êtes-vous sûr de vouloir supprimer cette adresse ?') }}"
                            spinner="deleteAddress({{ $address->id }})"
                            class="btn-ghost btn-circle btn-xs text-error"
                            tooltip="{{ __('Supprimer') }}"
                        />
                    </x-slot:actions>
                </x-card>
            @endforeach
        </div>
    @else
        {{-- État vide (Aucune adresse) --}}
        <x-card class="text-center py-12 bg-base-100 shadow-sm border border-base-200">
            <div class="flex flex-col items-center justify-center space-y-4">
                <div class="w-16 h-16 rounded-full bg-primary/10 flex items-center justify-center text-primary">
                    <x-icon name="o-map-pin" class="w-8 h-8" />
                </div>
                <div class="space-y-1">
                    <h3 class="font-bold text-lg">{{ __('Aucune adresse enregistrée') }}</h3>
                    <p class="text-sm text-base-content/60">
                        {{ __('Vous n\'avez pas encore ajouté d\'adresse à votre compte.') }}
                    </p>
                </div>
                <x-button
                    label="{{ __('Créer ma première adresse') }}"
                    icon="o-plus"
                    link="{{ route('addresses.create') }}"
                    class="btn-primary btn-sm mt-2"
                />
            </div>
        </x-card>
    @endif
</div>

Et voilà l'aspect :

En prévoyant le cas où il n'y a pas encore d'adresse, ce qui est la situation lorsque le client vient de s'inscrire :

La facturation électronique

Toutes les entreprises françaises, quelle que soit leur taille, devront émettre des factures électroniques à la date du 01/09/2027. Alors, il faut s'y préparer ! Un annuaire officiel recense les entreprises et entités publiques assujetties aux obligations d’émettre et de recevoir des factures électroniques. L'élément d'entrée dans cet annuaire est le SIREN ou SIRET. Vous pouvez trouver la documentation officielle ici. Il faudra prévoir cette mention sur les factures. Mais on ne doit pas rendre cette saisie obligatoire avant cette date. On prévoit cette date dans le fichier .env :

FACTURATION_ELECTRONIQUE_OBLIGATOIRE_DATE=2027-09-01

Et dans le fichier de configuration de l'application (app.php) pour récupérer la valeur :

'facturation_electronique' => [
    'obligatoire_a_partir_du' => env('FACTURATION_ELECTRONIQUE_OBLIGATOIRE_DATE', '2027-09-01'),
],

On va utiliser cette valeur pour déterminer si on doit faire apparaître ou pas cette saisie dans les adresses. Il faudra aussi tenir compte après cette date des adresses déjà saisies sans cette valeur pour une nouvelle commande, mais on verra ça plus tard...

Un formulaire pour les adresses

On va avoir un formulaire commun pour la création et la modification. On ajoute un composant Blade pour ce formulaire :

php artisan make:component addresses.form --view

Avec ce code :

<x-form wire:submit="save" class="w-full space-y-4">

    {{-- Adresse Pro --}}
    <x-checkbox
        label="{!! __('C\'est une adresse professionnelle') !!}"
        wire:model="professionnal"
        wire:change="$refresh"
        class="checkbox-primary checkbox-sm"
    />

    {{-- Civilités --}}
    <x-radio
        :options="$civilities"
        wire:model="civility"
        class="radio-primary radio-sm"
    />

    {{-- Nom & Prénom sur 2 colonnes en Desktop --}}
    <div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
        <x-input
            label="{{ __('Nom') }}"
            type="text"
            wire:model="name"
            icon="o-user"
            hint="{{ $professionnal ? __('Optionnel') : '' }}"
            :required="!$professionnal"
        />
        <x-input
            label="{{ __('Prénom') }}"
            type="text"
            wire:model="firstname"
            icon="o-user"
            hint="{{ $professionnal ? __('Optionnel') : '' }}"
            :required="!$professionnal"
        />
    </div>

    {{-- Raison Sociale --}}
    <x-input
        label="{{ __('Raison sociale') }}"
        type="text"
        wire:model="company"
        icon="o-building-office"
        :disabled="!$professionnal"
        :required="$professionnal"
    />

    <x-input
        label="{{ __('Numéro de TVA') }}"
        type="text"
        wire:model="buyer_tax_no"
        icon="o-identification"
        :disabled="!$professionnal"
        hint="A compléter si vous êtes redevable de la TVA. Example: FR12345678901"
    />

    @php
        $dateObligation = \Carbon\Carbon::parse(config('app.facturation_electronique.obligatoire_a_partir_du'));
        $sirenObligatoire = $professionnal && ($country?->requires_siren ?? false) && $dateObligation->isPast();
        $dateFormatee = $dateObligation->translatedFormat('d/m/Y');
    @endphp

    <x-input
        label="{{ __('Numéro de SIREN/SIRET') }}"
        type="text"
        wire:model="siren"
        icon="o-identification"
        :disabled="!$professionnal || !$country?->requires_siren || !$sirenObligatoire"
        :required="$sirenObligatoire"
        hint="{{ $sirenObligatoire
        ? __('Obligatoire pour la facturation électronique. Exemple: 123456789 (SIREN) ou 12345678901234 (SIRET)')
        : ($professionnal && $country?->requires_siren
            ? __('Ce champ sera obligatoire à partir du :date pour la facturation électronique.', ['date' => $dateFormatee])
            : '') }}"
    />

    {{-- Lignes d'adresse --}}
    <x-input
        label="{{ __('N° et nom de rue') }}"
        type="text"
        wire:model="address"
        icon="o-map-pin"
        required
    />

    <x-input
        label="{{ __('Bâtiment, étage, etc.') }}"
        type="text"
        hint="{{ __('Optionnel') }}"
        wire:model="addressbis"
        icon="o-map-pin"
    />

    <x-input
        label="{{ __('Lieu-dit ou Boîte Postale') }}"
        type="text"
        hint="{{ __('Optionnel') }}"
        wire:model="bp"
        icon="o-map-pin"
    />

    {{-- Code Postal & Ville sur 2 colonnes --}}
    <div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
        <x-input
            label="{{ __('Code postal') }}"
            type="text"
            wire:model="postal"
            icon="o-map-pin"
            required
        />
        <x-input
            label="{{ __('Ville') }}"
            type="text"
            wire:model="city"
            icon="o-map-pin"
            class="sm:col-span-2"
            required
        />
    </div>

    {{-- Pays & Téléphone --}}
    <x-select
        label="{{ __('Pays') }}"
        :options="$countries"
        wire:model.live.debounce="country_id"
        icon="o-globe-alt"
        required
    />

    <x-input
        label="{{ __('Numéro de téléphone') }}"
        type="tel"
        wire:model="phone"
        icon="o-phone"
        required
    />

    <p class="text-xs text-base-content/60 pt-2">
        <span class="text-error">*</span> @lang('Champs obligatoires')
    </p>

    <x-slot:actions>
        <div class="flex flex-col sm:flex-row items-center justify-between gap-3 w-full pt-2">
            <x-button
                label="{{ __('Annuler') }}"
                type="button"
                icon="o-x-mark"
                link="/account/addresses"
                class="btn-ghost btn-sm w-full sm:w-auto order-2 sm:order-1"
            />

            <x-button
                label="{{ __('Enregistrer') }}"
                type="submit"
                icon-right="o-check"
                spinner="save"
                class="btn-primary btn-sm w-full sm:w-auto order-1 sm:order-2"
            />
        </div>
    </x-slot:actions>
</x-form>

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'une adresse, on crée un trait :

On y place les propriétés, les règles de validation et deux fonctions :

<?php

namespace App\Traits;
use App\Models\Country;
use Illuminate\Support\Collection;
use Carbon\Carbon;

trait ManageAddress
{
    public Collection $countries;
    public bool $professionnal = false;
    public array $civilities = [['id' => 'M.', 'name' => 'M.'], ['id' => 'Mme', 'name' => 'Mme']];
    public string $civility = 'M.';
    public ?string $firstname = null;
    public ?string $name = null;
    public ?string $company = null;
    public ?string $address = null;
    public ?string $buyer_tax_no = null;
    public ?string $siren = null;
    public ?string $addressbis = null;
    public ?string $bp = null;
    public string $postal = '';
    public string $city = '';
    public ?Country $country = null;
    public int $country_id;
    public string $phone = '';

    /**
     * @return array
     */
    protected function rules(): array
    {
        $sirenObligatoire = $this->professionnal
            && $this->country?->requires_siren
            && Carbon::parse(config('app.facturation_electronique.obligatoire_a_partir_du'))->isPast();

        return [
            'professionnal' => 'required|boolean',
            'civility' => 'required|in:M.,Mme',
            'firstname' => 'required_unless:professionnal,true|nullable|string|max:100',
            'name' => 'required_unless:professionnal,true|nullable|string|max:100',
            'company' => 'required_unless:professionnal,false|nullable|string|max:100',
            'address' => 'required|string|max:255',
            'addressbis' => 'nullable|string|max:255',
            'bp' => 'nullable|string|max:100',
            'postal' => 'required|string',
            'city' => 'required|string|max:100',
            'country_id' => 'required|integer|exists:countries,id',
            'phone' => 'required|numeric',
            'buyer_tax_no' => [
                'nullable',
                'string',
                'max:20',
                function ($attribute, $value, $fail) {
                    // Si le client est un professionnel ET que buyer_tax_no est renseigné
                    if ($this->professionnal && $value !== null) {
                        // Regex pour les numéros de TVA intracommunautaire (format UE)
                        $tvaUeRegex = '/^[A-Z]{2}[0-9]{8,12}$/'; // Ex: FR12345678901, DE123456789

                        if (!preg_match($tvaUeRegex, $value)) {
                            $fail('Le numéro de TVA semble invalide. Format attendu : [PAYS][CHIFFRES] (ex: FR12345678901).');
                        }
                    }
                },
            ],
            'siren' => [
                'nullable',
                'string',
                'max:14',
                function ($attribute, $value, $fail) use ($sirenObligatoire) {
                    if ($sirenObligatoire) {
                        if (empty($value)) {
                            $fail('Le SIREN ou SIRET est obligatoire pour la facturation électronique.');
                            return;
                        }
                        $sirenRegex = '/^[0-9]{9}$/';   // SIREN
                        $siretRegex = '/^[0-9]{14}$/'; // SIRET
                        if (!preg_match($sirenRegex, $value) && !preg_match($siretRegex, $value)) {
                            $fail('Le SIREN ou SIRET doit contenir 9 ou 14 chiffres.');
                        }
                    }
                },
            ],
        ];
    }

    /**
     * Met à jour les propriétés en fonction des changements de champs.
     *
     * @param string $property Le nom de la propriété modifiée.
     * @param mixed $value La nouvelle valeur de la propriété.
     */
    public function updated(string $property, mixed $value): void
    {
        match ($property) {
            'country_id' => $this->country = Country::find($value),
            'professionnal' => $this->resetProfessionalFields(),
            default => null,
        };
    }

    /**
     * @return void
     */
    private function resetProfessionalFields(): void
    {
        $this->company = null;
        $this->buyer_tax_no = null;
        $this->siren = null;
    }
}

La création d'une adresse

On crée le composant Livewire pour la création d'une adresse :

php artisan make:livewire pages::account.addresses.create

Avec la création précédemment du composant pour le formulaire et le trait, le code est bien plus léger :

<?php

use App\Models\Country;
use Livewire\Component;
use Livewire\Attributes\Title;
use Mary\Traits\Toast;
use Illuminate\Support\Collection;
use App\Traits\ManageAddress;
use Illuminate\Support\Facades\Auth;

new #[Title('Créer une adresse')]
class extends Component {
    use Toast, ManageAddress;

    public function mount(): void
    {
        $this->countries = Country::all();
        if ($this->countries->isNotEmpty()) {
            $this->country_id = $this->countries->first()->id;
            $this->country = $this->countries->first(); // Charge le pays par défaut
        }
    }

    public function save(): void
    {
        $data = $this->validate($this->rules());

        Auth::user()->addresses()->create($data);

        $this->success(__('Adresse créée avec succès.'), redirectTo: route('addresses'));
    }
}

?>

<div class="max-w-3xl mx-auto py-8 px-4 space-y-6">
    {{-- En-tête avec bouton de retour --}}
    <x-header title="{{ __('Créer une adresse') }}" subtitle="{{ __('Renseignez vos coordonnées de livraison ou de facturation') }}" separator>
        <x-slot:actions>
            <x-button
                label="{{ __('Retour aux adresses') }}"
                icon="o-arrow-left"
                link="{{ route('addresses') }}"
                class="btn-ghost btn-sm"
            />
        </x-slot:actions>
    </x-header>

    {{-- Carte du formulaire --}}
    <x-card class="shadow-xl bg-base-100" separator progress-indicator>
        <x-addresses.form
            :civilities="$civilities"
            :countries="$countries"
            :professionnal="$professionnal"
            :country="$country"
        />
    </x-card>
</div>

On obtient ce gros formulaire :

Le nom et le prénom sont optionnels si on a affaire à une adresse professionnelle. Vérifiez que tout fonctionne correctement.

Le SIREN/SIRET est obligatoire si le pays est concerné (France Métropolitaine et territoires d'outre-mer) dans le cadre de la facturation électronique à partir de la date du 01/09/2027. En attendant, on affiche :

Mais après cette date pour une adresse localisée en France, on affichera :

La modification d'une adresse

On crée le composant pour la modification d'une adresse :

php artisan make:livewire pages::account.addresses.edit

Là aussi le code est léger :

<?php

use App\Models\{Address, Country};
use Livewire\Component;
use Livewire\Attributes\Title;
use Mary\Traits\Toast;
use Illuminate\Support\Collection;
use App\Traits\ManageAddress;
use Illuminate\Support\Facades\Auth;

new #[Title('Modifier l\'adresse')]
class extends Component {
    use Toast, ManageAddress;

    public Address $myAddress;

    public function mount(Address $address): void
    {
        if ($address->user_id !== Auth::id()) {
            abort(403);
        }

        $this->myAddress = $address;
        $this->fill($this->myAddress);
        $this->countries = Country::all();
        $this->country = $this->countries->find($this->country_id); // ✅ Charge le pays correspondant à l'adresse
    }

    public function save(): void
    {
        $data = $this->validate($this->rules());

        $this->myAddress->update($data);

        $this->success(__('Adresse mise à jour avec succès.'), redirectTo: route('addresses'));
    }
}; ?>

<div class="max-w-3xl mx-auto py-8 px-4 space-y-6">
    {{-- En-tête de la page --}}
    <x-header
        title="{!! __('Modifier l\'adresse') !!}"
        subtitle="{{ __('Mettez à jour vos informations de livraison ou de facturation') }}"
        separator
    >
        <x-slot:actions>
            <x-button
                label="{{ __('Retour aux adresses') }}"
                icon="o-arrow-left"
                link="{{ route('addresses') }}"
                class="btn-ghost btn-sm"
            />
        </x-slot:actions>
    </x-header>

    {{-- Carte du formulaire --}}
    <x-card class="shadow-xl bg-base-100 overflow-hidden" separator progress-indicator>
        <x-addresses.form
            :civilities="$civilities"
            :countries="$countries"
            :professionnal="$professionnal"
            :country="$country"
        />
    </x-card>
</div>

Le formulaire est évidemment identique, à la différence qu'il est maintenant renseigné avec l'adresse existante au chargement.

Conclusion

Les clients ont désormais accès à leurs données personnelles et peuvent même les modifier. D'autre part, ils disposent d'une gestion complète pour leurs adresses. La prochaine étape consistera à afficher les commandes passées et aussi de se plier aux exigences du RGPD.



Par bestmomo

Aucun commentaire

Article précédent : Shop V2 : oubli du mot de passe