Merge branch 'develop' into trunk

This commit is contained in:
Noah Lancien 2025-09-17 11:25:53 +02:00
commit 0d1ad0abf6
17 changed files with 150 additions and 125 deletions

View file

@ -45,6 +45,9 @@ CRM_ENDPOINT=
CRM_TOKEN=
CRM_PASSWORD=
CRM_QUESTION_ID=
CRM_BRAND_ID=
# - JSON ONLY (like this: {"subject1":44,"subject2":55,"subject3":66})
CRM_SUBJECTS=
# DIGITAL RECRUITER
DIGITAL_RECRUITER_URL=

7
.gitignore vendored
View file

@ -57,3 +57,10 @@ oldAssets
/public/*
!/public/newsletters/
!/public/tdf-downloads
# Folders from new VMs
/.npm
/.composer
psysh_history
.zshrc
.viminfo

View file

@ -113,7 +113,43 @@ enum ContactFormType: int
self::complain,
self::club,
self::games => true,
default => false,
default => false,
};
}
// * CRM CUBJECTS
/**
* Get the CRM subject ID for the current enum case.
*
* @return ?string
*/
public function crmId(): ?string
{
return self::getCrmSubjects()[$this->name] ?? null;
}
/**
* Get all CRM subject IDs keyed by enum case name.
*
* @return array<string, string>
*/
public static function crmIds(): array
{
$subjects = self::getCrmSubjects();
$subjectIds = collect(self::cases())
->mapWithKeys(fn($case) => [$case->name => $subjects[$case->name] ?? null])
->all();
return array_filter($subjectIds, fn($v) => !is_null($v));
}
/**
* Get CRM subjects config as an array.
*
* @return array<string, string>
*/
private static function getCrmSubjects(): array
{
return json_decode(config('crm.subjects'), true) ?? [];
}
}

View file

@ -7,11 +7,12 @@ use App\Enums\StaticPageType;
use App\Exceptions\AbortContactMailer;
use App\Http\Controllers\Controller;
use App\Http\Requests\Fo\ContactFormRequest;
use App\Jobs\Mails\SendArcadiaMail;
use App\Jobs\Mails\SendClaimMail;
use App\Jobs\Mails\SendNLMail;
use App\Jobs\Mails\SendPasserelleMail;
use App\Models\MailContactData;
use App\Models\StaticPage;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Storage;
@ -38,37 +39,59 @@ class ContactController extends Controller
*/
public function contactSubmit(ContactFormRequest $request): \Illuminate\Http\RedirectResponse
{
return DB::transaction(function () use ($request) {
/** @var array<string, mixed> $validatedData */
$validatedData = $request->validated();
return DB::transaction(function () use ($validatedData) {
try {
$joinFilePath = ContactFormType::make($request->type)->hasFileInput()
? $this->storeFile($request->file('joinFile'))
$joinFilePath = ContactFormType::make($validatedData['type'])->hasFileInput()
? $this->storeFile($validatedData['joinFile'])
: null;
$data = new MailContactData(
$request->safe()->except(['g-recaptcha-response', 'joinFile', 'locale']),
Arr::except($validatedData, ['g-recaptcha-response', 'joinFile', 'locale', 'crmsubject']),
$joinFilePath
);
$locale = $request->input('locale');
// Send to Passerelle if it is a complain about a product.
if (isset($locale) && ContactFormType::make($request->input('locale'))->equals(ContactFormType::nl)) {
app()->setLocale('nl');
\dispatch(new SendNLMail($data, $joinFilePath));
} elseif (ContactFormType::make($request->type)->equals(ContactFormType::complain)) {
if (ContactFormType::make($validatedData['type'])->equals(ContactFormType::complain)) {
\dispatch(new SendPasserelleMail($data, $joinFilePath));
} else {
\dispatch(new SendArcadiaMail($data, $joinFilePath));
}
// Send to the CRM.
if ($joinFilePath) {
$relativePath = str_replace(storage_path('app/public') . '/', '', $joinFilePath);
$binary = Storage::disk('public')->get($relativePath);
$base64 = base64_encode($binary);
}
/** @var \LDC\LDCCRM\CrmApiV4 */
$crm = app('crmAPI');
$crm->setClaim(
$validatedData['lastname'],
$validatedData['firstname'],
$validatedData['email'],
$validatedData['message'],
config('crm.brand'),
$validatedData['crmsubject'],
$validatedData['address'],
$validatedData['town'],
$validatedData['postal'],
$base64 ?? null,
);
// Send mail.
$locale = $validatedData['locale'] ?? null;
if (isset($locale) && ContactFormType::make($locale)->equals(ContactFormType::nl)) {
app()->setLocale('nl');
\dispatch(new SendNLMail($data, $joinFilePath));
} else {
\dispatch(new SendClaimMail($data, $joinFilePath));
}
} //end if
return back()->with('success', trans('contact.message_contact_send'));
} catch (AbortContactMailer $e) {
Log::error('Fail to build mail from complain page ', ['exception' => $e]);
} //end try
return back()->withInput()->with(
'error',
trans('contact.message_contact_failed')
);
return back()->withInput()->with('error', trans('contact.message_contact_failed'));
});
}
@ -91,7 +114,6 @@ class ContactController extends Controller
);
$joinFilePath = storage_path('app/' . $joinFile);
}
return $joinFilePath;
}
}

