feat: translation competition part

This commit is contained in:
Thibaut Valentin 2026-01-14 18:24:26 +01:00
parent d8b6f1ad25
commit 46f543abde
7 changed files with 503 additions and 304 deletions

View File

@ -4,15 +4,19 @@
"---ToutLesClubs---": "--- all clubs ---",
"---ToutLesPays---": "--- all countries ---",
"---TouteLesCatégories---": "--- all categories ---",
"--NonLicencier--": "-- Unlicensed --",
"--NonLicencier--": "-- Not licensed --",
"--SélectionnerCatégorie--": "-- Select category --",
"1Catégorie": "+1 category",
"2Catégorie": "+2 categories",
"activer": "Activate",
"admin": "Administration",
"administrateur": "Administrator",
"adresse": "Address",
"adresseAdministrative": "Administrative address",
"aff.ancienNom": "Former name: {{name}}",
"aff.byMembreSim": "By similar member",
"aff.byNewMenbre": "By new member",
"aff.byNoLicence": "By licence number",
"aff.byNoLicence": "By license number",
"aff.info1": "This club has already been affiliated (affiliation no. {{no}})",
"aff.membreNo": "Member no. {{no}}",
"aff.nomDuClub": "Club name",
@ -21,8 +25,8 @@
"aff.refusConfirm": "Are you sure you want to refuse this request?",
"aff.refuserLaDemande": "Refuse the request",
"aff.refuserLaDemande.detail": "Are you sure you want to refuse this request?",
"aff.submit.error1": "Please enter a valid licence number for member {{id}}",
"aff.submit.error2": "Please enter a valid licence number for member {{id}}",
"aff.submit.error1": "Please enter a valid license number for member {{id}}",
"aff.submit.error2": "Please enter a valid license number for member {{id}}",
"aff.submit.error3": "Please enter a valid email for member {{id}}",
"aff.toast.accept.error": "Failed to accept affiliation",
"aff.toast.accept.pending": "Accepting affiliation in progress",
@ -45,7 +49,7 @@
"aff_req.button.confirm": "Confirm my affiliation request",
"aff_req.button.save": "Save changes",
"aff_req.denomination": "Denomination",
"aff_req.disposeLicence": "Already has a licence",
"aff_req.disposeLicence": "Already has a license",
"aff_req.error1": "The role of member {{i}} is required",
"aff_req.error2": "The SIRET/RNA format is invalid",
"aff_req.nomDeLassociation": "Association name",
@ -54,28 +58,31 @@
"aff_req.text2.li": [
"Have its headquarters in France or the Principality of Monaco",
"Be constituted in accordance with Chapter 1 of Title II of Book 1 of the Sports Code",
"Pursue a social purpose that falls within the definition of Article 1 of the Federation's statutes",
"Pursue a purpose that falls within the definition of Article 1 of the Federation's statutes",
"Have statutes compatible with the principles of organization and operation of the Federation",
"Ensure freedom of opinion and respect for the rights of the defense within it, and prohibit any discrimination",
"Ensure freedom of opinion and respect for the rights of defense within it, and prohibit any discrimination",
"Comply with the rules of supervision, hygiene, and safety established by the Federation's regulations"
],
"aff_req.text3": "After your request is validated, you will receive a temporary ID and password to access your FFSAF space",
"aff_req.text3": "After validation of your request, you will receive a temporary identifier and password to access your FFSAF space",
"aff_req.text4": "Note that to finalize your affiliation, you will need to:",
"aff_req.text4.li1": "Have at least three licensed members, including the president",
"aff_req.text4.li2": "Have paid the fees provided for by the federal regulations",
"aff_req.text5": "You can later add publicly visible addresses for your training locations",
"aff_req.text6": "Leave blank to make no changes. (If a coat of arms has already been sent with this request, it will be used; otherwise, we will use the one from the previous affiliation)",
"aff_req.text7": "Affiliation request sent successfully",
"aff_req.text8": "Once your request is validated, you will receive a temporary ID and password to access your FFSAF space",
"aff_req.text8": "Once your request is validated, you will receive a temporary identifier and password to access your FFSAF space",
"aff_req.toast.undo.error": "Failed to cancel affiliation request",
"aff_req.toast.undo.pending": "Cancelling affiliation request in progress",
"aff_req.toast.undo.success": "Affiliation request cancelled successfully 🎉",
"afficherLétatDesAffiliation": "Display affiliation status",
"affiliation": "Affiliation",
"affiliationNo": "Affiliation no. {{no}}",
"ajout": "Addition",
"ajouterUnClub": "Add a club",
"ajouterUnMembre": "Add a member",
"all_season": "--- all seasons ---",
"au": "to",
"aucun": "None",
"aucunMembreSélectionné": "No member selected",
"back": "« back",
"blason": "Coat of arms",
@ -86,9 +93,12 @@
"button.appliquer": "Apply",
"button.confirmer": "Confirm",
"button.créer": "Create",
"button.enregister": "Save",
"button.enregistrer": "Save",
"button.fermer": "Close",
"button.modifier": "Edit",
"button.refuser": "Refuse",
"button.seDésinscrire": "Unsubscribe",
"button.suivant": "Next",
"button.supprimer": "Delete",
"cat.benjamin": "Benjamin",
@ -103,14 +113,15 @@
"cat.superMini": "Super Mini",
"cat.vétéran1": "Veteran 1",
"cat.vétéran2": "Veteran 2",
"categorie": "category",
"catégorie": "Category",
"certificatMédical": "Medical certificate",
"chargement...": "Loading...",
"chargerLexcel": "Upload Excel",
"chargerLexcel.msg": "Please use the file above as a base; do not rename the columns or modify the licence numbers.",
"chargerLexcel": "Load Excel",
"chargerLexcel.msg": "Please use the file above as a template; do not rename the columns or modify the license numbers.",
"choisir...": "Choose...",
"club.aff_renew.msg": "Please select 0 to 3 board members to fill out the pre-request. (If a non-board member will become one next year, you can enter them in the next step)",
"club.change.status": "To modify the above information, please contact FFSAF by email.",
"club.change.status": "To modify the above information, please contact the FFSAF by email.",
"club.contact.tt": {
"AUTRE": "Other club contact",
"COURRIEL": "Club email address<br>Example: contact@ffsaf.fr",
@ -135,15 +146,115 @@
"club_one": "Club",
"club_other": "Clubs",
"club_zero": "No club",
"combattant": "fighter",
"comp.aff.blason": "Display the club's coat of arms on screens",
"comp.aff.flag": "Display the fighter's country on screens",
"comp.ajoutRapide": "Quick add",
"comp.ajouterUnCombattant": "Add a fighter",
"comp.ajouterUnInvité": "Add a guest",
"comp.billetterie": "Ticketing",
"comp.billetterieHelloasso": "HelloAsso Ticketing",
"comp.catégorieNormalisée": "Standardized category",
"comp.combattantNonTrouvé": "Fighter not found",
"comp.combattantsInscrits": "Registered fighters",
"comp.compétitionFuture": "Future competition",
"comp.compétitionPassée": "Past competition",
"comp.créationCompétition": "Competition creation",
"comp.dateDinscription": "Registration date",
"comp.editionCompétition": "Competition edition",
"comp.error1": "The end date must be after the start date.",
"comp.error2": "Please enter the start and end dates of registration.",
"comp.error3": "The end date of registration must be after the start date of registration.",
"comp.exporterLesInscription": "Export registrations",
"comp.ha.emailDeRéceptionDesInscriptionséchoué": "Email for receiving failed registrations",
"comp.ha.error1": "Please enter the HelloAsso ticketing URL and associated rates.",
"comp.ha.error2": "The HelloAsso ticketing URL is invalid. Please check the URL format.",
"comp.ha.error3": "Please enter the email for receiving failed registrations.",
"comp.ha.tarifsHelloasso": "HelloAsso rates",
"comp.ha.text1": "To ensure good interconnection with HelloAsso, please follow these instructions:",
"comp.ha.text2": "<strong>Configure the notification URL:</strong> In order for us to receive a notification for each registration, it is necessary to configure the notification URL of your HelloAsso account to redirect to \"https://intra.ffsaf.fr/api/webhook/ha\". To do this, from the home page of your association on HelloAsso, go to <strong>My account</strong> > <strong>Settings</strong> > <strong>Integrations and API</strong> section Notification and copy-paste <strong>https://intra.ffsaf.fr/api/webhook/ha</strong> in the <strong>My callback URL</strong> field and save.",
"comp.ha.text3": "<strong>Copy-paste the exact name of the rates</strong> <em>-separated by semicolons-</em> that will result in automatic registration. All these rates must imperatively require the license number as a mandatory field. To do this, during the configuration of your ticketing at step 3, click on <strong>+ Add information</strong>, enter the exact title <strong>License number</strong>, in <strong>Type of response desired</strong> enter Number, select the rates entered previously and make the information mandatory.",
"comp.ha.text4": "<strong>Copy-paste the URL of your ticketing</strong> in the field below. It should look like this: https://www.helloasso.com/associations/__asso-name-on-helloasso__/events/__ticketing-name__",
"comp.ha.text5": "HelloAsso ticketing URL",
"comp.ha.text6": "If for any reason the automatic registration fails, an email will be sent to this address to inform you",
"comp.informationsGénéralesSurLaCompétition": "General information about the competition",
"comp.informationsSurLeModeDinscription": "Information on the registration mode",
"comp.informationsTechniques": "Technical information",
"comp.inscription": "Registration",
"comp.inscriptionModeAdministrateur": "Registration - administrator mode",
"comp.inscriptionsLibres": "Free registrations",
"comp.inscriptionsParLesAdministrateursDeLaCompétition": "Registrations by competition administrators",
"comp.inscriptionsParLesResponsablesDeClub": "Registrations by club managers",
"comp.inscriptionsSurLaBilletterieHelloasso": "Registrations on the HelloAsso ticketing",
"comp.modal.information": "Information",
"comp.modal.poids": "Weight (in kg)",
"comp.modal.recherche": "Search*",
"comp.modal.surclassement": "Overclassification",
"comp.modal.text1": "Guests are reserved for members not licensed by the federation. Fighters registered via this form will not be able to see their results from their profile.",
"comp.modal.text2": "Prevent members/clubs from modifying this registration",
"comp.modifierLesParticipants": "View/Edit participants",
"comp.monInscription": "My registration",
"comp.noDeLicence": "License number",
"comp.nouvelleCompétition": "New competition",
"comp.organisateur": "Organizer",
"comp.quiPeutInscrire": "Who can register",
"comp.reg.libres": "Free",
"comp.reg.parLesAdministrateursDeLaCompétition": "By competition administrators",
"comp.reg.parLesResponsablesDeClub": "By club managers",
"comp.reg.surLaBilletterieHelloasso": "On the HelloAsso ticketing",
"comp.responsablesEtBureauxDesAssociations": "Managers and boards of associations",
"comp.sinscrire": "Register",
"comp.supprimerLaCompétition": "Delete competition",
"comp.supprimerLaCompétition.msg": "Are you sure you want to delete this competition and all associated results?",
"comp.surclassement_one": "{{cat}} with 1 overclassification",
"comp.surclassement_other": "{{cat}} with {{count}} overclassifications",
"comp.surclassement_zero": "{{cat}}",
"comp.text2": "Visible to the public (appears in the list of competitions)",
"comp.text3": "If not checked, the competition will only be visible to people who can register participants.",
"comp.tips": "Tip 1: It is possible to ban a fighter, which will prevent them from being re-registered by any means other than by an administrator of this competition. To do this, click on the small <1/> next to their name.<br/>Tip 2: It is also possible to lock the modifications of their registration from their file, which will prevent it from being modified/deleted by themselves and/or a club manager.",
"comp.toast.del.error": "Failed to delete competition",
"comp.toast.del.pending": "Deleting competition in progress",
"comp.toast.del.success": "Competition deleted successfully 🎉",
"comp.toast.params.error": "Failed to update competition parameters",
"comp.toast.params.pending": "Updating competition parameters in progress...",
"comp.toast.params.success": "Competition parameters updated successfully 🎉",
"comp.toast.register.add.error": "Fighter not found",
"comp.toast.register.add.pending": "Search in progress",
"comp.toast.register.add.success": "Fighter found and added/updated",
"comp.toast.register.ban.error": "Error",
"comp.toast.register.ban.pending": "Unregistration in progress",
"comp.toast.register.ban.success": "Fighter unregistered and banned",
"comp.toast.register.del.error": "Error",
"comp.toast.register.del.pending": "Unregistration in progress",
"comp.toast.register.del.success": "Fighter unregistered",
"comp.toast.register.self.add.error": "Error during registration",
"comp.toast.register.self.add.pending": "Registration in progress",
"comp.toast.register.self.add.success": "Registration completed 🎉",
"comp.toast.register.self.del.error": "Error during unregistration",
"comp.toast.register.self.del.pending": "Unregistration in progress",
"comp.toast.register.self.del.success": "Unregistration completed",
"comp.toast.save.error": "Failed to save competition",
"comp.toast.save.pending": "Saving competition in progress",
"comp.toast.save.success": "Competition saved successfully 🎉",
"comp.tousLesMembresDeLaFfsaf": "All FFSAF members",
"comp.typeDinscription": "Registration type",
"comp.uniquementLesAdministrateursDeLaCompétition": "Only competition administrators",
"comp.warn1": "Are you sure you want to unregister and ban this fighter from the competition?\n(You can re-register them later)",
"comp.warn2": "Are you sure you want to unregister this fighter?\nThis will not unregister them from the HelloAsso ticketing and will not refund them.",
"comp.warn3": "Are you sure you want to unregister this fighter?",
"comp.warn4": "Are you sure you want to unregister yourself?",
"comp_manage": "Competitions Manager",
"competition_one": "Competition",
"competition_other": "Competitions",
"compte": "Account",
"compétition": "Competition",
"configuration": "Configuration",
"conserverLancienEmail": "Keep the old email",
"contactAdministratif": "Administrative contact",
"contactInterne": "Internal contact",
"contact_one": "Contact",
"contact_other": "Contacts",
"date": "Date",
"dateDeNaissance": "Date of birth",
"days": [
"Monday",
@ -156,14 +267,17 @@
],
"de": "of",
"demandeDaffiliationEnCours": "Affiliation request in progress",
"demandeDeLicence": "Licence request",
"demandeDeLicence": "License request",
"demander": "Request",
"description": "Description",
"dlAff": "Download affiliation certificate",
"donnéesAdministratives": "Administrative data",
"définirLidDuCompte": "Set account ID",
"du": "From",
"dun": "of a",
"définirLidDuCompte": "Define account ID",
"editionDeL'affiliation": "Editing affiliation",
"editionDeLaDemande": "Editing request",
"editionDeLaLicence": "Editing licence",
"editionDeLaLicence": "Editing license",
"editionDeLaSéléction": "Editing selection",
"editionDeLadresse": "Editing address",
"email": "Email",
@ -173,26 +287,30 @@
"erreurDePaiement.detail": "Error message:",
"erreurDePaiement.msg": "An error occurred while processing your payment. Please try again later.",
"espaceAdministration": "Administration space",
"f": "F",
"faitPar": "Done by",
"femme": "Female",
"filtre": "Filter",
"genre": "Gender",
"gestionGroupée": "Group management",
"gradeDarbitrage": "Referee grade",
"gradeDarbitrage": "Refereeing grade",
"h": "M",
"home": {
"header1": "For licensed members",
"header2": "For clubs",
"text1": "Here you will find all your information as well as the status of your registration with the federation. You can also download your registration certificate, sign up for competitions, and view your results, provided that the organizing club has entered them.<br/><br/>During your first registration, you will receive an email containing your login details; this email will be sent once your licence is validated by the secretariat.",
"text2": "This is where you can take out federal licences for your members, request or renew your affiliation, enter your schedules, training locations, and social networks, which will then be displayed on the ffsaf.fr website.<br/>You will also have the possibility to publish registration forms for your competitions and record the results.<br/><br/>Not yet affiliated with the federation? Click <1>here</1> to make your first request.",
"text1": "Here you will find all your information as well as the status of your registration with the federation. You can also download your registration certificate, register for competitions, and consult your results, provided that the organizing club has entered them.<br/><br/>During your first registration, you will receive an email containing your login information; this email will be sent once your license is validated by the secretariat.",
"text2": "This is where you can take out federal licenses for your members, where you can request or renew your affiliation, enter your schedules, training locations, and social networks, which will then be displayed on the ffsaf.fr website.<br/>You will also have the possibility to publish registration forms for your competitions and to record the results.<br/><br/>Not yet affiliated with the federation? Click <1>here</1> to make your first request.",
"welcome_message": "Welcome to the intranet of the Fédération France Soft Armored Fighting"
},
"homme": "Male",
"horairesD'entraînements": "Training schedules",
"information": "Information",
"invité": "guest",
"keepEmpty": "Leave blank to make no changes.",
"le": "the",
"licence": "Licence",
"licenceNo": "Licence no. {{no}}",
"licence": "License",
"licenceNo": "License no. {{no}}",
"lieu": "Place",
"lieuxDentraînements": "Training locations",
"loading": "Loading...",
"me": {
@ -207,33 +325,33 @@
"toast.settings.success": "Settings updated successfully 🎉"
},
"me.changerMonMotDePasse": "Change my password",
"me.formationDarbitrage": "Referee training",
"me.formationDarbitrage": "Refereeing training",
"me.paramètresDuCompte": "Account settings",
"me.rôleAuSienDuClub": "Role within the club",
"me.visibilitéDesRésultats": "Results visibility",
"me.visibilitéDesRésultats": "Visibility of results",
"member_one": "Member",
"member_other": "Members",
"membre.emailVideàLaLigne": "Empty email on line {{no}}",
"membre.emailVérifié": "Verified email",
"membre.emailVérifié": "Email verified",
"membre.filtre.inactif": "Show inactive fighters",
"membre.filtre.licence": "Show licence status",
"membre.filtre.licence": "Show license status",
"membre.filtre.licences": [
"No request or valid licence",
"With request or valid licence",
"No request or valid license",
"With request or valid license",
"Request in progress",
"Licence validated",
"All licence statuses",
"License validated",
"All license statuses",
"Complete request",
"Incomplete request"
],
"membre.filtre.payement": [
"No payment",
"Without payment",
"With payment",
"All payment statuses"
],
"membre.identifiant": "Identifier",
"membre.import.err1": "Invalid certificate date format on line {{no}}",
"membre.import.err2": "Invalid certificate date format on line {{no}}",
"membre.import.err1": "Invalid medical certificate date format on line {{no}}",
"membre.import.err2": "Invalid medical certificate date format on line {{no}}",
"membre.import.err3": "Empty date of birth on line {{no}}",
"membre.import.err4": "Invalid date of birth format on line {{no}}",
"membre.import.err5": "Invalid email on line {{no}}",
@ -248,8 +366,8 @@
"membre.initaccount.text1": "Enter the account UUID",
"membre.initaccount.text2": "Warning: only change a member's ID if you are sure of what you are doing...",
"membre.noAccount": "This member does not have an account...",
"membre.noAccount.clubMsg": "An account will be created by the federation when their first licence is validated",
"membre.nomVideàLaLigne": "Empty name on line {{no}}",
"membre.noAccount.clubMsg": "An account will be created by the federation when their first license is validated",
"membre.nomVideàLaLigne": "Empty last name on line {{no}}",
"membre.prénomVideàLaLigne": "Empty first name on line {{no}}",
"membre.toast.compte.created": "Account created successfully 🎉",
"membre.toast.compte.error": "Failed to create account",
@ -257,30 +375,30 @@
"membre.toast.del.error": "Failed to delete account",
"membre.toast.del.pending": "Deleting account in progress",
"membre.toast.del.success": "Account deleted successfully 🎉",
"membre.toast.id.error": "Failed to set identifier",
"membre.toast.id.pending": "Setting identifier in progress",
"membre.toast.id.success": "Identifier set successfully 🎉",
"membre.toast.licence.ask.del.error": "Failed to delete licence request",
"membre.toast.licence.ask.del.pending": "Deleting licence request in progress",
"membre.toast.licence.ask.del.success": "Licence request deleted successfully 🎉",
"membre.toast.licence.ask.error": "Failed to request licence",
"membre.toast.licence.ask.pending": "Recording licence request in progress",
"membre.toast.licence.ask.success": "Licence request recorded successfully 🎉",
"membre.toast.licence.del.error": "Failed to delete licence",
"membre.toast.licence.del.pending": "Deleting licence in progress",
"membre.toast.licence.del.success": "Licence deleted successfully 🎉",
"membre.toast.licence.save.error": "Failed to save licence",
"membre.toast.licence.save.pending": "Saving licence in progress",
"membre.toast.licence.save.success": "Licence saved successfully 🎉",
"membre.toast.licences.export.error": "Failed to export licences",
"membre.toast.licences.export.pending": "Exporting licences in progress",
"membre.toast.licences.export.success": "Licences exported successfully 🎉",
"membre.toast.id.error": "Failed to define identifier",
"membre.toast.id.pending": "Defining identifier in progress",
"membre.toast.id.success": "Identifier defined successfully 🎉",
"membre.toast.licence.ask.del.error": "Failed to delete license request",
"membre.toast.licence.ask.del.pending": "Deleting license request in progress",
"membre.toast.licence.ask.del.success": "License request deleted successfully 🎉",
"membre.toast.licence.ask.error": "Failed to request license",
"membre.toast.licence.ask.pending": "Saving license request in progress",
"membre.toast.licence.ask.success": "License request saved successfully 🎉",
"membre.toast.licence.del.error": "Failed to delete license",
"membre.toast.licence.del.pending": "Deleting license in progress",
"membre.toast.licence.del.success": "License deleted successfully 🎉",
"membre.toast.licence.save.error": "Failed to save license",
"membre.toast.licence.save.pending": "Saving license in progress",
"membre.toast.licence.save.success": "License saved successfully 🎉",
"membre.toast.licences.export.error": "Failed to export licenses",
"membre.toast.licences.export.pending": "Exporting licenses in progress",
"membre.toast.licences.export.success": "Licenses exported successfully 🎉",
"membre.toast.licences.import.error": "Failed to send changes",
"membre.toast.licences.import.pending": "Sending changes in progress",
"membre.toast.licences.import.success": "Changes sent successfully 🎉",
"membre.toast.licences.load.error": "Failed to load licences",
"membre.toast.licences.load.pending": "Loading licences in progress",
"membre.toast.licences.load.success": "Licences loaded successfully 🎉",
"membre.toast.licences.load.error": "Failed to load licenses",
"membre.toast.licences.load.pending": "Loading licenses in progress",
"membre.toast.licences.load.success": "Licenses loaded successfully 🎉",
"membre.toast.perm.error": "Failed to update permissions 😕",
"membre.toast.perm.pending": "Updating permissions in progress...",
"membre.toast.perm.success": "Permissions updated successfully 🎉",
@ -294,6 +412,7 @@
"membre.toast.select.save.pending": "Saving selection in progress",
"membre.toast.select.save.success": "Selection saved successfully 🎉",
"mettreàJours": "Update",
"modification": "Modification",
"nationalité": "Nationality",
"nav": {
"account": "My account",
@ -310,21 +429,22 @@
"space": "My space",
"title": "FFSAF Intranet"
},
"noLicence": "Licence no.",
"noLicence": "License no.",
"noSiretOuRna": "SIRET or RNA no.",
"nom": "Last name",
"nombreDeLicences": "Number of licences",
"nombreDeLicencesParCatégorie": "Number of licences by category for {{saison}}",
"nombreDeLicences": "Number of licenses",
"nombreDeLicencesParCatégorie": "Number of licenses by category for {{saison}}",
"non": "No",
"nonDéfinie": "Not defined",
"nonValidée": "Not validated",
"nouveauClub": "New club",
"nouveauMembre": "New member",
"nouvelEmail": "New email",
"ou": "or",
"oui": "Yes",
"outdated_session": {
"login_button": "Log in again",
"message": "Your session has expired. Please log in again to continue using the application.",
"message": "Your session has expired, please log in again to continue using the application.",
"title": "Session expired"
},
"pageClub": "Club page",
@ -332,20 +452,21 @@
"pageNouveauClub": "New club page",
"page_info_full": "Line {{line}} to {{tt_line}} (page {{page}} of {{tt_page}})",
"page_info_ligne": "{{show}} line(s) displayed out of {{total}}",
"paiementDeLaLicence": "Licence payment",
"paiementDesLicences": "Licence payments",
"pasDeLicence": "No licence",
"paiementDeLaLicence": "License payment",
"paiementDesLicences": "License payments",
"par": "by",
"pasDeLicence": "No license",
"payment.ha.info": "HelloAsso's solidarity model guarantees that 100% of your payment will be transferred to the chosen association. You can support the help they provide to associations by leaving a voluntary contribution to HelloAsso at the time of your payment.",
"payment.info": "About HelloAsso",
"payment.paiementSécurisé": "Secure payment",
"payment.payerAvec": "Pay with",
"payment.recap": "{{count}} licence(s) selected<br/>Total to pay: {{total}} €",
"paymentDesLicences": "Licence payments",
"paymentDesLicences.msg_one": "Are you sure you want to mark the licence as paid?",
"paymentDesLicences.msg_other": "Are you sure you want to mark the {{count}} licences as paid?",
"payment.recap": "{{count}} license(s) selected<br/>Total amount to pay: {{total}} €",
"paymentDesLicences": "License payments",
"paymentDesLicences.msg_one": "Are you sure you want to mark the license as paid?",
"paymentDesLicences.msg_other": "Are you sure you want to mark the {{count}} licenses as paid?",
"paymentDesLicences.msg_zero": "$t(paymentDesLicences.msg_other)",
"paymentOk": "🎉 Your payment has been processed successfully. 🎉",
"paymentOk.msg": "Thank you for your payment. The licences should be activated within the next hour, provided the medical certificate is completed.",
"paymentOk": "🎉Your payment has been processed successfully.🎉",
"paymentOk.msg": "Thank you for your payment. The licenses should be activated within the next hour, provided that the medical certificate is completed.",
"pays": "Country",
"perm.administrateurDeLaFédération": "Federation administrator",
"perm.créerDesCompétion": "Create competitions",
@ -353,6 +474,7 @@
"permission": "Permission",
"photos": "Photos",
"prenom": "First name",
"prénomEtNom": "First and last name",
"rechercher": "Search",
"rechercher...": "Search...",
"registration_one": "Registration",
@ -372,6 +494,7 @@
"role.vise-secrétaire": "Vice-Secretary",
"role.vise-trésorier": "Vice-Treasurer",
"saison": "Season",
"secrétariatsDeLice": "Ring secretariats",
"selectionner...": "Select...",
"siretOuRna": "SIRET or RNA",
"stats": "Statistics",
@ -387,32 +510,33 @@
"toast.edit.error": "Failed to save changes",
"toast.edit.pending": "Saving changes in progress",
"toast.edit.success": "Changes saved successfully 🎉",
"toast.licence.bulk.pay.error": "Failed to mark licences as paid",
"toast.licence.bulk.pay.pending": "Marking licences as paid in progress",
"toast.licence.bulk.pay.success": "Licences marked as paid successfully 🎉",
"toast.licence.bulk.valid.error": "Failed to validate licences",
"toast.licence.bulk.valid.pending": "Validating licences in progress",
"toast.licence.bulk.valid.success": "Licences validated successfully 🎉",
"toast.licence.bulk.pay.error": "Failed to mark licenses as paid",
"toast.licence.bulk.pay.pending": "Marking licenses as paid in progress",
"toast.licence.bulk.pay.success": "Licenses marked as paid successfully 🎉",
"toast.licence.bulk.valid.error": "Failed to validate licenses",
"toast.licence.bulk.valid.pending": "Validating licenses in progress",
"toast.licence.bulk.valid.success": "Licenses validated successfully 🎉",
"toast.licence.order.error": "Failed to create order",
"toast.licence.order.pending": "Creating order in progress",
"toast.licence.order.success": "Order created successfully 🎉",
"trie": "Sort",
"téléchargerLexcelDesMembres": "Download members' Excel",
"téléchargerLexcelDesMembres.info": "Use as a template to update information",
"téléchargéeLaLicence": "Download licence",
"validationDeLaLicence": "Licence validation",
"validationDesLicences": "Licence validations",
"validerDesLicences": "Validate licences",
"validerLePayement_one": "Validate payment for the selected licence",
"validerLePayement_other": "Validate payment for the {{count}} selected licences",
"téléchargerLexcelDesMembres.info": "To be used as a template to update information",
"téléchargéeLaLicence": "Download license",
"validationDeLaLicence": "License validation",
"validationDesLicences": "License validations",
"validerDesLicences": "Validate licenses",
"validerLePayement_one": "Validate payment for the selected license",
"validerLePayement_other": "Validate payment for the {{count}} selected licenses",
"validerLePayement_zero": "$t(validerLePayement_other)",
"validerLicence.msg_one": "Are you sure you want to validate the licence?",
"validerLicence.msg_other": "Are you sure you want to validate the {{count}} licences?",
"validerLicence.msg_one": "Are you sure you want to validate the license?",
"validerLicence.msg_other": "Are you sure you want to validate the {{count}} licenses?",
"validerLicence.msg_zero": "$t(validerLicence.msg_other)",
"validerLicence_one": "Validate the selected licence",
"validerLicence_other": "Validate the {{count}} selected licences",
"validerLicence_one": "Validate the selected license",
"validerLicence_other": "Validate the {{count}} selected licenses",
"validerLicence_zero": "$t(validerLicence_other)",
"validée": "Validated",
"voir/modifierLesParticipants": "View/Edit participants",
"voirLesStatues": "View statues",
"à": "at",
"étatDeLaDemande": "Request status"

View File

@ -5,8 +5,12 @@
"---ToutLesPays---": "--- tout les pays ---",
"---TouteLesCatégories---": "--- toute les catégories ---",
"--NonLicencier--": "-- Non licencier --",
"--SélectionnerCatégorie--": "-- Sélectionner catégorie --",
"1Catégorie": "+1 catégorie",
"2Catégorie": "+2 catégorie",
"activer": "Activer",
"admin": "Administration",
"administrateur": "Administrateur",
"adresse": "Adresse",
"adresseAdministrative": "Adresse administrative",
"aff.ancienNom": "Ancien nom: {{name}}",
@ -73,9 +77,12 @@
"afficherLétatDesAffiliation": "Afficher l'état des affiliation",
"affiliation": "Affiliation",
"affiliationNo": "Affiliation n°{{no}}",
"ajout": "Ajout",
"ajouterUnClub": "Ajouter un club",
"ajouterUnMembre": "Ajouter un membre",
"all_season": "--- tout les saisons ---",
"au": "au",
"aucun": "Aucun",
"aucunMembreSélectionné": "Aucun membre sélectionné",
"back": "« retour",
"blason": "Blason",
@ -86,9 +93,12 @@
"button.appliquer": "Appliquer",
"button.confirmer": "Confirmer",
"button.créer": "Créer",
"button.enregister": "Enregister",
"button.enregistrer": "Enregistrer",
"button.fermer": "Fermer",
"button.modifier": "Modifier",
"button.refuser": "Refuser",
"button.seDésinscrire": "Se désinscrire",
"button.suivant": "Suivant",
"button.supprimer": "Supprimer",
"cat.benjamin": "Benjamin",
@ -103,6 +113,7 @@
"cat.superMini": "Super Mini",
"cat.vétéran1": "Vétéran 1",
"cat.vétéran2": "Vétéran 2",
"categorie": "categorie",
"catégorie": "Catégorie",
"certificatMédical": "Certificat médical",
"chargement...": "Chargement...",
@ -135,15 +146,115 @@
"club_one": "Club",
"club_other": "Clubs",
"club_zero": "Sans club",
"combattant": "combattant",
"comp.aff.blason": "Afficher le blason du club sur les écrans",
"comp.aff.flag": "Afficher le pays du combattant sur les écrans",
"comp.ajoutRapide": "Ajout rapide",
"comp.ajouterUnCombattant": "Ajouter un combattant",
"comp.ajouterUnInvité": "Ajouter un invité",
"comp.billetterie": "Billetterie",
"comp.billetterieHelloasso": "Billetterie HelloAsso",
"comp.catégorieNormalisée": "Catégorie normalisée",
"comp.combattantNonTrouvé": "Combattant non trouvé",
"comp.combattantsInscrits": "Combattants inscrits",
"comp.compétitionFuture": "Compétition future",
"comp.compétitionPassée": "Compétition passée",
"comp.créationCompétition": "Création compétition",
"comp.dateDinscription": "Date d'inscription",
"comp.editionCompétition": "Edition compétition",
"comp.error1": "La date de fin doit être postérieure à la date de début.",
"comp.error2": "Veuillez renseigner les dates de début et de fin d'inscription.",
"comp.error3": "La date de fin d'inscription doit être postérieure à la date de début d'inscription.",
"comp.exporterLesInscription": "Exporter les inscription",
"comp.ha.emailDeRéceptionDesInscriptionséchoué": "Email de réception des inscriptions échoué",
"comp.ha.error1": "Veuillez renseigner l'URL de la billetterie HelloAsso et les tarifs associés.",
"comp.ha.error2": "L'URL de la billetterie HelloAsso n'est pas valide. Veuillez vérifier le format de l'URL.",
"comp.ha.error3": "Veuillez renseigner l'email de réception des inscriptions échouées.",
"comp.ha.tarifsHelloasso": "Tarifs HelloAsso",
"comp.ha.text1": "Afin de permettre une bonne interconnexion avec HelloAsso, merci de suivre les instructions suivantes :",
"comp.ha.text2": "<strong>Configurer l'url de notification : </strong>afin que nous puissions recevoir une notification à chaque inscription, il est nécessaire de configurer l'url de notification de votre compte HelloAsso pour qu'il redirige vers \"https://intra.ffsaf.fr/api/webhook/ha\". Pour ce faire, depuis la page d'accueil de votre association sur HelloAsso, allez dans <strong>Mon compte</strong> > <strong>Paramètres</strong> > <strong> Intégrations et API</strong> section Notification et copier-coller <strong>https://intra.ffsaf.fr/api/webhook/ha </strong> dans le champ <strong>Mon URL de callback</strong> et enregister.",
"comp.ha.text3": "<strong>Copier-coller le nom exacte des tarifs</strong> <em>-sépare par des point-virgules-</em> qui donneront lieux à une inscription automatique. Tous ces tarifs doivent impérativement demander le numéro de licence en champs obligatoire. Pour ce faire, lors de la configuration de votre billetterie à l'étape n°3, cliquer sur <strong>+ Ajouter une information</strong>, saisissez l'intituler exact suivant <strong> Numéro de licence</strong>, dans <strong>Type de réponse souhaitée</strong> rentrer Nombre, sélectionner les tarifs entrés plus précédemment et rendre l'information obligatoire.",
"comp.ha.text4": "<strong>Copier-coller l'url de votre billetterie</strong> dans le champs si dessous. Il devrais avoir la forme suivante: https://www.helloasso.com/associations/__nom-asso-sur-helloasso__/evenements/__nom-billetterie__",
"comp.ha.text5": "Url de la billetterie HelloAsso",
"comp.ha.text6": "Si pour une raison quelconque l'inscription automatique échoue, un email sera envoyé à cette adresse pour vous en informer",
"comp.informationsGénéralesSurLaCompétition": "Informations générales sur la compétition",
"comp.informationsSurLeModeDinscription": "Informations sur le mode d'inscription",
"comp.informationsTechniques": "Informations techniques",
"comp.inscription": "Inscription",
"comp.inscriptionModeAdministrateur": "Inscription - mode administrateur",
"comp.inscriptionsLibres": "Inscriptions libres",
"comp.inscriptionsParLesAdministrateursDeLaCompétition": "Inscriptions par les administrateurs de la compétition",
"comp.inscriptionsParLesResponsablesDeClub": "Inscriptions par les responsables de club",
"comp.inscriptionsSurLaBilletterieHelloasso": "Inscriptions sur la billetterie HelloAsso",
"comp.modal.information": "Information",
"comp.modal.poids": "Poids (en kg)",
"comp.modal.recherche": "Recherche*",
"comp.modal.surclassement": "Surclassement",
"comp.modal.text1": "Les invités sont réservés aux membres non licenciés par la fédération. Les combattants inscrits via ce formulaire ne pourront pas voir leur résultat depuis leur profil.",
"comp.modal.text2": "Empêcher les membres/club de modifier cette inscription",
"comp.modifierLesParticipants": "Voir/Modifier les participants",
"comp.monInscription": "Mon inscription",
"comp.noDeLicence": "N° de licence",
"comp.nouvelleCompétition": "Nouvelle compétition",
"comp.organisateur": "Organisateur",
"comp.quiPeutInscrire": "Qui peut inscrire",
"comp.reg.libres": "Libres",
"comp.reg.parLesAdministrateursDeLaCompétition": "Par les administrateurs de la compétition",
"comp.reg.parLesResponsablesDeClub": "Par les responsables de club",
"comp.reg.surLaBilletterieHelloasso": "Sur la billetterie HelloAsso",
"comp.responsablesEtBureauxDesAssociations": "Responsables et bureaux des associations",
"comp.sinscrire": "S'inscrire",
"comp.supprimerLaCompétition": "Supprimer la compétition",
"comp.supprimerLaCompétition.msg": "Êtes-vous sûr de vouloir supprimer cette compétition est tout les resultat associer?",
"comp.surclassement_one": "{{cat}} avec 1 de surclassement",
"comp.surclassement_other": "{{cat}} avec {{count}} de surclassements",
"comp.surclassement_zero": "{{cat}}",
"comp.text2": "Visible par le public (Apparaît dans la liste des compétitions)",
"comp.text3": "Si non coché, la compétition ne sera visible que par les personnes pouvant y inscrire des participants.",
"comp.tips": "Tips 1: Il est possible de bannir un combattant, ce qui l'empêchera d'être réinscrit par un autre moyen que par un administrateur de cette compétition. Pour cela, cliquez sur la petite <1/> à côté de son nom.<br/>Tips 2: Il est aussi possible de verrouiller les modifications de son inscription depuis sa fiche, ce qui l'empêchera d'être modifié/supprimé par lui-même et/ou un responsable de club.",
"comp.toast.del.error": "Échec de la suppression de la compétition",
"comp.toast.del.pending": "Suppression de la compétition en cours",
"comp.toast.del.success": "Compétition supprimée avec succès 🎉",
"comp.toast.params.error": "Échec de la mise à jours des paramètres de la compétition",
"comp.toast.params.pending": "Mise à jours des paramètres de la compétition en cours...",
"comp.toast.params.success": "Paramètres de la compétition mis à jours avec succès 🎉",
"comp.toast.register.add.error": "Combattant non trouvé",
"comp.toast.register.add.pending": "Recherche en cours",
"comp.toast.register.add.success": "Combattant trouvé et ajouté/mis à jour",
"comp.toast.register.ban.error": "Erreur",
"comp.toast.register.ban.pending": "Désinscription en cours",
"comp.toast.register.ban.success": "Combattant désinscrit et bannie",
"comp.toast.register.del.error": "Erreur",
"comp.toast.register.del.pending": "Désinscription en cours",
"comp.toast.register.del.success": "Combattant désinscrit",
"comp.toast.register.self.add.error": "Erreur lors de l'inscription",
"comp.toast.register.self.add.pending": "Inscription en cours",
"comp.toast.register.self.add.success": "Inscription réalisée 🎉",
"comp.toast.register.self.del.error": "Erreur lors de la désinscription",
"comp.toast.register.self.del.pending": "Désinscription en cours",
"comp.toast.register.self.del.success": "Désinscription réalisée",
"comp.toast.save.error": "Échec de l'enregistrement de la compétition",
"comp.toast.save.pending": "Enregistrement de la compétition en cours",
"comp.toast.save.success": "Compétition enregistrée avec succès 🎉",
"comp.tousLesMembresDeLaFfsaf": "Tous les membres de la FFSAF",
"comp.typeDinscription": "Type d'inscription",
"comp.uniquementLesAdministrateursDeLaCompétition": "Uniquement les administrateurs de la compétition",
"comp.warn1": "Êtes-vous sûr de vouloir désinscrire et bannir ce combattant de la compétition?\n(Vous pouvez le réinscrire plus tard)\"",
"comp.warn2": "Êtes-vous sûr de vouloir désinscrire ce combattant ?\nCela ne le désinscrira pas de la billetterie HelloAsso et ne le remboursera pas.",
"comp.warn3": "Êtes-vous sûr de vouloir désinscrire ce combattant ?",
"comp.warn4": "Êtes-vous sûr de vouloir vous désinscrire ?",
"comp_manage": "Compétitions Manager",
"competition_one": "Compétition",
"competition_other": "Compétitions",
"compte": "Compte",
"compétition": "Compétition",
"configuration": "Configuration",
"conserverLancienEmail": "Conserver l'ancien email",
"contactAdministratif": "Contact administratif",
"contactInterne": "Contact interne",
"contact_one": "Contact",
"contact_other": "Contacts",
"date": "Date",
"dateDeNaissance": "Date de naissance",
"days": [
"Lundi",
@ -158,8 +269,11 @@
"demandeDaffiliationEnCours": "Demande d'affiliation en cours",
"demandeDeLicence": "Demande de licence ",
"demander": "Demander",
"description": "Description",
"dlAff": "Téléchargée l'attestation d'affiliation",
"donnéesAdministratives": "Données administratives",
"du": "Du",
"dun": "d'un",
"définirLidDuCompte": "Définir l'id du compte",
"editionDeL'affiliation": "Edition de l'affiliation",
"editionDeLaDemande": "Edition de la demande ",
@ -173,12 +287,14 @@
"erreurDePaiement.detail": "Message d'erreur :",
"erreurDePaiement.msg": "Une erreur est survenue lors du traitement de votre paiement. Veuillez réessayer plus tard.",
"espaceAdministration": "Espace administration",
"f": "F",
"faitPar": "Fait par",
"femme": "Femme",
"filtre": "Filtre",
"genre": "Genre",
"gestionGroupée": "Gestion groupée",
"gradeDarbitrage": "Grade d'arbitrage",
"h": "H",
"home": {
"header1": "Pour les licenciés",
"header2": "Pour les clubs",
@ -189,10 +305,12 @@
"homme": "Homme",
"horairesD'entraînements": "Horaires d'entraînements",
"information": "Information",
"invité": "invité",
"keepEmpty": "Laissez vide pour ne rien changer.",
"le": "le",
"licence": "Licence",
"licenceNo": "Licence n°{{no}}",
"lieu": "Lieu",
"lieuxDentraînements": "Lieux d'entraînements",
"loading": "Chargement...",
"me": {
@ -294,6 +412,7 @@
"membre.toast.select.save.pending": "Enregistrement de la séléction en cours",
"membre.toast.select.save.success": "Séléction enregistrée avec succès 🎉",
"mettreàJours": "Mettre à jours",
"modification": "Modification",
"nationalité": "Nationalité",
"nav": {
"account": "Mon compte",
@ -321,6 +440,7 @@
"nouveauClub": "Nouveau club",
"nouveauMembre": "Nouveau membre",
"nouvelEmail": "Nouvel email",
"ou": "Ou",
"oui": "Oui",
"outdated_session": {
"login_button": "Se reconnecter",
@ -334,6 +454,7 @@
"page_info_ligne": "{{show}} ligne(s) affichée(s) sur {{total}}",
"paiementDeLaLicence": "Paiement de la licence",
"paiementDesLicences": "Paiement des licences",
"par": "par",
"pasDeLicence": "Pas de licence",
"payment.ha.info": "Le modèle solidaire de HelloAsso garantit que 100% de votre paiement sera versé à lassociation choisie. Vous pouvez soutenir laide quils apportent aux associations en laissant une contribution volontaire à HelloAsso au moment de votre paiement.",
"payment.info": "A propos de HelloAsso",
@ -353,6 +474,7 @@
"permission": "Permission",
"photos": "Photos",
"prenom": "Prénom",
"prénomEtNom": "Prénom et nom",
"rechercher": "Rechercher",
"rechercher...": "Rechercher...",
"registration_one": "Inscription",
@ -372,6 +494,7 @@
"role.vise-secrétaire": "Vise-Secrétaire",
"role.vise-trésorier": "Vise-Trésorier",
"saison": "Saison",
"secrétariatsDeLice": "Secrétariats de lice",
"selectionner...": "Sélectionner...",
"siretOuRna": "SIRET ou RNA",
"stats": "Statistiques",
@ -413,6 +536,7 @@
"validerLicence_other": "Valider les {{count}} licences sélectionnées",
"validerLicence_zero": "$t(validerLicence_other)",
"validée": "Validée",
"voir/modifierLesParticipants": "Voir/Modifier les participants",
"voirLesStatues": "Voir les statues",
"à": "à",
"étatDeLaDemande": "État de la demande"

View File

@ -6,31 +6,24 @@ import {CheckField, OptionField, TextField} from "../../components/MemberCustomF
import {ClubSelect} from "../../components/ClubSelect.jsx";
import {ConfirmDialog} from "../../components/ConfirmDialog.jsx";
import {toast} from "react-toastify";
import {apiAxios, errFormater} from "../../utils/Tools.js";
import {apiAxios, getToastMessage} from "../../utils/Tools.js";
import {useEffect, useReducer, useState} from "react";
import {SimpleReducer} from "../../utils/SimpleReducer.jsx";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faAdd, faTrashCan} from "@fortawesome/free-solid-svg-icons";
import {Trans, useTranslation} from "react-i18next";
export function CompetitionEdit() {
const {id} = useParams()
const navigate = useNavigate();
const {t} = useTranslation();
const setLoading = useLoadingSwitcher()
const {data, refresh, error} = useFetch(`/competition/${id}?light=false`, setLoading, 1)
const handleRm = () => {
toast.promise(
apiAxios.delete(`/competition/${id}`),
{
pending: "Suppression de la compétition en cours...",
success: "Compétition supprimé avec succès 🎉",
error: {
render({data}) {
return errFormater(data, "Échec de la suppression de la compétition")
}
},
}
apiAxios.delete(`/competition/${id}`), getToastMessage("comp.toast.del")
).then(_ => {
navigate("/competition")
})
@ -42,7 +35,7 @@ export function CompetitionEdit() {
return <>
<button type="button" className="btn btn-link" onClick={() => navigate("/competition")}>
&laquo; retour
{t('back')}
</button>
<div>
{data
@ -51,8 +44,8 @@ export function CompetitionEdit() {
<Content data={data} refresh={refresh}/>
{data.id !== null && <button style={{marginBottom: "1.5em", width: "100%"}} className="btn btn-primary"
onClick={_ => navigate(`/competition/${data.id}/register?type=${data.registerMode}`)}>Voir/Modifier
les participants</button>}
onClick={_ => navigate(`/competition/${data.id}/register?type=${data.registerMode}`)}>
{t('comp.modifierLesParticipants')}</button>}
{data.id !== null && (data.system === "SAFCA" || data.system === "INTERNAL") &&
<ContentSAFCAAndInternal data2={data} type={data.system}/>}
@ -60,11 +53,11 @@ export function CompetitionEdit() {
{data.id !== null && <>
<div className="col" style={{textAlign: 'right', marginTop: '1em'}}>
<button className="btn btn-danger btn-sm" data-bs-toggle="modal"
data-bs-target="#confirm-delete">Supprimer la compétition
data-bs-target="#confirm-delete">{t('comp.supprimerLaCompétition')}
</button>
</div>
<ConfirmDialog title="Supprimer la compétition"
message="Êtes-vous sûr de vouloir supprimer cette compétition est tout les resultat associer?"
<ConfirmDialog title={t('comp.supprimerLaCompétition')}
message={t('comp.supprimerLaCompétition.msg')}
onConfirm={handleRm}/>
</>}
</div>
@ -80,6 +73,7 @@ function ContentSAFCAAndInternal({data2, type = "SAFCA"}) {
const setLoading = useLoadingSwitcher()
const {data, error} = useFetch(getDataPath, setLoading, 1)
const {t} = useTranslation();
const [state, dispatch] = useReducer(SimpleReducer, [])
const [state2, dispatch2] = useReducer(SimpleReducer, [])
@ -113,18 +107,7 @@ function ContentSAFCAAndInternal({data2, type = "SAFCA"}) {
out['admin'] = state.map(d => d.data)
out['table'] = state2.map(d => d.data)
toast.promise(
apiAxios.post(setDataPath, out),
{
pending: "Enregistrement des paramètres en cours",
success: "Paramètres enregistrée avec succès 🎉",
error: {
render({data}) {
return errFormater(data, "Échec de l'enregistrement des paramètres")
}
},
}
)
toast.promise(apiAxios.post(setDataPath, out), getToastMessage("comp.toast.params"))
}
return <>
@ -132,26 +115,26 @@ function ContentSAFCAAndInternal({data2, type = "SAFCA"}) {
<form onSubmit={handleSubmit}>
<input name="id" value={data2.id || ""} readOnly hidden/>
<div className="card mb-4">
<div className="card-header">Configuration SAFCA</div>
<div className="card-header">{t('configuration')}</div>
<div className="card-body text-center">
<div className="input-group mb-1">
<div className="input-group-text">
<input className="form-check-input mt-0" type="checkbox" aria-label="Afficher le blason du club sur les écrans"
<input className="form-check-input mt-0" type="checkbox" aria-label={t('comp.aff.blason')}
defaultChecked={data.show_blason} name="show_blason"/>
</div>
<span className="input-group-text">Afficher le blason du club sur les écrans</span>
<span className="input-group-text">{t('comp.aff.blason')}</span>
</div>
<div className="input-group mb-3">
<div className="input-group-text">
<input className="form-check-input mt-0" type="checkbox" aria-label="Afficher le pays du combattant sur les écrans"
<input className="form-check-input mt-0" type="checkbox" aria-label={t('comp.aff.flag')}
defaultChecked={data.show_flag} name="show_flag"/>
</div>
<span className="input-group-text">Afficher le pays du combattant sur les écrans</span>
<span className="input-group-text">{t('comp.aff.flag')}</span>
</div>
<span className="input-group-text">Administrateur</span>
<span className="input-group-text">{t('administrateur')}</span>
<ul className="list-group form-control">
{state.map((d, index) => {
return <div key={index} className="input-group">
@ -180,7 +163,7 @@ function ContentSAFCAAndInternal({data2, type = "SAFCA"}) {
</ul>
<div className="mb-3"></div>
<span className="input-group-text">Table</span>
<span className="input-group-text">{t('secrétariatsDeLice')}</span>
<ul className="list-group form-control">
{state2.map((d, index) => {
return <div key={index} className="input-group">
@ -210,7 +193,7 @@ function ContentSAFCAAndInternal({data2, type = "SAFCA"}) {
</div>
<div className="row mb-3">
<div className="d-grid gap-2 d-md-flex justify-content-md-end">
<button type="submit" className="btn btn-primary">Enregistrer</button>
<button type="submit" className="btn btn-primary">{t('button.enregistrer')}</button>
</div>
</div>
</div>
@ -222,6 +205,7 @@ function ContentSAFCAAndInternal({data2, type = "SAFCA"}) {
function Content({data}) {
const navigate = useNavigate();
const [registerMode, setRegisterMode] = useState(data.registerMode || "FREE");
const {t} = useTranslation();
const handleSubmit = (event) => {
event.preventDefault();
@ -250,7 +234,7 @@ function Content({data}) {
const url = event.target.helloassoUrl?.value
if (!url || !event.target.data3?.value) {
toast.error("Veuillez renseigner l'URL de la billetterie HelloAsso et les tarifs associés.")
toast.error(t('comp.ha.error1'))
err = true;
} else {
const regex = /\/associations\/([^/]+)\/evenements\/([^/]+)/;
@ -260,29 +244,29 @@ function Content({data}) {
out['data1'] = match[1]
out['data2'] = match[2]
} else {
toast.error("L'URL de la billetterie HelloAsso n'est pas valide. Veuillez vérifier le format de l'URL.")
toast.error(t('comp.ha.error2'))
err = true;
}
}
out['data4'] = event.target.data4?.value
if (!out['data4']) {
toast.error("Veuillez renseigner l'email de réception des inscriptions échouées.")
toast.error(t('comp.ha.error3'))
err = true;
}
}
if (out['date'] > out['toDate']) {
toast.error("La date de fin doit être postérieure à la date de début.")
toast.error(t('comp.error1'))
err = true;
}
if ((out['registerMode'] === "FREE" || out['registerMode'] === "CLUB_ADMIN") && (!out['startRegister'] || !out['endRegister'])) {
toast.error("Veuillez renseigner les dates de début et de fin d'inscription.")
toast.error(t('comp.error2'))
err = true;
}
if ((out['registerMode'] === "FREE" || out['registerMode'] === "CLUB_ADMIN") && out['startRegister'] > out['endRegister']) {
toast.error("La date de fin d'inscription doit être postérieure à la date de début d'inscription.")
toast.error(t('comp.error3'))
err = true;
}
@ -291,16 +275,7 @@ function Content({data}) {
}
toast.promise(
apiAxios.post(`/competition`, out),
{
pending: "Enregistrement de la competition en cours",
success: "Competition enregistrée avec succès 🎉",
error: {
render({data}) {
return errFormater(data, "Échec de l'enregistrement de la competition")
}
},
}
apiAxios.post(`/competition`, out), getToastMessage("comp.toast.save")
).then(data => {
console.log(data.data)
if (data.data.id !== undefined)
@ -311,7 +286,7 @@ function Content({data}) {
return <form onSubmit={handleSubmit}>
<div className="card mb-4">
<input name="id" value={data.id || ""} readOnly hidden/>
<div className="card-header">{data.id ? "Edition compétition" : "Création compétition"}</div>
<div className="card-header">{data.id ? t('comp.editionCompétition') : t('comp.créationCompétition')}</div>
<div className="card-body text-center">
<div className="accordion" id="accordionExample">
@ -319,7 +294,7 @@ function Content({data}) {
<h2 className="accordion-header">
<button className="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseOne"
aria-expanded="false" aria-controls="collapseOne">
Informations techniques
{t('comp.informationsTechniques')}
</button>
</h2>
<div id="collapseOne" className="accordion-collapse collapse" data-bs-parent="#accordionExample">
@ -340,27 +315,27 @@ function Content({data}) {
<h2 className="accordion-header">
<button className="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#collapseTwo"
aria-expanded="true" aria-controls="collapseTwo">
Informations générales sur la compétition
{t('comp.informationsGénéralesSurLaCompétition')}
</button>
</h2>
<div id="collapseTwo" className="accordion-collapse collapse show" data-bs-parent="#accordionExample">
<div className="accordion-body">
<TextField name="name" text="Nom*" value={data.name}/>
<TextField name="name" text={t('nom') + "*"} value={data.name}/>
<div className="input-group mb-3">
<span className="input-group-text" id="date">Date*</span>
<span className="input-group-text" id="date">Du</span>
<span className="input-group-text" id="date">{t('date')}*</span>
<span className="input-group-text" id="date">{t('du')}</span>
<input type="date" className="form-control" placeholder="jj/mm/aaaa" aria-label="date"
name="date" aria-describedby="date" defaultValue={data.date ? data.date.split('T')[0] : ''} required/>
<span className="input-group-text" id="date">Au</span>
<span className="input-group-text" id="date">{t('au')}</span>
<input type="date" className="form-control" placeholder="jj/mm/aaaa" aria-label="toDate"
name="toDate" aria-describedby="toDate" defaultValue={data.toDate ? data.toDate.split('T')[0] : ''}
required/>
</div>
<TextField name="description" text="Description" value={data.description} required={false}/>
<TextField name="adresse" text="Adresse" value={data.adresse} required={false}/>
<CheckField name="publicVisible" text="Visible par le public (Apparaît dans la liste des compétitions)"
<TextField name="description" text={t('description')} value={data.description} required={false}/>
<TextField name="adresse" text={t('adresse')} value={data.adresse} required={false}/>
<CheckField name="publicVisible" text={t('comp.text2')}
value={data.publicVisible} row={true}/>
<small>Si non coché, la compétition ne sera visible que par les personnes pouvant y inscrire des participants.</small>
<small>{t('comp.text3')}</small>
</div>
</div>
</div>
@ -369,7 +344,7 @@ function Content({data}) {
<h2 className="accordion-header">
<button className="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#collapseThree"
aria-expanded="false" aria-controls="collapseThree">
Informations sur le mode d'inscription
{t('comp.informationsSurLeModeDinscription')}
</button>
</h2>
<div id="collapseThree" className="accordion-collapse collapse" data-bs-parent="#accordionExample">
@ -377,24 +352,24 @@ function Content({data}) {
<div className="row">
<div className="input-group mb-3">
<label className="input-group-text">Qui peut inscrire</label>
<label className="input-group-text">{t('comp.quiPeutInscrire')}</label>
<select className="form-select" value={registerMode} onChange={event => setRegisterMode(event.target.value)}>
<option value="FREE">Tous les membres de la FFSAF</option>
<option value="CLUB_ADMIN">Responsables et bureaux des associations</option>
<option value="ADMIN">Uniquement les administrateurs de la compétition</option>
<option value="HELLOASSO">Billetterie HelloAsso</option>
<option value="FREE">{t('comp.tousLesMembresDeLaFfsaf')}</option>
<option value="CLUB_ADMIN">{t('comp.responsablesEtBureauxDesAssociations')}</option>
<option value="ADMIN">{t('comp.uniquementLesAdministrateursDeLaCompétition')}</option>
<option value="HELLOASSO">{t('comp.billetterieHelloasso')}</option>
</select>
</div>
</div>
<div className="input-group mb-3"
style={{display: registerMode === "FREE" || registerMode === "CLUB_ADMIN" ? "flex" : "none"}}>
<span className="input-group-text" id="startRegister">Date d'inscription</span>
<span className="input-group-text" id="startRegister">Du</span>
<span className="input-group-text" id="startRegister">{t('comp.dateDinscription')}</span>
<span className="input-group-text" id="startRegister">{t('du')}</span>
<input type="datetime-local" className="form-control" placeholder="jj/mm/aaaa" aria-label="date"
name="startRegister" aria-describedby="startRegister"
defaultValue={data.startRegister ? data.startRegister.substring(0, 16) : ''}/>
<span className="input-group-text" id="endRegister">Au</span>
<span className="input-group-text" id="endRegister">{t('au')}</span>
<input type="datetime-local" className="form-control" placeholder="jj/mm/aaaa" aria-label="endRegister"
name="endRegister" aria-describedby="endRegister"
defaultValue={data.endRegister ? data.endRegister.substring(0, 16) : ''}/>
@ -402,41 +377,28 @@ function Content({data}) {
<div style={{display: registerMode === "HELLOASSO" ? "initial" : "none"}}>
<span style={{textAlign: "left"}}>
<div>Afin de permettre une bonne interconnexion avec HelloAsso, merci de suivre les instructions suivantes :</div>
<div>{t('comp.ha.text1')}</div>
<ul>
<li><strong>Configurer l'url de notification : </strong>afin que nous puissions recevoir une notification à
chaque inscription, il est nécessaire de configurer l'url de notification de votre compte HelloAsso pour
qu'il redirige vers "https://intra.ffsaf.fr/api/webhook/ha". Pour ce faire, depuis la page d'accueil de
votre association sur HelloAsso, allez dans <strong>Mon compte</strong> &gt;
<strong>Paramètres</strong> &gt;
<strong> Intégrations et API</strong> section Notification et copier-coller <strong>https://intra.ffsaf.fr/api/webhook/ha </strong>
dans le champ <strong>Mon URL de callback</strong> et enregister.
<li><Trans i18nKey="comp.ha.text2"></Trans>
<img src="/img/HA-help-4.png" alt="" className="img-fluid" style={{objectFit: "contain"}}/></li>
<li><strong>Copier-coller le nom exacte des tarifs</strong> <em>-sépare par des point-virgules-</em> qui donneront
lieux à une inscription automatique. Tous ces tarifs doivent impérativement demander le numéro de licence
en champs obligatoire. Pour ce faire, lors de la configuration de votre billetterie à l'étape n°3,
cliquer sur <strong>+ Ajouter une information</strong>, saisissez l'intituler exact suivant
<strong> Numéro de licence</strong>, dans <strong>Type de réponse souhaitée</strong> rentrer Nombre,
sélectionner les tarifs entrés plus précédemment et rendre l'information obligatoire.
<li><Trans i18nKey="comp.ha.text3"></Trans>
<img src="/img/HA-help-3.png" className="img-fluid" alt="..." style={{object_fit: 'contain'}}/>
<img src="/img/HA-help-2.png" className="img-fluid" alt="..." style={{object_fit: 'contain'}}/></li>
<li><strong>Copier-coller l'url de votre billetterie</strong> dans le champs si dessous. Il devrais avoir la forme suivante:
<em> https://www.helloasso.com/associations/&lt;nom-asso-sur-helloasso&gt;/evenements/&lt;nom-billetterie&gt;</em></li>
<li><Trans i18nKey="comp.ha.text4"></Trans></li>
</ul>
</span>
<TextField name="helloassoUrl" text="Url de la billetterie HelloAsso" required={false}
<TextField name="helloassoUrl" text={t('comp.ha.text5')} required={false}
value={data.data1 && data.data2 && `https://www.helloasso.com/associations/${data.data1}/evenements/${data.data2}` || ""}
placeholder="https://www.helloasso.com/associations/nom-asso-sur-helloasso/evenements/nom-billetterie"/>
<TextField name="data3" text="Tarifs HelloAsso"
<TextField name="data3" text={t('comp.ha.tarifsHelloasso')}
value={data.data3 || ""} required={false}
placeholder="Tarif1;Tarif2;Tarif3"/>
<TextField name="data4" text="Email de réception des inscriptions échoué"
<TextField name="data4" text={t('comp.ha.emailDeRéceptionDesInscriptionséchoué')}
value={data.data4 || ""} required={false}/>
<small>Si pour une raison quelconque l'inscription automatique échoue, un email sera envoyé à cette adresse pour
vous en informer</small>
<small>{t('comp.ha.text6')}</small>
</div>
</div>
</div>
@ -447,7 +409,7 @@ function Content({data}) {
<div className="row mb-3">
<div className="d-grid gap-2 d-md-flex justify-content-md-end">
<button type="submit" className="btn btn-primary">Enregistrer</button>
<button type="submit" className="btn btn-primary">{t('button.enregistrer')}</button>
</div>
</div>
</div>

View File

@ -4,6 +4,8 @@ import {useFetch} from "../../hooks/useFetch.js";
import {AxiosError} from "../../components/AxiosError.jsx";
import {ThreeDots} from "react-loader-spinner";
import {useAuth} from "../../hooks/useAuth.jsx";
import {useTranslation} from "react-i18next";
import i18n from "i18next";
export function CompetitionList() {
@ -28,14 +30,15 @@ export function CompetitionList() {
function MakeCentralPanel({data, navigate}) {
const {userinfo} = useAuth()
const {t} = useTranslation();
return <>
{userinfo?.roles?.includes("create_compet") &&
<div className="col mb-2" style={{textAlign: 'right', marginTop: '1em'}}>
<button type="button" className="btn btn-primary" onClick={() => navigate("/competition/0")}>Nouvelle compétition</button>
<button type="button" className="btn btn-primary" onClick={() => navigate("/competition/0")}>{t('comp.nouvelleCompétition')}</button>
</div>}
<div className="mb-4">
<h3>Compétition future</h3>
<h3>{t('comp.compétitionFuture')}</h3>
<div className="list-group">
{data.filter(req => new Date(req.toDate.split('T')[0]) >= new Date()).sort((a, b) => {
return new Date(a.date.split('T')[0]) - new Date(b.date.split(')T')[0])
@ -43,7 +46,7 @@ function MakeCentralPanel({data, navigate}) {
</div>
</div>
<div className="mb-4">
<h3>Compétition passée</h3>
<h3>{t('comp.compétitionPassée')}</h3>
<div className="list-group">
{data.filter(req => new Date(req.toDate.split('T')[0]) < new Date()).sort((a, b) => {
return new Date(b.date.split('T')[0]) - new Date(a.date.split(')T')[0])
@ -55,25 +58,26 @@ function MakeCentralPanel({data, navigate}) {
const inscText = (type) => {
if (type === "FREE") {
return "Inscriptions libres"
return i18n.t('comp.inscriptionsLibres')
} else if (type === "CLUB_ADMIN") {
return "Inscriptions par les responsables de club"
return i18n.t('comp.inscriptionsParLesResponsablesDeClub')
} else if (type === "ADMIN") {
return "Inscriptions par les administrateurs de la compétition"
return i18n.t('comp.inscriptionsParLesAdministrateursDeLaCompétition')
} else if (type === "HELLOASSO") {
return "Inscriptions sur la billetterie HelloAsso"
return i18n.t('comp.inscriptionsSurLaBilletterieHelloasso')
}
return ""
}
function MakeRow({data, navigate}) {
const {t} = useTranslation();
return <div className="list-group-item list-group-item-action"
onClick={() => data.canEdit ? navigate("" + data.id) : navigate("" + data.id + "/view")}>
<div className="row justify-content-between align-items-start ">
<div className="ms-2 col-auto">
<div><strong>{data.name}</strong> <small>par {data.clubName}</small></div>
<small>Du {new Date(data.date.split('T')[0]).toLocaleDateString()} au {new Date(data.toDate.split('T')[0]).toLocaleDateString()}</small>
<div><strong>{data.name}</strong> <small>{t('par')} {data.clubName}</small></div>
<small>{t('du')} {new Date(data.date.split('T')[0]).toLocaleDateString()} {t('au')} {new Date(data.toDate.split('T')[0]).toLocaleDateString()}</small>
</div>
<div className="ms-2 col-auto">
<small style={{textAlign: 'right'}}>{inscText(data.registerMode)}</small>

View File

@ -4,7 +4,7 @@ import {useFetch} from "../../hooks/useFetch.js";
import {AxiosError} from "../../components/AxiosError.jsx";
import {ThreeDots} from "react-loader-spinner";
import {useEffect, useReducer, useRef, useState} from "react";
import {apiAxios, CatList, getCatName} from "../../utils/Tools.js";
import {apiAxios, CatList, getCatName, getToastMessage} from "../../utils/Tools.js";
import {toast} from "react-toastify";
import {SimpleReducer} from "../../utils/SimpleReducer.jsx";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
@ -12,6 +12,7 @@ import {faAdd, faGavel, faTrashCan} from "@fortawesome/free-solid-svg-icons";
import "./CompetitionRegisterAdmin.css"
import * as XLSX from "xlsx-js-style";
import {useCountries} from "../../hooks/useCountries.jsx";
import {Trans, useTranslation} from "react-i18next";
export function CompetitionRegisterAdmin({source}) {
const {id} = useParams()
@ -20,6 +21,7 @@ export function CompetitionRegisterAdmin({source}) {
const [clubFilter, setClubFilter] = useState("")
const [catFilter, setCatFilter] = useState("")
const [modalState, setModalState] = useState({})
const {t} = useTranslation();
const setLoading = useLoadingSwitcher()
const {data, error} = useFetch(`/competition/${id}/register/${source}`, setLoading, 1)
@ -38,13 +40,8 @@ export function CompetitionRegisterAdmin({source}) {
}, [data, clubFilter, catFilter]);
const sendRegister = (new_state) => {
return toast.promise(apiAxios.post(`/competition/${id}/register/${source}`, new_state), {
pending: "Recherche en cours", success: "Combattant trouvé et ajouté/mis à jour", error: {
render({data}) {
return data.response.data || "Combattant non trouvé"
}
}
}).then((response) => {
return toast.promise(apiAxios.post(`/competition/${id}/register/${source}`, new_state), getToastMessage("comp.toast.register.add")
).then((response) => {
if (response.data.error) {
return
}
@ -55,10 +52,10 @@ export function CompetitionRegisterAdmin({source}) {
}
return <div>
<h2>Combattants inscrits</h2>
<h2>{t('comp.combattantsInscrits')}</h2>
<button type="button" className="btn btn-link"
onClick={() => source === "admin" ? navigate("/competition/" + id) : navigate("/competition/" + id + "/view")}>
&laquo; retour
{t('back')}
</button>
<div className="row">
@ -72,17 +69,17 @@ export function CompetitionRegisterAdmin({source}) {
<div className="col-lg-3">
<div className="mb-1">
<button type="button" className="btn btn-primary" data-bs-toggle="modal" data-bs-target="#registerModal"
onClick={() => setModalState({id: 0})}>Ajouter un combattant
onClick={() => setModalState({id: 0})}>{t('comp.ajouterUnCombattant')}
</button>
</div>
{source === "admin" && <div className="mb-4">
<button type="button" className="btn btn-primary" data-bs-toggle="modal" data-bs-target="#registerModal"
onClick={() => setModalState({id: -793548328091516928})}>Ajouter un invité
onClick={() => setModalState({id: -793548328091516928})}>{t('comp.ajouterUnInvité')}
</button>
</div>}
<QuickAdd sendRegister={sendRegister} source={source}/>
<div className="card mb-4">
<div className="card-header">Filtre</div>
<div className="card-header">{t('filtre')}</div>
<div className="card-body">
<FiltreBar data={data} clubFilter={clubFilter} setClubFilter={setClubFilter} catFilter={catFilter}
setCatFilter={setCatFilter} source={source}/>
@ -97,6 +94,7 @@ export function CompetitionRegisterAdmin({source}) {
}
function QuickAdd({sendRegister, source}) {
const {t} = useTranslation();
const handleAdd = (licence) => {
console.log("Quick add licence: " + licence)
@ -107,10 +105,10 @@ function QuickAdd({sendRegister, source}) {
}
return <div className="card mb-4">
<div className="card-header">Ajout rapide</div>
<div className="card-header">{t('comp.ajoutRapide')}</div>
<div className="card-body">
<div className="row">
<span>N° de licence</span>
<span>{t('comp.noDeLicence')}</span>
</div>
<div className="input-group">
<input type="text" className="form-control" placeholder="12345" id="quickAddLicence"
@ -144,11 +142,12 @@ function SearchMember({sendRegister}) {
const setLoading = useLoadingSwitcher()
const {data, error} = useFetch(`/club/members`, setLoading, 1)
const [suggestions, setSuggestions] = useState([])
const {t} = useTranslation();
const handleAdd = (name) => {
const member = data.find(m => `${m.fname} ${m.lname}`.trim() === name);
if (!member) {
toast.error("Combattant non trouvé");
toast.error(t('comp.combattantNonTrouvé'));
return;
}
@ -173,7 +172,7 @@ function SearchMember({sendRegister}) {
return <>
{data ? <div className="row mb-3" style={{marginTop: "0.5em"}}>
<span>Prénom et nom</span>
<span>{t('prénomEtNom')}</span>
<AutoCompleteInput suggestions={suggestions} handleAdd={handleAdd}/>
</div> : error ? <AxiosError error={error}/> : <Def/>}
</>
@ -185,6 +184,7 @@ const AutoCompleteInput = ({suggestions = [], handleAdd}) => {
const [showSuggestions, setShowSuggestions] = useState(false);
const [activeSuggestion, setActiveSuggestion] = useState(0);
const wrapperRef = useRef(null);
const {t} = useTranslation();
// Filtre les suggestions
useEffect(() => {
@ -252,7 +252,7 @@ const AutoCompleteInput = ({suggestions = [], handleAdd}) => {
onChange={(e) => setInputValue(e.target.value)}
onKeyDown={handleKeyDown}
onFocus={() => inputValue && setShowSuggestions(true)}
placeholder="Rechercher..."
placeholder={t('rechercher...')}
aria-autocomplete="list"
aria-expanded={showSuggestions}
aria-controls="suggestions-list"
@ -286,6 +286,7 @@ const AutoCompleteInput = ({suggestions = [], handleAdd}) => {
function Modal({sendRegister, modalState, setModalState, source}) {
const country = useCountries('fr')
const {t} = useTranslation();
const [licence, setLicence] = useState("")
const [fname, setFname] = useState("")
@ -357,31 +358,31 @@ function Modal({sendRegister, modalState, setModalState, source}) {
}}>
<div className="modal-header">
<h1 className="modal-title fs-5"
id="registerLabel">{editMode ? "Modification d'" : "Ajouter "}un {modalState.id >= 0 ? "combattant" : "invité"}</h1>
id="registerLabel">{editMode ? t('modification') : t('ajout')} {t('dun')} {modalState.id >= 0 ? t('combattant') : t('invité')}</h1>
<button type="button" className="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div className="modal-body">
{modalState.id < 0 &&
<div className="mb-2">Les invités sont réservés aux membres non licenciés par la fédération. Les combattants inscrits via
ce formulaire ne pourront pas voir leur résultat depuis leur profil.</div>}
<div className="mb-2">{t('comp.modal.text1')}</div>}
<div className="card" style={{marginBottom: "1em"}}>
<div className="card-header">{modalState.id >= 0 ? "Recherche*" : "Information"}</div>
<div className="card-header">{modalState.id >= 0 ? t('comp.modal.recherche') : t('comp.modal.information')}</div>
<div className="card-body">
<div className="row" hidden={modalState.id < 0}>
<div className="col">
<input type="number" min={0} step={1} className="form-control" placeholder="N° de licence" name="licence"
<input type="number" min={0} step={1} className="form-control" placeholder={t("comp.noDeLicence")}
name="licence"
value={licence} onChange={e => setLicence(e.target.value)} disabled={editMode}/>
</div>
</div>
<h5 style={{textAlign: "center", marginTop: "0.25em"}} hidden={modalState.id < 0}>Ou</h5>
<h5 style={{textAlign: "center", marginTop: "0.25em"}} hidden={modalState.id < 0}>{t('ou')}</h5>
<div className="row">
<div className="col">
<input type="text" className="form-control" placeholder="Prénom" name="fname"
<input type="text" className="form-control" placeholder={t('prenom')} name="fname"
disabled={editMode && modalState.id >= 0}
value={fname} onChange={e => setFname(e.target.value)}/>
</div>
<div className="col">
<input type="text" className="form-control" placeholder="Nom" name="lname"
<input type="text" className="form-control" placeholder={t('nom')} name="lname"
disabled={editMode && modalState.id >= 0}
value={lname} onChange={e => setLname(e.target.value)}/>
</div>
@ -390,16 +391,16 @@ function Modal({sendRegister, modalState, setModalState, source}) {
</div>
<div className="input-group mb-3" hidden={modalState.id >= 0}>
<span className="input-group-text" id="categorie">Club</span>
<input type="text" className="form-control" placeholder="Club" name="club"
<span className="input-group-text" id="categorie">{t("club", {count: 1})}</span>
<input type="text" className="form-control" placeholder={t("club", {count: 1})} name="club"
value={club} onChange={e => setClub(e.target.value)}/>
</div>
<div className="input-group mb-3" hidden={modalState.id >= 0}>
<span className="input-group-text" id="categorie">Catégorie</span>
<span className="input-group-text" id="categorie">{t('catégorie')}</span>
<select id="inputState2" className="form-select" value={gcat}
onChange={(e) => setGCat(e.target.value)}>
<option>-- Sélectionner catégorie --</option>
<option>{t('--SélectionnerCatégorie--')}</option>
{CatList.map((cat, index) => {
return (<option key={index} value={cat}>{getCatName(cat)}</option>)
})}
@ -407,7 +408,7 @@ function Modal({sendRegister, modalState, setModalState, source}) {
</div>
<div className="input-group mb-3" hidden={modalState.id >= 0}>
<span className="input-group-text" id="categorie">Pays</span>
<span className="input-group-text" id="pays">{t('pays')}</span>
<select id="inputState0" className="form-select" value={country_} onChange={(e) => setCountry_(e.target.value)}>
{country && Object.keys(country).sort((a, b) => {
if (a < b) return -1
@ -420,41 +421,40 @@ function Modal({sendRegister, modalState, setModalState, source}) {
</div>
<div className="input-group mb-3" hidden={modalState.id >= 0}>
<span className="input-group-text" id="categorie">Genre</span>
<select className="form-select" aria-label="categorie" name="categorie" value={genre}
<span className="input-group-text" id="genre">{t('genre')}</span>
<select className="form-select" aria-label={t('categorie')} name="genre" value={genre}
onChange={e => setGenre(e.target.value)}>
<option value={"NA"}>NA</option>
<option value={"H"}>H</option>
<option value={"F"}>F</option>
<option value={"H"}>{t('h')}</option>
<option value={"F"}>{t('f')}</option>
</select>
</div>
<div className="input-group mb-3">
<span className="input-group-text" id="weight">Poids (en kg)</span>
<span className="input-group-text" id="weight">{t('comp.modal.poids')}</span>
<input type="number" min={1} step={1} className="form-control" placeholder="42" aria-label="weight"
name="weight" aria-describedby="weight" value={weight} onChange={e => setWeight(e.target.value)}/>
</div>
<div className="input-group mb-3" hidden={modalState.id < 0}>
<span className="input-group-text" id="categorie">Surclassement</span>
<select className="form-select" aria-label="categorie" name="categorie" value={cat}
<span className="input-group-text" id="surclassement">{t('comp.modal.surclassement')}</span>
<select className="form-select" aria-label={t('comp.modal.surclassement')} name="surclassement" value={cat}
onChange={e => setCat(Number(e.target.value))}>
<option value={0}>Aucun</option>
<option value={1}>+1 catégorie</option>
<option value={2}>+2 catégorie</option>
<option value={0}>{t('aucun')}</option>
<option value={1}>{t('1Catégorie')}</option>
<option value={2}>{t('2Catégorie')}</option>
</select>
</div>
{editMode && source === "admin" && <div className="form-check form-switch form-check-reverse" hidden={modalState.id < 0}>
<input className="form-check-input" type="checkbox" id="switchCheckReverse" checked={lockEdit}
onChange={e => setLockEdit(e.target.checked)}/>
<label className="form-check-label" htmlFor="switchCheckReverse">Empêcher les membres/club de modifier cette
inscription</label>
<label className="form-check-label" htmlFor="switchCheckReverse">{t('comp.modal.text2')}</label>
</div>}
</div>
<div className="modal-footer">
<button type="submit" className="btn btn-primary">{editMode ? "Modifier" : "Ajouter"}</button>
<button type="reset" className="btn btn-secondary" data-bs-dismiss="modal" id="closeModal">Annuler</button>
<button type="submit" className="btn btn-primary">{editMode ? t('button.modifier') : t('button.ajouter')}</button>
<button type="reset" className="btn btn-secondary" data-bs-dismiss="modal" id="closeModal">{t('button.annuler')}</button>
</div>
</form>
</div>
@ -466,6 +466,7 @@ let allClub = []
let allCat = []
function FiltreBar({data, clubFilter, setClubFilter, catFilter, setCatFilter, source}) {
const {t} = useTranslation();
useEffect(() => {
if (!data) return;
allClub.push(...data.map((e) => e.club?.name))
@ -477,7 +478,7 @@ function FiltreBar({data, clubFilter, setClubFilter, catFilter, setCatFilter, so
return <div>
{source === "admin" && <div className="mb-3">
<select className="form-select" value={clubFilter} onChange={event => setClubFilter(event.target.value)}>
<option value="">--- tout les clubs ---</option>
<option value="">{t('---ToutLesClubs---')}</option>
{allClub && allClub.map((value, index) => {
return <option key={index} value={value}>{value}</option>
})}
@ -485,7 +486,7 @@ function FiltreBar({data, clubFilter, setClubFilter, catFilter, setCatFilter, so
</div>}
<div className="mb-3">
<select className="form-select" value={catFilter} onChange={event => setCatFilter(event.target.value)}>
<option value="">--- toute les catégories ---</option>
<option value="">{t('---TouteLesCatégories---')}</option>
{allCat && allCat.map((value, index) => {
return <option key={index} value={value}>{value}</option>
})}
@ -497,12 +498,11 @@ function FiltreBar({data, clubFilter, setClubFilter, catFilter, setCatFilter, so
function MakeCentralPanel({data, dispatch, id, setModalState, source}) {
const [searchParams] = useSearchParams();
const registerType = searchParams.get("type") || "FREE";
const {t} = useTranslation();
return <>
{(registerType === "FREE" || registerType === "CLUB_ADMIN") && source === "admin" &&
<span>Tips 1: Il est possible de bannir un combattant, ce qui l'empêchera d'être réinscrit par un autre moyen que par un administrateur de cette compétition.
Pour cela, cliquez sur la petite <FontAwesomeIcon icon={faGavel}/> à côté de son nom.<br/>
Tips 2: Il est aussi possible de verrouiller les modifications de son inscription depuis sa fiche, ce qui l'empêchera d'être modifié/supprimé par lui-même et/ou un responsable de club.
<span><Trans i18nKey="comp.tips"> petite <FontAwesomeIcon icon={faGavel}/> à</Trans>
</span>}
<div className="mb-4">
<div className="list-group">
@ -516,12 +516,12 @@ function MakeCentralPanel({data, dispatch, id, setModalState, source}) {
<span className="col-auto">{req.data.licence ? String(req.data.licence).padStart(5, '0') : "-------"}</span>
<div className="ms-2 col-auto">
<div><strong>{req.data.fname} {req.data.lname}</strong> <small>{req.data.genre}</small></div>
<small>{req.data.club?.name || "Sans club"}</small>
<small>{req.data.club?.name || t("club", {count: 0})}</small>
</div>
</div>
<div className="row">
<div className="col-auto" style={{textAlign: "right"}}>
<small>{getCatName(req.data.categorie) + (req.data.overCategory === 0 ? "" : (" avec " + req.data.overCategory + " de surclassement"))}<br/>
<small>{t("comp.surclassement", {count: req.data.overCategory, cat: getCatName(req.data.categorie)})}<br/>
{req.data.weight ? req.data.weight : "---"} kg
</small>
</div>
@ -535,16 +535,12 @@ function MakeCentralPanel({data, dispatch, id, setModalState, source}) {
e.preventDefault()
if (req.data.lockEdit && source !== "admin") return;
if (!window.confirm("Êtes-vous sûr de vouloir désinscrire et bannir ce combattant de la compétition?\n(Vous pouvez le réinscrire plus tard)"))
if (!window.confirm(t('comp.warn1')))
return;
toast.promise(apiAxios.delete(`/competition/${id}/register/${req.data.id}/${source}?ban=true`), {
pending: "Désinscription en cours", success: "Combattant désinscrit et bannie", error: {
render({data}) {
return data.response.data || "Erreur"
}
}
}).finally(() => {
toast.promise(
apiAxios.delete(`/competition/${id}/register/${req.data.id}/${source}?ban=true`), getToastMessage("comp.toast.register.ban")
).finally(() => {
dispatch({type: 'REMOVE', payload: req.id})
})
}}>
@ -556,20 +552,16 @@ function MakeCentralPanel({data, dispatch, id, setModalState, source}) {
if (req.data.lockEdit && source !== "admin") return;
if (registerType === "HELLOASSO") {
if (!window.confirm("Êtes-vous sûr de vouloir désinscrire ce combattant ?\nCela ne le désinscrira pas de la billetterie HelloAsso et ne le remboursera pas."))
if (!window.confirm(t('comp.warn2')))
return;
} else {
if (!window.confirm("Êtes-vous sûr de vouloir désinscrire ce combattant ?"))
if (!window.confirm(t('comp.warn3')))
return;
}
toast.promise(apiAxios.delete(`/competition/${id}/register/${req.data.id}/${source}?ban=false`), {
pending: "Désinscription en cours", success: "Combattant désinscrit", error: {
render({data}) {
return data.response.data || "Erreur"
}
}
}).finally(() => {
toast.promise(
apiAxios.delete(`/competition/${id}/register/${req.data.id}/${source}?ban=false`), getToastMessage("comp.toast.register.del")
).finally(() => {
dispatch({type: 'REMOVE', payload: req.id})
})
}}>
@ -584,6 +576,7 @@ function MakeCentralPanel({data, dispatch, id, setModalState, source}) {
}
function FileOutput({data}) {
const {t} = useTranslation();
const handleFileDownload = () => {
const dataOut = []
for (const e of data) {
@ -612,7 +605,7 @@ function FileOutput({data}) {
return (
<div>
<button className="btn btn-primary" onClick={handleFileDownload}>Exporter les inscription</button>
<button className="btn btn-primary" onClick={handleFileDownload}>{t('comp.exporterLesInscription')}</button>
</div>
);
}

View File

@ -4,10 +4,12 @@ import {CompetitionList} from "./CompetitionList.jsx";
import {CompetitionEdit} from "./CompetitionEdit.jsx";
import {CompetitionRegisterAdmin} from "./CompetitionRegisterAdmin.jsx";
import {CompetitionView} from "./CompetitionView.jsx";
import {useTranslation} from "react-i18next";
export function CompetitionRoot() {
const {t} = useTranslation();
return <>
<h1>Compétition</h1>
<h1>{t('compétition')}</h1>
<LoadingProvider>
<Outlet/>
</LoadingProvider>

View File

@ -3,10 +3,12 @@ import {useLoadingSwitcher} from "../../hooks/useLoading.jsx";
import {useFetch} from "../../hooks/useFetch.js";
import {AxiosError} from "../../components/AxiosError.jsx";
import {useAuth} from "../../hooks/useAuth.jsx";
import {apiAxios, isClubAdmin} from "../../utils/Tools.js";
import {apiAxios, getToastMessage, isClubAdmin} from "../../utils/Tools.js";
import {ThreeDots} from "react-loader-spinner";
import {useEffect, useState} from "react";
import {toast} from "react-toastify";
import {useTranslation} from "react-i18next";
import i18n from "i18next";
export function CompetitionView() {
@ -32,13 +34,13 @@ export function CompetitionView() {
const inscText = (type) => {
if (type === "FREE") {
return "Libres"
return i18n.t('comp.reg.libres')
} else if (type === "CLUB_ADMIN") {
return "Par les responsables de club"
return i18n.t('comp.reg.parLesResponsablesDeClub')
} else if (type === "ADMIN") {
return "Par les administrateurs de la compétition"
return i18n.t('comp.reg.parLesAdministrateursDeLaCompétition')
} else if (type === "HELLOASSO") {
return "Sur la billetterie HelloAsso"
return i18n.t('comp.reg.surLaBilletterieHelloasso')
}
return ""
@ -47,6 +49,7 @@ const inscText = (type) => {
function MakeContent({data}) {
const {userinfo} = useAuth()
const navigate = useNavigate()
const {t} = useTranslation();
return <div className="card mb-4">
<div className="card-header">
@ -54,35 +57,35 @@ function MakeContent({data}) {
</div>
<div className="card-body">
<p>{data.description}</p>
<p><strong>Date
:</strong> Du {new Date(data.date.split('T')[0]).toLocaleDateString()} au {new Date(data.toDate.split('T')[0]).toLocaleDateString()}
<p><strong>{t('date')}
:</strong> {t('du')} {new Date(data.date.split('T')[0]).toLocaleDateString()} {t('au')} {new Date(data.toDate.split('T')[0]).toLocaleDateString()}
</p>
<p><strong>Lieu :</strong> {data.adresse}</p>
<p><strong>Organisateur :</strong> {data.clubName}</p>
<p><strong>Type d'inscription :</strong> {inscText(data.registerMode)}</p>
<p><strong>{t('lieu')} :</strong> {data.adresse}</p>
<p><strong>{t('comp.organisateur')} :</strong> {data.clubName}</p>
<p><strong>{t('comp.typeDinscription')} :</strong> {inscText(data.registerMode)}</p>
{(data.registerMode === "FREE" || data.registerMode === "CLUB_ADMIN") &&
<p><strong>Date d'inscription
:</strong> Du {new Date(data.startRegister.split('+')[0]).toLocaleString()} au {new Date(data.endRegister.split('+')[0]).toLocaleString()}
<p><strong>{t('comp.dateDinscription')}
:</strong> {t('du')} {new Date(data.startRegister.split('+')[0]).toLocaleString()} {t('au')} {new Date(data.endRegister.split('+')[0]).toLocaleString()}
</p>
}
{(data.registerMode === "FREE" || data.registerMode === "CLUB_ADMIN") && isClubAdmin(userinfo) &&
<button type="button" className="btn btn-primary"
disabled={new Date() < new Date(data.startRegister.split('+')[0]) || new Date() > new Date(data.endRegister.split('+')[0])}
onClick={_ => navigate("/competition/" + data.id + "/club/register")}>Inscription</button>
onClick={_ => navigate("/competition/" + data.id + "/club/register")}>{t('comp.inscription')}</button>
}
{data.registerMode === "FREE" && !isClubAdmin(userinfo) &&
<SelfRegister data2={data}/>
}
{data.registerMode === "HELLOASSO" &&
<p><strong>Billetterie :</strong> <a
<p><strong>{t('comp.billetterie')} :</strong> <a
href={`https://www.helloasso.com/associations/${data.data1}/evenements/${data.data2}`} target="_blank"
rel="noopener noreferrer">{`https://www.helloasso.com/associations/${data.data1}/evenements/${data.data2}`}</a></p>
}
{data.canEditRegisters &&
<div style={{marginTop: "0.5em"}}>
<button type="button" className="btn btn-primary"
onClick={_ => navigate("/competition/" + data.id + "/register")}>Inscription - mode administrateur
onClick={_ => navigate("/competition/" + data.id + "/register")}>{t('comp.inscriptionModeAdministrateur')}
</button>
</div>
}
@ -94,6 +97,7 @@ function SelfRegister({data2}) {
const {id} = useParams()
const setLoading = useLoadingSwitcher()
const {data, refresh, error} = useFetch(`/competition/${id}/register/user`, setLoading, 1)
const {t} = useTranslation();
const [weight, setWeight] = useState("")
const [cat, setCat] = useState(0)
@ -108,36 +112,22 @@ function SelfRegister({data2}) {
const disabled = new Date() < new Date(data2.startRegister.split('+')[0]) || new Date() > new Date(data2.endRegister.split('+')[0])
const handleUnregister = () => {
if (window.confirm("Êtes-vous sûr de vouloir vous désinscrire ?")) {
toast.promise(apiAxios.delete(`/competition/${id}/register/${data[0].id}/user`), {
pending: "Désinscription en cours",
success: "Désinscription réalisée",
error: {
render({data}) {
return data.response.data || "Erreur"
}
}
}).finally(() => {
if (window.confirm(t('comp.warn4'))) {
toast.promise(apiAxios.delete(`/competition/${id}/register/${data[0].id}/user`), getToastMessage("comp.toast.register.self.del")
).finally(() => {
refresh(`/competition/${id}/register/user`)
})
}
}
const sendSubmit = (new_state) => {
toast.promise(apiAxios.post(`/competition/${id}/register/user`, new_state), {
pending: "Enregistrement en cours",
success: "Inscription réalisée",
error: {
render({data}) {
return data.response.data || "Combattant non trouvé"
}
}
}).finally(() => {
toast.promise(apiAxios.post(`/competition/${id}/register/user`, new_state), getToastMessage("comp.toast.register.self.add")
).finally(() => {
refresh(`/competition/${id}/register/user`)
})
}
const handleSubmit = (e) => {
const handleSubmit = () => {
sendSubmit({
licence: 0, fname: "", lname: "", weight: weight, overCategory: cat, lockEdit: false, id: null
})
@ -147,34 +137,34 @@ function SelfRegister({data2}) {
{data
? data.length > 0
? <div style={{textAlign: "right", maxWidth: "20em"}}>
<h4 style={{textAlign: "left"}}>Mon inscription</h4>
<h4 style={{textAlign: "left"}}>{t('comp.monInscription')}</h4>
<div className="input-group mb-3">
<span className="input-group-text" id="weight">Poids (en kg)</span>
<span className="input-group-text" id="weight">{t("comp.modal.poids")}</span>
<input type="number" min={1} step={1} className="form-control" placeholder="42" aria-label="weight" disabled={disabled}
name="weight" aria-describedby="weight" value={weight} onChange={e => setWeight(e.target.value)}/>
</div>
<div style={{textAlign: "left"}}>Catégorie normalisée: {data[0].categorie}</div>
<div style={{textAlign: "left"}}>{t('comp.catégorieNormalisée')}: {data[0].categorie}</div>
<div className="input-group mb-3">
<span className="input-group-text" id="categorie">Surclassement</span>
<span className="input-group-text" id="categorie">{t("comp.modal.surclassement")}</span>
<select className="form-select" aria-label="categorie" name="categorie" value={cat} disabled={disabled}
onChange={e => setCat(Number(e.target.value))}>
<option value={0}>Aucun</option>
<option value={1}>+1 catégorie</option>
<option value={2}>+2 catégorie</option>
<option value={0}>{t('aucun')}</option>
<option value={1}>{t('1Catégorie')}</option>
<option value={2}>{t('2Catégorie')}</option>
</select>
</div>
<div>
<button type="button" className="btn btn-danger" disabled={disabled} style={{marginRight: "0.5em"}}
onClick={handleUnregister}>Se désinscrire
onClick={handleUnregister}>{t('button.seDésinscrire')}
</button>
<button type="button" className="btn btn-primary" disabled={disabled}
onClick={handleSubmit}>Enregister
onClick={handleSubmit}>{t('button.enregister')}
</button>
</div>
</div>
: <button type="button" className="btn btn-primary" disabled={disabled} onClick={handleSubmit}>S'inscrire</button>
: <button type="button" className="btn btn-primary" disabled={disabled} onClick={handleSubmit}>{t('comp.sinscrire')}</button>
: error
? <AxiosError error={error}/>
: <Def/>