feat(Mail template & Membership relationship)

This commit is contained in:
2026-02-16 14:16:52 +01:00
parent 45920c083e
commit 6e73c82787
37 changed files with 1374 additions and 169 deletions

View File

@@ -1,6 +1,6 @@
Tu es un agent IA senior specialise en Laravel, Filament, React et Tailwind.
Tu respectes STRICTEMENT les regles du fichier .ai-rules.md.
Tu respectes STRICTEMENT les regles du fichier .ai-rules.md et tu parcours bien le ficher .PROJECT_STRUCTURE.md pour avoir le contexte du projet.
Ton role :
- proposer des solutions simples et maintenables

View File

@@ -3,7 +3,13 @@
"allow": [
"mcp__laravel-boost__application-info",
"mcp__laravel-boost__database-schema",
"mcp__laravel-boost__list-routes"
"mcp__laravel-boost__list-routes",
"Bash(php artisan make:migration:*)",
"mcp__laravel-boost__search-docs",
"Bash(vendor/bin/pint:*)",
"Bash(php artisan test:*)",
"Bash(php artisan migrate:*)",
"Bash(php artisan make:filament-relation-manager:*)"
]
},
"enableAllProjectMcpServers": true,

View File

@@ -2,35 +2,13 @@
namespace App\Events;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use App\Models\Member;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class MemberRegistered
{
use Dispatchable, InteractsWithSockets, SerializesModels;
use Dispatchable, SerializesModels;
/**
* Create a new event instance.
*/
public function __construct()
{
//
}
/**
* Get the channels the event should broadcast on.
*
* @return array<int, \Illuminate\Broadcasting\Channel>
*/
public function broadcastOn(): array
{
return [
new PrivateChannel('channel-name'),
];
}
public function __construct(public readonly Member $member) {}
}

View File

@@ -2,35 +2,13 @@
namespace App\Events;
use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use App\Models\Member;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class MemberValidated
{
use Dispatchable, InteractsWithSockets, SerializesModels;
use Dispatchable, SerializesModels;
/**
* Create a new event instance.
*/
public function __construct()
{
//
}
/**
* Get the channels the event should broadcast on.
*
* @return array<int, \Illuminate\Broadcasting\Channel>
*/
public function broadcastOn(): array
{
return [
new PrivateChannel('channel-name'),
];
}
public function __construct(public readonly Member $member) {}
}

View File

@@ -36,7 +36,7 @@ class MemberResource extends Resource
public static function getRelations(): array
{
return [
//
RelationManagers\MembershipsRelationManager::class,
];
}
@@ -65,5 +65,4 @@ class MemberResource extends Resource
MemberCount::class,
];
}
}

View File

@@ -0,0 +1,78 @@
<?php
namespace App\Filament\Resources\Members\RelationManagers;
use App\Models\Membership;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\CreateAction;
use Filament\Actions\DeleteAction;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
use Filament\Resources\RelationManagers\RelationManager;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
class MembershipsRelationManager extends RelationManager
{
protected static string $relationship = 'memberships';
protected static ?string $title = 'Adhésions';
public function table(Table $table): Table
{
return $table
->recordTitleAttribute('start_date')
->columns([
TextColumn::make('start_date')
->label(Membership::getAttributeLabel('start_date'))
->date()
->sortable(),
TextColumn::make('end_date')
->label(Membership::getAttributeLabel('end_date'))
->date()
->sortable(),
TextColumn::make('status')
->label(Membership::getAttributeLabel('status'))
->formatStateUsing(fn (string $state) => Membership::getAttributeLabel($state))
->badge()
->color(fn (string $state): string => match ($state) {
'active' => 'success',
'expired' => 'danger',
'pending' => 'warning',
}),
TextColumn::make('package.name')
->label(Membership::getAttributeLabel('package_id')),
TextColumn::make('amount')
->label(Membership::getAttributeLabel('amount'))
->money('EUR')
->sortable(),
TextColumn::make('payment_status')
->label(Membership::getAttributeLabel('payment_status'))
->formatStateUsing(fn (string $state) => Membership::getAttributeLabel($state))
->badge()
->color(fn (string $state): string => match ($state) {
'paid' => 'success',
'unpaid' => 'danger',
'partial' => 'warning',
}),
TextColumn::make('created_at')
->label(Membership::getAttributeLabel('created_at'))
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
])
->defaultSort('start_date', 'desc')
->headerActions([
CreateAction::make(),
])
->recordActions([
EditAction::make(),
DeleteAction::make(),
])
->toolbarActions([
BulkActionGroup::make([
DeleteBulkAction::make(),
]),
]);
}
}

View File

