feat(Notification process: text, app name, membership validation + project update)
All checks were successful
Deploy Roxane to Preprod / deploy (push) Successful in 1m21s
All checks were successful
Deploy Roxane to Preprod / deploy (push) Successful in 1m21s
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
APP_NAME="Roxane - Le Retzien Libre"
|
APP_NAME="Roxane - Le Retzien Libre"
|
||||||
|
FRONT_NAME="Le Retzien Libre"
|
||||||
APP_ENV=local
|
APP_ENV=local
|
||||||
APP_KEY=
|
APP_KEY=
|
||||||
APP_DEBUG=true
|
APP_DEBUG=true
|
||||||
|
|||||||
@@ -165,9 +165,24 @@ resources/js/components/
|
|||||||
|
|
||||||
## Notifications
|
## Notifications
|
||||||
|
|
||||||
| Classe | Canal | Description |
|
Toutes les notifications sont queueable (`ShouldQueue`). Le template Blade `notifications/mail-template.blade.php` est partagé et accepte une variable `$appName` pour différencier les emails membres des emails admin.
|
||||||
| ------------------------- | ----- | --------------------------------------------- |
|
|
||||||
| SubscriptionExpiredPhase1 | Email | Notification d'expiration d'adhesion (queued) |
|
| Classe | Canal | Destinataire | Déclencheur |
|
||||||
|
| ----------------------------------- | ----- | ------------ | ------------------------------------------------ |
|
||||||
|
| ContactNewRequestNotification | Email | Admin | Soumission formulaire de contact |
|
||||||
|
| MemberNewRequestAdminNotification | Email | Admin | Soumission formulaire d'adhésion |
|
||||||
|
| MemberNewRequestMemberNotification | Email | Membre | Soumission formulaire d'adhésion (confirmation) |
|
||||||
|
| MembershipValidatedNotification | Email | Membre | Validation admin d'une adhésion (action Filament)|
|
||||||
|
| MemberDeactivatedMemberNotification | Email | Membre | Désactivation d'un membre |
|
||||||
|
| MemberDeactivatedAdminNotification | Email | Admin | Désactivation d'un membre |
|
||||||
|
| AdminInvitationNotification | Email | Admin | Création d'un compte administrateur |
|
||||||
|
| AdminPasswordResetNotification | Email | Admin | Réinitialisation de mot de passe admin |
|
||||||
|
| ServiceActivationRequestNotification| Email | Admin | Demande d'activation de service (dashboard) |
|
||||||
|
| SubscriptionExpiredPhase1 | Email | Membre | Job planifié d'expiration d'adhésion |
|
||||||
|
|
||||||
|
### Config app_name dans les emails
|
||||||
|
- Emails membres : `config('app.front_name')` (variable `FRONT_NAME` dans `.env`)
|
||||||
|
- Emails admin : `config('app.name')` (variable `APP_NAME` dans `.env`)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -180,14 +195,15 @@ Langues : **fr**, **en** — fichiers dans `lang/{locale}/` : contacts, members,
|
|||||||
## TODOs identifies dans le code
|
## TODOs identifies dans le code
|
||||||
|
|
||||||
| Fichier | TODO |
|
| Fichier | TODO |
|
||||||
|---------------------------|----------------------------------------------------------------------------|
|
|--------------------------------|--------------------------------------------------------------------------------|
|
||||||
| ContactService | Envoyer un email a l'administrateur |
|
| Notifications (x9) | Propager `appName` dans `->view()` pour toutes les notifications restantes |
|
||||||
| MemberService | Envoyer des emails au membre + admin a la desactivation |
|
| SubscriptionExpiredPhase1 | UI backend pour éditer le contenu du template de notification |
|
||||||
| SubscriptionExpiredPhase1 | Creer un template generique + UI backend pour le contenu |
|
|
||||||
| User.php | Restreindre l'acces admin en prod aux emails @retzien.fr |
|
| User.php | Restreindre l'acces admin en prod aux emails @retzien.fr |
|
||||||
| SyncDolibarrMembers | Exporter la methode toDate() dans un service/helper |
|
| SyncDolibarrMembers | Exporter la methode toDate() dans un service/helper |
|
||||||
| SyncISPConfigMailMembers | Gerer plusieurs emails par membre |
|
| SyncISPConfigMailMembers | Gerer plusieurs emails par membre |
|
||||||
| SyncISPConfigMailMembers | Ajouter le suivi ispconfig_client_id |
|
| SyncISPConfigMailMembers | Ajouter le suivi ispconfig_client_id |
|
||||||
|
| MembershipValidatedNotification| Ajouter lien HelloAsso vers le paiement quand disponible |
|
||||||
| Global | Refactoriser pour rendre générique le projet Roxane (ERP pour association) |
|
| Global | Refactoriser pour rendre générique le projet Roxane (ERP pour association) |
|
||||||
| Traduction | Crawler le prrojet pour retrouver toutes les clés manquantes |
|
| Traduction | Crawler le projet pour retrouver toutes les clés manquantes |
|
||||||
| Global | PHPstan niveau 8 |
|
| Global | PHPstan niveau 8 |
|
||||||
|
| dev-routes.php | Supprimer la route de test mail (/test/mail) avant mise en production |
|
||||||
|
|||||||
24
README.md
24
README.md
@@ -12,17 +12,24 @@ The project is developed in the context of **Le Retzien Libre**, a non-profit as
|
|||||||
|
|
||||||
**Back office (administrators)**
|
**Back office (administrators)**
|
||||||
- Member management (status, nature, groups)
|
- Member management (status, nature, groups)
|
||||||
- Subscription and package management
|
- Subscription and package management with validation workflow (pending → active)
|
||||||
- Manual and automated synchronization with third-party services
|
- Manual and automated synchronization with third-party services
|
||||||
- Role and permission management (Spatie Permissions + Filament Shield)
|
- Role and permission management (Spatie Permissions + Filament Shield)
|
||||||
- Two-factor authentication for admin accounts
|
- Two-factor authentication for admin accounts
|
||||||
|
|
||||||
**Front office (members)**
|
**Front office (members)**
|
||||||
- Registration and membership form
|
- Registration and membership form with real-time field validation
|
||||||
- Personal dashboard with access to associated services
|
- Personal dashboard with access to associated services
|
||||||
- Profile and password management
|
- Profile and password management
|
||||||
- Two-factor authentication
|
- Two-factor authentication
|
||||||
|
|
||||||
|
**Email notifications**
|
||||||
|
- Admin notified on new contact request and new membership request (with direct link to the membership record)
|
||||||
|
- Member receives a confirmation email on registration and a validation email when approved
|
||||||
|
- Member notified on account deactivation and subscription expiry
|
||||||
|
- Queued notifications (`ShouldQueue`) — requires a running queue worker
|
||||||
|
- Separate application name for member emails (`FRONT_NAME`) vs admin emails (`APP_NAME`)
|
||||||
|
|
||||||
**Integrations**
|
**Integrations**
|
||||||
- Dolibarr ERP (member and subscription import via REST API)
|
- Dolibarr ERP (member and subscription import via REST API)
|
||||||
- ISPConfig (mail and web hosting account management via SOAP)
|
- ISPConfig (mail and web hosting account management via SOAP)
|
||||||
@@ -65,7 +72,7 @@ The project is developed in the context of **Le Retzien Libre**, a non-profit as
|
|||||||
### 1. Clone the repository
|
### 1. Clone the repository
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/your-org/roxane.git
|
git clone https://github.com/SabrinaNebulae/roxane.git
|
||||||
cd roxane
|
cd roxane
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -87,6 +94,8 @@ Edit `.env` and configure:
|
|||||||
- Database connection (`DB_*`)
|
- Database connection (`DB_*`)
|
||||||
- Redis connection (`REDIS_*`)
|
- Redis connection (`REDIS_*`)
|
||||||
- Mail configuration (`MAIL_*`)
|
- Mail configuration (`MAIL_*`)
|
||||||
|
- Application names: `APP_NAME` (admin panel), `FRONT_NAME` (member-facing emails)
|
||||||
|
- Admin email: `ADMIN_EMAIL` (recipient for all admin notifications)
|
||||||
- Third-party service credentials (Dolibarr, ISPConfig, Nextcloud)
|
- Third-party service credentials (Dolibarr, ISPConfig, Nextcloud)
|
||||||
|
|
||||||
### 4. Database setup
|
### 4. Database setup
|
||||||
@@ -187,10 +196,10 @@ php artisan test --compact
|
|||||||
## Known TODOs
|
## Known TODOs
|
||||||
|
|
||||||
| Area | Description |
|
| Area | Description |
|
||||||
|---------------------------|---------------------------------------------------------------------|
|
|-----------------------------|----------------------------------------------------------------------------------|
|
||||||
| ContactService | Send email notification to administrator on new contact request |
|
| Notifications | Propagate `appName` view variable to all remaining notifications |
|
||||||
| MemberService | Send emails to member and admin on deactivation |
|
| SubscriptionExpiredPhase1 | Backend UI for editing notification template content |
|
||||||
| SubscriptionExpiredPhase1 | Generic template + backend UI for notification content management |
|
| MembershipValidatedNotif. | Add HelloAsso payment link once integration is available |
|
||||||
| User.php | Restrict admin access in production to @retzien.fr emails |
|
| User.php | Restrict admin access in production to @retzien.fr emails |
|
||||||
| SyncDolibarrMembers | Extract `toDate()` method into a shared service or helper |
|
| SyncDolibarrMembers | Extract `toDate()` method into a shared service or helper |
|
||||||
| SyncISPConfigMailMembers | Handle multiple email addresses per member |
|
| SyncISPConfigMailMembers | Handle multiple email addresses per member |
|
||||||
@@ -198,6 +207,7 @@ php artisan test --compact
|
|||||||
| Global | Make Roxane fully generic for any association ERP use case |
|
| Global | Make Roxane fully generic for any association ERP use case |
|
||||||
| Translations | Audit project for missing translation keys |
|
| Translations | Audit project for missing translation keys |
|
||||||
| Global | Raise PHPStan to level 8 |
|
| Global | Raise PHPStan to level 8 |
|
||||||
|
| dev-routes.php | Remove `/test/mail` debug route before production deployment |
|
||||||
| V2 | Keycloak SSO integration (OIDC) for front office authentication |
|
| V2 | Keycloak SSO integration (OIDC) for front office authentication |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -57,7 +57,8 @@ class MembershipForm
|
|||||||
TextEntry::make('member.full_name')
|
TextEntry::make('member.full_name')
|
||||||
->label(Membership::getAttributeLabel('member_id')),
|
->label(Membership::getAttributeLabel('member_id')),
|
||||||
TextEntry::make('author.name')
|
TextEntry::make('author.name')
|
||||||
->label(Membership::getAttributeLabel('admin_id')),
|
->label(Membership::getAttributeLabel('admin_id'))
|
||||||
|
->default(__('memberships.sections.author_not_set')),
|
||||||
TextEntry::make('created_at')
|
TextEntry::make('created_at')
|
||||||
->label(Membership::getAttributeLabel('created_at')),
|
->label(Membership::getAttributeLabel('created_at')),
|
||||||
])
|
])
|
||||||
|
|||||||
@@ -12,6 +12,17 @@ class MembershipRequest extends FormRequest
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
public function messages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'phone1.digits' => 'Le numéro de téléphone doit contenir exactement 10 chiffres.',
|
||||||
|
'zipcode.digits' => 'Le code postal doit contenir exactement 5 chiffres.',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||||
*/
|
*/
|
||||||
@@ -23,9 +34,9 @@ class MembershipRequest extends FormRequest
|
|||||||
'email' => 'required|email|max:255',
|
'email' => 'required|email|max:255',
|
||||||
'company' => 'nullable|string|max:255',
|
'company' => 'nullable|string|max:255',
|
||||||
'address' => 'required|string|max:255',
|
'address' => 'required|string|max:255',
|
||||||
'zipcode' => 'required|string|max:255',
|
'zipcode' => ['required', 'digits:5'],
|
||||||
'city' => 'required|string|max:255',
|
'city' => 'required|string|max:255',
|
||||||
'phone1' => 'required|string|max:255',
|
'phone1' => ['required', 'digits:10'],
|
||||||
'package' => 'required|string|max:255',
|
'package' => 'required|string|max:255',
|
||||||
'amount' => 'required|numeric|min:0',
|
'amount' => 'required|numeric|min:0',
|
||||||
'cgu' => 'required|accepted',
|
'cgu' => 'required|accepted',
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ class MemberDeactivatedMemberNotification extends Notification implements Should
|
|||||||
|
|
||||||
$vars = [
|
$vars = [
|
||||||
'member_name' => $this->member->full_name,
|
'member_name' => $this->member->full_name,
|
||||||
'app_name' => config('app.name'),
|
'app_name' => config('app.front_name'),
|
||||||
];
|
];
|
||||||
|
|
||||||
return (new MailMessage)
|
return (new MailMessage)
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
namespace App\Notifications;
|
namespace App\Notifications;
|
||||||
|
|
||||||
use App\Filament\Resources\Members\MemberResource;
|
|
||||||
use App\Models\Member;
|
use App\Models\Member;
|
||||||
use App\Models\NotificationTemplate;
|
use App\Models\NotificationTemplate;
|
||||||
use App\Models\Package;
|
use App\Models\Package;
|
||||||
@@ -19,6 +18,7 @@ class MemberNewRequestAdminNotification extends Notification implements ShouldQu
|
|||||||
public readonly Member $member,
|
public readonly Member $member,
|
||||||
public readonly Package $package,
|
public readonly Package $package,
|
||||||
public readonly float $amount,
|
public readonly float $amount,
|
||||||
|
public readonly string $membershipUrl,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -44,7 +44,7 @@ class MemberNewRequestAdminNotification extends Notification implements ShouldQu
|
|||||||
])),
|
])),
|
||||||
'package_name' => $this->package->name,
|
'package_name' => $this->package->name,
|
||||||
'amount' => number_format($this->amount, 2, ',', ' '),
|
'amount' => number_format($this->amount, 2, ',', ' '),
|
||||||
'member_url' => MemberResource::getUrl('edit', ['record' => $this->member->id]),
|
'membership_url' => $this->membershipUrl,
|
||||||
'app_name' => config('app.name'),
|
'app_name' => config('app.name'),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ class MemberNewRequestMemberNotification extends Notification implements ShouldQ
|
|||||||
$vars = [
|
$vars = [
|
||||||
'member_name' => $this->member->full_name,
|
'member_name' => $this->member->full_name,
|
||||||
'package_name' => $this->package->name,
|
'package_name' => $this->package->name,
|
||||||
'app_name' => 'Le Retzien Libre',
|
'app_name' => config('app.front_name'),
|
||||||
];
|
];
|
||||||
|
|
||||||
return (new MailMessage)
|
return (new MailMessage)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ namespace App\Notifications;
|
|||||||
|
|
||||||
use App\Models\Membership;
|
use App\Models\Membership;
|
||||||
use App\Models\NotificationTemplate;
|
use App\Models\NotificationTemplate;
|
||||||
|
use Carbon\Carbon;
|
||||||
use Illuminate\Bus\Queueable;
|
use Illuminate\Bus\Queueable;
|
||||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
use Illuminate\Notifications\Messages\MailMessage;
|
use Illuminate\Notifications\Messages\MailMessage;
|
||||||
@@ -30,9 +31,9 @@ class MembershipValidatedNotification extends Notification implements ShouldQueu
|
|||||||
$vars = [
|
$vars = [
|
||||||
'member_name' => $this->membership->member->full_name,
|
'member_name' => $this->membership->member->full_name,
|
||||||
'package_name' => $this->membership->package->name,
|
'package_name' => $this->membership->package->name,
|
||||||
'start_date' => $this->membership->start_date?->format('d/m/Y') ?? '',
|
'start_date' => $this->membership->start_date ? Carbon::parse($this->membership->start_date)->format('d/m/Y') : '',
|
||||||
'end_date' => $this->membership->end_date?->format('d/m/Y') ?? '',
|
'end_date' => $this->membership->end_date ? Carbon::parse($this->membership->end_date)->format('d/m/Y') : '',
|
||||||
'app_name' => config('app.name'),
|
'app_name' => config('app.front_name'),
|
||||||
];
|
];
|
||||||
|
|
||||||
return (new MailMessage)
|
return (new MailMessage)
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ class SubscriptionExpiredPhase1 extends Notification implements ShouldQueue
|
|||||||
->subject($this->template->renderSubject($vars))
|
->subject($this->template->renderSubject($vars))
|
||||||
->view('notifications.mail-template', [
|
->view('notifications.mail-template', [
|
||||||
'body' => $this->template->renderBody($vars),
|
'body' => $this->template->renderBody($vars),
|
||||||
|
'appName' => config('app.front_name'),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,8 +5,11 @@ namespace App\Providers;
|
|||||||
use App\Listeners\PreprodMailInterceptor;
|
use App\Listeners\PreprodMailInterceptor;
|
||||||
use Illuminate\Http\Resources\Json\JsonResource;
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
use Illuminate\Mail\Events\MessageSending;
|
use Illuminate\Mail\Events\MessageSending;
|
||||||
|
use Illuminate\Queue\Events\JobFailed;
|
||||||
use Illuminate\Support\Facades\Event;
|
use Illuminate\Support\Facades\Event;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
use Illuminate\Support\ServiceProvider;
|
use Illuminate\Support\ServiceProvider;
|
||||||
|
use Queue;
|
||||||
|
|
||||||
class AppServiceProvider extends ServiceProvider
|
class AppServiceProvider extends ServiceProvider
|
||||||
{
|
{
|
||||||
@@ -26,5 +29,13 @@ class AppServiceProvider extends ServiceProvider
|
|||||||
JsonResource::withoutWrapping();
|
JsonResource::withoutWrapping();
|
||||||
|
|
||||||
Event::listen(MessageSending::class, PreprodMailInterceptor::class);
|
Event::listen(MessageSending::class, PreprodMailInterceptor::class);
|
||||||
|
|
||||||
|
// Log failed jobs
|
||||||
|
Queue::failing(function (JobFailed $event) {
|
||||||
|
Log::error('Job failed: '.$event->job->resolveName(), [
|
||||||
|
'exception' => $event->exception->getMessage(),
|
||||||
|
'trace' => $event->exception->getTraceAsString(),
|
||||||
|
]);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,16 +45,17 @@ class MemberService
|
|||||||
->firstOrFail();
|
->firstOrFail();
|
||||||
|
|
||||||
// Create a new membership
|
// Create a new membership
|
||||||
$member->memberships()->create([
|
$membership = $member->memberships()->create([
|
||||||
'status' => 'pending',
|
'status' => 'pending',
|
||||||
'package_id' => $package->id ?? null,
|
'package_id' => $package->id ?? null,
|
||||||
'amount' => $data['amount'],
|
'amount' => $data['amount'],
|
||||||
'payment_status' => 'unpaid',
|
'payment_status' => 'unpaid',
|
||||||
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$membershipUrl = route('filament.admin.resources.memberships.edit', ['record' => $membership->id]);
|
||||||
|
|
||||||
Notification::route('mail', config('app.admin_email'))
|
Notification::route('mail', config('app.admin_email'))
|
||||||
->notify(new MemberNewRequestAdminNotification($member, $package, (float) $data['amount']));
|
->notify(new MemberNewRequestAdminNotification($member, $package, (float) $data['amount'], $membershipUrl));
|
||||||
|
|
||||||
$member->notify(new MemberNewRequestMemberNotification($member, $package));
|
$member->notify(new MemberNewRequestMemberNotification($member, $package));
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ return [
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
'name' => env('APP_NAME', 'Laravel'),
|
'name' => env('APP_NAME', 'Laravel'),
|
||||||
|
'front_name' => env('FRONT_NAME', 'Laravel'),
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ class NotificationTemplateSeeder extends Seeder
|
|||||||
.'<strong>Formule :</strong> {package_name}<br>'
|
.'<strong>Formule :</strong> {package_name}<br>'
|
||||||
.'<strong>Montant :</strong> {amount} €'
|
.'<strong>Montant :</strong> {amount} €'
|
||||||
.'</p>'
|
.'</p>'
|
||||||
.'<p><a href="{member_url}" style="display:inline-block;padding:10px 20px;background:#f5a623;color:#000;font-weight:bold;text-decoration:none;border:3px solid #000;border-radius:6px;">Voir la fiche adhérent</a></p>',
|
.'<p><a href="{membership_url}" style="display:inline-block;padding:10px 20px;background:#f48fb1;color:#000;font-weight:bold;text-decoration:none;border:3px solid #000;border-radius:6px;">Voir la demande d\'adhésion</a></p>',
|
||||||
'variables' => [
|
'variables' => [
|
||||||
'member_name' => 'Nom complet du membre',
|
'member_name' => 'Nom complet du membre',
|
||||||
'member_email' => 'Adresse email du membre',
|
'member_email' => 'Adresse email du membre',
|
||||||
@@ -108,7 +108,7 @@ class NotificationTemplateSeeder extends Seeder
|
|||||||
'member_address' => 'Adresse postale du membre',
|
'member_address' => 'Adresse postale du membre',
|
||||||
'package_name' => 'Nom de la formule choisie',
|
'package_name' => 'Nom de la formule choisie',
|
||||||
'amount' => 'Montant de la cotisation',
|
'amount' => 'Montant de la cotisation',
|
||||||
'member_url' => 'URL de la fiche dans le back office',
|
'membership_url' => 'URL de la fiche d\'adhésion dans le back office',
|
||||||
'app_name' => 'Nom de l\'application',
|
'app_name' => 'Nom de l\'application',
|
||||||
],
|
],
|
||||||
'is_active' => true,
|
'is_active' => true,
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ return [
|
|||||||
'ispconfig_web' => 'Web Hosting',
|
'ispconfig_web' => 'Web Hosting',
|
||||||
'nextcloud' => 'NextCloud',
|
'nextcloud' => 'NextCloud',
|
||||||
'listmonk' => 'Listmonk',
|
'listmonk' => 'Listmonk',
|
||||||
|
'author_not_set' => 'Awaiting author',
|
||||||
],
|
],
|
||||||
|
|
||||||
'actions' => [
|
'actions' => [
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ return [
|
|||||||
'ispconfig_web' => 'Hébergements Web',
|
'ispconfig_web' => 'Hébergements Web',
|
||||||
'nextcloud' => 'NextCloud',
|
'nextcloud' => 'NextCloud',
|
||||||
'listmonk' => 'Listmonk',
|
'listmonk' => 'Listmonk',
|
||||||
|
'author_not_set' => 'En attente de l\'auteur',
|
||||||
],
|
],
|
||||||
|
|
||||||
'actions' => [
|
'actions' => [
|
||||||
|
|||||||
@@ -21,6 +21,16 @@ export default function Membership() {
|
|||||||
const [showFlashMessage, setFlashMessage] = useState(!!flash);
|
const [showFlashMessage, setFlashMessage] = useState(!!flash);
|
||||||
const [selectedPlan, setSelectedPlan] = useState(plans?.[0]?.identifier ?? null);
|
const [selectedPlan, setSelectedPlan] = useState(plans?.[0]?.identifier ?? null);
|
||||||
const [amount, setAmount] = useState(plans?.[0]?.price ?? 0);
|
const [amount, setAmount] = useState(plans?.[0]?.price ?? 0);
|
||||||
|
const [phone1Value, setPhone1Value] = useState('');
|
||||||
|
const [zipcodeValue, setZipcodeValue] = useState('');
|
||||||
|
|
||||||
|
const phone1LocalError = phone1Value.length > 0 && !/^\d{10}$/.test(phone1Value)
|
||||||
|
? 'Le numéro doit contenir exactement 10 chiffres.'
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const zipcodeLocalError = zipcodeValue.length > 0 && !/^\d{5}$/.test(zipcodeValue)
|
||||||
|
? 'Le code postal doit contenir exactement 5 chiffres.'
|
||||||
|
: null;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (plans && selectedPlan) {
|
if (plans && selectedPlan) {
|
||||||
@@ -98,8 +108,20 @@ export default function Membership() {
|
|||||||
|
|
||||||
<div className="grid gap-1">
|
<div className="grid gap-1">
|
||||||
<Label htmlFor="phone1">Téléphone*</Label>
|
<Label htmlFor="phone1">Téléphone*</Label>
|
||||||
<Input id="phone1" name="phone1" type="tel" required tabIndex={4} autoComplete="tel" placeholder="Votre numéro de téléphone" />
|
<Input
|
||||||
<InputError message={errors.phone1} />
|
id="phone1"
|
||||||
|
name="phone1"
|
||||||
|
type="tel"
|
||||||
|
required
|
||||||
|
tabIndex={4}
|
||||||
|
autoComplete="tel"
|
||||||
|
inputMode="numeric"
|
||||||
|
maxLength={10}
|
||||||
|
placeholder="0612345678"
|
||||||
|
value={phone1Value}
|
||||||
|
onChange={(e) => setPhone1Value(e.target.value.replace(/\D/g, '').slice(0, 10))}
|
||||||
|
/>
|
||||||
|
<InputError message={phone1LocalError ?? errors.phone1} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-1">
|
<div className="grid gap-1">
|
||||||
@@ -117,8 +139,20 @@ export default function Membership() {
|
|||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div className="grid gap-1">
|
<div className="grid gap-1">
|
||||||
<Label htmlFor="zipcode">Code postal*</Label>
|
<Label htmlFor="zipcode">Code postal*</Label>
|
||||||
<Input id="zipcode" name="zipcode" type="text" required tabIndex={7} autoComplete="postal-code" placeholder="Code postal" />
|
<Input
|
||||||
<InputError message={errors.zipcode} />
|
id="zipcode"
|
||||||
|
name="zipcode"
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
tabIndex={7}
|
||||||
|
autoComplete="postal-code"
|
||||||
|
inputMode="numeric"
|
||||||
|
maxLength={5}
|
||||||
|
placeholder="44000"
|
||||||
|
value={zipcodeValue}
|
||||||
|
onChange={(e) => setZipcodeValue(e.target.value.replace(/\D/g, '').slice(0, 5))}
|
||||||
|
/>
|
||||||
|
<InputError message={zipcodeLocalError ?? errors.zipcode} />
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-1">
|
<div className="grid gap-1">
|
||||||
<Label htmlFor="city">Ville*</Label>
|
<Label htmlFor="city">Ville*</Label>
|
||||||
|
|||||||
@@ -56,13 +56,13 @@
|
|||||||
<body>
|
<body>
|
||||||
<div class="wrapper">
|
<div class="wrapper">
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<h1>{{ config('app.name') }}</h1>
|
<h1>{{ $appName ?? config('app.name') }}</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
{!! $body !!}
|
{!! $body !!}
|
||||||
</div>
|
</div>
|
||||||
<div class="footer">
|
<div class="footer">
|
||||||
© {{ date('Y') }} {{ config('app.name') }}. Tous droits réservés.
|
© {{ date('Y') }} {{ $appName ?? config('app.name') }}. Tous droits réservés.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
Reference in New Issue
Block a user