fix: Correction de l'affichage des numéros de téléphone [#41]

This commit is contained in:
Luc SORIGNET
2025-04-11 16:59:15 +02:00
parent a157d53932
commit 4f774c18e4
11 changed files with 125 additions and 174 deletions

View File

@ -9,7 +9,6 @@ import Popup from '@/components/Popup';
import Loader from '@/components/Loader';
import AlertWithModal from '@/components/AlertWithModal';
import DropdownMenu from "@/components/DropdownMenu";
import { formatPhoneNumber } from '@/utils/Telephone';
import { MoreVertical, Send, Edit, Archive, FileText, CircleCheck, Plus, XCircle } from 'lucide-react';
import Modal from '@/components/Modal';
import InscriptionForm from '@/components/Inscription/InscriptionForm'
@ -50,6 +49,7 @@ import {
import DjangoCSRFToken from '@/components/DjangoCSRFToken'
import { useCsrfToken } from '@/context/CsrfContext';
import logger from '@/utils/logger';
import { PhoneLabel } from '@/components/PhoneLabel';
export default function Page({ params: { locale } }) {
const t = useTranslations('subscriptions');
@ -103,7 +103,7 @@ export default function Page({ params: { locale } }) {
setIsOpenAddGuardian(true);
setStudent(eleveSelected);
};
const handleCloseAddGuardian = () => {
setIsOpenAddGuardian(false);
};
@ -285,7 +285,7 @@ useEffect(() => {
fetchRegistrationTemplateMaster()
.then((data)=> {setTemplateMasters(data)})
.catch((err)=>{ err = err.message; logger.debug(err);});
setIsLoading(false);
setReloadFetch(false);
}
@ -383,7 +383,7 @@ useEffect(()=>{
};
const updateStatusAction = (id, newStatus) => {
logger.debug(`Mise à jour du statut du dossier d'inscription avec l'ID : ${id} vers le statut : ${newStatus}`);
logger.debug(`Mise à jour du statut du dossier d'inscription avec l'ID : ${id} vers le statut : ${newStatus}`);
};
const handleSearchChange = (event) => {
@ -427,7 +427,7 @@ useEffect(()=>{
profession: updatedData.guardianProfession
}];
}
// Si aucun profil existant n'est trouvé, créer un nouveau profil
return [{
profile_role_data: {
@ -531,7 +531,7 @@ useEffect(()=>{
profession: updatedData.guardianProfession
}];
}
// Si aucun profil existant n'est trouvé, créer un nouveau profil
return [{
profile_role_data: {
@ -614,7 +614,7 @@ useEffect(()=>{
},
],
};
// Combine actions for the specific status and default actions
return [...(actions[row.status] || []), ...(row.status !== 6 ? actions.default : [])];
};
@ -641,7 +641,7 @@ const columns = [
)
)
},
{ name: t('phone'), transform: (row) => formatPhoneNumber(row.student.guardians[0]?.phone) },
{ name: t('phone'), transform: (row) => <PhoneLabel phoneNumber={row.student.guardians[0]?.phone} /> },
{ name: t('lastUpdateDate'), transform: (row) => row.formatted_last_update},
{ name: t('registrationFileStatus'), transform: (row) => (
<div className="flex justify-center items-center h-full">

View File

@ -1,40 +1,32 @@
import { useEffect, useRef } from 'react';
export default function InputPhone({ name, label, value, onChange, errorMsg, placeholder, className, required }) {
const inputRef = useRef(null);
useEffect(() => {
if (inputRef.current) {
inputRef.current.focus();
}
}, []);
const handleChange = (e) => {
const newValue = e.target.value;
onChange(newValue);
};
import React from 'react';
import { PhoneInput } from 'react-international-phone';
import 'react-international-phone/style.css';
export default function InputPhone({ name, label, value, onChange, errorMsg, className, required }) {
return (
<>
<div className={`mb-4 ${className}`}>
<label htmlFor={name} className="block text-sm font-medium text-gray-700">
{label}
{required && <span className="text-red-500 ml-1">*</span>}
</label>
<div className={`mt-1 flex items-center border border-gray-200 rounded-md ${errorMsg ? 'border-red-500' : ''} hover:border-gray-400 focus-within:border-gray-500`}>
<input
type="tel"
name={name}
ref={inputRef}
className="flex-1 px-3 py-2 block w-full sm:text-sm focus:ring-0 rounded-md border-none outline-none"
value={typeof value === 'string' ? value : ''}
onChange={handleChange}
placeholder={placeholder}
/>
</div>
{errorMsg && <p className="mt-2 text-sm text-red-600">{errorMsg}</p>}
</div>
</>
)
<div className={`${className}`}>
<label htmlFor={name} className="block text-sm font-medium text-gray-700">
{label}
{required && <span className="text-red-500 ml-1">*</span>}
</label>
<PhoneInput
defaultCountry="fr"
value={value}
onChange={(phone) => onChange(phone)}
inputProps={{
name: name,
required: required,
}}
className="!w-full mt-1 !h-[38px]"
containerClassName="!w-full !h-[36px] !flex !items-center !rounded-md"
inputClassName={`flex-1 px-3 py-2 block w-full sm:text-sm border-none focus:ring-0 outline-none !rounded-r-md !outline-none items-center !border !border-gray-200 rounded-md ${errorMsg ? 'border-red-500' : ''} hover:border-gray-400 focus-within:border-gray-500` }
buttonClassName="!h-[38px] !flex !items-center !justify-center !rounded-l-md !border border-gray-200 !border-r-0"
/>
{errorMsg && (
<p className="mt-2 text-sm text-red-600">{errorMsg}</p>
)}
</div>
);
}

View File

@ -10,6 +10,8 @@ import SectionTitle from '@/components/SectionTitle';
import ProgressStep from '@/components/ProgressStep';
import logger from '@/utils/logger';
import Popup from '@/components/Popup';
import InputPhone from '../InputPhone';
import { PhoneLabel } from '../PhoneLabel';
const InscriptionForm = ( { students, registrationDiscounts, tuitionDiscounts, registrationFees, tuitionFees, profiles, onSubmit, currentStep, groups, showOnlyStep2 = false }) => {
const [formData, setFormData] = useState(() => {
@ -66,7 +68,7 @@ const InscriptionForm = ( { students, registrationDiscounts, tuitionDiscounts, r
const isStep1Valid = formData.studentLastName && formData.studentFirstName;
const isStep2Valid = (
formData.selectedGuardians.length > 0 ||
formData.selectedGuardians.length > 0 ||
(!formData.emailError && formData.guardianEmail.length > 0 && filteredStudents.length === 0)
);
const isStep3Valid = formData.selectedRegistrationFees?.length > 0;
@ -127,7 +129,7 @@ const InscriptionForm = ( { students, registrationDiscounts, tuitionDiscounts, r
const validateAndSubmit = async () => {
const existingProfile = profiles.find(profile => profile.email === formData.guardianEmail);
if (existingProfile) {
console.log('existingProfile : ', existingProfile);
await setFormData((prevData) => ({
@ -137,7 +139,7 @@ const InscriptionForm = ( { students, registrationDiscounts, tuitionDiscounts, r
existingProfileId: existingProfile.id,
}));
}
// Utiliser la dernière version de formData via formDataRef
logger.debug('Submitting form data:', formDataRef.current);
onSubmit(formDataRef.current);
@ -146,7 +148,7 @@ const InscriptionForm = ( { students, registrationDiscounts, tuitionDiscounts, r
const nextStep = async () => {
if (step === 2) {
const existingProfile = profiles.find(profile => profile.email === formData.guardianEmail);
if (existingProfile) {
console.log('existingProfile : ', existingProfile);
await setFormData((prevData) => ({
@ -157,7 +159,7 @@ const InscriptionForm = ( { students, registrationDiscounts, tuitionDiscounts, r
}));
}
}
if (!showOnlyStep2 && step < steps.length) {
setStep(step + 1);
}
@ -184,16 +186,16 @@ const InscriptionForm = ( { students, registrationDiscounts, tuitionDiscounts, r
const selectedGuardians = isSelected
? prevData.selectedGuardians.filter(id => id !== guardianId) // Retirer le guardian si déjà sélectionné
: [...prevData.selectedGuardians, guardianId]; // Ajouter le guardian s'il n'est pas sélectionné
// Mettre à jour l'email uniquement si un guardian est sélectionné
const updatedGuardianEmail = isSelected ? '' : guardianEmail;
// Si aucun guardian n'est sélectionné, réinitialiser le tableau des élèves
if (selectedGuardians.length === 0) {
setFilteredStudents(students); // Réafficher tous les élèves
setSelectedEleve(null);
}
return {
...prevData,
selectedGuardians,
@ -361,11 +363,9 @@ const InscriptionForm = ( { students, registrationDiscounts, tuitionDiscounts, r
onChange={handleChange}
className="w-full mt-4"
/>
<InputTextIcon
<InputPhone
name="guardianPhone"
type="tel"
IconItem={Phone}
placeholder="Numéro de téléphone (optionnel)"
label={t('Numéro de téléphone (optionnel)')}
value={formData.guardianPhone}
onChange={handleChange}
className="w-full mt-4"
@ -385,7 +385,7 @@ const InscriptionForm = ( { students, registrationDiscounts, tuitionDiscounts, r
setFormData((prevData) => ({
...prevData,
guardianEmail: email,
emailError:
emailError:
email.length > 0 && // Le champ de mail est non null
(!emailRegex.test(email) && filteredStudents.length === 0) // Format invalide ou aucun résultat
? "Format d'email invalide ou aucun élève trouvé"
@ -661,7 +661,7 @@ const InscriptionForm = ( { students, registrationDiscounts, tuitionDiscounts, r
<tbody>
<tr>
<td className="px-4 py-2 border">{formData.guardianEmail}</td>
<td className="px-4 py-2 border">{formData.guardianPhone}</td>
<td className="px-4 py-2 border"><PhoneLabel phoneNumber={formData.guardianPhone} /></td>
</tr>
</tbody>
</table>
@ -781,7 +781,7 @@ const InscriptionForm = ( { students, registrationDiscounts, tuitionDiscounts, r
</>
)}
</div>
<Popup
visible={popupVisible}
message={popupMessage}

View File

@ -2,8 +2,8 @@ import React from 'react';
import SelectChoice from '@/components/SelectChoice';
export default function PaymentMethodSelector({ formData, title, name, updateFormField, selected, paymentModes, paymentModesOptions, amount, getError }) {
console.log(paymentModes)
console.log(selected)
//console.log(paymentModes)
//console.log(selected)
return (
<div className="bg-white p-6 rounded-lg shadow-sm border border-gray-200">
{/* Titre */}

View File

@ -3,7 +3,6 @@ import InputPhone from '@/components/InputPhone';
import Button from '@/components/Button';
import React from 'react';
import { useTranslations } from 'next-intl';
import 'react-phone-number-input/style.css'
import { Trash2, Plus } from 'lucide-react';
export default function ResponsableInputFields({guardians, onGuardiansChange, addGuardian, deleteGuardian, errors = []}) {

View File

@ -0,0 +1,7 @@
import { formatPhoneNumber } from "@/utils/Telephone";
export function PhoneLabel({phoneNumber}){
return (
<a className="text-sm font-semibold text-gray-800" href={"tel:"+phoneNumber}>{formatPhoneNumber(phoneNumber)}</a>
);
}

View File

@ -1,36 +1,49 @@
const localePrefixes = {
"fr-FR": "+33",
// Ajoutez d'autres locales et leurs préfixes ici
};
export function formatPhoneNumber(phoneString, fromFormat = 'XX-XX-XX-XX-XX', toFormat = 'LX-XX-XX-XX-XX', locale = "fr-FR") {
/**
*
* @param {*} phoneString au format E.164
* @param {*} toFormat L=Country code, X=Number Format="LX XX XX XX XX"
* @returns
*/
export function formatPhoneNumber(phoneString, toFormat = 'LX XX XX XX XX') {
if (!phoneString) return;
// Extraire les chiffres du numéro de téléphone
const digits = phoneString.replace(/\D/g, '');
// Déterminer le préfixe international en fonction de la locale
let prefix = localePrefixes[locale] || '';
// Si le format d'entrée commence par 'L', détecter la locale
if (fromFormat.startsWith('L')) {
const detectedPrefix = phoneString.match(/^\+\d+/);
if (detectedPrefix) {
prefix = detectedPrefix[0];
phoneString = phoneString.replace(prefix, '');
}
// Vérifier si le numéro est au format international
if(!validateE164PhoneNumber(phoneString)){
return phoneString;
}
const matches = phoneString.match(/^\+(\d{1,3})(\d{9})$/);
if (!matches) return phoneString;
// Remplacer 'L' par le préfixe et 'X' par les chiffres du numéro de téléphone
const [_, countryCode, number] = matches;
const prefix = `+${countryCode}`;
const digits = number;
// Initialiser le numéro formaté avec le préfixe
let formattedNumber = toFormat.replace('L', prefix);
let digitIndex = 0;
formattedNumber = formattedNumber.replace(/X/g, () => {
// Remplacer les X par les chiffres
formattedNumber = formattedNumber.replace(/X/g, (match, offset) => {
// Si c'est le dernier groupe de X, mettre tous les chiffres restants
if (offset === formattedNumber.lastIndexOf('X') - match.length + 1) {
const remainingDigits = digits.substring(digitIndex);
digitIndex += remainingDigits.length;
return remainingDigits;
}
// Sinon, mettre un seul chiffre
return digits[digitIndex++] || '';
});
return formattedNumber;
}
// Fonction pour valider un numéro de téléphone au format E.164
// Le format E.164 est un format international pour les numéros de téléphone
// qui commence par un signe '+' suivi du code pays et du numéro de téléphone
// Par exemple : +33123456789 pour un numéro français
export function validateE164PhoneNumber(phoneNumber) {
const regEx = /^\+[1-9]\d{9,14}$/;
return regEx.test(phoneNumber);
};