@@ -0,0 +1,58 @@
<?php
namespace App\Filament\Resources\NotificationTemplates;
use App\Filament\Resources\NotificationTemplates\Pages\CreateNotificationTemplate;
use App\Filament\Resources\NotificationTemplates\Pages\EditNotificationTemplate;
use App\Filament\Resources\NotificationTemplates\Pages\ListNotificationTemplates;
use App\Filament\Resources\NotificationTemplates\Schemas\NotificationTemplateForm;
use App\Filament\Resources\NotificationTemplates\Tables\NotificationTemplatesTable;
use App\Models\NotificationTemplate;
use BackedEnum;
use Filament\Resources\Resource;
use Filament\Schemas\Schema;
use Filament\Support\Icons\Heroicon;
use Filament\Tables\Table;
class NotificationTemplateResource extends Resource
{
protected static ?string $model = NotificationTemplate::class;
protected static string|null|\UnitEnum $navigationGroup = 'Administration';
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedEnvelope;
public static function getModelLabel(): string
{
return NotificationTemplate::getAttributeLabel('singular_name');
}
public static function getPluralModelLabel(): string
{
return NotificationTemplate::getAttributeLabel('plural_name');
}
public static function form(Schema $schema): Schema
{
return NotificationTemplateForm::configure($schema);
}
public static function table(Table $table): Table
{
return NotificationTemplatesTable::configure($table);
}
public static function getRelations(): array
{
return [];
}
public static function getPages(): array
{
return [
'index' => ListNotificationTemplates::route('/'),
'create' => CreateNotificationTemplate::route('/create'),
'edit' => EditNotificationTemplate::route('/{record}/edit'),
];
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace App\Filament\Resources\NotificationTemplates\Pages;
use App\Filament\Resources\NotificationTemplates\NotificationTemplateResource;
use Filament\Resources\Pages\CreateRecord;
class CreateNotificationTemplate extends CreateRecord
{
protected static string $resource = NotificationTemplateResource::class;
}

View File

@@ -0,0 +1,23 @@
<?php
namespace App\Filament\Resources\NotificationTemplates\Pages;
use App\Filament\Resources\NotificationTemplates\NotificationTemplateResource;
use Filament\Actions\DeleteAction;
use Filament\Actions\ForceDeleteAction;
use Filament\Actions\RestoreAction;
use Filament\Resources\Pages\EditRecord;
class EditNotificationTemplate extends EditRecord
{
protected static string $resource = NotificationTemplateResource::class;
protected function getHeaderActions(): array
{
return [
DeleteAction::make(),
ForceDeleteAction::make(),
RestoreAction::make(),
];
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Filament\Resources\NotificationTemplates\Pages;
use App\Filament\Resources\NotificationTemplates\NotificationTemplateResource;
use Filament\Actions\CreateAction;
use Filament\Resources\Pages\ListRecords;
class ListNotificationTemplates extends ListRecords
{
protected static string $resource = NotificationTemplateResource::class;
protected function getHeaderActions(): array
{
return [
CreateAction::make(),
];
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace App\Filament\Resources\NotificationTemplates\Schemas;
use App\Models\NotificationTemplate;
use Filament\Forms\Components\RichEditor;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Schema;
class NotificationTemplateForm
{
public static function configure(Schema $schema): Schema
{
return $schema
->components([
Section::make(fn (?NotificationTemplate $record) => $record?->name ?? NotificationTemplate::getAttributeLabel('name'))
->afterHeader([
Toggle::make('is_active')
->label(NotificationTemplate::getAttributeLabel('is_active'))
->default(true),
])
->schema([
TextInput::make('name')
->label(NotificationTemplate::getAttributeLabel('name'))
->required(),
TextInput::make('identifier')
->label(NotificationTemplate::getAttributeLabel('identifier'))
->required()
->disabledOn('edit'),
TextInput::make('subject')
->label(NotificationTemplate::getAttributeLabel('subject'))
->required()
->helperText('Variables : {member_name}, {expiry_date}'),
RichEditor::make('body')
->label(NotificationTemplate::getAttributeLabel('body'))
->required()
->helperText('Variables : {member_name}, {expiry_date}')
->columnSpanFull(),
]),
]);
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace App\Filament\Resources\NotificationTemplates\Tables;
use App\Models\NotificationTemplate;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
use Filament\Actions\ForceDeleteBulkAction;
use Filament\Actions\RestoreBulkAction;
use Filament\Tables\Columns\IconColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
class NotificationTemplatesTable
{
public static function configure(Table $table): Table
{
return $table
->columns([
TextColumn::make('name')
->label(NotificationTemplate::getAttributeLabel('name'))
->searchable(),
TextColumn::make('identifier')
->label(NotificationTemplate::getAttributeLabel('identifier'))
->searchable(),
TextColumn::make('subject')
->label(NotificationTemplate::getAttributeLabel('subject'))
->searchable(),
IconColumn::make('is_active')
->label(NotificationTemplate::getAttributeLabel('is_active'))
->boolean(),
TextColumn::make('updated_at')
->dateTime()
->sortable()
->toggleable(isToggledHiddenByDefault: true),
])
->filters([])
->recordActions([
EditAction::make(),
])
->toolbarActions([
BulkActionGroup::make([
DeleteBulkAction::make(),
ForceDeleteBulkAction::make(),
RestoreBulkAction::make(),
]),
]);
}
}

View File

@@ -3,6 +3,7 @@
namespace App\Jobs;
use App\Models\Member;
use App\Models\NotificationTemplate;
use App\Notifications\SubscriptionExpiredPhase1;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
@@ -11,22 +12,21 @@ class SendSubscriptionExpiredPhase1Notifications implements ShouldQueue
{
use Queueable;
/**
* Create a new job instance.
*/
public function __construct()
{
}
public function __construct() {}
/**
* Execute the job.
*/
public function handle(): void
{
Member::isExpired()
->chunk(100, function ($members) {
$template = NotificationTemplate::findByIdentifier('subscription_expired_phase1');
if (! $template) {
return;
}
Member::query()
->whereHas('memberships', fn ($query) => $query->where('status', 'expired'))
->chunk(100, function ($members) use ($template) {
foreach ($members as $member) {
$member->notify(new SubscriptionExpiredPhase1());
$member->notify(new SubscriptionExpiredPhase1($template));
}
});
}

View File

@@ -1,26 +0,0 @@
<?php
namespace App\Listeners;
use App\Events\MemberValidated;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
class NotifiyMemberOfValidation
{
/**
* Create the event listener.
*/
public function __construct()
{
//
}
/**
* Handle the event.
*/
public function handle(MemberValidated $event): void
{
//
}
}

View File

@@ -1,30 +0,0 @@
<?php
namespace App\Listeners;
use App\Events\MemberRegistered;
use App\Models\User;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
class NotifyAdminForMembershipRequest
{
/**
* Create the event listener.
*/
public function __construct()
{
//
}
/**
* Handle the event.
*/
public function handle(MemberRegistered $event): void
{
$admin = User::where('name', 'SuperAdmin')->first();
$admin->notify(new AdminNewUserPending($event->user));
}
}

View File

@@ -2,7 +2,6 @@
namespace App\Models;
use App\Enums\IspconfigType;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
@@ -46,6 +45,7 @@ use Illuminate\Notifications\Notifiable;
* @property-read \Illuminate\Notifications\DatabaseNotificationCollection<int, \Illuminate\Notifications\DatabaseNotification> $notifications
* @property-read int|null $notifications_count
* @property-read \App\Models\User|null $user
*
* @method static \Database\Factories\MemberFactory factory($count = null, $state = [])
* @method static \Illuminate\Database\Eloquent\Builder<static>|Member newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Member newQuery()
@@ -75,11 +75,13 @@ use Illuminate\Notifications\Notifiable;
* @method static \Illuminate\Database\Eloquent\Builder<static>|Member whereUserId($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Member whereWebsiteUrl($value)
* @method static \Illuminate\Database\Eloquent\Builder<static>|Member whereZipcode($value)
*
* @mixin \Eloquent
*/
class Member extends Model
{
use HasFactory, Notifiable;
protected $fillable = [
'user_id',
'dolibarr_id',
@@ -100,7 +102,7 @@ class Member extends Model
'phone1',
'phone2',
'public_membership',
'website_url'
'website_url',
];
public static function getAttributeLabel(string $attribute): string
@@ -113,10 +115,11 @@ class Member extends Model
return "{$this->firstname} {$this->lastname}";
}
public function getRetzienEmailAttribute(): string
public function getRetzienEmailAttribute(): ?string
{
$emails = explode(';', $this->email);
return collect($emails)->filter(fn($email) => str_contains($email, '@retzien.fr'))->first();
$emails = explode(';', $this->email);
return collect($emails)->filter(fn ($email) => str_contains($email, '@retzien.fr'))->first();
}
public function user(): BelongsTo
@@ -152,6 +155,7 @@ class Member extends Model
public function hasService(string $serviceIdentifier): bool
{
$membership = $this->lastMembership();
return $membership->services()->where('identifier', $serviceIdentifier)->exists();
}
@@ -159,7 +163,7 @@ class Member extends Model
{
// Member ayant leur dernière adhésion non renouvellée de puis plus d'un mois
$lastMembership = $this->lastMembership();
return $lastMembership->status === 'expired' || $lastMembership->created_at->addMonths(1) < now();
}
}

View File

@@ -0,0 +1,64 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
class NotificationTemplate extends Model
{
use HasFactory, SoftDeletes;
protected $fillable = [
'identifier',
'name',
'subject',
'body',
'variables',
'is_active',
];
/**
* @return array<string, string>
*/
protected function casts(): array
{
return [
'variables' => 'array',
'is_active' => 'boolean',
];
}
public static function getAttributeLabel(string $attribute): string
{
return __('notification_templates.fields.'.$attribute);
}
public static function findByIdentifier(string $identifier): ?self
{
return self::query()
->where('identifier', $identifier)
->where('is_active', true)
->first();
}
public function renderSubject(array $vars): string
{
return $this->replacePlaceholders($this->subject, $vars);
}
public function renderBody(array $vars): string
{
return $this->replacePlaceholders($this->body, $vars);
}
private function replacePlaceholders(string $text, array $vars): string
{
foreach ($vars as $key => $value) {
$text = str_replace('{'.$key.'}', (string) $value, $text);
}
return $text;
}
}

View File

@@ -2,6 +2,7 @@
namespace App\Notifications;
use App\Models\NotificationTemplate;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
@@ -11,17 +12,9 @@ class SubscriptionExpiredPhase1 extends Notification implements ShouldQueue
{
use Queueable;
/**
* Create a new notification instance.
*/
public function __construct()
{
//
}
public function __construct(public readonly NotificationTemplate $template) {}
/**
* Get the notification's delivery channels.
*
* @return array<int, string>
*/
public function via(object $notifiable): array
@@ -29,30 +22,27 @@ class SubscriptionExpiredPhase1 extends Notification implements ShouldQueue
return ['mail'];
}
/**
* Get the mail representation of the notification.
*/
public function toMail(object $notifiable): MailMessage
{
//@todo: créer template générique + espace dans le BO pour alimenter les texte
$lastMembership = $notifiable->memberships()->latest()->first();
$vars = [
'member_name' => $notifiable->full_name,
'expiry_date' => $lastMembership?->end_date ?? '',
];
return (new MailMessage)
->subject('Votre adhésion est expirée')
->greeting('Bonjour ' . $notifiable->name)
->line('Votre adhésion est arrivée à expiration.')
->line('Pour continuer à profiter nos services, merci de le renouveler.')
->action('Renouveler mon adhésion', url('/devenir-membre'))
->line('Merci pour votre confiance.');
->subject($this->template->renderSubject($vars))
->view('notifications.mail-template', [
'body' => $this->template->renderBody($vars),
]);
}
/**
* Get the array representation of the notification.
*
* @return array<string, mixed>
*/
public function toArray(object $notifiable): array
{
return [
//
];
return [];
}
}

View File

@@ -0,0 +1,69 @@
<?php
declare(strict_types=1);
namespace App\Policies;
use App\Models\NotificationTemplate;
use Illuminate\Auth\Access\HandlesAuthorization;
use Illuminate\Foundation\Auth\User as AuthUser;
class NotificationTemplatePolicy
{
use HandlesAuthorization;
public function viewAny(AuthUser $authUser): bool
{
return $authUser->can('ViewAny:NotificationTemplate');
}
public function view(AuthUser $authUser, NotificationTemplate $notificationTemplate): bool
{
return $authUser->can('View:NotificationTemplate');
}
public function create(AuthUser $authUser): bool
{
return $authUser->can('Create:NotificationTemplate');
}
public function update(AuthUser $authUser, NotificationTemplate $notificationTemplate): bool
{
return $authUser->can('Update:NotificationTemplate');
}
public function delete(AuthUser $authUser, NotificationTemplate $notificationTemplate): bool
{
return $authUser->can('Delete:NotificationTemplate');
}
public function restore(AuthUser $authUser, NotificationTemplate $notificationTemplate): bool
{
return $authUser->can('Restore:NotificationTemplate');
}
public function forceDelete(AuthUser $authUser, NotificationTemplate $notificationTemplate): bool
{
return $authUser->can('ForceDelete:NotificationTemplate');
}
public function forceDeleteAny(AuthUser $authUser): bool
{
return $authUser->can('ForceDeleteAny:NotificationTemplate');
}
public function restoreAny(AuthUser $authUser): bool
{
return $authUser->can('RestoreAny:NotificationTemplate');
}
public function replicate(AuthUser $authUser, NotificationTemplate $notificationTemplate): bool
{
return $authUser->can('Replicate:NotificationTemplate');
}
public function reorder(AuthUser $authUser): bool
{
return $authUser->can('Reorder:NotificationTemplate');
}
}

View File

@@ -11,17 +11,15 @@ class MemberService
{
/**
* Register a new member.
* @param array $data
* @return Member
*/
public function registerNewMember(array $data): Member
{
// Check if the member already exists
$member = Member::where('email', $data['email'])->first();
if (!$member) {
if (! $member) {
// Create a new member
$member = new Member();
$member = new Member;
$member->status = 'pending';
$member->nature = 'physical';
$member->group_id = MemberGroup::where('identifier', 'website')->first()->id ?? null;
@@ -50,10 +48,7 @@ class MemberService
]);
// Notify Admin
$admin = Member::where('role', 'admin')->first();
event(new MemberRegistered($admin));
event(new MemberRegistered($member));
return $member;
}

View File

@@ -0,0 +1,33 @@
<?php
namespace Database\Factories;
use App\Models\NotificationTemplate;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends Factory<NotificationTemplate>
*/
class NotificationTemplateFactory extends Factory
{
protected $model = NotificationTemplate::class;
public function definition(): array
{
return [
'identifier' => $this->faker->unique()->slug(2),
'name' => $this->faker->sentence(3),
'subject' => $this->faker->sentence(),
'body' => $this->faker->paragraph(),
'variables' => ['name' => 'Nom'],
'is_active' => true,
];
}
public function inactive(): static
{
return $this->state(fn (array $attributes) => [
'is_active' => false,
]);
}
}

View File

@@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('notification_templates', function (Blueprint $table) {
$table->id();
$table->string('identifier')->unique();
$table->string('name');
$table->string('subject');
$table->longText('body');
$table->json('variables')->nullable();
$table->boolean('is_active')->default(true);
$table->timestamps();
$table->softDeletes();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('notification_templates');
}
};

View File

@@ -3,10 +3,9 @@
namespace Database\Seeders;
use App\Models\MemberGroup;
use App\Models\Package;
use App\Models\Service;
use App\Models\User;
use App\Models\Package;
// use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
@@ -36,11 +35,11 @@ class DatabaseSeeder extends Seeder
]);
$websiteGroup = MemberGroup::updateOrCreate([
'name' => 'Site Web'
'name' => 'Site Web',
],
[
'identifier' => 'website',
'description' => 'Groupe d\'utilisateurs provenant du site web.'
'description' => 'Groupe d\'utilisateurs provenant du site web.',
]);
// Subscription packages
@@ -49,20 +48,20 @@ class DatabaseSeeder extends Seeder
'identifier' => 'custom',
'name' => 'Sur-mesure',
'description' => 'Calcul du nombre de mois restant dans l\'année',
'price' => '1.00'
'price' => '1.00',
],
[
'identifier' => 'one-year',
'name' => 'Un an',
'description' => '12 mois à compter de la date de validation de l\'adhésion du membre',
'price' => '12.00'
'price' => '12.00',
],
[
'identifier' => 'two-years',
'name' => 'Deux ans',
'description' => '24 mois à compter de la date de validation de l\'adhésion du membre',
'price' => '24.00'
]
'price' => '24.00',
],
];
foreach ($packages as $package) {
@@ -112,7 +111,7 @@ class DatabaseSeeder extends Seeder
'description' => 'Service d\'hébergement web',
'url' => '#',
'icon' => 'database',
]
],
];
foreach ($services as $service) {
@@ -126,6 +125,9 @@ class DatabaseSeeder extends Seeder
]);
}
// Notification templates
$this->call(NotificationTemplateSeeder::class);
// JaneDoe
$userTest = User::updateOrCreate([
'name' => 'JaneDoe',

View File

@@ -0,0 +1,29 @@
<?php
namespace Database\Seeders;
use App\Models\NotificationTemplate;
use Illuminate\Database\Seeder;
class NotificationTemplateSeeder extends Seeder
{
public function run(): void
{
NotificationTemplate::updateOrCreate(
['identifier' => 'subscription_expired_phase1'],
[
'name' => 'Adhésion expirée - Phase 1',
'subject' => 'Votre adhésion est expirée',
'body' => '<p>Bonjour {member_name},</p>'
.'<p>Votre adhésion est arrivée à expiration le {expiry_date}.</p>'
.'<p>Pour continuer à profiter de nos services, merci de la renouveler.</p>'
.'<p>Merci pour votre confiance.</p>',
'variables' => [
'member_name' => 'Nom complet du membre',
'expiry_date' => 'Date de fin d\'adhésion',
],
'is_active' => true,
]
);
}
}

View File

@@ -0,0 +1,14 @@
<?php
return [
'fields' => [
'singular_name' => 'Template Mail',
'plural_name' => 'Templates Mail',
'identifier' => 'Identifiant',
'name' => 'Nom',
'subject' => 'Objet',
'body' => 'Contenu',
'variables' => 'Variables disponibles',
'is_active' => 'Actif',
],
];

View File

@@ -0,0 +1,83 @@
import { queryParams, type RouteQueryOptions, type RouteDefinition, type RouteFormDefinition } from './../../../../../../wayfinder'
/**
* @see \App\Filament\Resources\NotificationTemplates\Pages\CreateNotificationTemplate::__invoke
* @see app/Filament/Resources/NotificationTemplates/Pages/CreateNotificationTemplate.php:7
* @route '/admin/notification-templates/create'
*/
const CreateNotificationTemplate = (options?: RouteQueryOptions): RouteDefinition<'get'> => ({
url: CreateNotificationTemplate.url(options),
method: 'get',
})
CreateNotificationTemplate.definition = {
methods: ["get","head"],
url: '/admin/notification-templates/create',
} satisfies RouteDefinition<["get","head"]>
/**
* @see \App\Filament\Resources\NotificationTemplates\Pages\CreateNotificationTemplate::__invoke
* @see app/Filament/Resources/NotificationTemplates/Pages/CreateNotificationTemplate.php:7
* @route '/admin/notification-templates/create'
*/
CreateNotificationTemplate.url = (options?: RouteQueryOptions) => {
return CreateNotificationTemplate.definition.url + queryParams(options)
}
/**
* @see \App\Filament\Resources\NotificationTemplates\Pages\CreateNotificationTemplate::__invoke
* @see app/Filament/Resources/NotificationTemplates/Pages/CreateNotificationTemplate.php:7
* @route '/admin/notification-templates/create'
*/
CreateNotificationTemplate.get = (options?: RouteQueryOptions): RouteDefinition<'get'> => ({
url: CreateNotificationTemplate.url(options),
method: 'get',
})
/**
* @see \App\Filament\Resources\NotificationTemplates\Pages\CreateNotificationTemplate::__invoke
* @see app/Filament/Resources/NotificationTemplates/Pages/CreateNotificationTemplate.php:7
* @route '/admin/notification-templates/create'
*/
CreateNotificationTemplate.head = (options?: RouteQueryOptions): RouteDefinition<'head'> => ({
url: CreateNotificationTemplate.url(options),
method: 'head',
})
/**
* @see \App\Filament\Resources\NotificationTemplates\Pages\CreateNotificationTemplate::__invoke
* @see app/Filament/Resources/NotificationTemplates/Pages/CreateNotificationTemplate.php:7
* @route '/admin/notification-templates/create'
*/
const CreateNotificationTemplateForm = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({
action: CreateNotificationTemplate.url(options),
method: 'get',
})
/**
* @see \App\Filament\Resources\NotificationTemplates\Pages\CreateNotificationTemplate::__invoke
* @see app/Filament/Resources/NotificationTemplates/Pages/CreateNotificationTemplate.php:7
* @route '/admin/notification-templates/create'
*/
CreateNotificationTemplateForm.get = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({
action: CreateNotificationTemplate.url(options),
method: 'get',
})
/**
* @see \App\Filament\Resources\NotificationTemplates\Pages\CreateNotificationTemplate::__invoke
* @see app/Filament/Resources/NotificationTemplates/Pages/CreateNotificationTemplate.php:7
* @route '/admin/notification-templates/create'
*/
CreateNotificationTemplateForm.head = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({
action: CreateNotificationTemplate.url({
[options?.mergeQuery ? 'mergeQuery' : 'query']: {
_method: 'HEAD',
...(options?.query ?? options?.mergeQuery ?? {}),
}
}),
method: 'get',
})
CreateNotificationTemplate.form = CreateNotificationTemplateForm
export default CreateNotificationTemplate

View File

@@ -0,0 +1,101 @@
import { queryParams, type RouteQueryOptions, type RouteDefinition, type RouteFormDefinition, applyUrlDefaults } from './../../../../../../wayfinder'
/**
* @see \App\Filament\Resources\NotificationTemplates\Pages\EditNotificationTemplate::__invoke
* @see app/Filament/Resources/NotificationTemplates/Pages/EditNotificationTemplate.php:7
* @route '/admin/notification-templates/{record}/edit'
*/
const EditNotificationTemplate = (args: { record: string | number } | [record: string | number ] | string | number, options?: RouteQueryOptions): RouteDefinition<'get'> => ({
url: EditNotificationTemplate.url(args, options),
method: 'get',
})
EditNotificationTemplate.definition = {
methods: ["get","head"],
url: '/admin/notification-templates/{record}/edit',
} satisfies RouteDefinition<["get","head"]>
/**
* @see \App\Filament\Resources\NotificationTemplates\Pages\EditNotificationTemplate::__invoke
* @see app/Filament/Resources/NotificationTemplates/Pages/EditNotificationTemplate.php:7
* @route '/admin/notification-templates/{record}/edit'
*/
EditNotificationTemplate.url = (args: { record: string | number } | [record: string | number ] | string | number, options?: RouteQueryOptions) => {
if (typeof args === 'string' || typeof args === 'number') {
args = { record: args }
}
if (Array.isArray(args)) {
args = {
record: args[0],
}
}
args = applyUrlDefaults(args)
const parsedArgs = {
record: args.record,
}
return EditNotificationTemplate.definition.url
.replace('{record}', parsedArgs.record.toString())
.replace(/\/+$/, '') + queryParams(options)
}
/**
* @see \App\Filament\Resources\NotificationTemplates\Pages\EditNotificationTemplate::__invoke
* @see app/Filament/Resources/NotificationTemplates/Pages/EditNotificationTemplate.php:7
* @route '/admin/notification-templates/{record}/edit'
*/
EditNotificationTemplate.get = (args: { record: string | number } | [record: string | number ] | string | number, options?: RouteQueryOptions): RouteDefinition<'get'> => ({
url: EditNotificationTemplate.url(args, options),
method: 'get',
})
/**
* @see \App\Filament\Resources\NotificationTemplates\Pages\EditNotificationTemplate::__invoke
* @see app/Filament/Resources/NotificationTemplates/Pages/EditNotificationTemplate.php:7
* @route '/admin/notification-templates/{record}/edit'
*/
EditNotificationTemplate.head = (args: { record: string | number } | [record: string | number ] | string | number, options?: RouteQueryOptions): RouteDefinition<'head'> => ({
url: EditNotificationTemplate.url(args, options),
method: 'head',
})
/**
* @see \App\Filament\Resources\NotificationTemplates\Pages\EditNotificationTemplate::__invoke
* @see app/Filament/Resources/NotificationTemplates/Pages/EditNotificationTemplate.php:7
* @route '/admin/notification-templates/{record}/edit'
*/
const EditNotificationTemplateForm = (args: { record: string | number } | [record: string | number ] | string | number, options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({
action: EditNotificationTemplate.url(args, options),
method: 'get',
})
/**
* @see \App\Filament\Resources\NotificationTemplates\Pages\EditNotificationTemplate::__invoke
* @see app/Filament/Resources/NotificationTemplates/Pages/EditNotificationTemplate.php:7
* @route '/admin/notification-templates/{record}/edit'
*/
EditNotificationTemplateForm.get = (args: { record: string | number } | [record: string | number ] | string | number, options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({
action: EditNotificationTemplate.url(args, options),
method: 'get',
})
/**
* @see \App\Filament\Resources\NotificationTemplates\Pages\EditNotificationTemplate::__invoke
* @see app/Filament/Resources/NotificationTemplates/Pages/EditNotificationTemplate.php:7
* @route '/admin/notification-templates/{record}/edit'
*/
EditNotificationTemplateForm.head = (args: { record: string | number } | [record: string | number ] | string | number, options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({
action: EditNotificationTemplate.url(args, {
[options?.mergeQuery ? 'mergeQuery' : 'query']: {
_method: 'HEAD',
...(options?.query ?? options?.mergeQuery ?? {}),
}
}),
method: 'get',
})
EditNotificationTemplate.form = EditNotificationTemplateForm
export default EditNotificationTemplate

View File

@@ -0,0 +1,83 @@
import { queryParams, type RouteQueryOptions, type RouteDefinition, type RouteFormDefinition } from './../../../../../../wayfinder'
/**
* @see \App\Filament\Resources\NotificationTemplates\Pages\ListNotificationTemplates::__invoke
* @see app/Filament/Resources/NotificationTemplates/Pages/ListNotificationTemplates.php:7
* @route '/admin/notification-templates'
*/
const ListNotificationTemplates = (options?: RouteQueryOptions): RouteDefinition<'get'> => ({
url: ListNotificationTemplates.url(options),
method: 'get',
})
ListNotificationTemplates.definition = {
methods: ["get","head"],
url: '/admin/notification-templates',
} satisfies RouteDefinition<["get","head"]>
/**
* @see \App\Filament\Resources\NotificationTemplates\Pages\ListNotificationTemplates::__invoke
* @see app/Filament/Resources/NotificationTemplates/Pages/ListNotificationTemplates.php:7
* @route '/admin/notification-templates'
*/
ListNotificationTemplates.url = (options?: RouteQueryOptions) => {
return ListNotificationTemplates.definition.url + queryParams(options)
}
/**
* @see \App\Filament\Resources\NotificationTemplates\Pages\ListNotificationTemplates::__invoke
* @see app/Filament/Resources/NotificationTemplates/Pages/ListNotificationTemplates.php:7
* @route '/admin/notification-templates'
*/
ListNotificationTemplates.get = (options?: RouteQueryOptions): RouteDefinition<'get'> => ({
url: ListNotificationTemplates.url(options),
method: 'get',
})
/**
* @see \App\Filament\Resources\NotificationTemplates\Pages\ListNotificationTemplates::__invoke
* @see app/Filament/Resources/NotificationTemplates/Pages/ListNotificationTemplates.php:7
* @route '/admin/notification-templates'
*/
ListNotificationTemplates.head = (options?: RouteQueryOptions): RouteDefinition<'head'> => ({
url: ListNotificationTemplates.url(options),
method: 'head',
})
/**
* @see \App\Filament\Resources\NotificationTemplates\Pages\ListNotificationTemplates::__invoke
* @see app/Filament/Resources/NotificationTemplates/Pages/ListNotificationTemplates.php:7
* @route '/admin/notification-templates'
*/
const ListNotificationTemplatesForm = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({
action: ListNotificationTemplates.url(options),
method: 'get',
})
/**
* @see \App\Filament\Resources\NotificationTemplates\Pages\ListNotificationTemplates::__invoke
* @see app/Filament/Resources/NotificationTemplates/Pages/ListNotificationTemplates.php:7
* @route '/admin/notification-templates'
*/
ListNotificationTemplatesForm.get = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({
action: ListNotificationTemplates.url(options),
method: 'get',
})
/**
* @see \App\Filament\Resources\NotificationTemplates\Pages\ListNotificationTemplates::__invoke
* @see app/Filament/Resources/NotificationTemplates/Pages/ListNotificationTemplates.php:7
* @route '/admin/notification-templates'
*/
ListNotificationTemplatesForm.head = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({
action: ListNotificationTemplates.url({
[options?.mergeQuery ? 'mergeQuery' : 'query']: {
_method: 'HEAD',
...(options?.query ?? options?.mergeQuery ?? {}),
}
}),
method: 'get',
})
ListNotificationTemplates.form = ListNotificationTemplatesForm
export default ListNotificationTemplates

View File

@@ -0,0 +1,11 @@
import ListNotificationTemplates from './ListNotificationTemplates'
import CreateNotificationTemplate from './CreateNotificationTemplate'
import EditNotificationTemplate from './EditNotificationTemplate'
const Pages = {
ListNotificationTemplates: Object.assign(ListNotificationTemplates, ListNotificationTemplates),
CreateNotificationTemplate: Object.assign(CreateNotificationTemplate, CreateNotificationTemplate),
EditNotificationTemplate: Object.assign(EditNotificationTemplate, EditNotificationTemplate),
}
export default Pages

View File

@@ -0,0 +1,7 @@
import Pages from './Pages'
const NotificationTemplates = {
Pages: Object.assign(Pages, Pages),
}
export default NotificationTemplates

View File

@@ -1,6 +1,7 @@
import MemberGroups from './MemberGroups'
import Members from './Members'
import Memberships from './Memberships'
import NotificationTemplates from './NotificationTemplates'
import Packages from './Packages'
import Services from './Services'
import Users from './Users'
@@ -9,6 +10,7 @@ const Resources = {
MemberGroups: Object.assign(MemberGroups, MemberGroups),
Members: Object.assign(Members, Members),
Memberships: Object.assign(Memberships, Memberships),
NotificationTemplates: Object.assign(NotificationTemplates, NotificationTemplates),
Packages: Object.assign(Packages, Packages),
Services: Object.assign(Services, Services),
Users: Object.assign(Users, Users),

View File

@@ -0,0 +1,57 @@
import { queryParams, type RouteQueryOptions, type RouteDefinition, type RouteFormDefinition } from './../../wayfinder'
/**
* @see vendor/laravel/boost/src/BoostServiceProvider.php:117
* @route '/_boost/browser-logs'
*/
export const browserLogs = (options?: RouteQueryOptions): RouteDefinition<'post'> => ({
url: browserLogs.url(options),
method: 'post',
})
browserLogs.definition = {
methods: ["post"],
url: '/_boost/browser-logs',
} satisfies RouteDefinition<["post"]>
/**
* @see vendor/laravel/boost/src/BoostServiceProvider.php:117
* @route '/_boost/browser-logs'
*/
browserLogs.url = (options?: RouteQueryOptions) => {
return browserLogs.definition.url + queryParams(options)
}
/**
* @see vendor/laravel/boost/src/BoostServiceProvider.php:117
* @route '/_boost/browser-logs'
*/
browserLogs.post = (options?: RouteQueryOptions): RouteDefinition<'post'> => ({
url: browserLogs.url(options),
method: 'post',
})
/**
* @see vendor/laravel/boost/src/BoostServiceProvider.php:117
* @route '/_boost/browser-logs'
*/
const browserLogsForm = (options?: RouteQueryOptions): RouteFormDefinition<'post'> => ({
action: browserLogs.url(options),
method: 'post',
})
/**
* @see vendor/laravel/boost/src/BoostServiceProvider.php:117
* @route '/_boost/browser-logs'
*/
browserLogsForm.post = (options?: RouteQueryOptions): RouteFormDefinition<'post'> => ({
action: browserLogs.url(options),
method: 'post',
})
browserLogs.form = browserLogsForm
const boost = {
browserLogs: Object.assign(browserLogs, browserLogs),
}
export default boost

View File

@@ -1,6 +1,7 @@
import memberGroups from './member-groups'
import members from './members'
import memberships from './memberships'
import notificationTemplates from './notification-templates'
import packages from './packages'
import services from './services'
import users from './users'
@@ -10,6 +11,7 @@ const resources = {
memberGroups: Object.assign(memberGroups, memberGroups),
members: Object.assign(members, members),
memberships: Object.assign(memberships, memberships),
notificationTemplates: Object.assign(notificationTemplates, notificationTemplates),
packages: Object.assign(packages, packages),
services: Object.assign(services, services),
users: Object.assign(users, users),

View File

@@ -0,0 +1,269 @@
import { queryParams, type RouteQueryOptions, type RouteDefinition, type RouteFormDefinition, applyUrlDefaults } from './../../../../../wayfinder'
/**
* @see \App\Filament\Resources\NotificationTemplates\Pages\ListNotificationTemplates::__invoke
* @see app/Filament/Resources/NotificationTemplates/Pages/ListNotificationTemplates.php:7
* @route '/admin/notification-templates'
*/
export const index = (options?: RouteQueryOptions): RouteDefinition<'get'> => ({
url: index.url(options),
method: 'get',
})
index.definition = {
methods: ["get","head"],
url: '/admin/notification-templates',
} satisfies RouteDefinition<["get","head"]>
/**
* @see \App\Filament\Resources\NotificationTemplates\Pages\ListNotificationTemplates::__invoke
* @see app/Filament/Resources/NotificationTemplates/Pages/ListNotificationTemplates.php:7
* @route '/admin/notification-templates'
*/
index.url = (options?: RouteQueryOptions) => {
return index.definition.url + queryParams(options)
}
/**
* @see \App\Filament\Resources\NotificationTemplates\Pages\ListNotificationTemplates::__invoke
* @see app/Filament/Resources/NotificationTemplates/Pages/ListNotificationTemplates.php:7
* @route '/admin/notification-templates'
*/
index.get = (options?: RouteQueryOptions): RouteDefinition<'get'> => ({
url: index.url(options),
method: 'get',
})
/**
* @see \App\Filament\Resources\NotificationTemplates\Pages\ListNotificationTemplates::__invoke
* @see app/Filament/Resources/NotificationTemplates/Pages/ListNotificationTemplates.php:7
* @route '/admin/notification-templates'
*/
index.head = (options?: RouteQueryOptions): RouteDefinition<'head'> => ({
url: index.url(options),
method: 'head',
})
/**
* @see \App\Filament\Resources\NotificationTemplates\Pages\ListNotificationTemplates::__invoke
* @see app/Filament/Resources/NotificationTemplates/Pages/ListNotificationTemplates.php:7
* @route '/admin/notification-templates'
*/
const indexForm = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({
action: index.url(options),
method: 'get',
})
/**
* @see \App\Filament\Resources\NotificationTemplates\Pages\ListNotificationTemplates::__invoke
* @see app/Filament/Resources/NotificationTemplates/Pages/ListNotificationTemplates.php:7
* @route '/admin/notification-templates'
*/
indexForm.get = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({
action: index.url(options),
method: 'get',
})
/**
* @see \App\Filament\Resources\NotificationTemplates\Pages\ListNotificationTemplates::__invoke
* @see app/Filament/Resources/NotificationTemplates/Pages/ListNotificationTemplates.php:7
* @route '/admin/notification-templates'
*/
indexForm.head = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({
action: index.url({
[options?.mergeQuery ? 'mergeQuery' : 'query']: {
_method: 'HEAD',
...(options?.query ?? options?.mergeQuery ?? {}),
}
}),
method: 'get',
})
index.form = indexForm
/**
* @see \App\Filament\Resources\NotificationTemplates\Pages\CreateNotificationTemplate::__invoke
* @see app/Filament/Resources/NotificationTemplates/Pages/CreateNotificationTemplate.php:7
* @route '/admin/notification-templates/create'
*/
export const create = (options?: RouteQueryOptions): RouteDefinition<'get'> => ({
url: create.url(options),
method: 'get',
})
create.definition = {
methods: ["get","head"],
url: '/admin/notification-templates/create',
} satisfies RouteDefinition<["get","head"]>
/**
* @see \App\Filament\Resources\NotificationTemplates\Pages\CreateNotificationTemplate::__invoke
* @see app/Filament/Resources/NotificationTemplates/Pages/CreateNotificationTemplate.php:7
* @route '/admin/notification-templates/create'
*/
create.url = (options?: RouteQueryOptions) => {
return create.definition.url + queryParams(options)
}
/**
* @see \App\Filament\Resources\NotificationTemplates\Pages\CreateNotificationTemplate::__invoke
* @see app/Filament/Resources/NotificationTemplates/Pages/CreateNotificationTemplate.php:7
* @route '/admin/notification-templates/create'
*/
create.get = (options?: RouteQueryOptions): RouteDefinition<'get'> => ({
url: create.url(options),
method: 'get',
})
/**
* @see \App\Filament\Resources\NotificationTemplates\Pages\CreateNotificationTemplate::__invoke
* @see app/Filament/Resources/NotificationTemplates/Pages/CreateNotificationTemplate.php:7
* @route '/admin/notification-templates/create'
*/
create.head = (options?: RouteQueryOptions): RouteDefinition<'head'> => ({
url: create.url(options),
method: 'head',
})
/**
* @see \App\Filament\Resources\NotificationTemplates\Pages\CreateNotificationTemplate::__invoke
* @see app/Filament/Resources/NotificationTemplates/Pages/CreateNotificationTemplate.php:7
* @route '/admin/notification-templates/create'
*/
const createForm = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({
action: create.url(options),
method: 'get',
})
/**
* @see \App\Filament\Resources\NotificationTemplates\Pages\CreateNotificationTemplate::__invoke
* @see app/Filament/Resources/NotificationTemplates/Pages/CreateNotificationTemplate.php:7
* @route '/admin/notification-templates/create'
*/
createForm.get = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({
action: create.url(options),
method: 'get',
})
/**
* @see \App\Filament\Resources\NotificationTemplates\Pages\CreateNotificationTemplate::__invoke
* @see app/Filament/Resources/NotificationTemplates/Pages/CreateNotificationTemplate.php:7
* @route '/admin/notification-templates/create'
*/
createForm.head = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({
action: create.url({
[options?.mergeQuery ? 'mergeQuery' : 'query']: {
_method: 'HEAD',
...(options?.query ?? options?.mergeQuery ?? {}),
}
}),
method: 'get',
})
create.form = createForm
/**
* @see \App\Filament\Resources\NotificationTemplates\Pages\EditNotificationTemplate::__invoke
* @see app/Filament/Resources/NotificationTemplates/Pages/EditNotificationTemplate.php:7
* @route '/admin/notification-templates/{record}/edit'
*/
export const edit = (args: { record: string | number } | [record: string | number ] | string | number, options?: RouteQueryOptions): RouteDefinition<'get'> => ({
url: edit.url(args, options),
method: 'get',
})
edit.definition = {
methods: ["get","head"],
url: '/admin/notification-templates/{record}/edit',
} satisfies RouteDefinition<["get","head"]>
/**
* @see \App\Filament\Resources\NotificationTemplates\Pages\EditNotificationTemplate::__invoke
* @see app/Filament/Resources/NotificationTemplates/Pages/EditNotificationTemplate.php:7
* @route '/admin/notification-templates/{record}/edit'
*/
edit.url = (args: { record: string | number } | [record: string | number ] | string | number, options?: RouteQueryOptions) => {
if (typeof args === 'string' || typeof args === 'number') {
args = { record: args }
}
if (Array.isArray(args)) {
args = {
record: args[0],
}
}
args = applyUrlDefaults(args)
const parsedArgs = {
record: args.record,
}
return edit.definition.url
.replace('{record}', parsedArgs.record.toString())
.replace(/\/+$/, '') + queryParams(options)
}
/**
* @see \App\Filament\Resources\NotificationTemplates\Pages\EditNotificationTemplate::__invoke
* @see app/Filament/Resources/NotificationTemplates/Pages/EditNotificationTemplate.php:7
* @route '/admin/notification-templates/{record}/edit'
*/
edit.get = (args: { record: string | number } | [record: string | number ] | string | number, options?: RouteQueryOptions): RouteDefinition<'get'> => ({
url: edit.url(args, options),
method: 'get',
})
/**
* @see \App\Filament\Resources\NotificationTemplates\Pages\EditNotificationTemplate::__invoke
* @see app/Filament/Resources/NotificationTemplates/Pages/EditNotificationTemplate.php:7
* @route '/admin/notification-templates/{record}/edit'
*/
edit.head = (args: { record: string | number } | [record: string | number ] | string | number, options?: RouteQueryOptions): RouteDefinition<'head'> => ({
url: edit.url(args, options),
method: 'head',
})
/**
* @see \App\Filament\Resources\NotificationTemplates\Pages\EditNotificationTemplate::__invoke
* @see app/Filament/Resources/NotificationTemplates/Pages/EditNotificationTemplate.php:7
* @route '/admin/notification-templates/{record}/edit'
*/
const editForm = (args: { record: string | number } | [record: string | number ] | string | number, options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({
action: edit.url(args, options),
method: 'get',
})
/**
* @see \App\Filament\Resources\NotificationTemplates\Pages\EditNotificationTemplate::__invoke
* @see app/Filament/Resources/NotificationTemplates/Pages/EditNotificationTemplate.php:7
* @route '/admin/notification-templates/{record}/edit'
*/
editForm.get = (args: { record: string | number } | [record: string | number ] | string | number, options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({
action: edit.url(args, options),
method: 'get',
})
/**
* @see \App\Filament\Resources\NotificationTemplates\Pages\EditNotificationTemplate::__invoke
* @see app/Filament/Resources/NotificationTemplates/Pages/EditNotificationTemplate.php:7
* @route '/admin/notification-templates/{record}/edit'
*/
editForm.head = (args: { record: string | number } | [record: string | number ] | string | number, options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({
action: edit.url(args, {
[options?.mergeQuery ? 'mergeQuery' : 'query']: {
_method: 'HEAD',
...(options?.query ?? options?.mergeQuery ?? {}),
}
}),
method: 'get',
})
edit.form = editForm
const notificationTemplates = {
index: Object.assign(index, index),
create: Object.assign(create, create),
edit: Object.assign(edit, edit),
}
export default notificationTemplates

View File

@@ -0,0 +1,3 @@
<x-mail::message>
{!! $body !!}
</x-mail::message>

View File

@@ -0,0 +1,77 @@
<?php
namespace Tests\Feature;
use App\Models\NotificationTemplate;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class NotificationTemplateTest extends TestCase
{
use RefreshDatabase;
public function test_find_by_identifier_returns_active_template(): void
{
$template = NotificationTemplate::factory()->create([
'identifier' => 'test_template',
'is_active' => true,
]);
$found = NotificationTemplate::findByIdentifier('test_template');
$this->assertNotNull($found);
$this->assertEquals($template->id, $found->id);
}
public function test_find_by_identifier_returns_null_for_inactive_template(): void
{
NotificationTemplate::factory()->inactive()->create([
'identifier' => 'inactive_template',
]);
$found = NotificationTemplate::findByIdentifier('inactive_template');
$this->assertNull($found);
}
public function test_render_subject_replaces_placeholders(): void
{
$template = NotificationTemplate::factory()->create([
'subject' => 'Bonjour {member_name}, votre adhésion expire le {expiry_date}',
]);
$result = $template->renderSubject([
'member_name' => 'Jean Dupont',
'expiry_date' => '2026-01-31',
]);
$this->assertEquals('Bonjour Jean Dupont, votre adhésion expire le 2026-01-31', $result);
}
public function test_render_body_replaces_placeholders(): void
{
$template = NotificationTemplate::factory()->create([
'body' => '<p>Bonjour {member_name}</p><p>Expiration : {expiry_date}</p>',
]);
$result = $template->renderBody([
'member_name' => 'Marie Martin',
'expiry_date' => '2026-06-15',
]);
$this->assertEquals('<p>Bonjour Marie Martin</p><p>Expiration : 2026-06-15</p>', $result);
}
public function test_missing_placeholder_is_left_intact(): void
{
$template = NotificationTemplate::factory()->create([
'subject' => 'Bonjour {member_name}, date : {expiry_date}',
]);
$result = $template->renderSubject([
'member_name' => 'Jean Dupont',
]);
$this->assertEquals('Bonjour Jean Dupont, date : {expiry_date}', $result);
}
}

View File

@@ -0,0 +1,88 @@
<?php
namespace Tests\Feature;
use App\Jobs\SendSubscriptionExpiredPhase1Notifications;
use App\Models\Member;
use App\Models\Membership;
use App\Models\NotificationTemplate;
use App\Models\Package;
use App\Notifications\SubscriptionExpiredPhase1;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Notification;
use Tests\TestCase;
class SendSubscriptionExpiredPhase1NotificationsTest extends TestCase
{
use RefreshDatabase;
private function createMemberWithExpiredMembership(): Member
{
$package = Package::create([
'identifier' => 'test-package',
'name' => 'Test Package',
'is_active' => true,
]);
$member = Member::create([
'email' => fake()->unique()->safeEmail(),
'firstname' => fake()->firstName(),
'lastname' => fake()->lastName(),
'status' => 'valid',
'nature' => 'physical',
]);
Membership::create([
'member_id' => $member->id,
'package_id' => $package->id,
'status' => 'expired',
'end_date' => '2025-12-31',
'amount' => 12.00,
'payment_status' => 'paid',
]);
return $member;
}
public function test_job_sends_notifications_to_expired_members(): void
{
Notification::fake();
NotificationTemplate::factory()->create([
'identifier' => 'subscription_expired_phase1',
'is_active' => true,
]);
$expiredMember = $this->createMemberWithExpiredMembership();
(new SendSubscriptionExpiredPhase1Notifications)->handle();
Notification::assertSentTo($expiredMember, SubscriptionExpiredPhase1::class);
}
public function test_job_does_nothing_when_template_is_inactive(): void
{
Notification::fake();
NotificationTemplate::factory()->inactive()->create([
'identifier' => 'subscription_expired_phase1',
]);
$this->createMemberWithExpiredMembership();
(new SendSubscriptionExpiredPhase1Notifications)->handle();
Notification::assertNothingSent();
}
public function test_job_does_nothing_when_template_does_not_exist(): void
{
Notification::fake();
$this->createMemberWithExpiredMembership();
(new SendSubscriptionExpiredPhase1Notifications)->handle();
Notification::assertNothingSent();
}
}