View file

@ -15,7 +15,7 @@ class ContactFormRequest extends FormRequest
*
* @return boolean
*/
public function authorize()
public function authorize(): bool
{
return true;
}
@ -25,31 +25,29 @@ class ContactFormRequest extends FormRequest
*
* @return array
*/
public function rules()
public function rules(): array
{
ToolboxHelper::validateRecaptcha($this, \config('services.google.recaptcha.version'));
$rules = [
'locale' => [
'locale' => [
'nullable',
Rule::enum(ContactFormType::class)->only(ContactFormType::getLocalesContactType())
],
'type' => [
'type' => [
'required',
Rule::enum(ContactFormType::class)->only(ContactFormType::getContactTypes())
],
'civility' => ['required', Rule::enum(ContactFormCivility::class)],
// Max arcadia 50 chars.
'lastname' => 'required|min:2|max:50',
// Max arcadia 50 chars.
'firstname' => 'required|min:2|max:50',
'address' => 'required|min:4',
'email' => 'required|email:rfc,dns,spoof',
'postal' => 'required|postal_code:FR',
// Max arcadia 10 chars.
'town' => 'required|min:3|max:90',
'phone' => 'required|phone:FR',
'message' => 'required|min:20|max:1900'
'civility' => ['required', Rule::enum(ContactFormCivility::class)],
'lastname' => 'required|min:2|max:50',
'firstname' => 'required|min:2|max:50',
'address' => 'required|min:4',
'email' => 'required|email:rfc,dns,spoof',
'postal' => 'required|postal_code:FR',
'town' => 'required|min:3|max:90',
'phone' => 'required|phone:FR',
'message' => 'required|min:20|max:1900',
'crmsubject' => ['required', Rule::in(array_values(ContactFormType::crmIds()))],
];
if (ContactFormType::make($this->input('type'))->equals(ContactFormType::complain)) {
@ -74,7 +72,7 @@ class ContactFormRequest extends FormRequest
$rules = array_merge(
$rules,
[
'joinFile' => 'nullable|mimes:jpg,jpeg,png,pdf|max:5120'
'joinFile' => 'nullable|mimes:jpg,jpeg,png,pdf|max:5120'
],
);
}
@ -103,7 +101,8 @@ class ContactFormRequest extends FormRequest
'consumptionDateDay' => sprintf('%s - %s', trans('validation.custom.consumption_date'), trans('day')),
'consumptionDateMonth' => sprintf('%s - %s', trans('validation.custom.consumption_date'), trans('month')),
'consumptionDateYear' => sprintf('%s - %s', trans('validation.custom.consumption_date'), trans('year')),
'joinFile' => trans('validation.custom.attachment')
'joinFile' => trans('validation.custom.attachment'),
'crmsubject' => trans('validation.custom.crmsubject'),
];
}
}

