Compare commits
3 Commits
703a75a11a
...
c848a8b47f
| Author | SHA1 | Date | |
|---|---|---|---|
| c848a8b47f | |||
| ca464e8e06 | |||
| 6754d8684a |
@@ -55,6 +55,7 @@ MAIL_USERNAME=null
|
|||||||
MAIL_PASSWORD=null
|
MAIL_PASSWORD=null
|
||||||
MAIL_FROM_ADDRESS="hello@example.com"
|
MAIL_FROM_ADDRESS="hello@example.com"
|
||||||
MAIL_FROM_NAME="${APP_NAME}"
|
MAIL_FROM_NAME="${APP_NAME}"
|
||||||
|
ADMIN_EMAIL=
|
||||||
|
|
||||||
AWS_ACCESS_KEY_ID=
|
AWS_ACCESS_KEY_ID=
|
||||||
AWS_SECRET_ACCESS_KEY=
|
AWS_SECRET_ACCESS_KEY=
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,4 +1,5 @@
|
|||||||
*.log
|
*.log
|
||||||
|
TODO.txt
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.env
|
.env
|
||||||
.env.backup
|
.env.backup
|
||||||
|
|||||||
@@ -19,12 +19,11 @@ class HandleExpiredMembersDolibarr extends Command
|
|||||||
{--dry-run}';
|
{--dry-run}';
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
protected DolibarrService $dolibarr,
|
protected DolibarrService $dolibarr,
|
||||||
protected ISPConfigMailService $mailService,
|
protected ISPConfigMailService $mailService,
|
||||||
protected NextcloudService $nextcloud,
|
protected NextcloudService $nextcloud,
|
||||||
protected MemberService $memberService
|
protected MemberService $memberService
|
||||||
)
|
) {
|
||||||
{
|
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,11 +64,12 @@ class HandleExpiredMembersDolibarr extends Command
|
|||||||
|
|
||||||
if ($emailFilter) {
|
if ($emailFilter) {
|
||||||
$expiredMembers = $expiredMembers->filter(function ($member) use ($emailFilter) {
|
$expiredMembers = $expiredMembers->filter(function ($member) use ($emailFilter) {
|
||||||
return $this->extractRetzienEmail($member['email'] ?? null) === $emailFilter;
|
return Member::extractRetzienEmail($member['email'] ?? '') === $emailFilter;
|
||||||
});
|
});
|
||||||
|
|
||||||
if ($expiredMembers->isEmpty()) {
|
if ($expiredMembers->isEmpty()) {
|
||||||
$this->warn("Aucun adhérent expiré trouvé pour {$emailFilter}");
|
$this->warn("Aucun adhérent expiré trouvé pour {$emailFilter}");
|
||||||
|
|
||||||
return CommandAlias::SUCCESS;
|
return CommandAlias::SUCCESS;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -102,13 +102,13 @@ class HandleExpiredMembersDolibarr extends Command
|
|||||||
*/
|
*/
|
||||||
protected function processMember(array $member, bool $dryRun): void
|
protected function processMember(array $member, bool $dryRun): void
|
||||||
{
|
{
|
||||||
$email = $this->extractRetzienEmail($member['email'] ?? null);
|
$email = Member::extractRetzienEmail($member['email'] ?? '');
|
||||||
|
|
||||||
$this->line("• {$member['id']} - {$email}");
|
$this->line("• {$member['id']} - {$email}");
|
||||||
|
|
||||||
// Résiliation Dolibarr
|
// Résiliation Dolibarr
|
||||||
if ($dryRun) {
|
if ($dryRun) {
|
||||||
$this->info("[DRY-RUN] Résiliation Dolibarr");
|
$this->info('[DRY-RUN] Résiliation Dolibarr');
|
||||||
} else {
|
} else {
|
||||||
$this->dolibarr->updateMember($member['id'], [
|
$this->dolibarr->updateMember($member['id'], [
|
||||||
'statut' => 0,
|
'statut' => 0,
|
||||||
@@ -129,7 +129,7 @@ class HandleExpiredMembersDolibarr extends Command
|
|||||||
// Désactivation Nextcloud
|
// Désactivation Nextcloud
|
||||||
if ($email) {
|
if ($email) {
|
||||||
if ($dryRun) {
|
if ($dryRun) {
|
||||||
$this->info("[DRY-RUN] Désactivation Nextcloud");
|
$this->info('[DRY-RUN] Désactivation Nextcloud');
|
||||||
} else {
|
} else {
|
||||||
$this->nextcloud->disableUserByEmail($email);
|
$this->nextcloud->disableUserByEmail($email);
|
||||||
}
|
}
|
||||||
@@ -143,13 +143,15 @@ class HandleExpiredMembersDolibarr extends Command
|
|||||||
{
|
{
|
||||||
$details = $this->mailService->getMailUserDetails($email);
|
$details = $this->mailService->getMailUserDetails($email);
|
||||||
|
|
||||||
if (!$details) {
|
if (! $details) {
|
||||||
$this->warn("Boîte mail inexistante : {$email}");
|
$this->warn("Boîte mail inexistante : {$email}");
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($dryRun) {
|
if ($dryRun) {
|
||||||
$this->info("[DRY-RUN] Mail désactivé ({$email})");
|
$this->info("[DRY-RUN] Mail désactivé ({$email})");
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,22 +162,10 @@ class HandleExpiredMembersDolibarr extends Command
|
|||||||
'disablepop3' => 'y',
|
'disablepop3' => 'y',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (!$result) {
|
if (! $result) {
|
||||||
throw new \RuntimeException("Échec désactivation mail ISPConfig pour {$email}");
|
throw new \RuntimeException("Échec désactivation mail ISPConfig pour {$email}");
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->info("Mail désactivé : {$email}");
|
$this->info("Mail désactivé : {$email}");
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function extractRetzienEmail(?string $emails): ?string
|
|
||||||
{
|
|
||||||
if (!$emails) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return collect(explode(';', $emails))
|
|
||||||
->map(fn(string $email): string => trim($email))
|
|
||||||
->filter(fn(string $email): bool => str_contains($email, '@retzien.fr'))
|
|
||||||
->first();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ class SyncDolibarrMembers extends Command
|
|||||||
'lastname' => $member['lastname'],
|
'lastname' => $member['lastname'],
|
||||||
'firstname' => $member['firstname'],
|
'firstname' => $member['firstname'],
|
||||||
'email' => $member['email'] ?: null,
|
'email' => $member['email'] ?: null,
|
||||||
'retzien_email' => '',
|
'retzien_email' => Member::extractRetzienEmail($member['email'] ?? ''),
|
||||||
'company' => $member['societe'],
|
'company' => $member['societe'],
|
||||||
'website_url' => $member['url'],
|
'website_url' => $member['url'],
|
||||||
'address' => $member['address'],
|
'address' => $member['address'],
|
||||||
|
|||||||
@@ -7,20 +7,22 @@ use App\Models\IspconfigMember;
|
|||||||
use App\Models\Member;
|
use App\Models\Member;
|
||||||
use App\Services\ISPConfig\ISPConfigMailService;
|
use App\Services\ISPConfig\ISPConfigMailService;
|
||||||
use Illuminate\Console\Command;
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
use function Laravel\Prompts\progress;
|
use function Laravel\Prompts\progress;
|
||||||
|
|
||||||
class SyncISPConfigMailMembers extends Command
|
class SyncISPConfigMailMembers extends Command
|
||||||
{
|
{
|
||||||
protected $signature = 'sync:ispconfig-mail-members';
|
protected $signature = 'sync:ispconfig-mail-members';
|
||||||
|
|
||||||
protected $description = 'Synchronise les services MAIL ISPConfig des membres - Email Retzien';
|
protected $description = 'Synchronise les services MAIL ISPConfig des membres - Email Retzien';
|
||||||
|
|
||||||
public function handle(): void
|
public function handle(): void
|
||||||
{
|
{
|
||||||
$this->info('Synchronisation ISPConfig MAIL');
|
$this->info('Synchronisation ISPConfig MAIL');
|
||||||
|
|
||||||
$ispMail = new ISPConfigMailService();
|
$ispMail = new ISPConfigMailService;
|
||||||
|
|
||||||
//Récupération de tous les mail users
|
// Récupération de tous les mail users
|
||||||
$mailUsers = collect($ispMail->getAllMailUsers());
|
$mailUsers = collect($ispMail->getAllMailUsers());
|
||||||
|
|
||||||
$progressBar = progress(label: 'ISPConfig Mail Members import', steps: $mailUsers->count());
|
$progressBar = progress(label: 'ISPConfig Mail Members import', steps: $mailUsers->count());
|
||||||
@@ -35,40 +37,37 @@ class SyncISPConfigMailMembers extends Command
|
|||||||
$synced = 0;
|
$synced = 0;
|
||||||
|
|
||||||
// Parcours des membres
|
// Parcours des membres
|
||||||
Member::whereNotNull('email')->chunk(100, function ($members) use (
|
Member::whereNotNull('retzien_email')->where('retzien_email', '!=', '')->chunk(100, function ($members) use (
|
||||||
$progressBar,
|
$progressBar,
|
||||||
$emailToMailUserId,
|
$emailToMailUserId,
|
||||||
$ispMail,
|
$ispMail,
|
||||||
&$synced
|
&$synced
|
||||||
) {
|
) {
|
||||||
foreach ($members as $member) {
|
foreach ($members as $member) {
|
||||||
$emails = array_map('trim', explode(';', $member->email));
|
$retzienEmail = strtolower($member->retzien_email);
|
||||||
|
|
||||||
$retzienEmail = collect($emails)
|
if (! $retzienEmail) {
|
||||||
->map(fn ($e) => strtolower($e))
|
|
||||||
->first(fn ($e) => str_ends_with($e, '@retzien.fr'));
|
|
||||||
|
|
||||||
if (!$retzienEmail) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$mailUserId = $emailToMailUserId->get($retzienEmail);
|
$mailUserId = $emailToMailUserId->get($retzienEmail);
|
||||||
|
|
||||||
if (!$mailUserId) {
|
if (! $mailUserId) {
|
||||||
$this->warn("Aucun mail user ISPConfig pour {$retzienEmail}");
|
$this->warn("Aucun mail user ISPConfig pour {$retzienEmail}");
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
//Récupération des données complètes de la boîte mail
|
// Récupération des données complètes de la boîte mail
|
||||||
$mailUserData = $ispMail->getMailUserDetails($retzienEmail);
|
$mailUserData = $ispMail->getMailUserDetails($retzienEmail);
|
||||||
|
|
||||||
// Création / mise à jour
|
// Création / mise à jour
|
||||||
IspconfigMember::updateOrCreate(
|
IspconfigMember::updateOrCreate(
|
||||||
[
|
[
|
||||||
'member_id' => $member->id,
|
'member_id' => $member->id,
|
||||||
//@todo : 'ispconfig_client_id' => ?,
|
// @todo : 'ispconfig_client_id' => ?,
|
||||||
'type' => IspconfigType::MAIL,
|
'type' => IspconfigType::MAIL,
|
||||||
'email' => $retzienEmail,
|
'email' => $retzienEmail,
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
'ispconfig_service_user_id' => $mailUserId,
|
'ispconfig_service_user_id' => $mailUserId,
|
||||||
|
|||||||
119
app/Console/Commands/SyncListmonkMembers.php
Normal file
119
app/Console/Commands/SyncListmonkMembers.php
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\Models\ListmonkMember;
|
||||||
|
use App\Models\Member;
|
||||||
|
use App\Services\ListMonk\ListMonkService;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Http\Client\ConnectionException;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Symfony\Component\Console\Command\Command as CommandAlias;
|
||||||
|
|
||||||
|
use function Laravel\Prompts\progress;
|
||||||
|
|
||||||
|
class SyncListmonkMembers extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'listmonk:sync-members
|
||||||
|
{--dry-run : Run without writing to the database}
|
||||||
|
{--member= : Sync a single member by member_id}';
|
||||||
|
|
||||||
|
protected $description = 'Sync Listmonk user accounts with members';
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
protected ListMonkService $listmonk
|
||||||
|
) {
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws ConnectionException
|
||||||
|
*/
|
||||||
|
public function handle(): int
|
||||||
|
{
|
||||||
|
$dryRun = $this->option('dry-run');
|
||||||
|
$memberFilter = $this->option('member');
|
||||||
|
|
||||||
|
$this->info(
|
||||||
|
$dryRun
|
||||||
|
? 'DRY-RUN enabled'
|
||||||
|
: 'Syncing Listmonk → Members'
|
||||||
|
);
|
||||||
|
|
||||||
|
$members = Member::query()
|
||||||
|
->when($memberFilter, fn ($q) => $q->where('id', $memberFilter))
|
||||||
|
->get()
|
||||||
|
->filter(fn (Member $m) => ! empty($m->retzien_email))
|
||||||
|
->keyBy(fn (Member $m) => strtolower($m->retzien_email));
|
||||||
|
|
||||||
|
if ($members->isEmpty()) {
|
||||||
|
$this->warn('No members to sync');
|
||||||
|
|
||||||
|
return CommandAlias::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->info("{$members->count()} members to sync");
|
||||||
|
|
||||||
|
$listmonkUsers = $this->listmonk->getUsers();
|
||||||
|
|
||||||
|
dd($listmonkUsers);
|
||||||
|
|
||||||
|
$this->info(count($listmonkUsers).' Listmonk users found');
|
||||||
|
|
||||||
|
$progress = null;
|
||||||
|
|
||||||
|
if (! $dryRun) {
|
||||||
|
$progress = progress(
|
||||||
|
label: 'Syncing members',
|
||||||
|
steps: $members->count()
|
||||||
|
);
|
||||||
|
$progress->start();
|
||||||
|
}
|
||||||
|
|
||||||
|
$synced = 0;
|
||||||
|
|
||||||
|
foreach ($listmonkUsers as $user) {
|
||||||
|
try {
|
||||||
|
$email = strtolower($user['email'] ?? '');
|
||||||
|
|
||||||
|
if (! $email || ! $members->has($email)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$member = $members[$email];
|
||||||
|
|
||||||
|
if ($dryRun) {
|
||||||
|
$this->line("[DRY-RUN] {$member->id} ({$email}) ← Listmonk user #{$user['id']}");
|
||||||
|
} else {
|
||||||
|
ListmonkMember::query()->updateOrCreate(
|
||||||
|
['member_id' => $member->id],
|
||||||
|
[
|
||||||
|
'listmonk_user_id' => $user['id'],
|
||||||
|
'data' => $user,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
$progress?->advance();
|
||||||
|
}
|
||||||
|
|
||||||
|
$synced++;
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
Log::error('Listmonk sync error', [
|
||||||
|
'user' => $user['id'] ?? null,
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$progress?->advance();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($progress) {
|
||||||
|
$progress->finish();
|
||||||
|
$this->newLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->info("Sync complete — {$synced} accounts linked");
|
||||||
|
|
||||||
|
return CommandAlias::SUCCESS;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ use Illuminate\Console\Command;
|
|||||||
use Illuminate\Http\Client\ConnectionException;
|
use Illuminate\Http\Client\ConnectionException;
|
||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
use Symfony\Component\Console\Command\Command as CommandAlias;
|
use Symfony\Component\Console\Command\Command as CommandAlias;
|
||||||
|
|
||||||
use function Laravel\Prompts\progress;
|
use function Laravel\Prompts\progress;
|
||||||
|
|
||||||
class SyncNextcloudMembers extends Command
|
class SyncNextcloudMembers extends Command
|
||||||
@@ -40,14 +41,15 @@ class SyncNextcloudMembers extends Command
|
|||||||
);
|
);
|
||||||
|
|
||||||
$members = Member::query()
|
$members = Member::query()
|
||||||
->where('email', 'like', '%@retzien.fr%')
|
->whereNotNull('retzien_email')
|
||||||
|
->where('retzien_email', '!=', '')
|
||||||
->when($memberFilter, fn ($q) => $q->where('id', $memberFilter))
|
->when($memberFilter, fn ($q) => $q->where('id', $memberFilter))
|
||||||
->get()
|
->get()
|
||||||
->filter(fn (Member $m) => !empty($m->retzien_email))
|
|
||||||
->keyBy(fn (Member $m) => strtolower($m->retzien_email));
|
->keyBy(fn (Member $m) => strtolower($m->retzien_email));
|
||||||
|
|
||||||
if ($members->isEmpty()) {
|
if ($members->isEmpty()) {
|
||||||
$this->warn('Aucun membre à synchroniser');
|
$this->warn('Aucun membre à synchroniser');
|
||||||
|
|
||||||
return CommandAlias::SUCCESS;
|
return CommandAlias::SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,11 +57,11 @@ class SyncNextcloudMembers extends Command
|
|||||||
|
|
||||||
$userIds = $this->nextcloud->listUsers();
|
$userIds = $this->nextcloud->listUsers();
|
||||||
|
|
||||||
$this->info(count($userIds) . ' comptes Nextcloud trouvés');
|
$this->info(count($userIds).' comptes Nextcloud trouvés');
|
||||||
|
|
||||||
$progress = null;
|
$progress = null;
|
||||||
|
|
||||||
if (!$dryRun) {
|
if (! $dryRun) {
|
||||||
$progress = progress(
|
$progress = progress(
|
||||||
label: 'Synchronisation des membres',
|
label: 'Synchronisation des membres',
|
||||||
steps: $members->count()
|
steps: $members->count()
|
||||||
@@ -75,7 +77,7 @@ class SyncNextcloudMembers extends Command
|
|||||||
|
|
||||||
$email = strtolower($details['email'] ?? '');
|
$email = strtolower($details['email'] ?? '');
|
||||||
|
|
||||||
if (!$email || !$members->has($email)) {
|
if (! $email || ! $members->has($email)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ class Synchronisations extends Page
|
|||||||
'ispconfig_mail',
|
'ispconfig_mail',
|
||||||
'ispconfig_web',
|
'ispconfig_web',
|
||||||
'nextcloud',
|
'nextcloud',
|
||||||
|
'listmonk',
|
||||||
'services',
|
'services',
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -154,6 +155,17 @@ class Synchronisations extends Page
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function syncListmonkAction(): Action
|
||||||
|
{
|
||||||
|
return Action::make('syncListmonk')
|
||||||
|
->requiresConfirmation()
|
||||||
|
->modalHeading(__('synchronisations.sections.listmonk.modal_heading'))
|
||||||
|
->modalDescription(__('synchronisations.sections.listmonk.modal_description'))
|
||||||
|
->modalSubmitActionLabel(__('synchronisations.action.submit'))
|
||||||
|
->disabled(fn () => in_array($this->getCommandStatus('listmonk')['status'], ['pending', 'running']))
|
||||||
|
->action(fn () => $this->enqueueCommand('listmonk', 'listmonk:sync-members'));
|
||||||
|
}
|
||||||
|
|
||||||
public function syncServicesAction(): Action
|
public function syncServicesAction(): Action
|
||||||
{
|
{
|
||||||
return Action::make('syncServices')
|
return Action::make('syncServices')
|
||||||
|
|||||||
@@ -7,32 +7,33 @@ use App\Http\Requests\Forms\ContactRequest;
|
|||||||
use App\Services\ContactService;
|
use App\Services\ContactService;
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Inertia\Inertia;
|
use Inertia\Inertia;
|
||||||
|
use Inertia\Response;
|
||||||
|
|
||||||
class ContactFormController extends Controller
|
class ContactFormController extends Controller
|
||||||
{
|
{
|
||||||
public function __construct(protected ContactService $contactService) {}
|
public function __construct(protected ContactService $contactService) {}
|
||||||
/**
|
|
||||||
* Show the contact form page.
|
public function create(): Response
|
||||||
*/
|
|
||||||
public function create()
|
|
||||||
{
|
{
|
||||||
return Inertia::render('forms/contact');
|
return Inertia::render('forms/contact', [
|
||||||
|
'captcha_question' => $this->generateCaptcha(),
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle an incoming contact form submission.
|
|
||||||
* @throws \Illuminate\Validation\ValidationException
|
* @throws \Illuminate\Validation\ValidationException
|
||||||
*/
|
*/
|
||||||
public function store(ContactRequest $request): RedirectResponse
|
public function store(ContactRequest $request): RedirectResponse
|
||||||
{
|
{
|
||||||
$validated = $request->validated();
|
$validated = $request->validated();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$this->contactService->registerNewContactRequest($validated);
|
$this->contactService->registerNewContactRequest($validated);
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
\Log::error('Erreur lors de la création d\'un contact', [
|
\Log::error('Erreur lors de la création d\'un contact', [
|
||||||
'error' => $e->getMessage(),
|
'error' => $e->getMessage(),
|
||||||
'trace' => $e->getTraceAsString(),
|
'trace' => $e->getTraceAsString(),
|
||||||
'data' => $validated,
|
'data' => $validated,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return to_route('contact')->with('error', __('contacts.responses.error'));
|
return to_route('contact')->with('error', __('contacts.responses.error'));
|
||||||
@@ -41,4 +42,13 @@ class ContactFormController extends Controller
|
|||||||
return to_route('contact')->with('success', __('contacts.responses.success'));
|
return to_route('contact')->with('success', __('contacts.responses.success'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function generateCaptcha(): string
|
||||||
|
{
|
||||||
|
$a = random_int(1, 9);
|
||||||
|
$b = random_int(1, 9);
|
||||||
|
|
||||||
|
session(['captcha_contact' => (string) ($a + $b)]);
|
||||||
|
|
||||||
|
return "Combien font {$a} + {$b} ?";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,55 +4,69 @@ namespace App\Http\Controllers\Forms;
|
|||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Http\Requests\Forms\MembershipRequest;
|
use App\Http\Requests\Forms\MembershipRequest;
|
||||||
use App\Models\Membership;
|
|
||||||
use App\Models\Package;
|
use App\Models\Package;
|
||||||
|
use App\Models\Service;
|
||||||
use App\Services\MemberService;
|
use App\Services\MemberService;
|
||||||
use Carbon\Carbon;
|
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Inertia\Inertia;
|
use Inertia\Inertia;
|
||||||
|
use Inertia\Response;
|
||||||
|
|
||||||
class MembershipFormController extends Controller
|
class MembershipFormController extends Controller
|
||||||
{
|
{
|
||||||
public function __construct(protected MemberService $memberService) {}
|
public function __construct(protected MemberService $memberService) {}
|
||||||
|
|
||||||
/**
|
public function create(): Response
|
||||||
* Show the contact form page.
|
|
||||||
*/
|
|
||||||
public function create()
|
|
||||||
{
|
{
|
||||||
|
$remainingMonths = 13 - now()->month;
|
||||||
|
|
||||||
|
$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,
|
||||||
|
]);
|
||||||
|
|
||||||
return Inertia::render('forms/membership', [
|
return Inertia::render('forms/membership', [
|
||||||
'plans' => Package::query()
|
'plans' => $plans,
|
||||||
->where('is_active', true)
|
'services' => Service::query()->select('name', 'description')->get(),
|
||||||
->select('id', 'identifier', 'name', 'price', 'description')
|
'captcha_question' => $this->generateCaptcha(),
|
||||||
->get()
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle an incoming membership form request.
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
public function store(MembershipRequest $request): RedirectResponse
|
public function store(MembershipRequest $request): RedirectResponse
|
||||||
{
|
{
|
||||||
dd($request->validated());
|
|
||||||
$validated = $request->validated();
|
$validated = $request->validated();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$this->memberService->registerNewMember($validated);
|
$this->memberService->registerNewMember($validated);
|
||||||
} catch (\Throwable $e) {
|
} catch (\Throwable $e) {
|
||||||
\Log::error('Erreur lors de la création d’un membre', [
|
\Log::error('Erreur lors de la création d\'un membre', [
|
||||||
'error' => $e->getMessage(),
|
'error' => $e->getMessage(),
|
||||||
'trace' => $e->getTraceAsString(),
|
'trace' => $e->getTraceAsString(),
|
||||||
'data' => $validated,
|
'data' => $validated,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return redirect()
|
return to_route('membership')
|
||||||
->route('membership')
|
->with('error', __('memberships.fields.subscription.error'));
|
||||||
->with('error', Membership::getAttributeLabel('memberships.subscription.error'));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return redirect()
|
return to_route('membership')
|
||||||
->route('membership')
|
->with('success', __('memberships.fields.subscription.success'));
|
||||||
->with('success', Membership::getAttributeLabel('memberships.subscription.success'));
|
}
|
||||||
|
|
||||||
|
private function generateCaptcha(): string
|
||||||
|
{
|
||||||
|
$a = random_int(1, 9);
|
||||||
|
$b = random_int(1, 9);
|
||||||
|
|
||||||
|
session(['captcha_membership' => (string) ($a + $b)]);
|
||||||
|
|
||||||
|
return "Combien font {$a} + {$b} ?";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,21 +2,17 @@
|
|||||||
|
|
||||||
namespace App\Http\Requests\Forms;
|
namespace App\Http\Requests\Forms;
|
||||||
|
|
||||||
|
use App\Rules\ValidCaptcha;
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
class ContactRequest extends FormRequest
|
class ContactRequest extends FormRequest
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* Determine if the user is authorized to make this request.
|
|
||||||
*/
|
|
||||||
public function authorize(): bool
|
public function authorize(): bool
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the validation rules that apply to the request.
|
|
||||||
*
|
|
||||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||||
*/
|
*/
|
||||||
public function rules(): array
|
public function rules(): array
|
||||||
@@ -25,9 +21,10 @@ class ContactRequest extends FormRequest
|
|||||||
'lastname' => 'required|string|max:255',
|
'lastname' => 'required|string|max:255',
|
||||||
'firstname' => 'required|string|max:255',
|
'firstname' => 'required|string|max:255',
|
||||||
'email' => 'required|email|max:255',
|
'email' => 'required|email|max:255',
|
||||||
'address' => 'string|max:255',
|
'address' => 'nullable|string|max:255',
|
||||||
'subject' => 'required|string|max:255',
|
'subject' => 'required|string|max:255',
|
||||||
'message' => 'required|string',
|
'message' => 'required|string',
|
||||||
|
'captcha' => ['required', new ValidCaptcha('captcha_contact')],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,39 +2,34 @@
|
|||||||
|
|
||||||
namespace App\Http\Requests\Forms;
|
namespace App\Http\Requests\Forms;
|
||||||
|
|
||||||
|
use App\Rules\ValidCaptcha;
|
||||||
use Illuminate\Foundation\Http\FormRequest;
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
class MembershipRequest extends FormRequest
|
class MembershipRequest extends FormRequest
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* Determine if the user is authorized to make this request.
|
|
||||||
*/
|
|
||||||
public function authorize(): bool
|
public function authorize(): bool
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the validation rules that apply to the request.
|
|
||||||
*
|
|
||||||
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||||
*/
|
*/
|
||||||
public function rules(): array
|
public function rules(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
// Member
|
|
||||||
'lastname' => 'required|string|max:255',
|
'lastname' => 'required|string|max:255',
|
||||||
'firstname' => 'required|string|max:255',
|
'firstname' => 'required|string|max:255',
|
||||||
'email' => 'required|email|max:255',
|
'email' => 'required|email|max:255',
|
||||||
'company' => '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|string|max:255',
|
||||||
'city' => 'required|string|max:255',
|
'city' => 'required|string|max:255',
|
||||||
'phone1' => 'required|string|max:255',
|
'phone1' => 'required|string|max:255',
|
||||||
|
|
||||||
// Membership
|
|
||||||
'package' => 'required|string|max:255',
|
'package' => 'required|string|max:255',
|
||||||
'amount' => 'required|string|max:255',
|
'amount' => 'required|numeric|min:0',
|
||||||
|
'cgu' => 'required|accepted',
|
||||||
|
'captcha' => ['required', new ValidCaptcha('captcha_membership')],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
40
app/Models/ListmonkMember.php
Normal file
40
app/Models/ListmonkMember.php
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @property int $id
|
||||||
|
* @property int $member_id
|
||||||
|
* @property int|null $listmonk_user_id
|
||||||
|
* @property array<array-key, mixed>|null $data
|
||||||
|
* @property \Illuminate\Support\Carbon|null $created_at
|
||||||
|
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||||
|
* @property-read \App\Models\Member $member
|
||||||
|
*
|
||||||
|
* @mixin \Eloquent
|
||||||
|
*/
|
||||||
|
class ListmonkMember extends Model
|
||||||
|
{
|
||||||
|
protected $table = 'listmonks_members';
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'member_id',
|
||||||
|
'listmonk_user_id',
|
||||||
|
'data',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'data' => 'array',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function member(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Member::class);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
use Illuminate\Notifications\Notifiable;
|
use Illuminate\Notifications\Notifiable;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -33,7 +34,7 @@ use Illuminate\Notifications\Notifiable;
|
|||||||
* @property string|null $website_url
|
* @property string|null $website_url
|
||||||
* @property \Illuminate\Support\Carbon|null $created_at
|
* @property \Illuminate\Support\Carbon|null $created_at
|
||||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||||
* @property string|null $deleted_at
|
* @property \Illuminate\Support\Carbon|null $deleted_at
|
||||||
* @property-read string $full_name
|
* @property-read string $full_name
|
||||||
* @property-read \App\Models\MemberGroup|null $group
|
* @property-read \App\Models\MemberGroup|null $group
|
||||||
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\IspconfigMember> $ispconfigs
|
* @property-read \Illuminate\Database\Eloquent\Collection<int, \App\Models\IspconfigMember> $ispconfigs
|
||||||
@@ -80,7 +81,7 @@ use Illuminate\Notifications\Notifiable;
|
|||||||
*/
|
*/
|
||||||
class Member extends Model
|
class Member extends Model
|
||||||
{
|
{
|
||||||
use HasFactory, Notifiable;
|
use HasFactory, Notifiable, SoftDeletes;
|
||||||
|
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'user_id',
|
'user_id',
|
||||||
@@ -113,18 +114,19 @@ class Member extends Model
|
|||||||
return __("members.fields.$attribute");
|
return __("members.fields.$attribute");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function extractRetzienEmail(string $rawEmails): ?string
|
||||||
|
{
|
||||||
|
return collect(explode(';', $rawEmails))
|
||||||
|
->map(fn (string $email) => trim($email))
|
||||||
|
->filter(fn (string $email) => str_ends_with($email, '@retzien.fr'))
|
||||||
|
->first();
|
||||||
|
}
|
||||||
|
|
||||||
public function getFullNameAttribute(): string
|
public function getFullNameAttribute(): string
|
||||||
{
|
{
|
||||||
return "{$this->firstname} {$this->lastname}";
|
return "{$this->firstname} {$this->lastname}";
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getRetzienEmailAttribute(): ?string
|
|
||||||
{
|
|
||||||
$emails = explode(';', $this->email);
|
|
||||||
|
|
||||||
return collect($emails)->filter(fn ($email) => str_contains($email, '@retzien.fr'))->first();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function user(): BelongsTo
|
public function user(): BelongsTo
|
||||||
{
|
{
|
||||||
return $this->belongsTo(User::class);
|
return $this->belongsTo(User::class);
|
||||||
@@ -150,7 +152,7 @@ class Member extends Model
|
|||||||
return $this->hasMany(NextCloudMember::class, 'member_id');
|
return $this->hasMany(NextCloudMember::class, 'member_id');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function lastMembership(): Membership
|
public function lastMembership(): ?Membership
|
||||||
{
|
{
|
||||||
return $this->memberships()->where('status', 'active')->first();
|
return $this->memberships()->where('status', 'active')->first();
|
||||||
}
|
}
|
||||||
@@ -159,14 +161,21 @@ class Member extends Model
|
|||||||
{
|
{
|
||||||
$membership = $this->lastMembership();
|
$membership = $this->lastMembership();
|
||||||
|
|
||||||
|
if ($membership === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
return $membership->services()->where('identifier', $serviceIdentifier)->exists();
|
return $membership->services()->where('identifier', $serviceIdentifier)->exists();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function isExpired(): bool
|
public function isExpired(): bool
|
||||||
{
|
{
|
||||||
// Member ayant leur dernière adhésion non renouvellée de puis plus d'un mois
|
|
||||||
$lastMembership = $this->lastMembership();
|
$lastMembership = $this->lastMembership();
|
||||||
|
|
||||||
|
if ($lastMembership === null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
return $lastMembership->status === 'expired' || $lastMembership->created_at->addMonths(1) < now();
|
return $lastMembership->status === 'expired' || $lastMembership->created_at->addMonths(1) < now();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
52
app/Notifications/ContactNewRequestNotification.php
Normal file
52
app/Notifications/ContactNewRequestNotification.php
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Notifications;
|
||||||
|
|
||||||
|
use App\Models\Contact;
|
||||||
|
use App\Models\NotificationTemplate;
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
use Illuminate\Notifications\Messages\MailMessage;
|
||||||
|
use Illuminate\Notifications\Notification;
|
||||||
|
|
||||||
|
class ContactNewRequestNotification extends Notification implements ShouldQueue
|
||||||
|
{
|
||||||
|
use Queueable;
|
||||||
|
|
||||||
|
public function __construct(public readonly Contact $contact) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<int, string>
|
||||||
|
*/
|
||||||
|
public function via(object $notifiable): array
|
||||||
|
{
|
||||||
|
return ['mail'];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function toMail(object $notifiable): MailMessage
|
||||||
|
{
|
||||||
|
$template = NotificationTemplate::findByIdentifier('contact_new_request');
|
||||||
|
|
||||||
|
$vars = [
|
||||||
|
'contact_name' => $this->contact->full_name,
|
||||||
|
'contact_email' => $this->contact->email ?? '',
|
||||||
|
'contact_subject' => $this->contact->subject ?? '',
|
||||||
|
'contact_message' => $this->contact->message ?? '',
|
||||||
|
'app_name' => config('app.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 [];
|
||||||
|
}
|
||||||
|
}
|
||||||
50
app/Notifications/MemberDeactivatedAdminNotification.php
Normal file
50
app/Notifications/MemberDeactivatedAdminNotification.php
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
<?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 MemberDeactivatedAdminNotification 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_deactivated_admin');
|
||||||
|
|
||||||
|
$vars = [
|
||||||
|
'member_name' => $this->member->full_name,
|
||||||
|
'member_email' => $this->member->email ?? '',
|
||||||
|
'app_name' => config('app.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 [];
|
||||||
|
}
|
||||||
|
}
|
||||||
49
app/Notifications/MemberDeactivatedMemberNotification.php
Normal file
49
app/Notifications/MemberDeactivatedMemberNotification.php
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
<?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 MemberDeactivatedMemberNotification 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_deactivated_member');
|
||||||
|
|
||||||
|
$vars = [
|
||||||
|
'member_name' => $this->member->full_name,
|
||||||
|
'app_name' => config('app.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 [];
|
||||||
|
}
|
||||||
|
}
|
||||||
65
app/Notifications/MemberNewRequestAdminNotification.php
Normal file
65
app/Notifications/MemberNewRequestAdminNotification.php
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Notifications;
|
||||||
|
|
||||||
|
use App\Filament\Resources\Members\MemberResource;
|
||||||
|
use App\Models\Member;
|
||||||
|
use App\Models\NotificationTemplate;
|
||||||
|
use App\Models\Package;
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
use Illuminate\Notifications\Messages\MailMessage;
|
||||||
|
use Illuminate\Notifications\Notification;
|
||||||
|
|
||||||
|
class MemberNewRequestAdminNotification extends Notification implements ShouldQueue
|
||||||
|
{
|
||||||
|
use Queueable;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
public readonly Member $member,
|
||||||
|
public readonly Package $package,
|
||||||
|
public readonly float $amount,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<int, string>
|
||||||
|
*/
|
||||||
|
public function via(object $notifiable): array
|
||||||
|
{
|
||||||
|
return ['mail'];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function toMail(object $notifiable): MailMessage
|
||||||
|
{
|
||||||
|
$template = NotificationTemplate::findByIdentifier('member_new_request_admin');
|
||||||
|
|
||||||
|
$vars = [
|
||||||
|
'member_name' => $this->member->full_name,
|
||||||
|
'member_email' => $this->member->email ?? '',
|
||||||
|
'member_phone' => $this->member->phone1 ?? '',
|
||||||
|
'member_address' => implode(', ', array_filter([
|
||||||
|
$this->member->address,
|
||||||
|
$this->member->zipcode,
|
||||||
|
$this->member->city,
|
||||||
|
])),
|
||||||
|
'package_name' => $this->package->name,
|
||||||
|
'amount' => number_format($this->amount, 2, ',', ' '),
|
||||||
|
'member_url' => MemberResource::getUrl('edit', ['record' => $this->member->id]),
|
||||||
|
'app_name' => config('app.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 [];
|
||||||
|
}
|
||||||
|
}
|
||||||
21
app/Rules/ValidCaptcha.php
Normal file
21
app/Rules/ValidCaptcha.php
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Rules;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use Illuminate\Contracts\Validation\ValidationRule;
|
||||||
|
|
||||||
|
class ValidCaptcha implements ValidationRule
|
||||||
|
{
|
||||||
|
public function __construct(private readonly string $sessionKey = 'captcha_answer') {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param \Closure(string, ?string=): \Illuminate\Translation\PotentiallyTranslatedString $fail
|
||||||
|
*/
|
||||||
|
public function validate(string $attribute, mixed $value, Closure $fail): void
|
||||||
|
{
|
||||||
|
if (trim((string) $value) !== (string) session($this->sessionKey)) {
|
||||||
|
$fail('Le code de vérification est incorrect.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,23 +3,22 @@
|
|||||||
namespace App\Services;
|
namespace App\Services;
|
||||||
|
|
||||||
use App\Models\Contact;
|
use App\Models\Contact;
|
||||||
|
use App\Notifications\ContactNewRequestNotification;
|
||||||
|
use Illuminate\Support\Facades\Notification;
|
||||||
|
|
||||||
class ContactService
|
class ContactService
|
||||||
{
|
{
|
||||||
public function __construct()
|
public function __construct() {}
|
||||||
{
|
|
||||||
//
|
|
||||||
}
|
|
||||||
|
|
||||||
public function registerNewContactRequest(array $data): Contact
|
public function registerNewContactRequest(array $data): Contact
|
||||||
{
|
{
|
||||||
$contact = new Contact();
|
$contact = new Contact;
|
||||||
$contact->fill($data);
|
$contact->fill($data);
|
||||||
$contact->save();
|
$contact->save();
|
||||||
|
|
||||||
// Envoyer un email à l'administrateur
|
Notification::route('mail', config('app.admin_email'))
|
||||||
|
->notify(new ContactNewRequestNotification($contact));
|
||||||
|
|
||||||
return $contact;
|
return $contact;
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,15 +17,27 @@ class ListMonkService
|
|||||||
config('services.listmonk.password')
|
config('services.listmonk.password')
|
||||||
)
|
)
|
||||||
->withHeaders(['Accept' => 'application/json'])
|
->withHeaders(['Accept' => 'application/json'])
|
||||||
->baseUrl(config('services.listmonk.base_url').'/api');
|
->baseUrl(config('services.listmonk.base_url'));
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
/**
|
||||||
// Lists
|
* Retrieve all Listmonk user accounts.
|
||||||
// -------------------------------------------------------------------------
|
*
|
||||||
|
* @return array<array-key, mixed>
|
||||||
|
*
|
||||||
|
* @throws ConnectionException
|
||||||
|
*/
|
||||||
|
public function getUsers(): array
|
||||||
|
{
|
||||||
|
return $this->http
|
||||||
|
->get('/users')
|
||||||
|
->json('data') ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retrieve all mailing lists.
|
* Retrieve all mailing lists with their subscriber counts.
|
||||||
|
*
|
||||||
|
* @return array<array-key, mixed>
|
||||||
*
|
*
|
||||||
* @throws ConnectionException
|
* @throws ConnectionException
|
||||||
*/
|
*/
|
||||||
@@ -35,188 +47,4 @@ class ListMonkService
|
|||||||
->get('/lists', ['per_page' => 'all'])
|
->get('/lists', ['per_page' => 'all'])
|
||||||
->json('data.results') ?? [];
|
->json('data.results') ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve a single list by its ID.
|
|
||||||
*
|
|
||||||
* @throws ConnectionException
|
|
||||||
*/
|
|
||||||
public function getList(int $listId): ?array
|
|
||||||
{
|
|
||||||
$response = $this->http->get("/lists/{$listId}");
|
|
||||||
|
|
||||||
if (! $response->successful()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $response->json('data');
|
|
||||||
}
|
|
||||||
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
// Subscribers
|
|
||||||
// -------------------------------------------------------------------------
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve subscribers with optional pagination.
|
|
||||||
*
|
|
||||||
* @throws ConnectionException
|
|
||||||
*/
|
|
||||||
public function getSubscribers(int $page = 1, int $perPage = 100): array
|
|
||||||
{
|
|
||||||
return $this->http
|
|
||||||
->get('/subscribers', [
|
|
||||||
'page' => $page,
|
|
||||||
'per_page' => $perPage,
|
|
||||||
])
|
|
||||||
->json('data.results') ?? [];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieve a single subscriber by their Listmonk ID.
|
|
||||||
*
|
|
||||||
* @throws ConnectionException
|
|
||||||
*/
|
|
||||||
public function getSubscriber(int $subscriberId): ?array
|
|
||||||
{
|
|
||||||
$response = $this->http->get("/subscribers/{$subscriberId}");
|
|
||||||
|
|
||||||
if (! $response->successful()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $response->json('data');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find a subscriber by their email address.
|
|
||||||
*
|
|
||||||
* @throws ConnectionException
|
|
||||||
*/
|
|
||||||
public function getSubscriberByEmail(string $email): ?array
|
|
||||||
{
|
|
||||||
$results = $this->http
|
|
||||||
->get('/subscribers', ['query' => "subscribers.email = '{$email}'"])
|
|
||||||
->json('data.results') ?? [];
|
|
||||||
|
|
||||||
return $results[0] ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new subscriber and enrol them in the given lists.
|
|
||||||
*
|
|
||||||
* @param array<int> $listIds IDs of the lists to subscribe to.
|
|
||||||
* @param array<string, mixed> $attribs Custom attributes (e.g. language preference).
|
|
||||||
*
|
|
||||||
* @throws ConnectionException
|
|
||||||
*/
|
|
||||||
public function createSubscriber(
|
|
||||||
string $email,
|
|
||||||
string $name,
|
|
||||||
array $listIds = [],
|
|
||||||
array $attribs = [],
|
|
||||||
string $status = 'enabled',
|
|
||||||
): ?array {
|
|
||||||
$response = $this->http->post('/subscribers', [
|
|
||||||
'email' => $email,
|
|
||||||
'name' => $name,
|
|
||||||
'status' => $status,
|
|
||||||
'lists' => $listIds,
|
|
||||||
'attribs' => $attribs,
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (! $response->successful()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $response->json('data');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update an existing subscriber's information.
|
|
||||||
*
|
|
||||||
* @param array<int> $listIds
|
|
||||||
* @param array<string, mixed> $attribs
|
|
||||||
*
|
|
||||||
* @throws ConnectionException
|
|
||||||
*/
|
|
||||||
public function updateSubscriber(
|
|
||||||
int $subscriberId,
|
|
||||||
string $email,
|
|
||||||
string $name,
|
|
||||||
array $listIds = [],
|
|
||||||
array $attribs = [],
|
|
||||||
string $status = 'enabled',
|
|
||||||
): bool {
|
|
||||||
$response = $this->http->put("/subscribers/{$subscriberId}", [
|
|
||||||
'email' => $email,
|
|
||||||
'name' => $name,
|
|
||||||
'status' => $status,
|
|
||||||
'lists' => $listIds,
|
|
||||||
'attribs' => $attribs,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return $response->successful();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Subscribe or unsubscribe a set of subscribers from lists.
|
|
||||||
*
|
|
||||||
* @param array<int> $subscriberIds
|
|
||||||
* @param array<int> $listIds
|
|
||||||
* @param string $action subscribe | unsubscribe
|
|
||||||
* @param string $status confirmed | unconfirmed
|
|
||||||
*
|
|
||||||
* @throws ConnectionException
|
|
||||||
*/
|
|
||||||
public function updateSubscriberLists(
|
|
||||||
array $subscriberIds,
|
|
||||||
array $listIds,
|
|
||||||
string $action = 'subscribe',
|
|
||||||
string $status = 'confirmed',
|
|
||||||
): bool {
|
|
||||||
$response = $this->http->put('/subscribers/lists', [
|
|
||||||
'ids' => $subscriberIds,
|
|
||||||
'action' => $action,
|
|
||||||
'status' => $status,
|
|
||||||
'list_ids' => $listIds,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return $response->successful();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a subscriber to the blocklist.
|
|
||||||
*
|
|
||||||
* @throws ConnectionException
|
|
||||||
*/
|
|
||||||
public function blocklistSubscriber(int $subscriberId): bool
|
|
||||||
{
|
|
||||||
return $this->http
|
|
||||||
->put("/subscribers/{$subscriberId}/blocklist")
|
|
||||||
->successful();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Permanently delete a subscriber.
|
|
||||||
*
|
|
||||||
* @throws ConnectionException
|
|
||||||
*/
|
|
||||||
public function deleteSubscriber(int $subscriberId): bool
|
|
||||||
{
|
|
||||||
return $this->http
|
|
||||||
->delete("/subscribers/{$subscriberId}")
|
|
||||||
->successful();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send an opt-in confirmation email to a subscriber.
|
|
||||||
*
|
|
||||||
* @throws ConnectionException
|
|
||||||
*/
|
|
||||||
public function sendOptin(int $subscriberId): bool
|
|
||||||
{
|
|
||||||
return $this->http
|
|
||||||
->post("/subscribers/{$subscriberId}/optin")
|
|
||||||
->successful();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ use App\Events\MemberRegistered;
|
|||||||
use App\Models\Member;
|
use App\Models\Member;
|
||||||
use App\Models\MemberGroup;
|
use App\Models\MemberGroup;
|
||||||
use App\Models\Package;
|
use App\Models\Package;
|
||||||
|
use App\Notifications\MemberDeactivatedAdminNotification;
|
||||||
|
use App\Notifications\MemberDeactivatedMemberNotification;
|
||||||
|
use App\Notifications\MemberNewRequestAdminNotification;
|
||||||
|
use Illuminate\Support\Facades\Notification;
|
||||||
|
|
||||||
class MemberService
|
class MemberService
|
||||||
{
|
{
|
||||||
@@ -48,6 +52,9 @@ class MemberService
|
|||||||
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
Notification::route('mail', config('app.admin_email'))
|
||||||
|
->notify(new MemberNewRequestAdminNotification($member, $package, (float) $data['amount']));
|
||||||
|
|
||||||
event(new MemberRegistered($member));
|
event(new MemberRegistered($member));
|
||||||
|
|
||||||
return $member;
|
return $member;
|
||||||
@@ -58,14 +65,16 @@ class MemberService
|
|||||||
*/
|
*/
|
||||||
public function deactivateMember(Member $member): void
|
public function deactivateMember(Member $member): void
|
||||||
{
|
{
|
||||||
// todo: send email to member + admin
|
|
||||||
$member->update(['status' => 'excluded']);
|
$member->update(['status' => 'excluded']);
|
||||||
$membership = $member->memberships()
|
$membership = $member->memberships()
|
||||||
->where('status', 'active')->first();
|
->where('status', 'active')->first();
|
||||||
$membership->update(['status' => 'inactive']);
|
$membership->update(['status' => 'inactive']);
|
||||||
|
|
||||||
// On détache les services côté Roxane - à tester
|
|
||||||
$membership->services()->detach();
|
$membership->services()->detach();
|
||||||
|
|
||||||
|
$member->notify(new MemberDeactivatedMemberNotification($member));
|
||||||
|
|
||||||
|
Notification::route('mail', config('app.admin_email'))
|
||||||
|
->notify(new MemberDeactivatedAdminNotification($member));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -118,6 +118,8 @@ return [
|
|||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
'admin_email' => env('ADMIN_EMAIL'),
|
||||||
|
|
||||||
'maintenance' => [
|
'maintenance' => [
|
||||||
'driver' => env('APP_MAINTENANCE_DRIVER', 'file'),
|
'driver' => env('APP_MAINTENANCE_DRIVER', 'file'),
|
||||||
'store' => env('APP_MAINTENANCE_STORE', 'database'),
|
'store' => env('APP_MAINTENANCE_STORE', 'database'),
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<?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('listmonks_members', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('member_id')->constrained('members')->onDelete('NO ACTION');
|
||||||
|
$table->unsignedInteger('listmonk_user_id')->nullable();
|
||||||
|
$table->json('data')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('listmonks_members');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -47,6 +47,89 @@ class NotificationTemplateSeeder extends Seeder
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
NotificationTemplate::updateOrCreate(
|
||||||
|
['identifier' => 'member_new_request_admin'],
|
||||||
|
[
|
||||||
|
'name' => 'Nouvelle demande d\'adhésion — admin',
|
||||||
|
'subject' => 'Nouvelle demande d\'adhésion : {member_name}',
|
||||||
|
'body' => '<p>Une nouvelle demande d\'adhésion a été reçue.</p>'
|
||||||
|
.'<p>'
|
||||||
|
.'<strong>Nom :</strong> {member_name}<br>'
|
||||||
|
.'<strong>Email :</strong> {member_email}<br>'
|
||||||
|
.'<strong>Téléphone :</strong> {member_phone}<br>'
|
||||||
|
.'<strong>Adresse :</strong> {member_address}<br>'
|
||||||
|
.'<strong>Formule :</strong> {package_name}<br>'
|
||||||
|
.'<strong>Montant :</strong> {amount} €'
|
||||||
|
.'</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>',
|
||||||
|
'variables' => [
|
||||||
|
'member_name' => 'Nom complet du membre',
|
||||||
|
'member_email' => 'Adresse email du membre',
|
||||||
|
'member_phone' => 'Téléphone du membre',
|
||||||
|
'member_address' => 'Adresse postale du membre',
|
||||||
|
'package_name' => 'Nom de la formule choisie',
|
||||||
|
'amount' => 'Montant de la cotisation',
|
||||||
|
'member_url' => 'URL de la fiche dans le back office',
|
||||||
|
'app_name' => 'Nom de l\'application',
|
||||||
|
],
|
||||||
|
'is_active' => true,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
NotificationTemplate::updateOrCreate(
|
||||||
|
['identifier' => 'contact_new_request'],
|
||||||
|
[
|
||||||
|
'name' => 'Nouvelle demande de contact',
|
||||||
|
'subject' => 'Nouvelle demande de contact — {app_name}',
|
||||||
|
'body' => '<p>Une nouvelle demande de contact a été reçue.</p>'
|
||||||
|
.'<p><strong>Nom :</strong> {contact_name}<br>'
|
||||||
|
.'<strong>Email :</strong> {contact_email}<br>'
|
||||||
|
.'<strong>Sujet :</strong> {contact_subject}</p>'
|
||||||
|
.'<p><strong>Message :</strong><br>{contact_message}</p>',
|
||||||
|
'variables' => [
|
||||||
|
'contact_name' => 'Nom complet de l\'expéditeur',
|
||||||
|
'contact_email' => 'Adresse email de l\'expéditeur',
|
||||||
|
'contact_subject' => 'Sujet du message',
|
||||||
|
'contact_message' => 'Contenu du message',
|
||||||
|
'app_name' => 'Nom de l\'application',
|
||||||
|
],
|
||||||
|
'is_active' => true,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
NotificationTemplate::updateOrCreate(
|
||||||
|
['identifier' => 'member_deactivated_member'],
|
||||||
|
[
|
||||||
|
'name' => 'Compte membre désactivé — membre',
|
||||||
|
'subject' => 'Votre compte {app_name} a été désactivé',
|
||||||
|
'body' => '<p>Bonjour {member_name},</p>'
|
||||||
|
.'<p>Votre compte a été désactivé. Vos services associés ne sont plus accessibles.</p>'
|
||||||
|
.'<p>Pour toute question, n\'hésitez pas à nous contacter.</p>',
|
||||||
|
'variables' => [
|
||||||
|
'member_name' => 'Nom complet du membre',
|
||||||
|
'app_name' => 'Nom de l\'application',
|
||||||
|
],
|
||||||
|
'is_active' => true,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
NotificationTemplate::updateOrCreate(
|
||||||
|
['identifier' => 'member_deactivated_admin'],
|
||||||
|
[
|
||||||
|
'name' => 'Compte membre désactivé — admin',
|
||||||
|
'subject' => 'Compte désactivé : {member_name}',
|
||||||
|
'body' => '<p>Le compte du membre suivant a été désactivé.</p>'
|
||||||
|
.'<p><strong>Nom :</strong> {member_name}<br>'
|
||||||
|
.'<strong>Email :</strong> {member_email}</p>',
|
||||||
|
'variables' => [
|
||||||
|
'member_name' => 'Nom complet du membre',
|
||||||
|
'member_email' => 'Adresse email du membre',
|
||||||
|
'app_name' => 'Nom de l\'application',
|
||||||
|
],
|
||||||
|
'is_active' => true,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
NotificationTemplate::updateOrCreate(
|
NotificationTemplate::updateOrCreate(
|
||||||
['identifier' => 'admin_password_reset'],
|
['identifier' => 'admin_password_reset'],
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -46,6 +46,13 @@ return [
|
|||||||
'dry_run_label' => 'Simulation mode (dry-run)',
|
'dry_run_label' => 'Simulation mode (dry-run)',
|
||||||
'dry_run_helper' => 'Simulates the operation without making any changes.',
|
'dry_run_helper' => 'Simulates the operation without making any changes.',
|
||||||
],
|
],
|
||||||
|
'listmonk' => [
|
||||||
|
'heading' => 'Listmonk',
|
||||||
|
'description' => 'Link members to their Listmonk subscriber accounts (@retzien.fr).',
|
||||||
|
'action_label' => 'Listmonk',
|
||||||
|
'modal_heading' => 'Listmonk Synchronisation',
|
||||||
|
'modal_description' => 'Link members to their Listmonk subscriber accounts using their @retzien.fr address.',
|
||||||
|
],
|
||||||
'services' => [
|
'services' => [
|
||||||
'heading' => 'Member Services',
|
'heading' => 'Member Services',
|
||||||
'description' => 'Synchronise services associated with active members.',
|
'description' => 'Synchronise services associated with active members.',
|
||||||
|
|||||||
@@ -46,6 +46,13 @@ return [
|
|||||||
'dry_run_label' => 'Mode simulation (dry-run)',
|
'dry_run_label' => 'Mode simulation (dry-run)',
|
||||||
'dry_run_helper' => 'Simule l\'opération sans effectuer de modifications.',
|
'dry_run_helper' => 'Simule l\'opération sans effectuer de modifications.',
|
||||||
],
|
],
|
||||||
|
'listmonk' => [
|
||||||
|
'heading' => 'Listmonk',
|
||||||
|
'description' => 'Lie les membres à leurs comptes abonnés Listmonk (@retzien.fr).',
|
||||||
|
'action_label' => 'Listmonk',
|
||||||
|
'modal_heading' => 'Synchronisation Listmonk',
|
||||||
|
'modal_description' => 'Lie les membres à leurs comptes abonnés Listmonk en se basant sur leur adresse @retzien.fr.',
|
||||||
|
],
|
||||||
'services' => [
|
'services' => [
|
||||||
'heading' => 'Services membres',
|
'heading' => 'Services membres',
|
||||||
'description' => 'Synchronise les services associés aux membres actifs.',
|
'description' => 'Synchronise les services associés aux membres actifs.',
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { queryParams, type RouteQueryOptions, type RouteDefinition, type RouteFormDefinition } from './../../../../../wayfinder'
|
import { queryParams, type RouteQueryOptions, type RouteDefinition, type RouteFormDefinition } from './../../../../../wayfinder'
|
||||||
/**
|
/**
|
||||||
* @see \App\Http\Controllers\Forms\ContactFormController::create
|
* @see \App\Http\Controllers\Forms\ContactFormController::create
|
||||||
* @see app/Http/Controllers/Forms/ContactFormController.php:17
|
* @see app/Http/Controllers/Forms/ContactFormController.php:16
|
||||||
* @route '/contact'
|
* @route '/contact'
|
||||||
*/
|
*/
|
||||||
export const create = (options?: RouteQueryOptions): RouteDefinition<'get'> => ({
|
export const create = (options?: RouteQueryOptions): RouteDefinition<'get'> => ({
|
||||||
@@ -16,7 +16,7 @@ create.definition = {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @see \App\Http\Controllers\Forms\ContactFormController::create
|
* @see \App\Http\Controllers\Forms\ContactFormController::create
|
||||||
* @see app/Http/Controllers/Forms/ContactFormController.php:17
|
* @see app/Http/Controllers/Forms/ContactFormController.php:16
|
||||||
* @route '/contact'
|
* @route '/contact'
|
||||||
*/
|
*/
|
||||||
create.url = (options?: RouteQueryOptions) => {
|
create.url = (options?: RouteQueryOptions) => {
|
||||||
@@ -25,7 +25,7 @@ create.url = (options?: RouteQueryOptions) => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @see \App\Http\Controllers\Forms\ContactFormController::create
|
* @see \App\Http\Controllers\Forms\ContactFormController::create
|
||||||
* @see app/Http/Controllers/Forms/ContactFormController.php:17
|
* @see app/Http/Controllers/Forms/ContactFormController.php:16
|
||||||
* @route '/contact'
|
* @route '/contact'
|
||||||
*/
|
*/
|
||||||
create.get = (options?: RouteQueryOptions): RouteDefinition<'get'> => ({
|
create.get = (options?: RouteQueryOptions): RouteDefinition<'get'> => ({
|
||||||
@@ -35,7 +35,7 @@ create.get = (options?: RouteQueryOptions): RouteDefinition<'get'> => ({
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @see \App\Http\Controllers\Forms\ContactFormController::create
|
* @see \App\Http\Controllers\Forms\ContactFormController::create
|
||||||
* @see app/Http/Controllers/Forms/ContactFormController.php:17
|
* @see app/Http/Controllers/Forms/ContactFormController.php:16
|
||||||
* @route '/contact'
|
* @route '/contact'
|
||||||
*/
|
*/
|
||||||
create.head = (options?: RouteQueryOptions): RouteDefinition<'head'> => ({
|
create.head = (options?: RouteQueryOptions): RouteDefinition<'head'> => ({
|
||||||
@@ -45,7 +45,7 @@ create.head = (options?: RouteQueryOptions): RouteDefinition<'head'> => ({
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @see \App\Http\Controllers\Forms\ContactFormController::create
|
* @see \App\Http\Controllers\Forms\ContactFormController::create
|
||||||
* @see app/Http/Controllers/Forms/ContactFormController.php:17
|
* @see app/Http/Controllers/Forms/ContactFormController.php:16
|
||||||
* @route '/contact'
|
* @route '/contact'
|
||||||
*/
|
*/
|
||||||
const createForm = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({
|
const createForm = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({
|
||||||
@@ -55,7 +55,7 @@ const createForm = (options?: RouteQueryOptions): RouteFormDefinition<'get'> =>
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @see \App\Http\Controllers\Forms\ContactFormController::create
|
* @see \App\Http\Controllers\Forms\ContactFormController::create
|
||||||
* @see app/Http/Controllers/Forms/ContactFormController.php:17
|
* @see app/Http/Controllers/Forms/ContactFormController.php:16
|
||||||
* @route '/contact'
|
* @route '/contact'
|
||||||
*/
|
*/
|
||||||
createForm.get = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({
|
createForm.get = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({
|
||||||
@@ -65,7 +65,7 @@ createForm.get = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @see \App\Http\Controllers\Forms\ContactFormController::create
|
* @see \App\Http\Controllers\Forms\ContactFormController::create
|
||||||
* @see app/Http/Controllers/Forms/ContactFormController.php:17
|
* @see app/Http/Controllers/Forms/ContactFormController.php:16
|
||||||
* @route '/contact'
|
* @route '/contact'
|
||||||
*/
|
*/
|
||||||
createForm.head = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({
|
createForm.head = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { queryParams, type RouteQueryOptions, type RouteDefinition, type RouteFormDefinition } from './../../../../../wayfinder'
|
import { queryParams, type RouteQueryOptions, type RouteDefinition, type RouteFormDefinition } from './../../../../../wayfinder'
|
||||||
/**
|
/**
|
||||||
* @see \App\Http\Controllers\Forms\MembershipFormController::create
|
* @see \App\Http\Controllers\Forms\MembershipFormController::create
|
||||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:21
|
* @see app/Http/Controllers/Forms/MembershipFormController.php:18
|
||||||
* @route '/membership'
|
* @route '/membership'
|
||||||
*/
|
*/
|
||||||
export const create = (options?: RouteQueryOptions): RouteDefinition<'get'> => ({
|
export const create = (options?: RouteQueryOptions): RouteDefinition<'get'> => ({
|
||||||
@@ -16,7 +16,7 @@ create.definition = {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @see \App\Http\Controllers\Forms\MembershipFormController::create
|
* @see \App\Http\Controllers\Forms\MembershipFormController::create
|
||||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:21
|
* @see app/Http/Controllers/Forms/MembershipFormController.php:18
|
||||||
* @route '/membership'
|
* @route '/membership'
|
||||||
*/
|
*/
|
||||||
create.url = (options?: RouteQueryOptions) => {
|
create.url = (options?: RouteQueryOptions) => {
|
||||||
@@ -25,7 +25,7 @@ create.url = (options?: RouteQueryOptions) => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @see \App\Http\Controllers\Forms\MembershipFormController::create
|
* @see \App\Http\Controllers\Forms\MembershipFormController::create
|
||||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:21
|
* @see app/Http/Controllers/Forms/MembershipFormController.php:18
|
||||||
* @route '/membership'
|
* @route '/membership'
|
||||||
*/
|
*/
|
||||||
create.get = (options?: RouteQueryOptions): RouteDefinition<'get'> => ({
|
create.get = (options?: RouteQueryOptions): RouteDefinition<'get'> => ({
|
||||||
@@ -35,7 +35,7 @@ create.get = (options?: RouteQueryOptions): RouteDefinition<'get'> => ({
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @see \App\Http\Controllers\Forms\MembershipFormController::create
|
* @see \App\Http\Controllers\Forms\MembershipFormController::create
|
||||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:21
|
* @see app/Http/Controllers/Forms/MembershipFormController.php:18
|
||||||
* @route '/membership'
|
* @route '/membership'
|
||||||
*/
|
*/
|
||||||
create.head = (options?: RouteQueryOptions): RouteDefinition<'head'> => ({
|
create.head = (options?: RouteQueryOptions): RouteDefinition<'head'> => ({
|
||||||
@@ -45,7 +45,7 @@ create.head = (options?: RouteQueryOptions): RouteDefinition<'head'> => ({
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @see \App\Http\Controllers\Forms\MembershipFormController::create
|
* @see \App\Http\Controllers\Forms\MembershipFormController::create
|
||||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:21
|
* @see app/Http/Controllers/Forms/MembershipFormController.php:18
|
||||||
* @route '/membership'
|
* @route '/membership'
|
||||||
*/
|
*/
|
||||||
const createForm = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({
|
const createForm = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({
|
||||||
@@ -55,7 +55,7 @@ const createForm = (options?: RouteQueryOptions): RouteFormDefinition<'get'> =>
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @see \App\Http\Controllers\Forms\MembershipFormController::create
|
* @see \App\Http\Controllers\Forms\MembershipFormController::create
|
||||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:21
|
* @see app/Http/Controllers/Forms/MembershipFormController.php:18
|
||||||
* @route '/membership'
|
* @route '/membership'
|
||||||
*/
|
*/
|
||||||
createForm.get = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({
|
createForm.get = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({
|
||||||
@@ -65,7 +65,7 @@ createForm.get = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @see \App\Http\Controllers\Forms\MembershipFormController::create
|
* @see \App\Http\Controllers\Forms\MembershipFormController::create
|
||||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:21
|
* @see app/Http/Controllers/Forms/MembershipFormController.php:18
|
||||||
* @route '/membership'
|
* @route '/membership'
|
||||||
*/
|
*/
|
||||||
createForm.head = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({
|
createForm.head = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({
|
||||||
@@ -82,7 +82,7 @@ create.form = createForm
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @see \App\Http\Controllers\Forms\MembershipFormController::store
|
* @see \App\Http\Controllers\Forms\MembershipFormController::store
|
||||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:35
|
* @see app/Http/Controllers/Forms/MembershipFormController.php:42
|
||||||
* @route '/membership'
|
* @route '/membership'
|
||||||
*/
|
*/
|
||||||
export const store = (options?: RouteQueryOptions): RouteDefinition<'post'> => ({
|
export const store = (options?: RouteQueryOptions): RouteDefinition<'post'> => ({
|
||||||
@@ -97,7 +97,7 @@ store.definition = {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @see \App\Http\Controllers\Forms\MembershipFormController::store
|
* @see \App\Http\Controllers\Forms\MembershipFormController::store
|
||||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:35
|
* @see app/Http/Controllers/Forms/MembershipFormController.php:42
|
||||||
* @route '/membership'
|
* @route '/membership'
|
||||||
*/
|
*/
|
||||||
store.url = (options?: RouteQueryOptions) => {
|
store.url = (options?: RouteQueryOptions) => {
|
||||||
@@ -106,7 +106,7 @@ store.url = (options?: RouteQueryOptions) => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @see \App\Http\Controllers\Forms\MembershipFormController::store
|
* @see \App\Http\Controllers\Forms\MembershipFormController::store
|
||||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:35
|
* @see app/Http/Controllers/Forms/MembershipFormController.php:42
|
||||||
* @route '/membership'
|
* @route '/membership'
|
||||||
*/
|
*/
|
||||||
store.post = (options?: RouteQueryOptions): RouteDefinition<'post'> => ({
|
store.post = (options?: RouteQueryOptions): RouteDefinition<'post'> => ({
|
||||||
@@ -116,7 +116,7 @@ store.post = (options?: RouteQueryOptions): RouteDefinition<'post'> => ({
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @see \App\Http\Controllers\Forms\MembershipFormController::store
|
* @see \App\Http\Controllers\Forms\MembershipFormController::store
|
||||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:35
|
* @see app/Http/Controllers/Forms/MembershipFormController.php:42
|
||||||
* @route '/membership'
|
* @route '/membership'
|
||||||
*/
|
*/
|
||||||
const storeForm = (options?: RouteQueryOptions): RouteFormDefinition<'post'> => ({
|
const storeForm = (options?: RouteQueryOptions): RouteFormDefinition<'post'> => ({
|
||||||
@@ -126,7 +126,7 @@ const storeForm = (options?: RouteQueryOptions): RouteFormDefinition<'post'> =>
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @see \App\Http\Controllers\Forms\MembershipFormController::store
|
* @see \App\Http\Controllers\Forms\MembershipFormController::store
|
||||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:35
|
* @see app/Http/Controllers/Forms/MembershipFormController.php:42
|
||||||
* @route '/membership'
|
* @route '/membership'
|
||||||
*/
|
*/
|
||||||
storeForm.post = (options?: RouteQueryOptions): RouteFormDefinition<'post'> => ({
|
storeForm.post = (options?: RouteQueryOptions): RouteFormDefinition<'post'> => ({
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import {useEffect, useState} from "react";
|
import { useEffect, useState } from 'react';
|
||||||
import {Form, Head, usePage} from "@inertiajs/react";
|
import { Form, Head, usePage } from '@inertiajs/react';
|
||||||
import {LoaderCircle} from 'lucide-react';
|
import { LoaderCircle } from 'lucide-react';
|
||||||
import ContactFormController from "@/actions/App/Http/Controllers/Forms/ContactFormController";
|
import ContactFormController from '@/actions/App/Http/Controllers/Forms/ContactFormController';
|
||||||
import {Label} from "@/components/ui/label";
|
import { Label } from '@/components/ui/label';
|
||||||
import {Input} from "@/components/ui/input";
|
import { Input } from '@/components/ui/input';
|
||||||
import InputError from "@/components/input-error";
|
import InputError from '@/components/input-error';
|
||||||
import {Button} from "@/components/ui/button";
|
import { Button } from '@/components/ui/button';
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
@@ -13,16 +13,18 @@ import {
|
|||||||
SelectItem,
|
SelectItem,
|
||||||
SelectLabel,
|
SelectLabel,
|
||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue
|
SelectValue,
|
||||||
} from "@/components/ui/select";
|
} from '@/components/ui/select';
|
||||||
import {Textarea} from "@/components/ui/textarea";
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
import NavGuestLayout from "@/layouts/nav-guest-layout";
|
import NavGuestLayout from '@/layouts/nav-guest-layout';
|
||||||
import {PageProps} from "@/types";
|
import { PageProps } from '@/types';
|
||||||
import {FlashMessage} from "@/components/flash-message";
|
import { FlashMessage } from '@/components/flash-message';
|
||||||
|
import { Container } from '@/components/common/Container';
|
||||||
|
import { SectionHeading } from '@/components/common/SectionHeading';
|
||||||
|
import { Footer } from '@/components/footer';
|
||||||
|
|
||||||
export default function Contact() {
|
export default function Contact() {
|
||||||
const {flash} = usePage().props as PageProps;
|
const { flash, captcha_question } = usePage().props as PageProps;
|
||||||
|
|
||||||
const [showFlashMessage, setFlashMessage] = useState(!!flash);
|
const [showFlashMessage, setFlashMessage] = useState(!!flash);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -33,167 +35,129 @@ export default function Contact() {
|
|||||||
}
|
}
|
||||||
}, [flash]);
|
}, [flash]);
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head title="Nous contacter"/>
|
<Head title="Nous contacter" />
|
||||||
<div
|
<div className="flex flex-col min-h-screen bg-white dark:bg-[#0a0a0a] text-[#1b1b18] dark:text-[#EDEDEC]">
|
||||||
className="flex flex-col items-center bg-[#F5F5F5] p-6 text-[#1b1b18] lg:justify-center lg:p-8 dark:bg-[#0a0a0a]">
|
<div className="flex flex-col items-center px-4">
|
||||||
<NavGuestLayout/>
|
<NavGuestLayout />
|
||||||
|
</div>
|
||||||
|
|
||||||
<section className="flex flex-col h-screen items-center mt-15 gap-4">
|
<main className="flex-1 py-12">
|
||||||
<div>
|
<Container className="flex flex-col gap-10">
|
||||||
<h1>Nous contacter</h1>
|
<SectionHeading
|
||||||
<p>
|
title="Nous contacter"
|
||||||
Vous désirez nous contacter, merci de remplir le formulaire suivant :
|
color="primary"
|
||||||
</p>
|
subtitle="Une question, une remarque ? Remplissez le formulaire ci-dessous, nous vous répondrons dans les plus brefs délais."
|
||||||
</div>
|
align="left"
|
||||||
|
/>
|
||||||
|
|
||||||
{showFlashMessage && (
|
{showFlashMessage && <FlashMessage messages={flash ?? {}} />}
|
||||||
<FlashMessage messages={flash ?? {}} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
<Form
|
<Form
|
||||||
{...ContactFormController.store.form()}
|
{...ContactFormController.store.form()}
|
||||||
resetOnSuccess
|
resetOnSuccess
|
||||||
disableWhileProcessing
|
disableWhileProcessing
|
||||||
>
|
>
|
||||||
{({processing, errors}) => (
|
{({ processing, errors }) => (
|
||||||
<div className="lg:w-5xl px-10">
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8 max-w-4xl">
|
||||||
<div className="flex gap-6 w-full">
|
|
||||||
<div className="w-1/2">
|
{/* Left — Identité + adresse */}
|
||||||
<div className="grid gap-2 my-4">
|
<div className="bg-white dark:bg-[#171717] rounded-2xl border-3 border-black p-6 shadow-[4px_4px_0px_rgba(0,0,0,1)] flex flex-col gap-4">
|
||||||
<Label htmlFor="lastname">Nom*</Label>
|
<h2 className="text-lg font-semibold text-primary">Vos informations</h2>
|
||||||
<Input
|
|
||||||
id="lastname"
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
type="text"
|
<div className="grid gap-1">
|
||||||
required
|
<Label htmlFor="lastname">Nom*</Label>
|
||||||
autoFocus
|
<Input id="lastname" name="lastname" type="text" required tabIndex={1} autoComplete="family-name" placeholder="Votre nom" />
|
||||||
tabIndex={1}
|
<InputError message={errors.lastname} />
|
||||||
autoComplete="lastname"
|
</div>
|
||||||
name="lastname"
|
<div className="grid gap-1">
|
||||||
placeholder="Nom"
|
<Label htmlFor="firstname">Prénom*</Label>
|
||||||
/>
|
<Input id="firstname" name="firstname" type="text" required tabIndex={2} autoComplete="given-name" placeholder="Votre prénom" />
|
||||||
<InputError
|
<InputError message={errors.firstname} />
|
||||||
message={errors.name}
|
</div>
|
||||||
className="mt-2"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-2 my-4">
|
<div className="grid gap-1">
|
||||||
<Label htmlFor="firstname">Prénom*</Label>
|
<Label htmlFor="email">Adresse e-mail*</Label>
|
||||||
<Input
|
<Input id="email" name="email" type="email" required tabIndex={3} autoComplete="email" placeholder="email@exemple.com" />
|
||||||
id="firstname"
|
<InputError message={errors.email} />
|
||||||
type="text"
|
|
||||||
required
|
|
||||||
autoFocus
|
|
||||||
tabIndex={2}
|
|
||||||
autoComplete="firstname"
|
|
||||||
name="firstname"
|
|
||||||
placeholder="Prénom"
|
|
||||||
/>
|
|
||||||
<InputError
|
|
||||||
message={errors.name}
|
|
||||||
className="mt-2"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-2 my-4">
|
<div className="grid gap-1">
|
||||||
<Label htmlFor="email">Adresse Mail*</Label>
|
<Label htmlFor="address">
|
||||||
<Input
|
Adresse postale <span className="text-muted-foreground text-xs">(facultatif)</span>
|
||||||
id="email"
|
</Label>
|
||||||
type="email"
|
<Input id="address" name="address" type="text" tabIndex={4} autoComplete="street-address" placeholder="Votre adresse postale" />
|
||||||
required
|
<InputError message={errors.address} />
|
||||||
tabIndex={3}
|
|
||||||
autoComplete="email"
|
|
||||||
name="email"
|
|
||||||
placeholder="email@exemple.com"
|
|
||||||
/>
|
|
||||||
<InputError message={errors.email}/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid gap-2 my-4">
|
|
||||||
<Label htmlFor="address">Votre adresse postale</Label>
|
|
||||||
<Input
|
|
||||||
id="address"
|
|
||||||
type="text"
|
|
||||||
required
|
|
||||||
autoFocus
|
|
||||||
tabIndex={4}
|
|
||||||
autoComplete="address"
|
|
||||||
name="address"
|
|
||||||
placeholder="Votre adresse postale (facultatif)"
|
|
||||||
/>
|
|
||||||
<InputError
|
|
||||||
message={errors.name}
|
|
||||||
className="mt-2"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="w-1/2">
|
{/* Right — Message + captcha + submit */}
|
||||||
<div className="grid gap-2 my-4">
|
<div className="flex flex-col gap-6">
|
||||||
<Label htmlFor="subject">Objet de votre demande*</Label>
|
<div className="bg-white dark:bg-[#171717] rounded-2xl border-3 border-black p-6 shadow-[4px_4px_0px_rgba(0,0,0,1)] flex flex-col gap-4">
|
||||||
<Select name="subject" required>
|
<h2 className="text-lg font-semibold text-primary">Votre message</h2>
|
||||||
<SelectTrigger tabIndex={5}>
|
|
||||||
<SelectValue placeholder="Sélectionnez un objet"/>
|
<div className="grid gap-1">
|
||||||
</SelectTrigger>
|
<Label htmlFor="subject">Objet*</Label>
|
||||||
<SelectContent>
|
<Select name="subject" required>
|
||||||
<SelectGroup>
|
<SelectTrigger tabIndex={5}>
|
||||||
<SelectLabel>Objets</SelectLabel>
|
<SelectValue placeholder="Sélectionnez un objet" />
|
||||||
<SelectItem value="info-request">Demande
|
</SelectTrigger>
|
||||||
d'informations</SelectItem>
|
<SelectContent>
|
||||||
<SelectItem value="service-request">Services</SelectItem>
|
<SelectGroup>
|
||||||
<SelectItem value="other">Autres</SelectItem>
|
<SelectLabel>Objets</SelectLabel>
|
||||||
</SelectGroup>
|
<SelectItem value="info-request">Demande d'informations</SelectItem>
|
||||||
</SelectContent>
|
<SelectItem value="service-request">Services</SelectItem>
|
||||||
</Select>
|
<SelectItem value="other">Autres</SelectItem>
|
||||||
|
</SelectGroup>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<InputError message={errors.subject} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-1">
|
||||||
|
<Label htmlFor="message">Message*</Label>
|
||||||
|
<Textarea id="message" name="message" tabIndex={6} required placeholder="Entrez votre message ici..." className="h-32 resize-none" />
|
||||||
|
<InputError message={errors.message} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-2 my-4">
|
<div className="bg-white dark:bg-[#171717] rounded-2xl border-3 border-black p-6 shadow-[4px_4px_0px_rgba(0,0,0,1)] flex flex-col gap-5">
|
||||||
<Label htmlFor="message">Votre message</Label>
|
<div className="grid gap-1">
|
||||||
<Textarea
|
<Label htmlFor="captcha" className="font-semibold">{captcha_question}</Label>
|
||||||
className="h-28"
|
<Input
|
||||||
id="message"
|
id="captcha"
|
||||||
name="message"
|
name="captcha"
|
||||||
tabIndex={6}
|
type="text"
|
||||||
required
|
tabIndex={7}
|
||||||
placeholder="Entrez votre message ici..."
|
placeholder="Votre réponse"
|
||||||
/>
|
autoComplete="off"
|
||||||
</div>
|
className="max-w-[180px]"
|
||||||
|
/>
|
||||||
|
<InputError message={errors.captcha} />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-2 my-4">
|
<Button
|
||||||
<Label htmlFor="captcha">Captcha</Label>
|
type="submit"
|
||||||
<Input
|
variant="secondary"
|
||||||
id="captcha"
|
tabIndex={8}
|
||||||
type="text"
|
className="w-full border-3 border-black shadow-[4px_4px_0px_rgba(0,0,0,1)] hover:shadow-none hover:translate-x-0.5 hover:translate-y-0.5 font-bold text-base py-5"
|
||||||
autoFocus
|
>
|
||||||
tabIndex={7}
|
{processing && <LoaderCircle className="h-4 w-4 animate-spin" />}
|
||||||
name="captcha"
|
Envoyer
|
||||||
placeholder="Entrez le captcha ci-dessous"
|
</Button>
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mx-auto justify-center">
|
)}
|
||||||
<Button
|
</Form>
|
||||||
variant="outline"
|
</Container>
|
||||||
type="submit"
|
</main>
|
||||||
className="mt-2"
|
|
||||||
tabIndex={8}
|
<Footer />
|
||||||
data-test="register-user-button"
|
|
||||||
>
|
|
||||||
{processing && (
|
|
||||||
<LoaderCircle className="h-4 w-4 animate-spin"/>
|
|
||||||
)}
|
|
||||||
Envoyer
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Form>
|
|
||||||
</section>
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,56 +1,30 @@
|
|||||||
import {Form, Head, usePage} from "@inertiajs/react";
|
import { Form, Head, usePage } from '@inertiajs/react';
|
||||||
import {cn} from "@/lib/utils";
|
import { cn } from '@/lib/utils';
|
||||||
import {CheckCircle2, CheckIcon, LoaderCircle} from 'lucide-react';
|
import { CheckIcon, LoaderCircle } from 'lucide-react';
|
||||||
import MembershipFormController from "@/actions/App/Http/Controllers/Forms/MembershipFormController";
|
import MembershipFormController from '@/actions/App/Http/Controllers/Forms/MembershipFormController';
|
||||||
import {Label} from "@/components/ui/label";
|
import { Label } from '@/components/ui/label';
|
||||||
import {Input} from "@/components/ui/input";
|
import { Input } from '@/components/ui/input';
|
||||||
import InputError from "@/components/input-error";
|
import InputError from '@/components/input-error';
|
||||||
import {Button} from "@/components/ui/button";
|
import { Button } from '@/components/ui/button';
|
||||||
import NavGuestLayout from "@/layouts/nav-guest-layout";
|
import NavGuestLayout from '@/layouts/nav-guest-layout';
|
||||||
import {Checkbox} from "@/components/ui/checkbox";
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
import {useEffect, useState} from "react";
|
import { useEffect, useState } from 'react';
|
||||||
import {PageProps} from "@/types";
|
import { PageProps } from '@/types';
|
||||||
import {FlashMessage} from "@/components/flash-message";
|
import { FlashMessage } from '@/components/flash-message';
|
||||||
|
import { Container } from '@/components/common/Container';
|
||||||
|
import { SectionHeading } from '@/components/common/SectionHeading';
|
||||||
|
import { Footer } from '@/components/footer';
|
||||||
|
|
||||||
export default function Membership() {
|
export default function Membership() {
|
||||||
const {flash, plans} = usePage().props as PageProps
|
const { flash, plans, services, captcha_question } = usePage().props as PageProps;
|
||||||
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 features = [
|
|
||||||
"Boîte Mail",
|
|
||||||
"NextCloud",
|
|
||||||
"Mailing list",
|
|
||||||
"Hébergement de site",
|
|
||||||
"Sondage",
|
|
||||||
"Et plus encore ...",
|
|
||||||
];
|
|
||||||
|
|
||||||
// /!\ Existant à discuter avec client
|
|
||||||
/*const today = new Date();
|
|
||||||
const actualMonth = today.getMonth() + 1;
|
|
||||||
const leftMonths = 12 - actualMonth;*/
|
|
||||||
/*const getAmount = (plan: string | null): number => {
|
|
||||||
if (!plan) return 0;
|
|
||||||
const baseAmount = leftMonths;
|
|
||||||
switch (plan) {
|
|
||||||
case 'custom':
|
|
||||||
return baseAmount;
|
|
||||||
case 'one-year':
|
|
||||||
return baseAmount + 12;
|
|
||||||
case 'two-year':
|
|
||||||
return baseAmount + 24;
|
|
||||||
default:
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
};*/
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (plans && selectedPlan) {
|
if (plans && selectedPlan) {
|
||||||
const plan = plans.find(p => p.identifier === selectedPlan);
|
const plan = plans.find((p) => p.identifier === selectedPlan);
|
||||||
if (plan) {
|
if (plan) setAmount(plan.price);
|
||||||
setAmount(plan.price);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, [selectedPlan, plans]);
|
}, [selectedPlan, plans]);
|
||||||
|
|
||||||
@@ -64,257 +38,196 @@ export default function Membership() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Head title="Adhérer au Retzien Libre"/>
|
<Head title="Adhérer au Retzien Libre" />
|
||||||
<div
|
<div className="flex flex-col min-h-screen bg-white dark:bg-[#0a0a0a] text-[#1b1b18] dark:text-[#EDEDEC]">
|
||||||
className="flex min-h-screen flex-col items-center bg-[#F5F5F5] p-6 text-[#1b1b18] lg:justify-center lg:p-8 dark:bg-[#0a0a0a]">
|
<div className="flex flex-col items-center px-4">
|
||||||
<NavGuestLayout/>
|
<NavGuestLayout />
|
||||||
|
</div>
|
||||||
|
|
||||||
<section className="flex flex-col items-center justify-center gap-4">
|
<main className="flex-1 py-12">
|
||||||
<div>
|
<Container className="flex flex-col gap-10">
|
||||||
<h1>Adhérer au Retzien Libre</h1>
|
<SectionHeading
|
||||||
<p>
|
title="Adhérer au Retzien Libre"
|
||||||
Saisissez vos informations ci-dessous pour créer une demande d'adhésion :
|
color="primary"
|
||||||
</p>
|
subtitle="Rejoignez notre association et accédez à des outils libres, éthiques et respectueux de vos données."
|
||||||
</div>
|
align="left"
|
||||||
|
/>
|
||||||
|
|
||||||
{showFlashMessage && (
|
{showFlashMessage && (
|
||||||
<FlashMessage messages={flash ?? {}}/>
|
<FlashMessage messages={flash ?? {}} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Form
|
<Form
|
||||||
{...MembershipFormController.store.form()}
|
{...MembershipFormController.store.form()}
|
||||||
resetOnSuccess
|
resetOnSuccess
|
||||||
disableWhileProcessing
|
disableWhileProcessing
|
||||||
className="flex flex-col gap-6"
|
>
|
||||||
>
|
{({ processing, errors }) => (
|
||||||
{({processing, errors}) => (
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||||||
<div className="lg:w-5xl px-10">
|
|
||||||
<div className="flex flex-col md:flex-row gap-6 w-full">
|
{/* Left — Personal info */}
|
||||||
<div className="w-full lg:w-1/2">
|
<div className="bg-white dark:bg-[#171717] rounded-2xl border-3 border-black p-6 shadow-[4px_4px_0px_rgba(0,0,0,1)] flex flex-col gap-4">
|
||||||
<div className="grid gap-2 my-4">
|
<h2 className="text-lg font-semibold text-primary">Vos informations</h2>
|
||||||
<Label htmlFor="lastname">Nom*</Label>
|
|
||||||
<Input
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
id="lastname"
|
<div className="grid gap-1">
|
||||||
type="text"
|
<Label htmlFor="lastname">Nom*</Label>
|
||||||
required
|
<Input id="lastname" name="lastname" type="text" required tabIndex={1} autoComplete="family-name" placeholder="Votre nom" />
|
||||||
autoFocus
|
<InputError message={errors.lastname} />
|
||||||
tabIndex={1}
|
</div>
|
||||||
autoComplete="lastname"
|
<div className="grid gap-1">
|
||||||
name="lastname"
|
<Label htmlFor="firstname">Prénom*</Label>
|
||||||
placeholder="Votre Nom"
|
<Input id="firstname" name="firstname" type="text" required tabIndex={2} autoComplete="given-name" placeholder="Votre prénom" />
|
||||||
/>
|
<InputError message={errors.firstname} />
|
||||||
<InputError
|
</div>
|
||||||
message={errors.lastname}
|
|
||||||
className="mt-2"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-2 my-4">
|
|
||||||
<Label htmlFor="firstname">Prénom*</Label>
|
<div className="grid gap-1">
|
||||||
<Input
|
<Label htmlFor="email">Adresse e-mail*</Label>
|
||||||
id="firstname"
|
<Input id="email" name="email" type="email" required tabIndex={3} autoComplete="email" placeholder="email@exemple.com" />
|
||||||
type="text"
|
<InputError message={errors.email} />
|
||||||
required
|
|
||||||
autoFocus
|
|
||||||
tabIndex={2}
|
|
||||||
autoComplete="firstname"
|
|
||||||
name="firstname"
|
|
||||||
placeholder="Votre Prénom"
|
|
||||||
/>
|
|
||||||
<InputError
|
|
||||||
message={errors.firstname}
|
|
||||||
className="mt-2"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-2 my-4">
|
|
||||||
<Label htmlFor="company">Société</Label>
|
<div className="grid gap-1">
|
||||||
<Input
|
|
||||||
id="company"
|
|
||||||
type="text"
|
|
||||||
autoFocus
|
|
||||||
tabIndex={3}
|
|
||||||
autoComplete="company"
|
|
||||||
name="company"
|
|
||||||
placeholder="Votre société"
|
|
||||||
/>
|
|
||||||
<InputError
|
|
||||||
message={errors.firstname}
|
|
||||||
className="mt-2"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="grid gap-2 my-4">
|
|
||||||
<Label htmlFor="email">Adresse Mail*</Label>
|
|
||||||
<Input
|
|
||||||
id="email"
|
|
||||||
type="email"
|
|
||||||
required
|
|
||||||
tabIndex={4}
|
|
||||||
autoComplete="email"
|
|
||||||
name="email"
|
|
||||||
placeholder="email@exemple.com"
|
|
||||||
/>
|
|
||||||
<InputError message={errors.email}/>
|
|
||||||
</div>
|
|
||||||
<div className="grid gap-2 my-4">
|
|
||||||
<Label htmlFor="phone1">Téléphone*</Label>
|
<Label htmlFor="phone1">Téléphone*</Label>
|
||||||
<Input
|
<Input id="phone1" name="phone1" type="tel" required tabIndex={4} autoComplete="tel" placeholder="Votre numéro de téléphone" />
|
||||||
id="phone1"
|
<InputError message={errors.phone1} />
|
||||||
type="phone"
|
|
||||||
required
|
|
||||||
tabIndex={5}
|
|
||||||
autoComplete="phone"
|
|
||||||
name="phone1"
|
|
||||||
placeholder="Votre numéro de téléphone"
|
|
||||||
/>
|
|
||||||
<InputError message={errors.phone}/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-2 my-4">
|
|
||||||
<Label htmlFor="address">Votre adresse*</Label>
|
<div className="grid gap-1">
|
||||||
<Input
|
<Label htmlFor="company">Société <span className="text-muted-foreground text-xs">(facultatif)</span></Label>
|
||||||
id="address"
|
<Input id="company" name="company" type="text" tabIndex={5} autoComplete="organization" placeholder="Votre société" />
|
||||||
type="text"
|
<InputError message={errors.company} />
|
||||||
required
|
|
||||||
autoFocus
|
|
||||||
tabIndex={6}
|
|
||||||
autoComplete="address"
|
|
||||||
name="address"
|
|
||||||
placeholder="Votre adresse"
|
|
||||||
/>
|
|
||||||
<InputError
|
|
||||||
message={errors.address}
|
|
||||||
className="mt-2"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-2 my-4">
|
|
||||||
<Label htmlFor="zipcode">Votre code postale*</Label>
|
<div className="grid gap-1">
|
||||||
<Input
|
<Label htmlFor="address">Adresse*</Label>
|
||||||
id="zipcode"
|
<Input id="address" name="address" type="text" required tabIndex={6} autoComplete="street-address" placeholder="Votre adresse" />
|
||||||
type="text"
|
<InputError message={errors.address} />
|
||||||
required
|
|
||||||
autoFocus
|
|
||||||
tabIndex={7}
|
|
||||||
autoComplete="zipcode"
|
|
||||||
name="zipcode"
|
|
||||||
placeholder="Votre code postale"
|
|
||||||
/>
|
|
||||||
<InputError
|
|
||||||
message={errors.zipcode}
|
|
||||||
className="mt-2"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-2 my-4">
|
|
||||||
<Label htmlFor="city">Votre ville*</Label>
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<Input
|
<div className="grid gap-1">
|
||||||
id="city"
|
<Label htmlFor="zipcode">Code postal*</Label>
|
||||||
type="text"
|
<Input id="zipcode" name="zipcode" type="text" required tabIndex={7} autoComplete="postal-code" placeholder="Code postal" />
|
||||||
required
|
<InputError message={errors.zipcode} />
|
||||||
autoFocus
|
</div>
|
||||||
tabIndex={8}
|
<div className="grid gap-1">
|
||||||
autoComplete="city"
|
<Label htmlFor="city">Ville*</Label>
|
||||||
name="city"
|
<Input id="city" name="city" type="text" required tabIndex={8} autoComplete="address-level2" placeholder="Ville" />
|
||||||
placeholder="Votre ville"
|
<InputError message={errors.city} />
|
||||||
/>
|
</div>
|
||||||
<InputError
|
|
||||||
message={errors.city}
|
|
||||||
className="mt-2"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full lg:w-1/2">
|
|
||||||
<div className="space-y-4">
|
{/* Right — Plan, services, captcha, submit */}
|
||||||
<Label htmlFor="package">Formule d'adhésion*</Label>
|
<div className="flex flex-col gap-6">
|
||||||
<div className="grid grid-cols-3 gap-2 my-4">
|
|
||||||
|
{/* Plan selection */}
|
||||||
|
<div className="bg-white dark:bg-[#171717] rounded-2xl border-3 border-black p-6 shadow-[4px_4px_0px_rgba(0,0,0,1)] flex flex-col gap-4">
|
||||||
|
<h2 className="text-lg font-semibold text-primary">Choisissez votre formule</h2>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
{plans?.map((plan) => (
|
{plans?.map((plan) => (
|
||||||
<button
|
<button
|
||||||
key={plan.id}
|
key={plan.id}
|
||||||
type="button"
|
type="button"
|
||||||
tabIndex={8}
|
tabIndex={9}
|
||||||
onClick={() => setSelectedPlan(plan.identifier)}
|
onClick={() => setSelectedPlan(plan.identifier)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex flex-col items-center justify-center rounded border-3 p-4 transition-colors",
|
'flex items-center justify-between rounded-xl border-3 border-black px-5 py-4 text-left transition-all duration-150',
|
||||||
|
'shadow-[3px_3px_0px_rgba(0,0,0,1)] hover:shadow-none hover:translate-x-0.5 hover:translate-y-0.5',
|
||||||
selectedPlan === plan.identifier
|
selectedPlan === plan.identifier
|
||||||
? "border-primary"
|
? 'bg-primary text-black'
|
||||||
: "border-black hover:border-primary"
|
: 'bg-white dark:bg-[#1a1a1a] hover:bg-primary/20',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<span className="font-semibold">{plan.name}</span>
|
<div className="flex flex-col">
|
||||||
<span className="font-bold text-lg">{plan.price}€</span>
|
<span className="font-bold text-base">{plan.name}</span>
|
||||||
<span className="text-center text-muted-foreground text-xs">
|
{plan.months != null ? (
|
||||||
{plan.description}
|
<span className="text-xs text-muted-foreground">
|
||||||
</span>
|
{plan.months} mois × 1€/mois
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="text-xs text-muted-foreground">{plan.description}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<span className="text-2xl font-black">{plan.price}€</span>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
<input type="hidden" name="package" value={selectedPlan ?? ''} />
|
|
||||||
<input type="hidden" name="amount" value={amount} />
|
|
||||||
</div>
|
|
||||||
<div className="flex-col gap-6 ">
|
|
||||||
<div className="text-center">
|
|
||||||
<p className="text-center font-semibold text-lg">
|
|
||||||
Montant à payer : <br/> <span className="text-primary">{amount} €</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="pl-10 space-y-2">
|
|
||||||
<h4 className="font-semibold text-sm">Fonctionnalités inclues :</h4>
|
|
||||||
<ul className="space-y-2">
|
|
||||||
{features.map((feature, index) => (
|
|
||||||
<li className="flex items-center gap-2 my-4 text-sm"
|
|
||||||
key={index}>
|
|
||||||
<CheckIcon className="h-4 w-4 text-primary"/>
|
|
||||||
<span>{feature}</span>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<input type="hidden" name="package" value={selectedPlan ?? ''} />
|
||||||
|
<input type="hidden" name="amount" value={amount} />
|
||||||
|
<InputError message={errors.package} />
|
||||||
|
|
||||||
|
<p className="text-center text-sm text-muted-foreground border-t border-border pt-3">
|
||||||
|
Montant total : <strong className="text-primary text-lg">{amount}€</strong>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Services included */}
|
||||||
|
<div className="bg-white dark:bg-[#171717] rounded-2xl border-3 border-black p-6 shadow-[4px_4px_0px_rgba(0,0,0,1)] flex flex-col gap-4">
|
||||||
|
<h2 className="text-lg font-semibold text-primary">Services inclus</h2>
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||||||
|
{services?.map((service) => (
|
||||||
|
<div key={service.name} className="flex items-start gap-2">
|
||||||
|
<CheckIcon className="h-4 w-4 mt-0.5 shrink-0 text-accent" />
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium leading-tight">{service.name}</p>
|
||||||
|
<p className="text-xs text-muted-foreground">{service.description}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Captcha + CGU + Submit */}
|
||||||
|
<div className="bg-white dark:bg-[#171717] rounded-2xl border-3 border-black p-6 shadow-[4px_4px_0px_rgba(0,0,0,1)] flex flex-col gap-5">
|
||||||
|
<div className="grid gap-1">
|
||||||
|
<Label htmlFor="captcha" className="font-semibold">{captcha_question}</Label>
|
||||||
|
<Input
|
||||||
|
id="captcha"
|
||||||
|
name="captcha"
|
||||||
|
type="text"
|
||||||
|
tabIndex={10}
|
||||||
|
placeholder="Votre réponse"
|
||||||
|
autoComplete="off"
|
||||||
|
className="max-w-[180px]"
|
||||||
|
/>
|
||||||
|
<InputError message={errors.captcha} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<Checkbox id="cgu" name="cgu" tabIndex={11} required className="mt-0.5" />
|
||||||
|
<Label htmlFor="cgu" className="text-sm leading-snug cursor-pointer">
|
||||||
|
J'ai lu et j'accepte les <a href="#">C.G.U.</a> et je comprends la
|
||||||
|
nécessité des enregistrements de mes données personnelles.
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
<InputError message={errors.cgu} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
variant="secondary"
|
||||||
|
tabIndex={12}
|
||||||
|
className="w-full border-3 border-black shadow-[4px_4px_0px_rgba(0,0,0,1)] hover:shadow-none hover:translate-x-0.5 hover:translate-y-0.5 font-bold text-base py-5"
|
||||||
|
>
|
||||||
|
{processing && <LoaderCircle className="h-4 w-4 animate-spin" />}
|
||||||
|
Envoyer ma demande d'adhésion
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mx-auto justify-center">
|
)}
|
||||||
<div className="w-[300px] grid gap-2 my-4">
|
</Form>
|
||||||
<Label htmlFor="captcha">Captcha</Label>
|
</Container>
|
||||||
<Input
|
</main>
|
||||||
id="captcha"
|
|
||||||
type="text"
|
<Footer />
|
||||||
autoFocus
|
|
||||||
tabIndex={9}
|
|
||||||
name="captcha"
|
|
||||||
placeholder="Entrez le captcha ci-dessous"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="grid gap-2 my-4">
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<Checkbox
|
|
||||||
id="cgu"
|
|
||||||
name="cgu"
|
|
||||||
tabIndex={10}
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
<Label htmlFor="remember">J'ai lu et j'accepte les <a
|
|
||||||
href="#">C.G.U.</a>,
|
|
||||||
je comprends la nécessité des enregistrements de mes données
|
|
||||||
personnelles.</Label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-center items-center">
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
type="submit"
|
|
||||||
className="mt-2 w-full max-w-1/3"
|
|
||||||
tabIndex={11}
|
|
||||||
data-test="register-user-button"
|
|
||||||
>
|
|
||||||
{processing && (
|
|
||||||
<LoaderCircle className="h-4 w-4 animate-spin"/>
|
|
||||||
)}
|
|
||||||
Envoyer
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Form>
|
|
||||||
</section>
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -441,7 +441,7 @@ register.form = registerForm
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @see \App\Http\Controllers\Forms\ContactFormController::contact
|
* @see \App\Http\Controllers\Forms\ContactFormController::contact
|
||||||
* @see app/Http/Controllers/Forms/ContactFormController.php:17
|
* @see app/Http/Controllers/Forms/ContactFormController.php:16
|
||||||
* @route '/contact'
|
* @route '/contact'
|
||||||
*/
|
*/
|
||||||
export const contact = (options?: RouteQueryOptions): RouteDefinition<'get'> => ({
|
export const contact = (options?: RouteQueryOptions): RouteDefinition<'get'> => ({
|
||||||
@@ -456,7 +456,7 @@ contact.definition = {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @see \App\Http\Controllers\Forms\ContactFormController::contact
|
* @see \App\Http\Controllers\Forms\ContactFormController::contact
|
||||||
* @see app/Http/Controllers/Forms/ContactFormController.php:17
|
* @see app/Http/Controllers/Forms/ContactFormController.php:16
|
||||||
* @route '/contact'
|
* @route '/contact'
|
||||||
*/
|
*/
|
||||||
contact.url = (options?: RouteQueryOptions) => {
|
contact.url = (options?: RouteQueryOptions) => {
|
||||||
@@ -465,7 +465,7 @@ contact.url = (options?: RouteQueryOptions) => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @see \App\Http\Controllers\Forms\ContactFormController::contact
|
* @see \App\Http\Controllers\Forms\ContactFormController::contact
|
||||||
* @see app/Http/Controllers/Forms/ContactFormController.php:17
|
* @see app/Http/Controllers/Forms/ContactFormController.php:16
|
||||||
* @route '/contact'
|
* @route '/contact'
|
||||||
*/
|
*/
|
||||||
contact.get = (options?: RouteQueryOptions): RouteDefinition<'get'> => ({
|
contact.get = (options?: RouteQueryOptions): RouteDefinition<'get'> => ({
|
||||||
@@ -475,7 +475,7 @@ contact.get = (options?: RouteQueryOptions): RouteDefinition<'get'> => ({
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @see \App\Http\Controllers\Forms\ContactFormController::contact
|
* @see \App\Http\Controllers\Forms\ContactFormController::contact
|
||||||
* @see app/Http/Controllers/Forms/ContactFormController.php:17
|
* @see app/Http/Controllers/Forms/ContactFormController.php:16
|
||||||
* @route '/contact'
|
* @route '/contact'
|
||||||
*/
|
*/
|
||||||
contact.head = (options?: RouteQueryOptions): RouteDefinition<'head'> => ({
|
contact.head = (options?: RouteQueryOptions): RouteDefinition<'head'> => ({
|
||||||
@@ -485,7 +485,7 @@ contact.head = (options?: RouteQueryOptions): RouteDefinition<'head'> => ({
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @see \App\Http\Controllers\Forms\ContactFormController::contact
|
* @see \App\Http\Controllers\Forms\ContactFormController::contact
|
||||||
* @see app/Http/Controllers/Forms/ContactFormController.php:17
|
* @see app/Http/Controllers/Forms/ContactFormController.php:16
|
||||||
* @route '/contact'
|
* @route '/contact'
|
||||||
*/
|
*/
|
||||||
const contactForm = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({
|
const contactForm = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({
|
||||||
@@ -495,7 +495,7 @@ const contactForm = (options?: RouteQueryOptions): RouteFormDefinition<'get'> =>
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @see \App\Http\Controllers\Forms\ContactFormController::contact
|
* @see \App\Http\Controllers\Forms\ContactFormController::contact
|
||||||
* @see app/Http/Controllers/Forms/ContactFormController.php:17
|
* @see app/Http/Controllers/Forms/ContactFormController.php:16
|
||||||
* @route '/contact'
|
* @route '/contact'
|
||||||
*/
|
*/
|
||||||
contactForm.get = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({
|
contactForm.get = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({
|
||||||
@@ -505,7 +505,7 @@ contactForm.get = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => (
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @see \App\Http\Controllers\Forms\ContactFormController::contact
|
* @see \App\Http\Controllers\Forms\ContactFormController::contact
|
||||||
* @see app/Http/Controllers/Forms/ContactFormController.php:17
|
* @see app/Http/Controllers/Forms/ContactFormController.php:16
|
||||||
* @route '/contact'
|
* @route '/contact'
|
||||||
*/
|
*/
|
||||||
contactForm.head = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({
|
contactForm.head = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({
|
||||||
@@ -522,7 +522,7 @@ contact.form = contactForm
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @see \App\Http\Controllers\Forms\MembershipFormController::membership
|
* @see \App\Http\Controllers\Forms\MembershipFormController::membership
|
||||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:21
|
* @see app/Http/Controllers/Forms/MembershipFormController.php:18
|
||||||
* @route '/membership'
|
* @route '/membership'
|
||||||
*/
|
*/
|
||||||
export const membership = (options?: RouteQueryOptions): RouteDefinition<'get'> => ({
|
export const membership = (options?: RouteQueryOptions): RouteDefinition<'get'> => ({
|
||||||
@@ -537,7 +537,7 @@ membership.definition = {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @see \App\Http\Controllers\Forms\MembershipFormController::membership
|
* @see \App\Http\Controllers\Forms\MembershipFormController::membership
|
||||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:21
|
* @see app/Http/Controllers/Forms/MembershipFormController.php:18
|
||||||
* @route '/membership'
|
* @route '/membership'
|
||||||
*/
|
*/
|
||||||
membership.url = (options?: RouteQueryOptions) => {
|
membership.url = (options?: RouteQueryOptions) => {
|
||||||
@@ -546,7 +546,7 @@ membership.url = (options?: RouteQueryOptions) => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @see \App\Http\Controllers\Forms\MembershipFormController::membership
|
* @see \App\Http\Controllers\Forms\MembershipFormController::membership
|
||||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:21
|
* @see app/Http/Controllers/Forms/MembershipFormController.php:18
|
||||||
* @route '/membership'
|
* @route '/membership'
|
||||||
*/
|
*/
|
||||||
membership.get = (options?: RouteQueryOptions): RouteDefinition<'get'> => ({
|
membership.get = (options?: RouteQueryOptions): RouteDefinition<'get'> => ({
|
||||||
@@ -556,7 +556,7 @@ membership.get = (options?: RouteQueryOptions): RouteDefinition<'get'> => ({
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @see \App\Http\Controllers\Forms\MembershipFormController::membership
|
* @see \App\Http\Controllers\Forms\MembershipFormController::membership
|
||||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:21
|
* @see app/Http/Controllers/Forms/MembershipFormController.php:18
|
||||||
* @route '/membership'
|
* @route '/membership'
|
||||||
*/
|
*/
|
||||||
membership.head = (options?: RouteQueryOptions): RouteDefinition<'head'> => ({
|
membership.head = (options?: RouteQueryOptions): RouteDefinition<'head'> => ({
|
||||||
@@ -566,7 +566,7 @@ membership.head = (options?: RouteQueryOptions): RouteDefinition<'head'> => ({
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @see \App\Http\Controllers\Forms\MembershipFormController::membership
|
* @see \App\Http\Controllers\Forms\MembershipFormController::membership
|
||||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:21
|
* @see app/Http/Controllers/Forms/MembershipFormController.php:18
|
||||||
* @route '/membership'
|
* @route '/membership'
|
||||||
*/
|
*/
|
||||||
const membershipForm = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({
|
const membershipForm = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({
|
||||||
@@ -576,7 +576,7 @@ const membershipForm = (options?: RouteQueryOptions): RouteFormDefinition<'get'>
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @see \App\Http\Controllers\Forms\MembershipFormController::membership
|
* @see \App\Http\Controllers\Forms\MembershipFormController::membership
|
||||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:21
|
* @see app/Http/Controllers/Forms/MembershipFormController.php:18
|
||||||
* @route '/membership'
|
* @route '/membership'
|
||||||
*/
|
*/
|
||||||
membershipForm.get = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({
|
membershipForm.get = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({
|
||||||
@@ -586,7 +586,7 @@ membershipForm.get = (options?: RouteQueryOptions): RouteFormDefinition<'get'> =
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @see \App\Http\Controllers\Forms\MembershipFormController::membership
|
* @see \App\Http\Controllers\Forms\MembershipFormController::membership
|
||||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:21
|
* @see app/Http/Controllers/Forms/MembershipFormController.php:18
|
||||||
* @route '/membership'
|
* @route '/membership'
|
||||||
*/
|
*/
|
||||||
membershipForm.head = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({
|
membershipForm.head = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { queryParams, type RouteQueryOptions, type RouteDefinition, type RouteFormDefinition } from './../../wayfinder'
|
import { queryParams, type RouteQueryOptions, type RouteDefinition, type RouteFormDefinition } from './../../wayfinder'
|
||||||
/**
|
/**
|
||||||
* @see \App\Http\Controllers\Forms\MembershipFormController::store
|
* @see \App\Http\Controllers\Forms\MembershipFormController::store
|
||||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:35
|
* @see app/Http/Controllers/Forms/MembershipFormController.php:42
|
||||||
* @route '/membership'
|
* @route '/membership'
|
||||||
*/
|
*/
|
||||||
export const store = (options?: RouteQueryOptions): RouteDefinition<'post'> => ({
|
export const store = (options?: RouteQueryOptions): RouteDefinition<'post'> => ({
|
||||||
@@ -16,7 +16,7 @@ store.definition = {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @see \App\Http\Controllers\Forms\MembershipFormController::store
|
* @see \App\Http\Controllers\Forms\MembershipFormController::store
|
||||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:35
|
* @see app/Http/Controllers/Forms/MembershipFormController.php:42
|
||||||
* @route '/membership'
|
* @route '/membership'
|
||||||
*/
|
*/
|
||||||
store.url = (options?: RouteQueryOptions) => {
|
store.url = (options?: RouteQueryOptions) => {
|
||||||
@@ -25,7 +25,7 @@ store.url = (options?: RouteQueryOptions) => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @see \App\Http\Controllers\Forms\MembershipFormController::store
|
* @see \App\Http\Controllers\Forms\MembershipFormController::store
|
||||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:35
|
* @see app/Http/Controllers/Forms/MembershipFormController.php:42
|
||||||
* @route '/membership'
|
* @route '/membership'
|
||||||
*/
|
*/
|
||||||
store.post = (options?: RouteQueryOptions): RouteDefinition<'post'> => ({
|
store.post = (options?: RouteQueryOptions): RouteDefinition<'post'> => ({
|
||||||
@@ -35,7 +35,7 @@ store.post = (options?: RouteQueryOptions): RouteDefinition<'post'> => ({
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @see \App\Http\Controllers\Forms\MembershipFormController::store
|
* @see \App\Http\Controllers\Forms\MembershipFormController::store
|
||||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:35
|
* @see app/Http/Controllers/Forms/MembershipFormController.php:42
|
||||||
* @route '/membership'
|
* @route '/membership'
|
||||||
*/
|
*/
|
||||||
const storeForm = (options?: RouteQueryOptions): RouteFormDefinition<'post'> => ({
|
const storeForm = (options?: RouteQueryOptions): RouteFormDefinition<'post'> => ({
|
||||||
@@ -45,7 +45,7 @@ const storeForm = (options?: RouteQueryOptions): RouteFormDefinition<'post'> =>
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @see \App\Http\Controllers\Forms\MembershipFormController::store
|
* @see \App\Http\Controllers\Forms\MembershipFormController::store
|
||||||
* @see app/Http/Controllers/Forms/MembershipFormController.php:35
|
* @see app/Http/Controllers/Forms/MembershipFormController.php:42
|
||||||
* @route '/membership'
|
* @route '/membership'
|
||||||
*/
|
*/
|
||||||
storeForm.post = (options?: RouteQueryOptions): RouteFormDefinition<'post'> => ({
|
storeForm.post = (options?: RouteQueryOptions): RouteFormDefinition<'post'> => ({
|
||||||
|
|||||||
11
resources/js/types/index.d.ts
vendored
11
resources/js/types/index.d.ts
vendored
@@ -54,8 +54,13 @@ export interface Plans {
|
|||||||
identifier: string;
|
identifier: string;
|
||||||
name: string;
|
name: string;
|
||||||
price: number;
|
price: number;
|
||||||
description?: string,
|
description?: string;
|
||||||
is_active: boolean;
|
months?: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MembershipService {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -73,6 +78,8 @@ export interface PageProps {
|
|||||||
flash?: FlashMessages;
|
flash?: FlashMessages;
|
||||||
auth?: Auth;
|
auth?: Auth;
|
||||||
plans?: Plans[];
|
plans?: Plans[];
|
||||||
|
services?: MembershipService[];
|
||||||
|
captcha_question?: string;
|
||||||
|
|
||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
$ispMail = $this->getCommandStatus('ispconfig_mail');
|
$ispMail = $this->getCommandStatus('ispconfig_mail');
|
||||||
$ispWeb = $this->getCommandStatus('ispconfig_web');
|
$ispWeb = $this->getCommandStatus('ispconfig_web');
|
||||||
$nextcloud = $this->getCommandStatus('nextcloud');
|
$nextcloud = $this->getCommandStatus('nextcloud');
|
||||||
|
$listmonk = $this->getCommandStatus('listmonk');
|
||||||
$services = $this->getCommandStatus('services');
|
$services = $this->getCommandStatus('services');
|
||||||
@endphp
|
@endphp
|
||||||
|
|
||||||
@@ -118,6 +119,27 @@
|
|||||||
</div>
|
</div>
|
||||||
</x-filament::section>
|
</x-filament::section>
|
||||||
|
|
||||||
|
<x-filament::section>
|
||||||
|
<x-slot name="heading">
|
||||||
|
@include('filament.pages.partials.sync-heading', [
|
||||||
|
'label' => __('synchronisations.sections.listmonk.heading'),
|
||||||
|
'status' => $listmonk,
|
||||||
|
])
|
||||||
|
</x-slot>
|
||||||
|
<div class="space-y-3">
|
||||||
|
<p class="text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
{{ __('synchronisations.sections.listmonk.description') }}
|
||||||
|
</p>
|
||||||
|
@include('filament.pages.partials.sync-status', ['status' => $listmonk])
|
||||||
|
<x-filament::button
|
||||||
|
wire:click="mountAction('syncListmonk')"
|
||||||
|
:disabled="in_array($listmonk['status'], ['pending', 'running'])"
|
||||||
|
>
|
||||||
|
{{ __('synchronisations.action.submit') }}
|
||||||
|
</x-filament::button>
|
||||||
|
</div>
|
||||||
|
</x-filament::section>
|
||||||
|
|
||||||
<x-filament::section>
|
<x-filament::section>
|
||||||
<x-slot name="heading">
|
<x-slot name="heading">
|
||||||
@include('filament.pages.partials.sync-heading', [
|
@include('filament.pages.partials.sync-heading', [
|
||||||
|
|||||||
@@ -1,3 +1,69 @@
|
|||||||
<x-mail::message>
|
<!DOCTYPE html>
|
||||||
{!! $body !!}
|
<html lang="fr">
|
||||||
</x-mail::message>
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
||||||
|
background-color: #f4f4f4;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
color: #1b1b18;
|
||||||
|
}
|
||||||
|
.wrapper {
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 40px auto;
|
||||||
|
background: #ffffff;
|
||||||
|
border: 3px solid #000;
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
background-color: #f5a623;
|
||||||
|
padding: 24px 32px;
|
||||||
|
border-bottom: 3px solid #000;
|
||||||
|
}
|
||||||
|
.header h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
padding: 32px;
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 1.7;
|
||||||
|
color: #1b1b18;
|
||||||
|
}
|
||||||
|
.content p {
|
||||||
|
margin: 0 0 16px;
|
||||||
|
}
|
||||||
|
.content a {
|
||||||
|
color: #00473e;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.footer {
|
||||||
|
padding: 16px 32px;
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
border-top: 2px solid #e0e0e0;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #888;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="wrapper">
|
||||||
|
<div class="header">
|
||||||
|
<h1>{{ config('app.name') }}</h1>
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
{!! $body !!}
|
||||||
|
</div>
|
||||||
|
<div class="footer">
|
||||||
|
© {{ date('Y') }} {{ config('app.name') }}. Tous droits réservés.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|||||||
@@ -20,4 +20,6 @@ Route::middleware(['auth', 'verified'])->group(function () {
|
|||||||
require __DIR__.'/settings.php';
|
require __DIR__.'/settings.php';
|
||||||
require __DIR__.'/auth.php';
|
require __DIR__.'/auth.php';
|
||||||
require __DIR__.'/forms.php';
|
require __DIR__.'/forms.php';
|
||||||
require __DIR__.'/dev-routes.php';
|
if (app()->environment('local')) {
|
||||||
|
require __DIR__.'/dev-routes.php';
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user