feat(Membership, FAQ, Relance): update membership form (member type radio,retzien email field, calendar-year pricing), create FAQ page with accordion, wire renewal reminder notification
All checks were successful
Deploy Roxane to Preprod / deploy (push) Successful in 1m23s

This commit is contained in:
2026-07-27 16:30:48 +02:00
parent 87e23ea50f
commit 6d300ac79c
27 changed files with 1517 additions and 396 deletions

View File

@@ -9,6 +9,7 @@ use App\Models\ListmonkMember;
use App\Models\Member;
use App\Models\Membership;
use App\Models\Package;
use App\Notifications\MemberRenewalReminderNotification;
use Filament\Actions\Action;
use Filament\Forms\Components\DatePicker;
use Filament\Forms\Components\Select;
@@ -17,6 +18,7 @@ use Filament\Forms\Components\Toggle;
use Filament\Infolists\Components\RepeatableEntry;
use Filament\Infolists\Components\TextEntry;
use Filament\Infolists\Components\ViewEntry;
use Filament\Notifications\Notification;
use Filament\Schemas\Components\Grid;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Components\Tabs;
@@ -83,6 +85,11 @@ class MemberForm
->default('physical')
->required(),
Select::make('type_id')
->label(Member::getAttributeLabel('type_id'))
->relationship('type', 'name')
->default(null),
Select::make('group_id')
->label(Member::getAttributeLabel('group_id'))
->relationship('group', 'name')
@@ -98,6 +105,10 @@ class MemberForm
->email()
->required(),
TextInput::make('retzien_email')
->label(Member::getAttributeLabel('retzien_email'))
->email(),
TextInput::make('phone1')
->label(Member::getAttributeLabel('phone1'))
->tel(),
@@ -366,8 +377,28 @@ class MemberForm
->label(__('members.actions.send_renewal_mail'))
->icon('heroicon-o-envelope')
->color('primary')
->action(function () {
// Mail de relance à créer (Job)
->requiresConfirmation()
->modalHeading(__('members.actions.send_renewal_mail'))
->modalDescription('Êtes-vous sûr de vouloir envoyer un mail de relance à cet adhérent ?')
->action(function (Member $record) {
try {
$record->notify(new MemberRenewalReminderNotification($record));
Notification::make()
->title('Mail de relance envoyé')
->success()
->send();
} catch (\Throwable $e) {
\Log::error('Erreur envoi mail de relance', [
'member_id' => $record->id,
'error' => $e->getMessage(),
]);
Notification::make()
->title('Erreur lors de l\'envoi du mail')
->danger()
->send();
}
}),
])
->extraAttributes(['class' => 'sticky top-4 h-fit']),

View File

@@ -4,6 +4,7 @@ namespace App\Http\Controllers\Forms;
use App\Http\Controllers\Controller;
use App\Http\Requests\Forms\MembershipRequest;
use App\Models\MemberType;
use App\Models\Package;
use App\Models\Service;
use App\Services\MemberService;
@@ -18,23 +19,39 @@ class MembershipFormController extends Controller
public function create(): Response
{
$remainingMonths = 13 - now()->month;
$currentYear = now()->year;
$plans = Package::query()
->where('is_active', true)
->select('id', 'identifier', 'name', 'price', 'description')
->get()
->map(fn (Package $p) => [
'id' => $p->id,
'identifier' => $p->identifier,
'name' => $p->name,
'description' => $p->description,
'price' => $p->identifier === 'custom' ? $remainingMonths : (float) $p->price,
'months' => $p->identifier === 'custom' ? $remainingMonths : null,
]);
->map(function (Package $p) use ($remainingMonths, $currentYear) {
$months = match ($p->identifier) {
'custom' => $remainingMonths,
'one-year' => $remainingMonths + 12,
'two-years' => $remainingMonths + 24,
default => null,
};
return [
'id' => $p->id,
'identifier' => $p->identifier,
'name' => $p->name,
'description' => $p->description,
'price' => $months ?? (float) $p->price,
'months' => $months,
];
});
$memberTypes = MemberType::query()
->select('id', 'identifier', 'name')
->get();
return Inertia::render('forms/membership', [
'plans' => $plans,
'services' => Service::query()->select('name', 'description')->get(),
'member_types' => $memberTypes,
'current_year' => $currentYear,
'captcha_question' => $this->generateCaptcha(),
]);
}

View File

@@ -29,10 +29,12 @@ class MembershipRequest extends FormRequest
public function rules(): array
{
return [
'member_type' => 'required|string|exists:member_types,identifier',
'lastname' => 'required|string|max:255',
'firstname' => 'required|string|max:255',
'email' => 'required|email|max:255',
'company' => 'nullable|string|max:255',
'desired_retzien_email' => ['nullable', 'string', 'max:64', 'regex:/^[a-z0-9._-]+$/'],
'address' => 'required|string|max:255',
'zipcode' => ['required', 'digits:5'],
'city' => 'required|string|max:255',

View File

@@ -133,6 +133,11 @@ class Member extends Model
return $this->belongsTo(User::class);
}
public function type(): BelongsTo
{
return $this->belongsTo(MemberType::class);
}
public function group(): BelongsTo
{
return $this->belongsTo(MemberGroup::class);

17
app/Models/MemberType.php Normal file
View File

@@ -0,0 +1,17 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class MemberType extends Model
{
use SoftDeletes;
protected $fillable = [
'identifier',
'name',
'description',
];
}

View File

@@ -0,0 +1,51 @@
<?php
namespace App\Notifications;
use App\Models\Member;
use App\Models\NotificationTemplate;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
class MemberRenewalReminderNotification extends Notification implements ShouldQueue
{
use Queueable;
public function __construct(
public readonly Member $member,
) {}
/**
* @return array<int, string>
*/
public function via(object $notifiable): array
{
return ['mail'];
}
public function toMail(object $notifiable): MailMessage
{
$template = NotificationTemplate::findByIdentifier('member_renewal_reminder');
$vars = [
'member_name' => $this->member->full_name,
'app_name' => config('app.front_name'),
];
return (new MailMessage)
->subject($template->renderSubject($vars))
->view('notifications.mail-template', [
'body' => $template->renderBody($vars),
]);
}
/**
* @return array<string, mixed>
*/
public function toArray(object $notifiable): array
{
return [];
}
}

View File

@@ -5,6 +5,7 @@ namespace App\Services;
use App\Events\MemberRegistered;
use App\Models\Member;
use App\Models\MemberGroup;
use App\Models\MemberType;
use App\Models\Package;
use App\Notifications\MemberDeactivatedAdminNotification;
use App\Notifications\MemberDeactivatedMemberNotification;
@@ -22,11 +23,14 @@ class MemberService
// Check if the member already exists
$member = Member::where('email', $data['email'])->first();
$memberType = MemberType::where('identifier', $data['member_type'])->first();
if (! $member) {
// Create a new member
$member = new Member;
$member->status = 'pending';
$member->nature = 'physical';
$member->type_id = $memberType?->id;
$member->group_id = MemberGroup::where('identifier', 'website')->first()->id ?? null;
$member->lastname = $data['lastname'];
$member->firstname = $data['firstname'];
@@ -37,6 +41,11 @@ class MemberService
$member->city = $data['city'];
$member->country = 'FR';
$member->phone1 = $data['phone1'];
if (! empty($data['desired_retzien_email'])) {
$member->retzien_email = $data['desired_retzien_email'].'@retzien.fr';
}
$member->save();
}