View file

@ -11,7 +11,7 @@ use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Mail;
class SendArcadiaMail implements ShouldQueue
class SendClaimMail implements ShouldQueue
{
use Dispatchable;
use InteractsWithQueue;
@ -49,7 +49,7 @@ class SendArcadiaMail implements ShouldQueue
*
* @return void
*/
public function handle()
public function handle(): void
{
Mail::to(config('mail.contactsList.contact'))->send(new ContactMail($this->data, $this->file));
if (file_exists($this->file)) {

View file

@ -7,9 +7,6 @@ use Illuminate\Bus\Queueable;
use Illuminate\Mail\Attachment;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
use League\Csv\Bom;
use League\Csv\Writer;
use SplTempFileObject;
class ContactMail extends Mailable
{
@ -54,12 +51,7 @@ class ContactMail extends Mailable
->subject(sprintf('Nouveau message consommateur Le Gaulois (%s)', $this->data->type->label()))
->view('emails.contact')
->text('emails.contact-plain')
->with('data', $this->data)
->attachData(
$this->makeCsvFile(),
sprintf('contact_le_gaulois_%d.csv', time()),
['mime' => 'text/csv']
);
->with('data', $this->data);
}
/**
@ -76,39 +68,4 @@ class ContactMail extends Mailable
Attachment::fromPath($this->file)->as($this->data->joinFile),
];
}
/**
* @return string
*/
protected function makeCsvFile(): string
{
// Creation of the CSV into memory.
$csv = Writer::createFromFileObject(new SplTempFileObject());
$csv->setDelimiter(';');
$csv->setEnclosure('"');
$csv->setOutputBOM(Bom::Utf8);
// Insert the CSV header.
$csv->insertOne([
'EMETTEUR_DATE',
'EMETTEUR_HEURE',
'EMETTEUR_CIVILITE',
'EMETTEUR_NOM',
'EMETTEUR_PRENOM',
'EMETTEUR_ADRESSE',
'EMETTEUR_CP',
'EMETTEUR_VILLE',
'EMETTEUR_EMAIL',
'EMETTEUR_OBJETDEMANDE',
'EMETTEUR_MESSAGE',
'EMETTEUR_CATEGORIE',
'EMETTEUR_ACTIVITE',
'REPONSE_ETAT',
'TYPE_REPONSE'
]);
$csv->insertOne($this->data->exportToArcadiaDataArray());
return $csv->toString();
}
}

View file

@ -112,32 +112,6 @@ class MailContactData extends Model
$this->joinFile = self::getFileName($filePath ?? '');
}
/**
* Export the model data to a Formatted Arcadia Data array.
*
* @return array
*/
public function exportToArcadiaDataArray(): array
{
return [
date("Ymd"),
date("Gis"),
$this->civility->value,
$this->lastname,
$this->firstname,
$this->address,
$this->postal,
$this->town,
$this->email,
$this->type->value,
$this->message,
'0',
'0',
'0',
'0'
];
}
/**
* Export the model data to a Formatted Passerelle Data array.
*

View file

@ -25,7 +25,7 @@
"laravel/scout": "^10.11",
"laravel/tinker": "^2.8",
"laravel/ui": "^4.2",
"ldc/ldccrmv4client": "^4.0",
"ldc/ldccrmv4client": "^4.1",
"league/csv": "^9.16",
"meilisearch/meilisearch-php": "^1.11",
"mews/purifier": "^3.4",

16
composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "8fc50392358e865d6aaa16358da98cd5",
"content-hash": "429c24f83ed59ad888013ce2787525ed",
"packages": [
{
"name": "24slides/laravel-saml2",
@ -2659,16 +2659,16 @@
},
{
"name": "ldc/ldccrmv4client",
"version": "4.0.9",
"version": "4.1.4",
"source": {
"type": "git",
"url": "git@lab.s-web.fr:ldc-web/projets/crm-client.git",
"reference": "b54f61add50e93c68d26cadbe55d29216fcfc666"
"reference": "9aa66a8b3f62c401b0fd43616ba5fd52fe15541a"
},
"dist": {
"type": "zip",
"url": "https://lab.s-web.fr/api/v4/projects/26/packages/composer/archives/ldc/ldccrmv4client.zip?sha=b54f61add50e93c68d26cadbe55d29216fcfc666",
"reference": "b54f61add50e93c68d26cadbe55d29216fcfc666",
"url": "https://lab.s-web.fr/api/v4/projects/26/packages/composer/archives/ldc/ldccrmv4client.zip?sha=9aa66a8b3f62c401b0fd43616ba5fd52fe15541a",
"reference": "9aa66a8b3f62c401b0fd43616ba5fd52fe15541a",
"shasum": ""
},
"require": {
@ -13946,13 +13946,13 @@
],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": {},
"stability-flags": [],
"prefer-stable": true,
"prefer-lowest": false,
"platform": {
"php": "^8.3",
"ext-bcmath": "*"
},
"platform-dev": {},
"plugin-api-version": "2.6.0"
"platform-dev": [],
"plugin-api-version": "2.3.0"
}

View file

@ -5,4 +5,6 @@ return [
'token' => env('CRM_TOKEN'),
'password' => env('CRM_PASSWORD'),
'question' => env('CRM_QUESTION_ID'),
'brand' => env('CRM_BRAND_ID'),
'subjects' => env('CRM_SUBJECTS'),
];

View file

@ -28,6 +28,8 @@ data:
CRM_TOKEN: "${APP_CRM_TOKEN}"
CRM_PASSWORD: "${APP_CRM_PASSWORD}"
CRM_QUESTION_ID: "${APP_CRM_QUESTION_ID}"
CRM_BRAND_ID: "${APP_CRM_BRAND_ID}"
CRM_SUBJECTS=: "${APP_CRM_SUBJECTS}"
MESBR_REF_SITE: "${APP_MESBR_REF_SITE}"
MESBR_REF_CAMP: "${APP_MESBR_REF_CAMP}"
MESBR_ENDPOINT: "${APP_MESBR_ENDPOINT}"

View file

@ -256,6 +256,7 @@ return [
'color' => 'color',
'contract' => 'contract',
'cooking_time' => 'cooking time (in minutes)',
'crmsubject' => 'claim subject',
'demarche' => 'demarche',
'details' => 'details',
'difficulty' => 'difficulty',

View file

@ -259,6 +259,7 @@ return [
'color' => 'couleur',
'contract' => 'contrat',
'cooking_time' => 'temps de cuisson (en minutes)',
'crmsubject' => 'sujet de réclamation',
'demarche' => 'démarche',
'details' => 'détails',
'difficulty' => 'difficulté',

View file

@ -2,21 +2,34 @@
document.addEventListener("DOMContentLoaded", function () {
const checkedSubject = document.querySelector("input[name=\"type\"]:checked") as HTMLInputElement;
const typeRadioInputs = document.querySelectorAll("input[name=\"type\"]") as NodeListOf<HTMLInputElement>;
const crmSubjectInput = document.querySelector("input[name=\"crmsubject\"]") as HTMLInputElement;
if (!document.querySelector("#contact-form") || !checkedSubject || !typeRadioInputs) {
return;
}
checkRadioChecked(checkedSubject.value);
updateCrmSubjectValue(checkedSubject, crmSubjectInput);
for (const typeRadioInput of typeRadioInputs) {
typeRadioInput.addEventListener("change", (e: Event) => {
if (e.currentTarget instanceof HTMLInputElement) {
checkRadioChecked(e.currentTarget.value);
updateCrmSubjectValue(e.currentTarget, crmSubjectInput);
}
});
}
});
/**
* Update the crm subject value depending on checked radio input.
* @param input HTMLInputElement
* @param crmSubjectInput HTMLInputElement
* @return void
*/
function updateCrmSubjectValue(input: HTMLInputElement, crmSubjectInput: HTMLInputElement): void {
crmSubjectInput.value = input.getAttribute("data-crm-subject") ?? "";
}
function checkRadioChecked(value: string) {
// Display complain form if the current checked radio is equal to ContactFormType::complain.
displayComplainForm(value === "3");
@ -28,8 +41,10 @@ function checkRadioChecked(value: string) {
/**
* Display the complain part if shouldDisplay is true.
* @param shouldDisplay boolean
* @return void
*/
function displayComplainForm(shouldDisplay: boolean) {
function displayComplainForm(shouldDisplay: boolean): void {
const complainForm = document.querySelector("#complain-part");
if (!complainForm) {
return;
@ -51,8 +66,10 @@ function displayComplainForm(shouldDisplay: boolean) {
/**
* Display the file input if shouldDisplay is true.
* @param shouldDisplay boolean
* @return void
*/
function displayFileInput(shouldDisplay: boolean) {
function displayFileInput(shouldDisplay: boolean): void {
const fileInputContainer = document.querySelector("#file-input") as HTMLInputElement;
if (!fileInputContainer) {
return;

View file

@ -34,8 +34,8 @@
<label class="col-form-label ms-3 pb-1" for="message">
{{ __('validation.custom.message') }}&nbsp;*
</label>
<textarea id="message" name="message" type="date" value="{{ old('message', '') }}" aria-describedby="messageHelp"
@class(['form-control', 'is-invalid' => $errors->has('message')]) placeholder="{{ __('Votre message ...') }}" required> </textarea>
<textarea id="message" name="message" placeholder="{{ __('Votre message ...') }}" aria-describedby="messageHelp"
@class(['form-control', 'is-invalid' => $errors->has('message')]) required>{{ old('message', '') }}</textarea>
@include('modules.input-error', [
'inputName' => 'message',
'helperId' => 'messageHelp',
@ -59,7 +59,9 @@
sprintf(
'%s<br>%s',
__('contact.attachment.allowed-formats', ['formats' => 'jpg, png, pdf']),
__('contact.attachment.max-size', ['max' => '5 Mo']))),
__('contact.attachment.max-size', ['max' => '5 Mo'])
)
),
])
</div>
</div>
@ -74,7 +76,8 @@
</div>
@endif
</div>
{{-- CRM SUBJECT --}}
<input class="d-none" name="crmsubject">
{{-- SUBMIT BUTTON --}}
<div class="d-flex justify-content-end">
<button class="g-recaptcha btn btn-secondary ms-auto"

View file

@ -7,7 +7,8 @@
<div class="col-12 col-md-6 col-xl-12 mb-3 h-auto">
<div class="radio-subject position-relative h-100">
<input id="{{ $contactType->name }}" name="type" type="radio" value="{{ $contactType->value }}"
@checked($loop->first || ($contactType->value === intval(old('type')) && !is_null(old('type')))) />
@checked($loop->first || ($contactType->value === intval(old('type')) && !is_null(old('type'))))
data-crm-subject="{{ $contactType->crmId() }}" />
<label class="d-flex align-items-center" for="{{ $contactType->name }}">
<img class="me-3" src="{{ Vite::asset($contactType->picto()['src']) }}" alt="{{ $contactType->picto()['alt'] }}">
<p class="mb-0 h-fit">