diff --git a/app/Http/Controllers/Forms/ContactFormController.php b/app/Http/Controllers/Forms/ContactFormController.php index 3c6efa9..015993f 100644 --- a/app/Http/Controllers/Forms/ContactFormController.php +++ b/app/Http/Controllers/Forms/ContactFormController.php @@ -7,32 +7,33 @@ use App\Http\Requests\Forms\ContactRequest; use App\Services\ContactService; use Illuminate\Http\RedirectResponse; use Inertia\Inertia; +use Inertia\Response; class ContactFormController extends Controller { public function __construct(protected ContactService $contactService) {} - /** - * Show the contact form page. - */ - public function create() + + public function create(): Response { - return Inertia::render('forms/contact'); + return Inertia::render('forms/contact', [ + 'captcha_question' => $this->generateCaptcha(), + ]); } /** - * Handle an incoming contact form submission. * @throws \Illuminate\Validation\ValidationException */ public function store(ContactRequest $request): RedirectResponse { $validated = $request->validated(); + try { $this->contactService->registerNewContactRequest($validated); } catch (\Throwable $e) { \Log::error('Erreur lors de la création d\'un contact', [ 'error' => $e->getMessage(), 'trace' => $e->getTraceAsString(), - 'data' => $validated, + 'data' => $validated, ]); 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')); } + 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} ?"; + } } diff --git a/app/Http/Controllers/Forms/MembershipFormController.php b/app/Http/Controllers/Forms/MembershipFormController.php index 0447a86..c2a21ac 100644 --- a/app/Http/Controllers/Forms/MembershipFormController.php +++ b/app/Http/Controllers/Forms/MembershipFormController.php @@ -4,55 +4,69 @@ namespace App\Http\Controllers\Forms; use App\Http\Controllers\Controller; use App\Http\Requests\Forms\MembershipRequest; -use App\Models\Membership; use App\Models\Package; +use App\Models\Service; use App\Services\MemberService; -use Carbon\Carbon; use Illuminate\Http\RedirectResponse; use Inertia\Inertia; +use Inertia\Response; class MembershipFormController extends Controller { public function __construct(protected MemberService $memberService) {} - /** - * Show the contact form page. - */ - public function create() + public function create(): Response { + $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', [ - 'plans' => Package::query() - ->where('is_active', true) - ->select('id', 'identifier', 'name', 'price', 'description') - ->get() + 'plans' => $plans, + 'services' => Service::query()->select('name', 'description')->get(), + 'captcha_question' => $this->generateCaptcha(), ]); } - /** - * Handle an incoming membership form request. - * - */ public function store(MembershipRequest $request): RedirectResponse { - dd($request->validated()); $validated = $request->validated(); try { $this->memberService->registerNewMember($validated); } 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(), 'trace' => $e->getTraceAsString(), - 'data' => $validated, + 'data' => $validated, ]); - return redirect() - ->route('membership') - ->with('error', Membership::getAttributeLabel('memberships.subscription.error')); + return to_route('membership') + ->with('error', __('memberships.fields.subscription.error')); } - return redirect() - ->route('membership') - ->with('success', Membership::getAttributeLabel('memberships.subscription.success')); + return to_route('membership') + ->with('success', __('memberships.fields.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} ?"; } } diff --git a/app/Http/Requests/Forms/ContactRequest.php b/app/Http/Requests/Forms/ContactRequest.php index 5d52e3f..f583476 100644 --- a/app/Http/Requests/Forms/ContactRequest.php +++ b/app/Http/Requests/Forms/ContactRequest.php @@ -2,21 +2,17 @@ namespace App\Http\Requests\Forms; +use App\Rules\ValidCaptcha; use Illuminate\Foundation\Http\FormRequest; class ContactRequest extends FormRequest { - /** - * Determine if the user is authorized to make this request. - */ public function authorize(): bool { return true; } /** - * Get the validation rules that apply to the request. - * * @return array|string> */ public function rules(): array @@ -25,9 +21,10 @@ class ContactRequest extends FormRequest 'lastname' => 'required|string|max:255', 'firstname' => 'required|string|max:255', 'email' => 'required|email|max:255', - 'address' => 'string|max:255', + 'address' => 'nullable|string|max:255', 'subject' => 'required|string|max:255', 'message' => 'required|string', + 'captcha' => ['required', new ValidCaptcha('captcha_contact')], ]; } } diff --git a/app/Http/Requests/Forms/MembershipRequest.php b/app/Http/Requests/Forms/MembershipRequest.php index ee9fd4a..2329833 100644 --- a/app/Http/Requests/Forms/MembershipRequest.php +++ b/app/Http/Requests/Forms/MembershipRequest.php @@ -2,39 +2,34 @@ namespace App\Http\Requests\Forms; +use App\Rules\ValidCaptcha; use Illuminate\Foundation\Http\FormRequest; class MembershipRequest extends FormRequest { - /** - * Determine if the user is authorized to make this request. - */ public function authorize(): bool { return true; } /** - * Get the validation rules that apply to the request. - * * @return array|string> */ public function rules(): array { return [ - // Member 'lastname' => 'required|string|max:255', 'firstname' => 'required|string|max:255', 'email' => 'required|email|max:255', - 'company' => 'string|max:255', + 'company' => 'nullable|string|max:255', 'address' => 'required|string|max:255', 'zipcode' => 'required|string|max:255', 'city' => 'required|string|max:255', 'phone1' => 'required|string|max:255', - - // Membership 'package' => 'required|string|max:255', - 'amount' => 'required|string|max:255', + 'amount' => 'required|numeric|min:0', + 'cgu' => 'required|accepted', + 'captcha' => ['required', new ValidCaptcha('captcha_membership')], ]; } } diff --git a/app/Notifications/MemberNewRequestAdminNotification.php b/app/Notifications/MemberNewRequestAdminNotification.php new file mode 100644 index 0000000..8b9a87c --- /dev/null +++ b/app/Notifications/MemberNewRequestAdminNotification.php @@ -0,0 +1,65 @@ + + */ + 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 + */ + public function toArray(object $notifiable): array + { + return []; + } +} diff --git a/app/Rules/ValidCaptcha.php b/app/Rules/ValidCaptcha.php new file mode 100644 index 0000000..b16ec4f --- /dev/null +++ b/app/Rules/ValidCaptcha.php @@ -0,0 +1,21 @@ +sessionKey)) { + $fail('Le code de vérification est incorrect.'); + } + } +} diff --git a/app/Services/MemberService.php b/app/Services/MemberService.php index a1bbe91..3b6215e 100644 --- a/app/Services/MemberService.php +++ b/app/Services/MemberService.php @@ -8,6 +8,7 @@ use App\Models\MemberGroup; use App\Models\Package; use App\Notifications\MemberDeactivatedAdminNotification; use App\Notifications\MemberDeactivatedMemberNotification; +use App\Notifications\MemberNewRequestAdminNotification; use Illuminate\Support\Facades\Notification; class MemberService @@ -51,6 +52,9 @@ class MemberService ]); + Notification::route('mail', config('app.admin_email')) + ->notify(new MemberNewRequestAdminNotification($member, $package, (float) $data['amount'])); + event(new MemberRegistered($member)); return $member; diff --git a/database/seeders/NotificationTemplateSeeder.php b/database/seeders/NotificationTemplateSeeder.php index 3ead803..61c8d26 100644 --- a/database/seeders/NotificationTemplateSeeder.php +++ b/database/seeders/NotificationTemplateSeeder.php @@ -47,6 +47,35 @@ 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' => '

Une nouvelle demande d\'adhésion a été reçue.

' + .'

' + .'Nom : {member_name}
' + .'Email : {member_email}
' + .'Téléphone : {member_phone}
' + .'Adresse : {member_address}
' + .'Formule : {package_name}
' + .'Montant : {amount} €' + .'

' + .'

Voir la fiche adhérent

', + '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'], [ diff --git a/resources/js/actions/App/Http/Controllers/Forms/ContactFormController.ts b/resources/js/actions/App/Http/Controllers/Forms/ContactFormController.ts index 20a549b..d9874f0 100644 --- a/resources/js/actions/App/Http/Controllers/Forms/ContactFormController.ts +++ b/resources/js/actions/App/Http/Controllers/Forms/ContactFormController.ts @@ -1,7 +1,7 @@ import { queryParams, type RouteQueryOptions, type RouteDefinition, type RouteFormDefinition } from './../../../../../wayfinder' /** * @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' */ 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.php:17 +* @see app/Http/Controllers/Forms/ContactFormController.php:16 * @route '/contact' */ 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.php:17 +* @see app/Http/Controllers/Forms/ContactFormController.php:16 * @route '/contact' */ 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.php:17 +* @see app/Http/Controllers/Forms/ContactFormController.php:16 * @route '/contact' */ 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.php:17 +* @see app/Http/Controllers/Forms/ContactFormController.php:16 * @route '/contact' */ 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.php:17 +* @see app/Http/Controllers/Forms/ContactFormController.php:16 * @route '/contact' */ 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.php:17 +* @see app/Http/Controllers/Forms/ContactFormController.php:16 * @route '/contact' */ createForm.head = (options?: RouteQueryOptions): RouteFormDefinition<'get'> => ({ diff --git a/resources/js/actions/App/Http/Controllers/Forms/MembershipFormController.ts b/resources/js/actions/App/Http/Controllers/Forms/MembershipFormController.ts index 1710ea9..b224154 100644 --- a/resources/js/actions/App/Http/Controllers/Forms/MembershipFormController.ts +++ b/resources/js/actions/App/Http/Controllers/Forms/MembershipFormController.ts @@ -1,7 +1,7 @@ import { queryParams, type RouteQueryOptions, type RouteDefinition, type RouteFormDefinition } from './../../../../../wayfinder' /** * @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' */ 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.php:21 +* @see app/Http/Controllers/Forms/MembershipFormController.php:18 * @route '/membership' */ 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.php:21 +* @see app/Http/Controllers/Forms/MembershipFormController.php:18 * @route '/membership' */ 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.php:21 +* @see app/Http/Controllers/Forms/MembershipFormController.php:18 * @route '/membership' */ 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.php:21 +* @see app/Http/Controllers/Forms/MembershipFormController.php:18 * @route '/membership' */ 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.php:21 +* @see app/Http/Controllers/Forms/MembershipFormController.php:18 * @route '/membership' */ 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.php:21 +* @see app/Http/Controllers/Forms/MembershipFormController.php:18 * @route '/membership' */ 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.php:35 +* @see app/Http/Controllers/Forms/MembershipFormController.php:42 * @route '/membership' */ 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.php:35 +* @see app/Http/Controllers/Forms/MembershipFormController.php:42 * @route '/membership' */ 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.php:35 +* @see app/Http/Controllers/Forms/MembershipFormController.php:42 * @route '/membership' */ 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.php:35 +* @see app/Http/Controllers/Forms/MembershipFormController.php:42 * @route '/membership' */ 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.php:35 +* @see app/Http/Controllers/Forms/MembershipFormController.php:42 * @route '/membership' */ storeForm.post = (options?: RouteQueryOptions): RouteFormDefinition<'post'> => ({ diff --git a/resources/js/pages/forms/contact.tsx b/resources/js/pages/forms/contact.tsx index 47dcff1..0f61f12 100644 --- a/resources/js/pages/forms/contact.tsx +++ b/resources/js/pages/forms/contact.tsx @@ -1,11 +1,11 @@ -import {useEffect, useState} from "react"; -import {Form, Head, usePage} from "@inertiajs/react"; -import {LoaderCircle} from 'lucide-react'; -import ContactFormController from "@/actions/App/Http/Controllers/Forms/ContactFormController"; -import {Label} from "@/components/ui/label"; -import {Input} from "@/components/ui/input"; -import InputError from "@/components/input-error"; -import {Button} from "@/components/ui/button"; +import { useEffect, useState } from 'react'; +import { Form, Head, usePage } from '@inertiajs/react'; +import { LoaderCircle } from 'lucide-react'; +import ContactFormController from '@/actions/App/Http/Controllers/Forms/ContactFormController'; +import { Label } from '@/components/ui/label'; +import { Input } from '@/components/ui/input'; +import InputError from '@/components/input-error'; +import { Button } from '@/components/ui/button'; import { Select, SelectContent, @@ -13,16 +13,18 @@ import { SelectItem, SelectLabel, SelectTrigger, - SelectValue -} from "@/components/ui/select"; -import {Textarea} from "@/components/ui/textarea"; -import NavGuestLayout from "@/layouts/nav-guest-layout"; -import {PageProps} from "@/types"; -import {FlashMessage} from "@/components/flash-message"; + SelectValue, +} from '@/components/ui/select'; +import { Textarea } from '@/components/ui/textarea'; +import NavGuestLayout from '@/layouts/nav-guest-layout'; +import { PageProps } from '@/types'; +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() { - const {flash} = usePage().props as PageProps; - + const { flash, captcha_question } = usePage().props as PageProps; const [showFlashMessage, setFlashMessage] = useState(!!flash); useEffect(() => { @@ -33,167 +35,129 @@ export default function Contact() { } }, [flash]); - return ( <> - -
- + +
+
+ +
-
-
-

Nous contacter

-

- Vous désirez nous contacter, merci de remplir le formulaire suivant : -

-
+
+ + - {showFlashMessage && ( - - )} + {showFlashMessage && } -
- {({processing, errors}) => ( -
-
-
-
- - - + + {({ processing, errors }) => ( +
+ + {/* Left — Identité + adresse */} +
+

Vos informations

+ +
+
+ + + +
+
+ + + +
-
- - - +
+ + +
-
- - - -
- -
- - - +
+ + +
-
-
- - + {/* Right — Message + captcha + submit */} +
+
+

Votre message

+ +
+ + + +
+ +
+ +