595 Commits

Author SHA1 Message Date
2d86789938 MSIN-4467 — Implement new workspace architecture for bib management, including preparation, production, and operations sections. Update CSS for improved layout and styling of workspace elements. Increment version code to 4.72.843. 2026-07-21 10:27:01 -04:00
f9d304484e MSIN-4465 — Refactor report access functions to support V2 permissions and enhance legacy menu handling. Update SQL queries for better data retrieval and add comments for clarity. Increment version code. 2026-07-20 14:54:24 -04:00
f0bdefd805 MSIN-4419 — Update SQL query for participant statistics to count total participations without deduplication by email. Adjust comments for clarity regarding participation counting. Increment version code. 2026-07-20 12:54:44 -04:00
b4637ddeca MSIN-4459 — Update participant list rendering to include team names with improved inline display. Adjust CSS for better layout and visual consistency. Increment version code to 4.72.842. 2026-07-20 12:35:28 -04:00
fb2de52739 MSIN-4459 — Enhance team display in participant list with visual grouping and indentation for teammates. Update CSS for new styles and increment version code to 4.72.841. 2026-07-20 12:34:12 -04:00
90f496c684 MSIN-4459 — Refactor participant list rendering to improve layout and clarity. Update SQL query for team member sorting and adjust CSS for new styling conventions. Increment version code to 4.72.840. 2026-07-20 12:31:48 -04:00
f7057bd7cb MSIN-4459 — Refactor participant list to display individual entries per participant, excluding certain roles. Update SQL queries for improved data retrieval and modify CSS for new styling conventions. Increment version code to 4.72.839. 2026-07-20 12:26:32 -04:00
afac2f175e MSIN-4459 — Refactor participant list to include individual bib numbers and team details. Update SQL queries and PHP functions for enhanced data retrieval. Modify CSS to remove deprecated styles. Increment version code to 4.72.838. 2026-07-20 12:19:55 -04:00
44b7e4cf84 MSIN-4459 — Update participant list to include team number and enhance duplicate bib checks. Modify SQL queries and PHP functions for improved data handling. Adjust CSS for new layout elements. Increment version code to 4.72.837. 2026-07-20 12:12:06 -04:00
39fd110388 MSIN-4459 — Implement team member display under captain in participant list. Update CSS for layout adjustments and add new SQL query to fetch team members. Increment version code to 4.72.836. 2026-07-20 12:01:41 -04:00
da4de58b08 Aligner les règles agent sur le modèle entreprise/projet/ops.
Conserve les conventions Inscription sous projet/ et ajoute ops Git/deploy.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-19 12:05:59 -04:00
211f1e2638 MSIN-4452 — Update badge styles in promoteur hub for improved visual alignment and reduced alert intensity. Adjust CSS properties for danger, warning, info, and soft badges. Increment version code to 4.72.835. 2026-07-16 11:41:07 -04:00
64ef6c2a8e MSIN-4454 — Update product display in promoteur hub to show sold quantities instead of available. Adjust labels and HTML structure for clarity and consistency in the user interface. Increment version code accordingly. 2026-07-16 11:27:11 -04:00
0ff8299fdc MSIN-4451 — Add participant count under the "entries sold" KPI in the promoteur hub. Introduce new CSS styles for displaying participant information and update SQL queries to calculate unique participants. Increment version code to 4.72.834. 2026-07-16 11:24:11 -04:00
37f8bec215 Update 'promoteur_hub_flash_inscrits' label to reflect total entries sold instead of registered. This change clarifies the terminology used in the interface, aligning it with the intended meaning of the data. Increment version code accordingly. 2026-07-16 11:00:04 -04:00
7b94e0daa0 Remove unused sorting parameters from AJAX response in bib range actions, streamlining the data returned for improved performance. This change simplifies the response structure while maintaining the necessary HTML output. Increment version code to 4.72.834. 2026-07-16 10:55:10 -04:00
e9752ddcb7 MSIN-4456 — Enhance sorting functionality for team names in bib assignments. Introduce new functions to ensure reliable ordering by team name and implement PHP-based sorting for participants when the primary sort criterion is a team. Update AJAX responses to include additional sorting parameters. Increment version code to 4.72.833. 2026-07-16 10:42:33 -04:00
26b85d1892 MSIN-4446 — Enhance AJAX responses for bib range actions by including summary HTML and sequence counts. Update JavaScript functions to handle new response structure, ensuring accurate display of sequence information after create, delete, and refresh actions. Increment version code to 4.72.832. 2026-07-16 10:07:06 -04:00
527e3909e8 MSIN-4445 — Refine free assignment logic by enhancing duplicate bib number checks and improving feedback mechanisms for overlapping ranges. Update backend functions to ensure accurate detection of assigned bibs within specified ranges and adjust UI components to reflect these changes. Increment version code to 4.72.830. 2026-07-16 10:02:14 -04:00
ce63b9325e MSIN-4445 — Enhance free assignment functionality by adding duplicate bib number warnings for overlapping ranges. Implement backend checks for bibs already assigned to other events and update UI components to display relevant feedback. Increment version code to 4.72.829. 2026-07-16 09:58:07 -04:00
77799add6d MSIN-4445 — Implement free assignment functionality for solo events without sequence creation. Add new actions for analysis, simulation, and planning of free ranges. Update UI components and JavaScript logic to support the new features, including feedback mechanisms for quantity validation. Increment version code to 4.72.828. 2026-07-16 09:51:34 -04:00
b72ea70242 MSIN-4464 — Update audit history button styles for improved visibility and user experience. Change button design to a more discreet circular format, adjusting colors and dimensions for better integration into the UI. Increment version code to 4.72.827. 2026-07-15 13:21:15 -04:00
d23d471de4 MSIN-4464 — Implement audit history feature for participant fields. Add functionality to retrieve and display recent changes for specific fields, enhancing traceability of modifications. Introduce a new button in the UI to access the audit history, along with corresponding styles and JavaScript logic for improved user interaction. Update relevant backend functions to support this feature, ensuring proper access control and data retrieval. 2026-07-15 13:18:44 -04:00
3ac01a6f00 MSIN-4464 — Revamp event selection form in audit logs. Replace dropdown with a search input and direct ID entry for improved usability. Enhance event filtering with live search capabilities and update layout for better accessibility, ensuring a more intuitive experience for users managing audit logs. 2026-07-15 13:08:19 -04:00
ffb679626e MSIN-4464 — Refactor audit event selection in admin interface. Update SQL query to improve sorting logic for events and enhance the event selection form with a live search feature. Adjust layout and add direct input for event IDs, improving usability and accessibility for users managing audit logs. 2026-07-15 13:06:05 -04:00
c8aa6394f5 MSIN-4464 — Enhance audit logging functionality for participant management. Introduce new functions to track last touch events and handle cancellation/restoration of participant registrations. Update admin interface to display recent logs and improve oversight of audit activities, ensuring better traceability of changes made to participant records. 2026-07-15 13:04:09 -04:00
1260270c7d MSIN-4464 — Implement audit logging for participant updates. Introduce functions to track changes to 'no_bib', 'no_bib_remis', and participant status, ensuring traceability of modifications. Enhance the admin interface to support these audit features, improving oversight and accountability in participant management. 2026-07-15 13:00:01 -04:00
97ac1807b0 MSIN-4464 — Add configuration for fiche audit traceability in admin interface. Introduce a new sidebar section for audited fields and retention, enhancing the user experience by providing easy access to audit-related features. 2026-07-15 12:57:18 -04:00
260c617e35 Remove reference to MSIN-4460 from UI messaging in admin kit management. Clarify text regarding delegable kits to enhance user understanding without altering functionality. 2026-07-15 12:40:01 -04:00
689b0a12f7 MSIN-4460 — Update delegation logic for kits in admin interface. Remove restrictions on delegable kits, allowing super admins to decide on delegation per kit. Clarify UI messaging regarding kit assignments and delegation options for improved user understanding. 2026-07-15 12:38:18 -04:00
dbe25c8a77 MSIN-4457 — Adjust layout of event access form by modifying column sizes for improved responsiveness. Update label for days input to enhance clarity, ensuring a better user experience in the event management interface. 2026-07-15 12:32:04 -04:00
7b881be0a7 MSIN-4457 — Refactor event selection UI to improve usability by removing absolute positioning and implementing a responsive height for result lists. This change enhances the visibility of event options and prevents clipping issues, ensuring a better user experience during event searches. 2026-07-15 12:28:46 -04:00
44241a5010 MSIN-4457 — Enhance event selection UI by adding scrollable result lists for better usability. Update input fields to include position-relative styling and adjust result limits in SQL queries to accommodate more historical events, improving the overall user experience in event searches. 2026-07-15 12:26:22 -04:00
6202665862 MSIN-4457 — Update event search logic to prioritize recent events. Modify SQL query to sort by event end date and ID, improving the visibility of upcoming events in search results. Increase result limit to enhance user experience. 2026-07-15 12:22:50 -04:00
9cf4367521 MSIN-4430 — Add button for bib confirmation in inscriptions management interface. This change introduces a new button for confirming bib entries, enhancing user interaction and streamlining the process. The previous button implementation has been removed to avoid duplication. 2026-07-15 12:16:59 -04:00
2dae908074 MSIN-4328 — Add custom field probe functionality to ChronoTrack API. Implement a new button for custom field synchronization in the admin interface, allowing for the testing of custom fields similar to import processes. Enhance backend logic to handle custom field creation and validation, improving the overall synchronization capabilities and user experience. 2026-07-15 12:03:42 -04:00
66590a2627 MSIN-4328 — Enhance ChronoTrack API sync logic to include check-in status and bib retrieval date. Update filtering to account for 'no_bib_remis_date' and improve participant data retrieval by adding relevant fields. This change aims to improve the accuracy of participant synchronization and check-in tracking. 2026-07-15 12:02:26 -04:00
36c6e5dd2b MSIN-4328 — Update bib assignment logic in ChronoTrack API sync process. Ensure bib numbers are only assigned when the entry status is confirmed and the entry is not already linked to a ChronoTrack entry. This change improves the accuracy of bib assignments during synchronization. 2026-07-15 09:26:19 -04:00
80624eebfe MSIN-4328 — Refactor countdown functionality for screen refresh in ChronoTrack API. Simplify UI messages and remove unused countdown logic for auto sync. Enhance user experience by providing clearer feedback on the status refresh interval and streamline related JavaScript functions. 2026-07-15 09:20:38 -04:00
05fc3b0048 MSIN-4328 — Implement countdown for next auto sync in ChronoTrack API. Enhance UI to display countdown timer for upcoming cron jobs and update push panel messages for clarity on sync operations. Introduce new functions for managing auto sync metadata and improve overall user feedback during synchronization processes. 2026-07-15 09:18:35 -04:00
4bfd89d358 MSIN-4328 — Remove logging of 'push_skip' in participant sync to reduce spam during preparation. Blocked participants will still be visible in the UI without cluttering logs. 2026-07-15 09:14:49 -04:00
4ce59663ad MSIN-4448 — Pastille dossard cliquable dans la liste (édition à la demande).
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-15 08:56:12 -04:00
0e42cc45d1 Implement inline editing for bib numbers and status confirmation in the participant management interface. Enhance CSS for better layout and usability, including wider columns for editable bibs and improved button styles. Update JavaScript to handle inline edits and confirmations, ensuring a smoother user experience. This change addresses MSIN-4448 and improves overall functionality in the inscriptions management system. 2026-07-15 08:52:27 -04:00
a18197b31c Refactor auto ChronoTrack sync script to enhance logging and error handling. Remove local heartbeat logging and update cron execution feedback. Improve admin interface with clearer instructions and a new button for manual sync diagnostics. Implement auto-sync diagnostics to provide detailed status and logs for events, facilitating better monitoring and troubleshooting of synchronization processes. 2026-07-14 12:46:27 -04:00
c3340605f2 Enhance auto ChronoTrack sync script with logging and error handling. Implement local heartbeat logging to track script execution and improve feedback during cron job execution. Update admin interface to include a button for manual cron testing and provide clearer instructions for using the sync functionality. This change aims to improve monitoring and troubleshooting of synchronization processes. 2026-07-14 12:43:54 -04:00
0e0e1cf2a2 Add sync logs functionality to ChronoTrack API. Implement a new method for retrieving the latest synchronization logs for events, enhancing the admin interface with a button to view logs. Update AJAX handling to support log retrieval and improve user feedback during synchronization processes. This change aims to provide better visibility into sync operations and facilitate troubleshooting. 2026-07-14 12:27:14 -04:00
eac42923fd Implement auto-sync functionality in ChronoTrack API, including new methods for managing auto-sync events and handling expiration. Update push panel messages for clarity on cron job execution and enhance user feedback. This change supports improved synchronization management and lays the foundation for future enhancements. 2026-07-14 12:15:34 -04:00
0db8da9810 Implement auto-sync configuration for ChronoTrack API. Add functionality to save auto-sync settings, including frequency and end date, enhancing user control over synchronization. Update push panel to reflect new options and improve user feedback during the sync process. This change lays the groundwork for future cron integration. 2026-07-14 12:08:03 -04:00
f9a75e8aba Add auto-repair logic for push synchronization in ChronoTrack API. Implement a mechanism to handle pending pushes when all eligible participants are ready, ensuring accurate differential tracking. Update status messages during the process to enhance user feedback and improve overall synchronization reliability. 2026-07-14 11:58:32 -04:00
c567fd1adf Add sync_mark_current action to ChronoTrack API for updating last push timestamp without sending data. Enhance push panel with a new button for manual refresh, improving user control over synchronization status. Update SQL query to prevent timestamp auto-update during entry ID persistence, ensuring accurate differential tracking. 2026-07-14 11:56:42 -04:00
71e57d6eb1 Refactor ChronoTrack API push panel to enhance user experience with clearer status messages and improved layout. Update button actions and add collapsible details section for better visibility of synchronization results. This change aims to streamline participant data management and provide more intuitive feedback during the push process. 2026-07-14 11:53:07 -04:00
7ce5dd0941 Enhance ChronoTrack API integration by adding support for direct links to athlete profiles and persisting entry IDs. Update CSS for improved layout on management pages. Adjust AJAX handling to accommodate new entry ID parameters, ensuring seamless synchronization and user experience. This change aims to streamline access to athlete data and improve overall functionality in the ChronoTrack system. 2026-07-14 11:48:12 -04:00
a860db05d3 Refactor ChronoTrack API synchronization to streamline entry counting and improve performance. Introduce new functions for fetching total entries without detailed scanning, enhancing efficiency. Adjust AJAX handling for synchronization processes and update timeout settings for better reliability. This change aims to optimize data handling during participant synchronization. 2026-07-14 11:37:59 -04:00
1b9d62c69e Add fxChronotrackApiSyncEpreuveLabel function for improved race labeling in exclusion messages. Update error handling to include detailed race information when mapping issues occur, enhancing clarity in synchronization logs. 2026-07-14 11:18:26 -04:00
bdc9c8fdde Enhance ChronoTrack API synchronization by adding validation for missing first and last names. Introduce new error codes and messages for participants lacking these fields, improving data integrity checks during synchronization. This change ensures that participant data is complete before processing, enhancing overall reliability. 2026-07-14 11:14:27 -04:00
784ef238a2 Implement changes to ChronoTrack API synchronization to track pending pushes and last successful push timestamps. Introduce logic to filter participants eligible for pushing based on their modification dates relative to the last push. Adjust push confirmation messages and update the handling of push job results to ensure accurate reporting of pending and successful pushes. This enhances the reliability and efficiency of participant data synchronization. 2026-07-14 11:05:12 -04:00
746ddea4b2 Adjust ChronoTrack API synchronization parameters to a cautious batch and chunk size of 100 for improved validation. Introduce a performance testing function to evaluate optimal batch sizes for entry posting, enhancing error handling and efficiency in participant data synchronization. This change aims to calibrate the push process before full implementation. 2026-07-14 09:54:41 -04:00
d1733b2848 Update ChronoTrack API synchronization parameters to improve performance and error handling. Increase chunk sizes for data processing and adjust timeout settings for HTTP requests. Introduce adaptive entry posting to handle failures more efficiently, reducing the number of HTTP calls. This change enhances the reliability and efficiency of participant data synchronization. 2026-07-14 09:53:14 -04:00
13e6f6dcf2 Enhance push synchronization process in ChronoTrack API by implementing chunked data handling for improved performance and user feedback. Introduce job management functions to store and manage push jobs, reducing session load. Update AJAX handling for push preparation and execution, allowing for better error handling and progress tracking. Increment batch size for synchronization to optimize HTTP requests. This change significantly improves the efficiency and reliability of participant data synchronization. 2026-07-14 09:42:22 -04:00
2e17136f01 Refactor promoteur back URL logic to prioritize new hub structure, enabling event focus functionality. Introduce fxPromoteurHubOrLegacyBackUrl for streamlined navigation based on event ID availability. Update related functions for consistency and clarity. This change enhances user experience by ensuring proper redirection based on session and event context. 2026-07-14 08:35:59 -04:00
19b9369e77 Implement event focus functionality in promoteur hub, allowing automatic scrolling and menu opening based on event ID. Update back URL logic to prioritize the new hub structure and enhance deep-linking capabilities. Increment version code to 4.72.823 to reflect these changes. 2026-07-14 08:29:58 -04:00
c6bc9ce4b0 Update bib matching logic in inscription management to set 'exact' as the default option and adjust related comments for clarity. Enhance search reset functionality to include a query parameter for all inscriptions. Refactor conditions for matching to improve consistency in handling exact and partial matches. 2026-07-14 08:25:59 -04:00
b9a8d5ffd2 Implement course status column in inscription management, enhancing the display of race statuses. Update CSS for new status pill styling and adjust grid layout for better responsiveness. Increment version code to 4.72.822 to reflect these changes. 2026-07-14 08:18:27 -04:00
1f7a92c8ea Enhance access control for super admin actions by adding validation for report preferences and question approvals. Update action handling to include new permissions for 'actif' and 'validequestion', ensuring proper session checks and error responses. Refactor action array for clarity. Increment version code to reflect these changes. 2026-07-13 19:26:34 -04:00
899ba89a2b Update version code to 4.72.821, enhance error handling in JavaScript and PHP for better user feedback, and improve access control for report preferences in super admin actions. Refactor alert messages and database insert logic to provide clearer error messages and ensure proper session handling. 2026-07-13 19:23:36 -04:00
2eafd998c0 Update version code to 4.72.819 and enhance access control for report preferences in super admin actions, ensuring proper session handling and permissions. Refactor JSON response handling for specific actions. 2026-07-13 18:50:55 -04:00
b0cfc629f9 Refactor access control for super admin sessions to eliminate bypass permissions, ensuring that only specific kits and extras are accessible. Update version code to 4.72.818 to reflect these changes. 2026-07-13 18:03:05 -04:00
3da6977daf Refactor inscription management permissions to enhance control over sensitive actions, specifically excluding refunds from general edit permissions. Update version code to 4.72.817 to reflect these changes. 2026-07-13 17:57:02 -04:00
e52c3b7b45 Refactor inscription management access control to implement granular permissions for transaction actions. Update CSS for improved button layout and alignment, ensuring consistent styling across action items. Increment version code to 4.72.816 to reflect these changes. 2026-07-13 17:53:55 -04:00
6c56fb7af7 Implement exact and partial matching options for bib search in inscription management. Update CSS for improved layout and styling of the new matching interface. Increment version code to 4.72.815 to reflect these changes. 2026-07-13 17:47:10 -04:00
dc3d6a5c0b Refactor inscription management to improve localization handling by replacing hardcoded strings with dynamic keys. Enhance CSS for better layout and responsiveness in the inscription management UI. Increment version code to 4.72.814 to reflect these changes. 2026-07-13 17:42:21 -04:00
8272192815 Refactor crop handling in inscription management to improve accuracy of target area measurements. Update CSS comments for clarity on spotlight functionality. Increment version code to 4.72.813 to reflect these changes. 2026-07-13 17:38:09 -04:00
1c8782db3c Update inscription management query logic to handle "Sans dossard" conditions for solo and team entries. Increment version code to 4.72.812 to reflect these changes. 2026-07-13 17:34:19 -04:00
cc2bd15658 Refactor inscription management UI to eliminate hardcoded language strings, replacing them with dynamic keys for better localization. Update CSS for improved grid layout and visual consistency, including responsive adjustments for mobile views. Increment version code to 4.72.811 to reflect these changes. 2026-07-13 17:32:09 -04:00
080e8bdcd9 Enhance visual consistency for inscription management panels by standardizing color schemes and styles across various components. Update CSS for improved panel aesthetics, including border-radius adjustments and box shadows. Increment version code to 4.72.809 to reflect these changes. 2026-07-13 17:26:25 -04:00
0ea8a48591 Implement responsive grid layout for the inscription management form, enhancing mobile and tablet views. Update form fields for better structure and accessibility, including multi-status filters and action buttons. Increment version code to 4.72.807 to reflect these changes. 2026-07-13 17:23:25 -04:00
d3bb32eb97 Enhance multi-page AJAX handling by enforcing context for translations in ajax_inscr_gestion and ajax_promoteur_hub. Introduce new language keys for improved user feedback and error messages. Increment version code to 4.72.806 to reflect these changes. 2026-07-13 17:17:53 -04:00
af2ddba2fa Implement email resend functionality for event access invitations, including success and error handling. Update language translations for resend button and messages. Increment version code to 4.72.804 to reflect these changes. 2026-07-13 16:59:49 -04:00
8191970086 Implement invitation handling for account switching in mobile interface. Introduce signature validation for invite links to prevent spoofing and enhance security. Update logout redirection to support both reset and invite links. Increment version code to 4.72.803 to reflect these changes. 2026-07-13 16:55:47 -04:00
b948f401d1 Enhance QR scanning interface by adjusting CSS for improved layout and centering of the QR camera view on open. Introduce JavaScript function to focus the QR camera view upon opening and ensure proper sizing of the QR box. Increment version code to 4.72.802 to reflect these changes. 2026-07-13 16:45:16 -04:00
5f9ce3f0e7 Enhance QR scanning permissions in inscriptions management to align with V2 API. Introduce fallback logic for permissions and streamline role checks. Update SQL to reflect new permission handling for targeted roles. No version code increment required. 2026-07-13 16:40:45 -04:00
2c11be5796 Refactor permissions for inscriptions management to align with V2 API standards. Remove deprecated permission keys and update related functions for QR scanning access. No version code increment required. 2026-07-13 15:57:57 -04:00
a1a74565a4 Update CSS to allow text selection for help button and enhance JavaScript error handling by removing timeout for showing ready state. Increment version code to 4.72.801 to reflect these changes. 2026-07-13 15:39:11 -04:00
77607608e7 Enhance OCR functionality for bib numbers by introducing multipart image upload support and improving error handling. Update JavaScript to handle image processing and cropping more effectively, including a new method for converting canvas to JPEG blob. Increment version code to 4.72.800 to reflect these changes. 2026-07-13 15:28:45 -04:00
33eb70261d Increment version code to 4.72.799 to reflect recent updates in settings. 2026-07-13 15:25:51 -04:00
a1aefe7bbe Update OCR API key in settings for Google Cloud Vision integration. This change enables the OCR functionality for bib numbers by providing the necessary API credentials. No version code increment required. 2026-07-13 15:24:37 -04:00
a929a8d859 Implement OCR functionality for bib numbers using Google Cloud Vision. Introduce new methods for enabling OCR, processing images, and extracting numbers from OCR text. Update JavaScript to handle cloud-based OCR processing and improve error handling. Increment version code to 4.72.798 to reflect these changes. 2026-07-13 15:12:54 -04:00
e2d8dee1a6 Refactor OCR canvas processing to enhance contrast and improve cropping logic for video and image sources. Introduce a new method for capturing frames from video sessions, prioritizing ImageCapture for better clarity on mobile devices. Increment version code to 4.72.797 to reflect these changes. 2026-07-13 15:01:25 -04:00
e7662a304e Enhance OCR functionality by improving number extraction logic to handle various formats and ensuring better accuracy. Introduce a new method for preprocessing the OCR canvas to enhance contrast for better recognition. Update video frame capture to consider the visible rectangle for improved cropping. Increment version code to 4.72.796 to reflect these changes. 2026-07-13 14:17:30 -04:00
7efc81235d Update CSS styles for mobile scanning interface to improve button dimensions and positioning, ensuring better visibility and user experience. Introduce a new method in JavaScript to center the capture view within the viewport, enhancing usability during scanning. Increment version code to 4.72.795 to reflect these changes. 2026-07-13 14:07:53 -04:00
8efbb4c4a7 Update session handling in AJAX responses to improve user experience. Modify error messages to include session codes instead of generic messages, prompting a page reload for expired sessions. Increment version code to 4.72.794 to reflect these changes. 2026-07-13 14:04:10 -04:00
d3b9fab7ce Update OCR settings to enhance number recognition accuracy by increasing maximum width. Adjust CSS styles for button dimensions and positioning, improving user interface consistency. Increment version code to 4.72.793 to reflect these changes. 2026-07-13 14:01:32 -04:00
93378a5a2d Adjust video frame capture dimensions and button size for improved OCR alignment. Update related comments and increment version code to 4.72.792 to reflect these changes. 2026-07-13 13:46:01 -04:00
5fefdccd17 Update video frame capture dimensions to improve OCR accuracy and adjust related styles. Increase button size to 70% width and 40% height for better alignment with the OCR reading area. Increment version code to 4.72.791 to reflect these changes. 2026-07-13 13:43:06 -04:00
a18f25dc84 Enhance video frame capture functionality by introducing a new method to calculate the visible rectangle for object-fit:cover. Update the captureCenterFrame function to utilize this new method, improving cropping accuracy based on the display element. Increment version code to 4.72.790 to reflect these changes. 2026-07-13 13:41:13 -04:00
8a9369bbaf Update styles for mobile scanning interface and enhance user guidance. Adjust button dimensions and positioning for better visibility. Improve hint messages for scanning instructions and update version code to 4.72.789, aligning with MSIN-4401 requirements for enhanced event management functionality. 2026-07-13 13:38:20 -04:00
397eb4651b Refactor account link rendering in mobile interface to improve user navigation. Move the full account link functionality to inc_header.php for better organization and visibility control. Update version code to 4.72.788, aligning with MSIN-4401 requirements for enhanced event management functionality. 2026-07-13 13:35:10 -04:00
ec15b07f21 Add functionality to render a full account link in the mobile interface for users with extended permissions. Implement new functions to determine visibility and URL for the link, enhancing user navigation to the dashboard or account page. Update styles for the new button and increment version code to 4.72.787, aligning with MSIN-4401 requirements for improved event management functionality. 2026-07-13 13:19:08 -04:00
2970007d2b Enhance email invitation functionality in fxEveAccesPromoteurSendInviteMail to include a mobile access link for users. Update message content to guide users on accessing their accounts via mobile, improving user experience and accessibility. This change aligns with MSIN-4401 requirements for better event management functionality. 2026-07-13 13:15:01 -04:00
ee9d5943c9 Refactor fxAdminCanEditEveAccesKits function to utilize a global array for access control in kit management. Move the list of editors to inc_settings.php for better organization and maintainability. This change aligns with MSIN-4401 requirements for improved event management functionality. 2026-07-13 12:36:52 -04:00
f232a862ac Refactor fxAdminCanEditEveAccesKits function for improved access control in kit management. Move the function definition to inc_functions.php to ensure it is available for super admin without loading the entire inc_fonctions_sl.php file. Update logic to validate user permissions based on session information, aligning with MSIN-4401 requirements for enhanced event management functionality. 2026-07-13 12:34:38 -04:00
0fcb7dc9f8 Add access control for kit management in event administration. Introduce the function fxAdminCanEditEveAccesKits to validate user permissions based on email or login. Update event access page to restrict modification actions and enhance UI feedback for non-editable states, aligning with MSIN-4401 requirements for improved event management functionality. 2026-07-13 12:31:22 -04:00
ccf895c8fb Update UI components for improved accessibility and readability in event management. Add styling for access keys and codes, ensuring better visual distinction. Refactor HTML structure in the super admin panel for enhanced layout consistency. Increment version code to 4.72.786. 2026-07-13 12:25:36 -04:00
95aa7c9cf5 Add functionality to manage 'sections ouvertes' in event registration. Implement checks for the presence of the 'role_fiche_sections_open' column and update related functions to ensure sections are opened based on user roles. Enhance UI to reflect these changes in the registration management interface, aligning with MSIN-4401 requirements for improved event management functionality. 2026-07-13 12:19:05 -04:00
9ec4fd9ead Implement multi-selection filter for race status in the registration management interface. Enhance UI with a mobile-friendly design for better user interaction. Update backend logic to handle selected statuses and ensure proper filtering in requests. This aligns with MSIN-4401 requirements for improved event management functionality. 2026-07-13 11:52:58 -04:00
dd33cb6873 Implement access control for dashboard viewing in promoteur hub. Introduce permission checks to restrict access based on user roles, enhancing security and user experience. Update event panel rendering logic to conditionally display features based on available permissions, aligning with MSIN-4401 requirements for improved event management functionality. 2026-07-13 11:43:54 -04:00
4de952967d Refactor fxPromoteurHubGetEventLinks to enhance event access management by distinguishing between legacy and V2 permissions. Update documentation to clarify menu access based on user roles, ensuring a more intuitive experience for super admins and promoters. This aligns with MSIN-4401 requirements for improved event management functionality. 2026-07-13 11:41:25 -04:00
5b87d02d9e Refactor event access management to support V2 features, updating permission checks and enhancing UI elements for better user experience. Remove legacy API keys and streamline access permissions in fxEveAcces functions. This aligns with MSIN-4401 requirements for improved event management functionality. 2026-07-13 11:36:30 -04:00
7d08e7ec7c Refactor account management functionality to enhance password reset process. Exclude 'compte_reset' from session checks in compte.php to prevent conflicts with active sessions. Implement safe return link handling after logout in inc_fx_compte.php, ensuring users can navigate back to the reset page securely. Add user notifications for active sessions during password reset attempts, aligning with MSIN-4401 requirements for improved user experience. 2026-07-13 11:19:13 -04:00
4816bcf024 Enhance password reset functionality by updating the SQL query to clear the reset token upon successful password change. Introduce a new function to activate invite status for accounts after password creation, ensuring a smoother transition from invite to active status. Update localization keys for account status to improve user feedback. This aligns with MSIN-4401 requirements for enhanced event management functionality. 2026-07-13 10:41:35 -04:00
37dd64e4c4 Update localization keys for existing account messages in fxEveAccesPromoteurT function to simplify feedback for users. This change enhances clarity in user notifications regarding account status, aligning with ongoing improvements in event management functionality. 2026-07-13 10:19:03 -04:00
29af6c75cf Enhance event access invitation form with AJAX email verification and improved feedback handling. Add new localization keys for account existence checks and update UI elements for better user experience. This aligns with MSIN-4401 requirements for streamlined event management functionality. 2026-07-13 10:16:12 -04:00
591d93d699 Implement event access invitation and revocation handling in compte.php, ensuring proper processing before HTML output to prevent blank pages. Update inc_tableau_eve_acces.php to reflect new error handling and success messaging. Enhance fxEveAccesPromoteurInvite function to handle mail sending failures gracefully. This aligns with MSIN-4401 requirements for improved event management functionality. 2026-07-13 10:11:19 -04:00
5cd00a1ff1 Enhance event access management for promoters by adding invitation and revocation functionalities. Update the UI to handle success and error messages, and improve session handling for actor identification. This aligns with MSIN-4401 requirements for a more robust event management experience. 2026-07-13 10:08:25 -04:00
fdf34afcd3 Refactor promoteur hub integration to support V2 features, including new dashboard links and menu updates. Remove legacy event access checks and streamline menu display for improved user experience. This aligns with MSIN-4401 requirements for a more cohesive event management interface. 2026-07-13 10:02:51 -04:00
c1d730ac98 Refactor hub promoteur access management to support V2 only, removing legacy event handling. Update related functions and documentation to reflect changes in event access permissions and menu integration. This aligns with MSIN-4401 requirements for improved event management. 2026-07-13 09:57:29 -04:00
5f73db0a59 MSIN-4401 Implement event access management features, including team invitation capabilities and menu integration
This commit introduces functions for managing event access, allowing promoters to invite team members and manage permissions. The `fxEveAccesCanTeamInvite` function is added to check if a user can invite others, and the UI is updated to include access management options in the navigation. Additionally, relevant includes are added to ensure the necessary functions are available. These changes enhance the event management experience for promoters.
2026-07-13 09:49:12 -04:00
54a7e1dae9 Add logo path function and enhance logo image creation for QR key chains
This commit introduces a new function to define the logo path for QR key chains and enhances the image creation process by adding functionality to load, resize, and center the logo within a specified size. If the logo file is missing, a fallback to a text drawing is implemented. These changes improve the visual presentation of QR codes with logos.
2026-07-11 18:53:02 -04:00
33706bb195 Implement global batch completion UI, enhance JavaScript handling, and increment version code to 4.72.784
This commit introduces a new user interface for displaying the completion status of global batch assignments, including success, warning, and error states. The CSS is updated to style the completion messages appropriately. Additionally, JavaScript functions are added to manage the display of these messages based on assignment results. Localization keys are also added for improved user feedback. The version code is incremented to 4.72.784 to reflect these changes.
2026-07-09 12:48:02 -04:00
9e8055bfee Refactor global simulation view handling, enhance styles, and increment version code to 4.72.783
This commit refines the rendering of the global simulation view by introducing a new wrapper function for better structure and maintainability. The CSS is updated to improve the layout and appearance of the simulation toolbar and title. Additionally, JavaScript functions are added to manage the insertion and removal of global simulation views. The version code is incremented to 4.72.783 to reflect these changes.
2026-07-09 12:30:17 -04:00
4fefc8c053 Refactor global batch section handling in JavaScript, enhance CSS for collapsible sections, and increment version code to 4.72.782
This commit refines the JavaScript functions for managing global batch sections, improving the retrieval of section bodies and ensuring proper toggling functionality. The CSS is updated to support collapsible sections, allowing for better organization of content. Additionally, the version code is incremented to 4.72.782 to reflect these changes.
2026-07-09 12:23:11 -04:00
2aa398b1f0 Implement collapsible sections for global batch assignments, enhance styles, and increment version code to 4.72.781
This commit introduces collapsible sections for global batch assignments in the user interface, improving organization and user experience. The CSS is updated to style these sections, including headers and content areas, while the JavaScript is modified to handle the toggling functionality. Additionally, the version code is incremented to 4.72.781 to reflect these changes.
2026-07-09 12:20:08 -04:00
91ee36c121 Add batch global analysis, simulation, and creation functionalities; update styles and localization, increment version code to 4.72.780
This commit introduces new actions for batch global analysis, simulation, and creation without existing sequences. The JavaScript and PHP files are updated to handle these new functionalities, including validation and response handling. CSS styles are enhanced for better layout and user experience. Localization keys are added for improved clarity in user messages. The version code is incremented to 4.72.780 to reflect these changes.
2026-07-09 12:15:04 -04:00
dc55a855c3 Enhance anomaly block styles and functionality, increment version code to 4.72.779
This commit introduces new styles for the anomaly blocks, including collapsible sections and improved padding. The JavaScript is updated to handle click and keyboard events for toggling these sections, enhancing user interaction. The version code is incremented to 4.72.779 to reflect these changes.
2026-07-09 11:44:15 -04:00
1f96cce839 Update CSS styles for bib tool, enhance JavaScript tippy configuration, and increment version code to 4.72.778
This commit modifies the CSS to change overflow properties for menu items and adds new styles for help button text visibility. The JavaScript is updated to include z-index and appendTo properties for tippy tooltips, improving their display. The version code is incremented to 4.72.778 to reflect these changes.
2026-07-09 11:40:11 -04:00
8b5968e1da Update styles for bib tool buttons and detail panel, increment version code to 4.72.777
This commit enhances the CSS for the bib tool buttons, improving hover and active states with new color and box-shadow effects. The detail panel styles are also updated for better visual consistency. The version code is incremented to 4.72.777 to reflect these changes.
2026-07-09 11:34:02 -04:00
9d2223af3b Add view range button functionality and update styles for bib module, increment version code to 4.72.776
This commit introduces a new function to render a view range button for the bib module, enhancing the user interface with improved styling for the button and its container. The CSS is updated to ensure proper alignment and spacing. The version code is incremented to 4.72.776 to reflect these changes.
2026-07-09 11:29:32 -04:00
afdd50489f Update bib quantity summary panel to reflect new totals and improve localization, increment version code to 4.72.776
This commit modifies the rendering logic of the bib quantity summary panel to include updated totals for 'sans_bib', 'places_dispo', and 'avec_bib'. The localization keys are adjusted for better clarity, and unnecessary elements are removed to streamline the display. The version code is incremented to 4.72.776 to reflect these changes.
2026-07-09 11:26:02 -04:00
b85b01f2d2 Refactor registration management styles and remove unused filter section, update version code to 4.72.775
This commit cleans up the CSS by removing unnecessary styles related to the registration management form and eliminates the filter subsection from the PHP rendering function. The version code is incremented to 4.72.775 to reflect these changes.
2026-07-09 11:21:01 -04:00
fdecff66d5 Refactor registration management styles and enhance AJAX handling, update version code to 4.72.773
This commit refines the CSS styles for the registration management form, adjusting margins and padding for better layout. It also updates the JavaScript to include the epr_id in the delete action, improving the handling of AJAX requests. Additionally, the version code is incremented to 4.72.773 to reflect these changes.
2026-07-09 11:16:12 -04:00
e5f8ca953e Enhance registration management UI with new styles and localization, update version code to 4.72.772
This commit introduces new CSS styles for the registration management form, improving the visual structure and user experience. A new subsection for filters is added, along with a localized label for cancelled registrations. The version code is incremented to 4.72.772 to reflect these updates.
2026-07-09 11:11:03 -04:00
0d01affb89 Refactor cancellation cause handling and localization for orphan bibs, update version code to 4.72.773
This commit simplifies the cancellation cause logic by removing references to 'modif' and updating related localization keys to focus on 'annule' and 'transfert'. CSS comments and SQL scripts are also adjusted to reflect these changes, enhancing clarity and consistency in the user interface. The version code is incremented to 4.72.773 to reflect these updates.
2026-07-09 10:47:53 -04:00
ee7e463d36 Enhance orphan bib handling and localization, update version code to 4.72.772
This commit introduces new localization keys for orphan bib statuses, improving user feedback on cancellation and transfer scenarios. The logic for identifying orphan bibs is refined, ensuring accurate classification based on cancellation reasons. Additionally, a new function is added to standardize the cause key for orphan bibs. The version code is incremented to 4.72.772 to reflect these updates.
2026-07-09 10:24:10 -04:00
ea52e72086 Update anomaly navigation badge functionality and localization, increment version code to 4.72.771
This commit enhances the anomaly navigation badge by updating the label format to include a count of items to review. The JavaScript and PHP files are modified to support this new format, improving user feedback on the number of anomalies. Additionally, new localization keys are added for the formatted label in both French and English. The version code is incremented to 4.72.771 to reflect these updates.
2026-07-08 15:04:30 -04:00
6dd1ca3aad Enhance bib tool styles and functionality, increment version code to 4.72.770
This commit introduces new CSS styles for the bib tool buttons and detail panel, improving their visual presentation and interactivity. It also adds functionality for displaying anomaly navigation badges, enhancing user experience. The version code is incremented to 4.72.770 to reflect these updates.
2026-07-08 14:55:53 -04:00
885dcf9b8f Update CSS styles for bib tool buttons and detail panel, increment version code to 4.72.767
This commit enhances the visual presentation of the bib tool buttons and detail panel by introducing new styles for various button states and backgrounds. The CSS updates improve usability and aesthetics, particularly for the help buttons and detail sections. The version code is incremented to 4.72.767 to reflect these changes.
2026-07-08 14:48:45 -04:00
545cbe5dd4 Enhance bib tool navigation and detail functionality, update version code to 4.72.766
This commit refines the bib tool navigation layout by implementing a grid-based structure and adding a detail panel for tool descriptions. New CSS styles improve the visual presentation and responsiveness of the tool buttons. Additionally, JavaScript functions are introduced to manage tool detail titles and keyboard interactions, enhancing user experience. The version code is incremented to 4.72.766 to reflect these updates.
2026-07-08 14:46:25 -04:00
74700fcc19 Implement new bib tool navigation layout and detail panel (MSIN-4440), update version code to 4.72.765
This commit introduces a redesigned navigation layout for the bib management tools, featuring a grid-based button arrangement and a unified detail panel for tool content. New CSS styles are added for improved visual presentation and responsiveness. Additionally, the version code is incremented to 4.72.765 to reflect these updates.
2026-07-08 14:41:17 -04:00
1b76f0fece Refactor bib tool layout and remove unused SQL file, update version code to 4.72.764
This commit streamlines the bib management interface by removing unnecessary CSS styles and PHP functions related to the previous layout. The SQL file for visual zones is deleted as it is no longer needed. The version code is incremented to 4.72.764 to reflect these updates.
2026-07-08 12:21:34 -04:00
f47f472aaa Refactor bib tool layout for improved UI and update version code to 4.72.763
This commit enhances the bib management interface by restructuring the layout of tool sections into a more compact and organized format. New CSS styles are introduced for better spacing and alignment, and a new function is added to render tool group rails. The version code is incremented to 4.72.763 to reflect these updates.
2026-07-08 12:19:41 -04:00
bfac5a92cc Enhance bib management UI with new sections and update version code to 4.72.762
This commit introduces several new sections in the bib management interface, including configuration, monitoring, reports, actions, and races, each with updated titles and structured layouts. CSS styles are adjusted for better spacing and presentation. Additionally, new language keys are added for improved localization. The version code is incremented to 4.72.762 to reflect these updates.
2026-07-08 12:13:56 -04:00
23ca040ac0 Enhance participant data handling in PDF distribution and update version code to 4.72.761
This commit adds the original participant ID to the selection for better data integrity and introduces a new function to generate a key for mapping participant responses. The SQL queries are updated to reflect these changes, ensuring compatibility with legacy data structures. Additionally, the version code is incremented to 4.72.761 to reflect these updates.
2026-07-08 12:07:54 -04:00
21b28a0d06 Implement PDF distribution panel for bib management (MSIN-4433)
This commit introduces a new feature for generating and managing PDF distributions of bibs. It adds a new AJAX action to handle requests for the distribution panel, along with corresponding JavaScript functions to refresh and initialize the panel. CSS styles are updated to enhance the layout of the distribution panel, and new PHP functions are added to support the generation and metadata handling for the PDF prints. The version code is incremented to 4.72.759 to reflect these changes.
2026-07-08 12:00:58 -04:00
76533287dd Implement contextual return functionality for bib management and enhance UI interactions
This commit introduces several enhancements related to the bib management system, specifically focusing on contextual return paths from various panels. New functions are added to encode and decode return URLs, allowing users to navigate back to specific sections (e.g., anomalies, quantity summary) seamlessly. Additionally, JavaScript functions are updated to manage UI interactions, such as expanding panels based on URL parameters. The overall user experience is improved by ensuring that the correct panels are focused and displayed upon return. This change is part of the ongoing efforts to enhance the bib assignment management system (MSIN-4436).
2026-07-08 11:34:31 -04:00
dbffb288d6 Add bib anomaly registration link and update styles for anomaly details
This commit introduces a new feature to display a registration link for bib anomalies, enhancing user navigation. It includes new CSS styles for better layout and presentation of anomaly details. Additionally, the PHP functions are updated to support the rendering of the registration link, improving the overall functionality of the bib anomaly management system.
2026-07-08 11:26:19 -04:00
17611a9009 Update team summary display in bib event management and increment version code to 4.72.758
This commit modifies the team summary feature to correctly display the unit count for teams by changing the translation parameter. Additionally, the version code is updated to reflect this change.
2026-07-08 11:18:19 -04:00
90c126a014 Update team summary feature in bib event management and increment version code to 4.72.757
This commit enhances the team summary display by adding support for multiple teams in the CSS and PHP functions. The previous team stack styles are removed in favor of a more streamlined approach. The version code is updated to reflect these changes.
2026-07-08 11:13:57 -04:00
cfffe77c93 Implement team summary feature in bib event management and update version code to 4.72.756
This commit introduces a new feature to display the number of active teams for team-based events. It includes CSS styles for the team summary layout and updates to the PHP functions to retrieve and render team data. The version code is incremented to reflect these changes.
2026-07-08 11:11:47 -04:00
588d573703 Refactor batch sorting functionality and update version code to 4.72.755
This commit enhances the batch sorting mechanism by introducing new functions to normalize and manage sorting keys for bib assignments. The AJAX handlers are updated to utilize these new sorting parameters, improving the overall flexibility and accuracy of batch operations. Additionally, the version code is incremented to reflect these changes.
2026-07-08 11:05:00 -04:00
7d342216f8 Enhance bib event configuration with inter-epreuve option and update version code to 4.72.754
This commit introduces a new feature allowing the same bib numbers across different epreuves, enhancing event management flexibility. It includes updates to the CSS for improved layout, JavaScript functions to manage visibility of configuration notes, and new database entries for the inter-epreuve setting. The version code is incremented to reflect these changes.
2026-07-08 10:32:03 -04:00
5184544d6f Implement quantity summary panel and global configuration settings for bib assignments (MSIN-4424)
This commit introduces a new AJAX action for retrieving a summary of quantities related to bib assignments, enhancing event management capabilities. A collapsible configuration panel is added for global settings, allowing users to manage bib assignment options more effectively. The CSS is updated to style the new elements, and relevant database entries are created to support the new features. The version code is incremented to 4.72.751 to reflect these changes.
2026-07-08 10:26:52 -04:00
70f4dac177 MSIN-4424 Implement inter-epreuve duplicate handling and update version code to 4.72.750
This commit introduces functionality to allow the same bib numbers across different epreuves, enhancing the flexibility of event management. A new AJAX action, `save_allow_inter_epr`, is added to manage this setting, along with corresponding JavaScript to handle user interactions. The database functions are updated to support this feature, ensuring proper validation and anomaly reporting. The version code is incremented to reflect these changes.
2026-07-08 10:17:15 -04:00
a7e027177c Enhance bib range handling and update version code to 4.72.749
This commit introduces a new parameter to the `fxInfosBibRange` function to conditionally include gaps in the statistics calculation, improving data accuracy. Additionally, the AJAX handler is updated to support a 'lite' mode for optimized data retrieval. JavaScript functions are modified to pass this new option, enhancing the overall functionality of the bib assignments. The version code is incremented to reflect these changes.
2026-07-08 10:04:02 -04:00
c2972aa015 Implement reset all apply functionality for epreuve and update version code to 4.72.747
This commit introduces a new action, `reset_all_apply`, in the AJAX handler to reset all bib numbers for participants in a specified epreuve. It includes validation for locked ranges and updates the database accordingly. Additionally, new CSS styles are added for the reset button, and JavaScript functions are implemented to manage the button's state based on participant conditions. The version code is incremented to reflect these changes.
2026-07-08 09:57:49 -04:00
4cf2ef98d0 Enhance fxPromoteurHubGetEventLinks function to conditionally load access management and update version code to 4.72.746
This commit modifies the `fxPromoteurHubGetEventLinks` function to include a conditional check for the existence of the `fxInscrGestionCanAccess` function. If it does not exist, the `inc_fx_inscriptions_gestion.php` file is loaded to evaluate access rights. This change improves the modularity and functionality of the promoteur hub by ensuring that access management is properly handled in AJAX contexts.
2026-07-08 09:50:09 -04:00
1322511cfd Refactor identity header styles and update version code to 4.72.745
This commit refines the CSS for the identity header, adjusting alignment, padding, and gap properties for better layout and user interaction. The toolbar is restructured to enhance the visual hierarchy, and the version code is incremented to reflect these updates.
2026-07-08 09:46:44 -04:00
ed3ca4e6ea Refactor CSS styles for identity panel and update version code to 4.72.744
This commit modifies the CSS for the identity panel, changing alignment properties and adjusting gaps for improved layout. Additionally, a new toolbar is introduced for the identity header, enhancing user interaction with a button for viewing ranges. The version code is incremented to reflect these updates.
2026-07-08 09:42:33 -04:00
75f17f5846 Enhance fxInfosBibRange function to include gap handling and update version code to 4.72.743
This commit modifies the `fxInfosBibRange` function to add a new parameter for including gaps in the statistics calculation. The logic for generating SQL queries is updated to conditionally include or exclude gaps based on this parameter, improving the accuracy of the statistics retrieved. Additionally, the version code is incremented to reflect these changes.
2026-07-08 09:35:38 -04:00
f13a5d3c01 Implement global batch panel functionality with deferred loading and increment version code to 4.72.742
This commit introduces a new action for the global batch panel in the AJAX handler, allowing for deferred loading of the panel upon user interaction. The JavaScript is updated to manage the loading state and ensure the panel is populated with relevant data when opened. Additionally, a new rendering function is added to handle the display of the global batch panel shell, improving user experience by ensuring that the panel only loads when necessary. The version code is incremented to reflect these enhancements.
2026-07-08 09:31:03 -04:00
6d3fd04232 Implement preloader enhancements for quantity management and increment version code to 4.72.741
This commit introduces a loading overlay for heavy page loads in the quantity management section, improving user experience during data retrieval. Additionally, a loader is displayed upon clicking specific links to enhance feedback while waiting for server responses. Minor adjustments are made to the JavaScript handling of anomalies, ensuring smoother interactions. The version code is incremented to reflect these updates.
2026-07-08 09:27:01 -04:00
47a2b051ba IMSIN-4425mplement quantity management enhancements in bib assignments and increment version code to 4.72.740
This commit introduces new JavaScript functions for managing quantity data in the bib assignments, including `bibApplyQteData` for updating UI elements and `bibSaveQte` for handling quantity submissions via AJAX. Additionally, the PHP file is updated to render a new panel for anomalies, improving the overall functionality and user experience. The version code is incremented to reflect these changes, ensuring proper versioning in the application.
2026-07-08 09:25:44 -04:00
40625572e6 Refactor range locking function and increment version code to 4.72.739
This commit updates the function name from `fxBibIsRangeLocked` to `fxBibRangeIsLocked` for improved clarity and consistency in the codebase. Additionally, the version code is incremented to reflect this change, ensuring proper versioning in the application.
2026-07-08 09:15:21 -04:00
fde81a3985 Update localized messages in fxBibStaticFallback and increment version code to 4.72.738
This commit modifies the French translation for the global batch assignment message in the `fxBibStaticFallback` function to ensure proper formatting. Additionally, the version code is updated to reflect these changes, maintaining consistency in the application versioning.
2026-07-08 09:11:31 -04:00
da8fc64458 MSIN-2365 Implement global batch assignment enhancements with improved error handling and UI feedback
This commit introduces several updates to the global batch assignment functionality, including new CSS styles for unavailable states, enhanced JavaScript functions for batch processing, and improved error handling in AJAX requests. Language support is expanded with new localized messages for user guidance. The version code is incremented to reflect these changes, aiming to enhance user experience and maintainability in the bib management system.
2026-07-08 09:09:25 -04:00
d535f3d9c5 Enhance AJAX promoteur hub functionality with improved language handling and text retrieval
This commit updates the `ajax_promoteur_hub.php` and `inc_fx_promoteur_hub.php` files to improve language handling and text retrieval for the promoteur hub. The language is now determined based on session data, with a fallback to French if the specified language is invalid. New functions are introduced to manage text retrieval from the database, ensuring that legacy entries from `ajax_promoteur_hub.php` are handled correctly. Additionally, the text retrieval logic is streamlined to enhance maintainability and user experience across the promoteur hub.
2026-07-08 09:02:43 -04:00
43750fb8f5 MSIN-2365 Implement batch global analysis and simulation for solo events
This commit introduces two new actions, `batch_global_analysis` and `batch_global_simulate`, to handle batch processing for solo events in the bib management system. The PHP file `ajax_bib_range.php` is updated to include logic for analyzing and simulating batch assignments, enhancing user feedback with detailed statistics. Additionally, new CSS styles are added to improve the presentation of the global batch interface, and JavaScript functions are implemented to facilitate the collection and submission of batch plans. These changes aim to streamline the batch processing workflow and improve user experience in managing solo event assignments.
2026-07-08 08:59:41 -04:00
4522ddde8d Enhance promoteur hub functionality with AJAX loading and access control improvements
This commit introduces significant updates to the promoteur hub, including the addition of a new JavaScript file for AJAX handling of event statistics and links. The PHP file `ajax_promoteur_hub.php` is modified to support new actions for fetching event statistics and links, improving the user experience with better error handling and loading indicators. Additionally, a new batch access control function is implemented in `inc_fx_promoteur_hub.php` to streamline permission checks for events. The version code is incremented to reflect these enhancements, aiming to improve functionality and maintainability across the promoteur hub.
2026-07-08 08:48:23 -04:00
dd4a83043a 4436 Add payment session handling and mode checks in inc_fonctions.php and inc_fx_panier_js.php
This commit introduces new functions for managing payment session data, including `fxPaiementSessionComId`, `fxPaiementCanUseModesPromoteur`, and `fxPaiementCanUseModesAdmin`. These functions enhance the payment processing logic by determining the appropriate payment modes based on user roles (promoteur or admin). The existing payment display functions in `inc_fx_panier_js.php` are updated to utilize these new functions, improving the clarity and maintainability of the payment handling process. These changes aim to enhance user experience and ensure proper access control for payment options.
2026-07-07 16:09:11 -04:00
96765aac84 MSIN-4435 Add inc_v2_assets.php integration across multiple files
This commit integrates the `inc_v2_assets.php` file into several PHP scripts, including `compte.php`, `inc_footer.php`, `inc_tableau_gestion_epreuves.php`, `inc_tableau_inscriptions_gestion.php`, and `mobile.php`. Additionally, it registers new scripts for improved functionality in the `inscriptions gestion` context. The version code is incremented to reflect these updates, aiming to enhance asset management and script handling across the application.
2026-07-07 15:27:33 -04:00
b3e3c72be5 Implement batch processing enhancements for bib management system
This commit introduces a new action, `batch_apply_chunk`, to facilitate chunked processing of batch assignments in the bib management system. It enhances the AJAX handling for batch operations, allowing for better progress tracking and user feedback during the assignment process. Additionally, new JavaScript functions are added to manage batch progress display, and CSS styles are updated to support the new loading indicators. The version code is incremented to reflect these changes, aiming to improve user experience and efficiency in batch processing.
2026-07-07 15:16:23 -04:00
d90df7c46f Refactor bib range validation and error handling in management system
This commit enhances the JavaScript functions for bib range validation by introducing new error handling mechanisms and improving user feedback. The previous preview functionality has been removed, and error messages are now displayed more effectively. CSS styles are updated to support the new error message display. Additionally, the version code is incremented to reflect these changes, aiming to improve user experience and data integrity in the bib input process.
2026-07-07 15:04:27 -04:00
20c1f898a1 Enhance bib range validation and user feedback in management system
This commit introduces new JavaScript functions for validating range inputs for quantity and end values in the bib management system. It adds visual feedback for invalid fields and updates the preview display based on user input. CSS styles are also added to highlight invalid fields and error messages. Additionally, new SQL entries are created for localized validation messages, improving user guidance. The version code is incremented to reflect these enhancements, aiming to improve user experience and data integrity in the bib input process.
2026-07-07 15:01:55 -04:00
666c4ad180 Update CSS and PHP for bib management enhancements
This commit introduces several updates to the CSS and PHP files related to the bib management system. Key changes include the addition of overflow handling and layout adjustments in the CSS to improve the display of bib containers and lines. The PHP function for rendering the range lock button is modified to include a new wrapper div for better structure. Additionally, the version code is incremented to reflect these updates. These changes aim to enhance the user interface and maintainability of the bib management features.
2026-07-07 14:55:25 -04:00
eca8b34d88 Update bib management for MSIN-4429: Refine quantity and end input handling
This commit enhances the bib management system by updating the CSS layout for quantity and end inputs, ensuring proper alignment and display. It also modifies PHP functions to introduce a new column for the "or" separator between quantity and end values, improving the user interface. Additionally, new SQL entries are added to support the updated functionality, including localized labels and hints for user guidance. These changes aim to improve usability and clarity in the bib input process.
2026-07-07 14:53:02 -04:00
ec024e6fba MSIN-4429 Implement range input functionality for quantity and end values in bib management
This commit introduces new JavaScript functions and PHP methods to enhance the handling of range inputs for quantity and end values in the bib management system. It includes the addition of input fields for quantity and end values, along with logic to manage their states based on user interaction. The CSS is updated to support the new layout and visual states for these fields. Additionally, data attributes are added to improve the handling of range validation and preview functionality. These changes aim to improve user experience and data integrity in the bib input process.
2026-07-07 14:49:57 -04:00
f35d651ffc MSIN-4432
Update participant statistics queries in fxInfosBibRange and fxInfosBibEpreuve functions

This commit modifies the SQL queries in the `fxInfosBibRange` and `fxInfosBibEpreuve` functions to improve the accuracy of participant statistics. The `nb_utilises` count now reflects distinct bib numbers, ensuring correct tallying for teams sharing bibs. Additionally, the logic for fetching event statistics is refined to handle team configurations more effectively, enhancing the overall data retrieval process. These changes aim to improve data integrity and clarity in participant reporting.
2026-07-07 14:40:21 -04:00
2aed8052d0 Enhance loading indicators and AJAX handling in promoteur hub
This commit updates the loading mechanism for event statistics in the promoteur hub by introducing a new inline loader with an image and message. It also improves the AJAX loading process by adding a wait message and ensuring the loader is displayed during data fetching. Additionally, the error handling for AJAX requests is refined to hide the loader upon completion, enhancing user experience and clarity in the loading process.
2026-07-07 14:33:57 -04:00
2570785030 Refactor account tracing and session handling in compte.php and related files
This commit removes the `fxTraceCompte` functionality from various locations in `compte.php`, `inc_fx_compte.php`, and other related files to streamline the account management flow. The tracing logic is eliminated to enhance performance and reduce complexity. Additionally, the session handling for login redirection is improved by simplifying the logic and removing unnecessary function calls. These changes aim to improve maintainability and clarity in the account management system.
2026-07-07 14:30:53 -04:00
c7482016ce Implement superadmin login redirection and session handling improvements
This commit introduces a new function, `fxSuperadmRedirectToLoginIfNeeded`, to streamline the login redirection process for superadmin users. It enhances session management by checking for user authentication and allowing legacy account access under specific conditions. Additionally, the login flow is updated to ensure users are redirected appropriately after login, improving the overall security and user experience in the superadmin interface. These changes aim to enhance maintainability and clarity in session handling across various superadmin pages.
2026-07-07 14:15:48 -04:00
20ab5a8f08 Add event stats rendering and AJAX loading for promoteur hub 2026-07-07 13:48:59 -04:00
29c411a23c Implement enhanced tracing functionality in inc_fx_trace_compte.php and integrate bootstrap call in inc_start_time.php
This commit introduces new functions for logging and host validation in the account tracing system, improving the ability to track user actions in development and pre-production environments. Additionally, it adds a bootstrap function call in inc_start_time.php to initialize tracing early in the request lifecycle. These changes aim to enhance debugging capabilities and ensure reliable logging across different environments.
2026-07-07 13:32:25 -04:00
eeb08bc563 Enhance account tracing functionality and error handling in inc_fx_trace_compte.php
This commit updates the account tracing logic by adding a check for both development and pre-production environments to determine if tracing is enabled. Additionally, it introduces a fallback error logging mechanism to capture log entries when the log file is not writable, improving the robustness of the tracing functionality. These changes aim to enhance debugging capabilities and ensure reliable logging in various environments.
2026-07-07 13:28:10 -04:00
316583d108 Enhance account tracing and redirect logic in compte.php and inc_fx_compte.php
This commit introduces comprehensive tracing functionality to the account management flow by adding calls to `fxTraceCompte` and `fxTraceCompteRedirect` at key points in the login and routing processes. The changes improve the tracking of user actions and session states, facilitating better debugging and monitoring of account-related activities. Additionally, the promoteur hub event retrieval function is updated to include tracing, enhancing visibility into hub interactions. These updates aim to improve maintainability and user experience in the account management system.
2026-07-07 13:24:49 -04:00
fb278bfdd6 Refactor session handling and redirect logic for login flow
This commit enhances the session management and redirect behavior in the login process. It introduces a new function, `fxCompteRedirectAfterLoginConsume`, to streamline the handling of post-login redirects while preventing loops for login-related pages. Additionally, comments are updated for clarity regarding the session's role in protecting access to certain pages. These changes aim to improve user experience and maintainability of the authentication flow.
2026-07-07 10:57:03 -04:00
138448212e Refactor promoteur hub access logic and update comments for clarity
This commit simplifies the `fxPromoteurHubShouldUse` function by directly returning the result of `fxPromoteurHubIsAvailable`, enhancing code readability. Additionally, comments in `compte.php` are updated to clarify the landing behavior for the promoteur hub and participant home, improving understanding of the session handling logic. These changes aim to streamline the promoteur hub functionality and improve maintainability.
2026-07-07 10:50:57 -04:00
762ac838b6 Refactor promoteur hub logic and enhance session handling
This commit updates the promoteur hub functionality by introducing a new function, `fxPromoteurHubIsAvailable`, to streamline the logic for determining hub access. The existing `fxPromoteurHubShouldUse` function is modified to incorporate this new check, improving clarity and maintainability. Additionally, comments are added to clarify session handling in the `fxLoginCompte` function, ensuring better understanding of public login behavior. These changes aim to enhance the robustness of the promoteur hub interface and improve code readability.
2026-07-07 10:45:03 -04:00
93cb9f5e81 Refactor authentication logic in superadmin interface and streamline session handling
This commit updates the authentication process in `index.php` and `rapport_new.php` by introducing new functions for checking user permissions, specifically `fxSuperadmIsLoggedIn` and `fxSuperadmReportsAllowed`. The session handling is simplified, removing redundant checks and enhancing clarity. Additionally, the memory limit setting in `rapport_new.php` is adjusted for consistency. These changes aim to improve the security and maintainability of the superadmin interface.
2026-07-06 19:47:51 -04:00
4c9725b282 Enhance error handling and user feedback in static synchronization process
This commit updates the connection timeout for MySQL operations and introduces new functions for managing connection errors and flash messages. The `fxStaticSyncConnectError` and `fxStaticSyncFormatConnectionError` functions provide clearer error reporting during synchronization. Additionally, the user interface is improved to display relevant flash messages, enhancing the overall user experience and robustness of the synchronization workflow.
2026-07-06 15:38:32 -04:00
a8274dd640 Update key column references and enhance error handling in static synchronization functions
This commit modifies key column references in the `fxStaticSyncTableDefinitions` function to align with updated database schema identifiers. Additionally, it introduces a new utility function, `fxStaticSyncKeyColumnsValid`, to validate key columns before executing queries, improving error handling in the `fxStaticSyncFetchRowsByKey` and `fxStaticSyncLoadKeyedRows` functions. The changes aim to enhance the robustness of the synchronization process and provide clearer error feedback during data operations.
2026-07-06 15:34:20 -04:00
2792b06d79 Enhance static synchronization functions with new utilities and improved error handling
This commit introduces several new functions to enhance the static synchronization process, including `fxStaticSyncNormalizeCellValue`, `fxStaticSyncSqlOrderByKeys`, and `fxStaticSyncGetTableRowCount`. These additions improve data handling and error reporting during synchronization. The `fxStaticSyncLoadKeyedRows` function is updated to provide more detailed feedback on physical and duplicate row counts, while the user interface is enhanced to display duplicate key counts in the source data. Overall, these changes aim to improve the robustness and clarity of the database synchronization workflow.
2026-07-06 15:29:44 -04:00
3c0188fba4 Remove database confirmation requirement for client environments in synchronization process
This commit eliminates the need for database name confirmation when syncing to client environments, simplifying the user experience. The `fxStaticSyncRequiresDbConfirm` function is removed, and related logic is adjusted in the synchronization workflow. Additionally, the user interface is updated to reflect these changes, providing clearer messaging about the synchronization process and reinforcing the read-only nature of the client production environment.
2026-07-06 15:24:07 -04:00
5f65e1a339 Refactor data synchronization functions for improved clarity and status reporting
This commit renames and refines several functions related to data synchronization, enhancing the clarity of status reporting. The `fxStaticSyncResolveDataStatus` function is replaced with `fxStaticSyncFinalizeCompareResult`, which consolidates status determination logic. Additionally, user feedback is improved in the synchronization process, with clearer messages regarding data discrepancies and alignment. These changes aim to streamline the synchronization workflow and enhance overall user experience.
2026-07-06 15:17:04 -04:00
709044efef Add enhanced data synchronization functions and improve user feedback
This commit introduces new functions for confirming database names during synchronization, enhancing safety when syncing to production environments. The `fxStaticSyncResolveDataStatus` and `fxStaticSyncFormatDataDiffSummary` functions are added to provide clearer status reporting and summaries of data discrepancies. Additionally, the synchronization process is refined to improve user feedback, including clearer messages regarding data alignment and discrepancies. These changes aim to enhance the robustness and clarity of the database synchronization workflow.
2026-07-06 14:58:59 -04:00
bac8ed2be0 Enhance database synchronization functions and improve schema validation
This commit introduces new functions for resolving and comparing database schema columns, allowing for better handling of structural discrepancies during synchronization. The `fxStaticSyncCopyTable` function is updated to check for schema alignment before proceeding with data copying, providing clearer error messages when structures do not match. Additionally, the synchronization process is refined to allow for read-only operations towards production environments, enhancing safety and user feedback. Overall, these changes aim to improve the robustness and clarity of the database synchronization workflow.
2026-07-06 14:33:05 -04:00
7c7bea3666 Add database synchronization utility functions and enhance error handling
This commit introduces several utility functions for database synchronization, including error handling for MySQL operations and retrieval of table metadata. The `fxStaticSyncDbLastError`, `fxStaticSyncExecQuery`, `fxStaticSyncGetTableColumnNames`, and `fxStaticSyncGetTableEngine` functions are added to improve the robustness of the synchronization process. Additionally, the `fxStaticSyncCopyTable` function is updated to handle column mismatches more gracefully and to provide clearer feedback on the synchronization results, enhancing overall user experience and error reporting in the database synchronization workflow.
2026-07-06 14:23:27 -04:00
670fd20e44 Implement sidebar static synchronization logic and update sync function documentation
This commit introduces a new variable for sidebar static synchronization based on the HTTP_HOST, enhancing the conditions under which static database synchronization is allowed. Additionally, the documentation for the sync function is updated to reflect alignment with the new logic, improving clarity on its operational context. These changes aim to streamline the synchronization process and enhance the overall functionality of the superadmin interface.
2026-07-06 14:11:06 -04:00
338f5ed6f6 Refine ChronoTrack API configuration and enhance error handling
This commit updates the ChronoTrack API configuration by introducing a variable for the base URL, allowing for easier management of test and production environments. It also improves the admin interface to display the API endpoint status, indicating whether the test or production API is in use. Additionally, error handling in the OAuth token fetching process is enhanced to provide clearer messages when access tokens are absent or when errors occur, improving overall user feedback and robustness of the API.
2026-07-03 20:58:43 -04:00
53d406ae74 Enhance ChronoTrack API configuration and improve base URL handling
This commit refines the configuration of the ChronoTrack API by introducing a variable for the base URL, allowing for easier adjustments. It also adds a conditional definition for the token key based on the environment, ensuring appropriate settings for both test and production environments. Additionally, the admin interface is updated to display the API endpoint when configured, enhancing user feedback and clarity. These changes aim to improve the overall configuration management and user experience within the API.
2026-07-03 20:56:28 -04:00
29f836e615 Implement orphan management functionality in ChronoTrack API
This commit introduces new features for handling orphan entries within the ChronoTrack API. It adds a new endpoint for syncing orphan entries, enhancing the ability to identify and manage entries that are present in the system but not eligible for transfer. The user interface is updated to include a dedicated section for displaying orphan entries, improving clarity and user interaction. Additionally, the synchronization logic is refined to compute and report orphan entries effectively, aiming to enhance overall data management and user experience in the API.
2026-07-03 20:49:19 -04:00
38b7e9b72c Enhance ChronoTrack API functionality with new PUT methods and payload management
This commit introduces new functions for handling PUT requests within the ChronoTrack API, improving the management of entry updates. It includes the implementation of payload processing functions for both POST and PUT requests, ensuring that entry IDs are appropriately excluded from the payloads. Additionally, the synchronization logic is refined to differentiate between creating and updating entries, enhancing the overall clarity and efficiency of the API. These changes aim to streamline entry management and improve user experience in the API.
2026-07-03 20:41:59 -04:00
f2519dc987 Enhance ChronoTrack API push functionality and improve user feedback
This commit updates the ChronoTrack API push process by refining the user interface messaging for manual pushes, clarifying the status of participants, and improving the confirmation prompts. Key changes include the introduction of new variables for tracking eligible participants and orphans, as well as enhanced HTML output for better clarity on the current state of entries. These improvements aim to streamline the push process and enhance the overall user experience in managing participant data.
2026-07-03 20:35:43 -04:00
8c7f3e13ed Refactor superadmin UI components and enhance ChronoTrack API integration
This commit introduces a new UI structure for the superadmin interface by implementing the fxSuperadmV2Panel functions, which improve the organization and presentation of event access and ChronoTrack API sections. The changes include the addition of collapsible panels for better user interaction and visual clarity. Additionally, the versioning in the settings file is updated to reflect the latest changes. These enhancements aim to streamline the user experience and improve the overall functionality of the superadmin interface.
2026-07-03 20:27:30 -04:00
0343a8e4a7 Refactor session management and enhance user feedback in superadmin interface
This commit improves session management within the superadmin interface by introducing a new session validation function that ensures active sessions are maintained and provides JSON responses for session expiration. Additionally, the session timeout duration is standardized, and user feedback is enhanced with alerts for expired sessions. The codebase is streamlined by removing deprecated session handling logic, improving overall clarity and maintainability. These changes aim to enhance user experience and ensure robust session handling across the application.
2026-07-03 20:06:59 -04:00
57912b60ae Refactor ChronoTrack API entry management and remove deprecated functions
This commit streamlines the entry management process within the ChronoTrack API by removing deprecated functions related to entry withdrawal and deletion. The admin interface is updated to eliminate unused actions, enhancing clarity and usability. Additionally, comments are refined to improve code readability. These changes aim to simplify the codebase and improve the overall user experience in managing entries.
2026-07-03 12:47:49 -04:00
2370a2b344 Implement withdraw functionality and enhance entry management in ChronoTrack API
This commit introduces new functions for managing entry withdrawal within the ChronoTrack API, including the ability to probe and withdraw entries based on their status. The admin interface is updated to include buttons for testing withdrawal actions, providing clearer user feedback. Additionally, the synchronization logic is enhanced to summarize entry statuses and handle batch withdrawals more effectively. These changes aim to improve the overall management of entries and enhance user experience in the API.
2026-07-03 12:41:30 -04:00
f065eb5cb6 Enhance ChronoTrack API entry deletion and session management
This commit improves the entry deletion process within the ChronoTrack API by introducing a new function to handle deletion attempts with context requirements. It also enhances session management by implementing a session validation function that provides JSON responses for session expiration, improving user feedback during AJAX calls. Additionally, the synchronization logic is updated to utilize the new deletion function, ensuring better error handling and reporting. These changes aim to streamline entry management and enhance the overall user experience within the API.
2026-07-03 12:28:38 -04:00
3a0161a0bb Update ChronoTrack API entry wipe functionality and enhance user feedback
This commit modifies the entry wipe functionality in the ChronoTrack API admin interface. The wipe button is now disabled when there are no entries to delete, and the confirmation prompt has been updated to clarify the action's scope. Additionally, the API response for the wipe action now returns a warning state when no entries are found, providing clearer messaging about the visibility of entries. These changes aim to improve user experience and ensure accurate communication regarding entry management.
2026-07-03 12:16:48 -04:00
88f0e8138e Enhance ChronoTrack API entry synchronization and reporting
This commit introduces improvements to the entry synchronization process within the ChronoTrack API. Key changes include the renaming of functions for clarity, the implementation of pagination for fetching entries, and enhanced reporting of entry counts before and after synchronization actions. Additionally, the admin interface is updated to provide clearer feedback regarding the state of entries, including details on entries found before a wipe and remaining entries after synchronization. These enhancements aim to improve the overall user experience and data management capabilities of the API.
2026-07-03 12:13:02 -04:00
7071b3792a Implement ChronoTrack Live entry wipe functionality and enhance synchronization logic
This commit introduces a new feature to wipe all entries from ChronoTrack Live for a specific event, including user confirmation prompts and error handling. Additionally, it enhances the participant synchronization process by refining the handling of entry data and improving the reporting of synchronization results. The admin interface is updated to include a new button for the wipe action, providing clearer user feedback and control over event data management. These changes aim to streamline the management of ChronoTrack entries and improve the overall user experience within the API.
2026-07-03 10:33:11 -04:00
cef2e672cb Enhance participant synchronization and team handling in ChronoTrack API
This commit introduces new functions to better manage team-related data during participant synchronization. Key updates include the addition of team identification and role labeling, as well as improved handling of blocked team registrations. The participant summary now includes team-specific fields, enhancing the clarity of participant data. Additionally, the admin interface is updated to provide clearer messaging regarding blocked team registrations, improving user feedback during synchronization processes. These changes aim to streamline participant management and enhance the overall user experience within the ChronoTrack API.
2026-07-03 10:04:11 -04:00
ec1e98d898 Enhance participant synchronization and error handling in ChronoTrack API
This commit introduces a new function to classify blocked participants by type, improving the handling of synchronization errors related to missing or invalid data. The changes include updates to the participant classification logic, allowing for better reporting of issues such as duplicate bibs, missing external IDs, and unmapped races. Additionally, the push preview functionality is enhanced to display blocked participants clearly, improving user feedback during the synchronization process. These updates aim to streamline participant management and enhance the overall user experience within the ChronoTrack API.
2026-07-03 09:56:48 -04:00
338734db7a Enhance MS1 loader functionality and integrate loading indicators in ChronoTrack API
This commit updates the MS1 loader to include a message parameter for inline HTML usage, ensuring consistent messaging during AJAX operations. Additionally, new functions are introduced to render loading indicators and assets, improving user feedback during data synchronization processes. The integration of these features aims to enhance the overall user experience and clarity within the ChronoTrack API admin interface.
2026-07-03 09:46:32 -04:00
411f2a4d8f Add probe push functionality and enhance error handling in ChronoTrack API
This commit introduces a new 'sync_probe_push' action to the ChronoTrack API, allowing for the probing of participant data synchronization. It includes a new button in the admin interface for initiating probe pushes, along with improved error handling in the API response messages. Additionally, the batch size for synchronization is adjusted, and the entry payload building process is refined to include event IDs and registration choices, enhancing the overall synchronization capabilities and user experience.
2026-07-03 09:21:40 -04:00
9ea4dd2ecf Refactor ChronoTrack API event handling and synchronization logic
This commit updates the ChronoTrack API to improve event handling and synchronization features. Changes include enhanced messaging for closed events, adjustments to the push panel functionality, and refined status handling for event synchronization. The logic for managing event states is streamlined, allowing for clearer user interactions and better management of synchronization processes. These improvements aim to enhance the overall usability and effectiveness of the ChronoTrack API in event management.
2026-07-03 09:14:47 -04:00
903feebcdb Add synchronization functionality to ChronoTrack API
This commit introduces new synchronization features to the ChronoTrack API, including the ability to preview and push participant data to ChronoTrack. A new push panel is added to the admin interface, allowing users to view eligible participants and initiate synchronization. Additionally, backend functions are implemented to handle synchronization requests, enhancing the overall data management capabilities of the API. These changes aim to streamline the process of updating participant information within the ChronoTrack system.
2026-07-03 09:10:57 -04:00
2037b27c4c Enhance ChronoTrack API with entry handling and diagnostic improvements
This commit introduces several enhancements to the ChronoTrack API, focusing on entry handling and diagnostic capabilities. New functions are added to normalize and summarize entry data, improving the management of entry-related information. Additionally, the diagnostic functionality is updated to include entry endpoints, providing more comprehensive insights during API diagnostics. These changes aim to enhance the overall usability and effectiveness of the ChronoTrack API for event management.
2026-07-03 09:03:03 -04:00
beb84339e2 Add new table definitions for 'inscriptions_pays' and 'inscriptions_provinces' in static sync configuration
This commit introduces new entries for the 'inscriptions_pays' and 'inscriptions_provinces' tables in the fxStaticSyncTableDefinitions function. Each entry includes a label, keys, and columns to ignore, enhancing the static synchronization configuration for better data management.
2026-07-02 20:22:22 -04:00
df9f964f11 Refactor ChronoTrack API functions for improved race and registration choice handling
This commit enhances the ChronoTrack API by refining functions related to race and registration choice entities. Key updates include the introduction of new functions for collecting race and registration choice entities, improved normalization logic, and adjustments to existing functions for better clarity and functionality. Additionally, terminology is updated to ensure consistency across the API, enhancing overall usability and integration with the ChronoTrack system.
2026-07-02 19:51:35 -04:00
3aea111392 Add diagnostic functionality to ChronoTrack API for event management
This commit introduces a new diagnostic feature in the ChronoTrack API, allowing users to run diagnostics on events and retrieve detailed reports. The functionality includes a new diagnostic panel in the admin interface, which displays API diagnostic results, including OAuth status and endpoint responses. Additionally, the backend is updated to handle diagnostic requests, ensuring comprehensive error handling and reporting. This enhancement aims to improve troubleshooting and support for event management within the ChronoTrack system.
2026-07-02 19:46:57 -04:00
55b7ce7061 Enhance ChronoTrack API admin interface with registration choice support and improved race terminology
This commit updates the ChronoTrack API admin interface to incorporate support for registration choices, enhancing the functionality of race mapping. Terminology is refined throughout the interface, replacing "race" with "course" where appropriate to improve clarity. New functions are added to handle registration choices, ensuring a more comprehensive integration with the ChronoTrack API. Additionally, existing functions are modified to accommodate these changes, improving overall user experience and interaction.
2026-07-02 19:29:00 -04:00
f826a0e409 Implement event link picker and enhance race mapping display in ChronoTrack API admin interface
This commit introduces a new function to render an event link picker in the ChronoTrack API admin interface, allowing users to search and select events more efficiently. The event page rendering logic is updated to conditionally display the link picker based on event status. Additionally, the race mapping display is enhanced to show saved mappings more clearly, improving user interaction and overall experience when managing events.
2026-07-02 19:24:51 -04:00
00ee7484df Add race status display and reload functionality in ChronoTrack API admin interface
This commit enhances the ChronoTrack API admin interface by adding a status display for race loading and a button to reload races. The status updates provide user feedback during the loading process, indicating success or failure. Additionally, the race loading logic is improved to handle network errors gracefully, ensuring a better user experience when interacting with the API.
2026-07-02 19:20:23 -04:00
6728a541a8 Enhance documentation and clarify ChronoTrack API credentials usage in settings file
This commit adds comments in the `inc_settings.php` file to clarify the default values for ChronoTrack API credentials and their intended usage. It also notes that the credentials can be overridden by other switch blocks, improving the understanding of the configuration process for developers.
2026-07-02 19:11:28 -04:00
c3aed5e9fe Fix syntax error in ChronoTrack API credentials in settings file
This commit corrects a syntax error in the `inc_settings.php` file by adding a missing quotation mark to the `service_pass` entry. This change ensures proper parsing of the credentials, maintaining the integrity of the ChronoTrack API configuration.
2026-07-02 18:46:48 -04:00
2e691df76b Update ChronoTrack API credentials in settings file
This commit updates the ChronoTrack API configuration in the `inc_settings.php` file by adding the client ID, client secret, service user, and service password. These changes are essential for establishing a connection with the ChronoTrack API, ensuring proper functionality and access to its features.
2026-07-02 18:45:10 -04:00
89d389c76d Update version code to 4.72.726 and enhance superadmin sidebar layout
This commit increments the version code to 4.72.726 and introduces structural improvements to the superadmin sidebar. The HTML layout is refined with the addition of a dedicated body section for the V2 panel, while CSS adjustments enhance padding, margins, and overall styling for a more cohesive user experience. These changes aim to improve accessibility and visual organization within the sidebar.
2026-07-02 18:40:11 -04:00
7e3eb9522f Refactor superadmin sidebar layout and styling for improved user experience
This commit updates the superadmin sidebar by restructuring the HTML layout and enhancing the CSS styles. The sidebar now features a dedicated legacy section and a new V2 panel with improved visual hierarchy and organization. Key changes include updated padding, margins, and background styles to create a cohesive and user-friendly interface. Additionally, the sidebar navigation has been refined for better accessibility and interaction.
2026-07-02 18:37:40 -04:00
a94077874d Implement V2 sidebar panel for superadmin with enhanced access management
This commit introduces a new V2 sidebar panel in the superadmin interface, featuring improved layout and organization for access management tools. The panel includes sections for production synchronization and legacy access controls, with updated styling for better user experience. Additionally, the CSS is enhanced to support the new sidebar design, ensuring a cohesive look and feel across the application.
2026-07-02 18:35:54 -04:00
fee6f5c69d Refactor ChronoTrack API integration in event modification form
This commit updates the event modification form to conditionally display the ChronoTrack API section with a toggle button for better user interaction. The rendering function is adjusted to improve layout and heading levels, ensuring a consistent and user-friendly experience. Additionally, the inline rendering logic is refined to enhance the overall integration of ChronoTrack features.
2026-07-02 18:26:26 -04:00
2b4060770a Enhance ChronoTrack API event page rendering and form integration
This commit updates the `fxChronotrackApiAdminRenderEventPage` function to include an optional parameter for inline rendering, allowing for a more flexible display of the ChronoTrack API section. The event modification form is also adjusted to conditionally render the ChronoTrack API section based on the event ID, improving user experience and navigation. Additionally, the layout is refined to ensure proper heading levels based on the inline status.
2026-07-02 18:20:34 -04:00
da9feee83b Add ChronoTrack API section to event modification form
This commit introduces a new section in the event modification form for configuring the MSIN API ChronoTrack. It includes a card layout with a title, description, and a button linking to the ChronoTrack configuration page. Additionally, a button is added in the sidebar for quick access to the ChronoTrack section, enhancing user navigation and integration of ChronoTrack features within the application.
2026-07-02 18:17:02 -04:00
0906eb6548 Add database selection instructions for ChronoTrack SQL script
This commit updates the MSIN API ChronoTrack SQL script to include important instructions for selecting the appropriate database before execution. It adds comments to guide users in uncommenting the relevant `USE` statement based on their environment, ensuring proper execution and preventing errors. No changes to the table structure were made in this update.
2026-07-02 18:12:14 -04:00
121186a0b9 Update version code and integrate MSIN API ChronoTrack functionality
This commit updates the version code to 4.72.725 and introduces the `fxSettingsApplyChronotrackApi` function to manage ChronoTrack API credentials based on the environment. It also adds relevant UI elements in the sidebar and form sections for ChronoTrack configuration, enhancing the integration of ChronoTrack features within the application.
2026-07-02 18:10:51 -04:00
841fea5103 Add bib anomaly detection for unconfigured sequences
This commit introduces a new function, `fxBibSqlEpreuveHasBibRanges`, to check for the presence of bib ranges in events. It enhances the existing anomaly detection by adding support for cases where no bib ranges are configured, allowing for better reporting of duplicate bibs. Additionally, relevant language strings are added for both French and English to support multilingual reporting. The version code is incremented to reflect these changes.
2026-07-02 12:46:21 -04:00
81e55ea0ec Add out-of-range bib anomaly detection and reporting
This commit introduces a new function, `fxBibAnomalyOutOfRangeBibs`, to identify and report bibs assigned outside of configured sequences for events. It enhances the existing anomaly collection by including this new category and updates the rendering logic to display out-of-range anomalies in the user interface. Additionally, relevant language strings are added for multilingual support. The version code is incremented to reflect these changes.
2026-07-02 12:34:45 -04:00
3ee8ec05be Implement loader functionality for AJAX calls and enhance user feedback
This commit introduces a new loader mechanism for AJAX requests, improving user experience by providing visual feedback during data fetching processes. The `bibFetch` function is created to manage loader visibility, ensuring a consistent loading experience across various AJAX calls. Additionally, a new language string for the loading message is added to support multilingual feedback. The version code is incremented to reflect these changes.
2026-07-02 11:47:47 -04:00
a5ad7a0ec6 Refactor gestion fiche card layout and update rendering functions
This commit enhances the CSS styles for the gestion fiche card layout, transitioning from a rolodex to a grid format for improved organization and readability. The PHP rendering functions are updated to support this new layout, including logic for full-width value display. The version code is incremented to reflect these changes.
2026-07-02 10:27:57 -04:00
011aa65d19 Refactor gestion fiche styling and update PHP rendering logic
This commit refines the CSS styles for gestion fiche sections, introducing a new rolodex layout for improved visual presentation and mobile responsiveness. The PHP function for rendering key-value lists is updated to support the new layout. Additionally, the version code is incremented to reflect these changes.
2026-07-02 10:14:14 -04:00
6e386d95ee Enhance styling and functionality for gestion fiche sections
This commit introduces new CSS styles for the gestion fiche sections, improving the layout and visual presentation of details. It adds background colors and flexbox layouts for better readability on mobile devices. Additionally, the PHP file is updated to ensure specific sections are collapsed or expanded based on user interactions, enhancing the user experience. The version code is incremented to reflect these changes.
2026-07-02 10:09:37 -04:00
ef11a5a011 Refactor loader integration and enhance preloader UI
This commit refines the MS1 loader integration across scripts, ensuring a smoother user experience during loading processes. It updates the preloader UI to align with the new design, improving visual consistency. The version code is incremented to reflect these changes.
2026-07-02 09:26:13 -04:00
d7f3b0156b Implement MS1 loader functionality and update preloader UI
This commit introduces the MS1 loader functionality across various scripts, enhancing the user experience during loading processes. It integrates the loader into form submissions and AJAX calls, ensuring consistent feedback for users. Additionally, the preloader UI is updated to utilize the new loader design, improving visual consistency. The version code is incremented to reflect these changes.
2026-07-02 09:14:34 -04:00
5936cdb61c Add legacy promoteur functionality and migration enhancements
This commit introduces several enhancements related to the legacy promoteur system. It adds a new function to check if the legacy promoteur site is enabled and updates various functions to utilize this check, ensuring that legacy features are only accessible when appropriate. Additionally, it refines the migration process for legacy accounts, including improved handling of migration statistics and event access. The admin interface is updated to reflect these changes, enhancing the overall management of legacy promoteur accounts. The version code is updated to reflect these changes.
2026-07-01 14:14:25 -04:00
abb63c293f Enhance session management and access control for super admins
This commit introduces improvements to session management by implementing a new check for super admin sessions, allowing them to bypass certain access restrictions. It updates the logout function to retain super admin session data and modifies event access functions to grant permissions based on super admin status. Additionally, the session timeout duration is adjusted for better alignment with site requirements. The version code is updated to reflect these changes.
2026-07-01 13:48:43 -04:00
b1699cd10c Add legacy report functionality and update migration statistics display
This commit introduces a new function to generate a comprehensive report of legacy promoters, including details such as group, last connection, and migration date. The admin interface is updated to display migration statistics and legacy report data, enhancing the visibility of pending and completed migrations. The UI is refined for better user experience, ensuring clear presentation of account statuses. The version code is updated to reflect these changes.
2026-07-01 13:42:44 -04:00
048c4a398f Add access management enhancements and migration statistics retrieval
This commit updates the access management functionality by ensuring role_id is updated on duplicate key entries and adds error handling for unregistered access attempts. Additionally, it introduces a new function to retrieve migration statistics for legacy accounts, providing insights into pending and completed migrations. The version code is updated to reflect these changes.
2026-07-01 12:57:01 -04:00
57143d538e Add role assignment management features in admin interface
This commit introduces a new section for managing role assignments within the admin interface. It adds functionality to display active and historical assignments for roles, enhancing the user experience by allowing admins to view and filter assignments. Additionally, the sidebar is updated to include the new 'assignations' page, ensuring consistent navigation. The version code is updated to reflect these changes.
2026-07-01 12:48:54 -04:00
4d3260b2ca Add role deletion functionality in admin interface
This commit introduces the ability to delete roles within the admin interface. It adds new functions to check if a role can be deleted based on active assignments and historical data. The UI is updated to include delete buttons with confirmation prompts, enhancing user interaction and management capabilities. The version code is updated to reflect these changes.
2026-07-01 12:41:10 -04:00
0b3ea4811a Update sidebar menu item rendering to include icon handling
This commit enhances the sidebar menu item rendering by introducing a check for menu icons. If an icon is provided, it is displayed; otherwise, a default folder icon is shown. This improves the visual representation of menu items in the admin interface, ensuring a more consistent user experience. The version code is updated to reflect these changes.
2026-07-01 12:38:43 -04:00
30c4103212 Enhance permission inspection features in admin interface
This commit introduces new functions for displaying permission badges in various sections of the admin interface, improving visibility for super admins. It updates the sidebar and management sections to include these badges, ensuring that access rights are clearly indicated. Additionally, new CSS styles are added for better layout and alignment of the badges. The version code is updated to reflect these changes.
2026-07-01 12:35:52 -04:00
080ba99b2f Remove unused permission inspection functions to streamline codebase 2026-07-01 12:03:11 -04:00
7bd586b619 Implement permission inspection features and enhance admin interface
This commit introduces new functions for permission inspection, allowing super admins to toggle and manage access rights more effectively. It adds visual elements for permission badges in the admin interface and updates the sidebar navigation to include a migration section for legacy accounts. Additionally, CSS styles are enhanced for better user experience and clarity. The version code is updated to reflect these changes.
2026-07-01 12:00:22 -04:00
2a5c81ecc3 Refactor sidebar titles and enhance CSS styles for improved navigation in admin interface
This commit updates the sidebar titles in the admin interface to use consistent heading levels and introduces new CSS styles for better visual hierarchy. The changes include the addition of classes for main titles and subtitles, enhancing the overall user experience and readability. The version code is updated to reflect these changes.
2026-07-01 11:00:35 -04:00
63b0a1d4c5 Refactor sidebar navigation and improve documentation management in admin interface
This commit enhances the sidebar navigation by consolidating sections for site management, access management, and documentation. It introduces a new help section for production deployment and streamlines the documentation links for better accessibility. Unused code related to module selection has been removed, improving maintainability. The version code is updated to reflect these changes.
2026-07-01 10:58:23 -04:00
885a5b652b Enhance sidebar navigation and documentation management in admin interface
This commit introduces a revamped sidebar navigation in the admin interface, adding new sections for site management, access management, and documentation. It improves user experience by providing clearer access to various functionalities, including a new hub for managing pages and modules. Additionally, the code structure is refined to ensure better organization and maintainability. The version code is updated to reflect these changes.
2026-07-01 10:47:22 -04:00
c19f70031c Enhance search functionality in registration management with single result redirection
This commit introduces logic to automatically redirect users to a detailed view when a single result is returned from a search query in the registration management interface. It improves user experience by streamlining navigation after search submissions. Additionally, the code structure is refined to ensure that search results are fetched only when necessary, enhancing performance. The version code is updated to reflect these changes.
2026-06-30 15:46:48 -04:00
7c9968063a Implement search form submission and UI enhancements for registration management
This commit introduces new JavaScript functions to handle search context detection and form submission within the registration management interface. It enhances the user experience by automatically submitting the search form when a number is detected in the BibScan session. Additionally, new CSS styles are added to improve the layout of the search fields, ensuring better alignment and spacing. The version code is incremented to 4.72.720 to reflect these changes.
2026-06-30 15:40:06 -04:00
a955932086 Refine refund success messaging and UI for cancellation in registration management
This commit updates the refund success messaging to clarify the cancellation process, ensuring users understand how to proceed with cancellations in the registration system. It introduces an inline cancel button within the explanatory banner, enhancing user interaction and visibility. Additionally, the cancel button is now hidden to prevent duplication, improving the overall user experience during the refund process.
2026-06-30 15:36:15 -04:00
8933753fe0 MSIN-4417 Update refund context handling in registration management
This commit refines the refund context handling by removing the unnecessary boolean parameter for Super Admin in the `fxRefundComputeContext` function, simplifying the logic. It ensures that refunds are consistently linked to the correct event context across various functions, enhancing authorization checks and calculations. The version code is incremented to 4.72.719 to reflect these changes.
2026-06-30 15:33:40 -04:00
797e66fec3 MSIN-4417 Refactor refund handling in registration management to support event context
This commit enhances the refund functionality by introducing event context handling for refunds. It updates the logic to ensure that refunds are linked to the correct event, improving authorization checks and refund calculations. Additionally, the refund context is now passed through various functions to ensure accurate processing. The version code is incremented to 4.72.718 to reflect these changes.
2026-06-30 15:13:06 -04:00
0d042deaf2 MSIN-4416 Enhance refund functionality in registration management with new documentation panel
This commit introduces a new documentation panel for the refund process in the registration management interface. It adds a function to render the refund documentation aside from the main refund form, improving user guidance during the refund process. Additionally, CSS styles are added to ensure proper layout and responsiveness of the new panel. The version code is updated to 4.72.717 to reflect these changes.
2026-06-30 14:40:48 -04:00
5e767e77a9 Update CSS styles for cancellation badges and increment version code to 4.72.716
This commit enhances the visual representation of cancellation badges in the UI by adding new CSS styles for better alignment and spacing. Additionally, the version code has been updated to reflect the latest changes. These updates improve the overall user experience in the registration management interface.
2026-06-29 13:47:24 -04:00
afff43abd6 MSIN-4405 Implement cancellation cause logic and UI enhancements
This commit introduces a new function, `fxGetAnnulationCause`, to determine the reason for a cancelled registration (e.g., transfer, modification, or true cancellation) without altering the database. Additionally, it adds CSS styles for displaying cancellation badges in the UI, enhancing the visual representation of registration statuses. The changes improve user experience by providing clear and informative feedback on registration cancellations throughout the management interface.
2026-06-29 13:31:28 -04:00
bcc7d7974e MSIN-4415 Enhance event display in promoteur hub with section title
This commit adds a section title for the events grid in the promoteur hub, improving the visual structure and clarity of the displayed information. The title indicates the availability of events for sale, enhancing user engagement and overall experience within the interface.
2026-06-29 12:01:32 -04:00
50e8fea3e9 MSIN-4415 Add product display functionality in promoteur hub
This commit introduces new features for displaying products available for sale within the promoteur hub. It adds a function to retrieve product data, including quantities available, sold, and in carts, enhancing the event experience by providing users with relevant product information. Additionally, CSS styles are added to improve the visual presentation of the product tiles. The changes ensure that product availability is clearly communicated to users, improving overall engagement and usability in the event management interface.
2026-06-29 11:59:20 -04:00
377f4993f8 MSIN-4413 Enhance captain role logic in profile fetching function
This commit refines the logic for determining the captain role within the `fxInscrGestionFetchFicheProfile` function. It introduces a new condition to ensure that the captain title is only displayed for team events, improving the accuracy of role representation. The changes enhance the clarity of participant profiles by correctly reflecting the captain status based on event type, thereby improving the overall user experience in the registration management interface.
2026-06-29 11:51:15 -04:00
8be4bafce1 Refine PayPal refund display logic to enhance user experience
This commit improves the handling of PayPal refunds by ensuring that no summary is displayed when there are no refunds available. The logic now checks for empty refund details before rendering the summary section, preventing unnecessary display of empty elements. This change enhances the clarity of the user interface by only showing relevant information, thereby improving the overall user experience in the registration management interface.
2026-06-29 11:47:48 -04:00
bdd0454240 Refactor PayPal refund handling to improve currency and ID retrieval logic
This commit enhances the handling of PayPal refunds by refining the logic for retrieving currency and identifiers. It introduces new utility functions, `strFirst` and `floatFirst`, to ensure that fallback values are correctly utilized when the refunds section is empty or when API reporting fails. This change improves the robustness of the refund summary display, ensuring accurate information is presented to users.
2026-06-29 11:43:58 -04:00
f52816da32 Enhance refund panel functionality in registration management
This commit introduces a new function, `focusRefundPanel`, to improve user experience by automatically scrolling the refund panel into view and focusing on the amount input field when the panel is loaded. Additionally, the refund panel is repositioned above the transaction panel to ensure better visibility. The changes streamline the interaction process, making it easier for users to access and input refund details.
2026-06-29 11:36:08 -04:00
7c88dd595d Implement compact PayPal refund summary feature in registration management
This commit introduces a new function, `renderPayPalRefundSummaryHtml`, to provide a concise overview of PayPal refunds within the registration management interface. The summary displays essential information such as status, captured amount, total refunded, and remaining balance, enhancing the user experience by presenting key details in a more accessible format. Additionally, the existing code is updated to utilize this new summary function, improving the overall clarity and usability of transaction information.
2026-06-29 11:33:07 -04:00
028a9af4aa MSIN-4412 Enhance transaction resume feature in registration management with detailed PayPal refund information
This commit improves the transaction resume functionality by adding detailed PayPal refund information to the transaction summary. The changes include a refined SQL query to retrieve complete command details and the integration of a new section displaying PayPal capture details if available. This enhancement provides users with a comprehensive view of their transactions, including refunds, directly within the management interface, thereby improving usability and transparency.
2026-06-29 11:24:02 -04:00
b5daa5a4ba Enhance bib management with restore functionality on duplicate check
This commit adds a restore feature for bib numbers in the registration management interface. When a duplicate bib is detected, the previously entered value is restored, improving user experience by preventing accidental data loss. The changes include new functions to manage the restoration process and updates to the existing AJAX handling to ensure the correct bib value is maintained during interactions. This enhancement builds on the previously implemented duplicate check feature, further refining the registration workflow.
2026-06-29 11:12:14 -04:00
811cf02f16 MSIN-4406 Implement duplicate bib check feature in registration management
This commit introduces a new AJAX functionality to check for duplicate bib numbers during the registration process. It adds a case in `ajax_inscr_gestion.php` to handle duplicate checks, returning relevant participant information and policies for handling duplicates. The front-end script in `inc_footer_scripts.php` is updated to incorporate this check before saving a bib, enhancing user interaction with confirmation dialogs for duplicate assignments. Additionally, new functions are added in `inc_fx_inscriptions_gestion.php` to manage duplicate policies and retrieve existing participants with the same bib number, improving data integrity and user experience in the registration management interface.
2026-06-29 11:09:10 -04:00
3b007261d6 MSIN-4404 Refactor AJAX handling for "bib retrieved" status toggle in registration management
This commit refines the AJAX functionality for toggling the "bib retrieved" status in the registration management interface. It introduces a confirmation dialog for removing the status, enhancing user interaction. The preloader display logic is also improved, ensuring a smoother user experience during the status update process. Additionally, the code structure is cleaned up for better readability and maintainability.
2026-06-29 10:58:31 -04:00
dfc5b8866a Implement transaction resume feature in registration management
This commit introduces a new AJAX-based feature to display transaction details in the registration management interface. A button labeled "Afficher la transaction" is added, allowing users to toggle the visibility of transaction summaries independently from refunds. The transaction details are fetched dynamically, enhancing user experience by providing immediate access to relevant information. Additionally, corresponding labels for the button are defined in both French and English to support internationalization. The changes improve the overall usability and functionality of the management interface.
2026-06-29 10:52:36 -04:00
d8252fd77f Update confirmation button labels and reposition in registration management
This commit modifies the labels for the confirmation button in both French and English to provide clearer instructions. The button is also repositioned to appear last in the management form for better user experience. Additionally, corresponding changes are made in the SQL file to reflect the updated labels in the database. These enhancements aim to improve clarity and usability in the registration management interface.
2026-06-29 10:46:32 -04:00
2d518e54ce Enhance registration management display with new column headers and responsive design
This commit introduces a new grid layout for the registration management list, adding column headers for better organization and clarity. The CSS has been updated to ensure proper alignment and styling of the headers, while also implementing responsive behavior to hide the header in mobile view. The version code has been updated to 4.72.713.
2026-06-29 10:41:48 -04:00
46c75f14b6 Enhance OCR scanning interface and improve internationalization handling
This commit updates the CSS for the OCR scanning interface, increasing the aspect ratio and adjusting dimensions for better usability. It also introduces a new function, `fxInscrGestionScanLabel`, to provide robust labels for scanner buttons, ensuring that users receive clear instructions regardless of language settings. The version code has been updated to 4.72.712.
2026-06-29 10:37:09 -04:00
10d800959f Refactor BibScanSession to improve internationalization handling in status messages
This commit enhances the `BibScanSession` class by introducing a new function, `scanMsg`, to manage the retrieval of translated status messages. It replaces direct attribute access with this function to ensure that raw internationalization keys are not displayed to users. The changes improve the user experience by providing clearer and properly translated messages during the scanning process, including camera access and number detection feedback.
2026-06-29 10:28:21 -04:00
0b5da05d42 Enhance management form usability with improved CSS styles and enable OCR scanning
This commit introduces several CSS enhancements to the editable management form, increasing padding, font sizes, and button dimensions for better touch accessibility on mobile devices. Additionally, it sets the OCR scanning feature to always be available on the management form, ensuring a seamless user experience during data entry. The version code has been updated to 4.72.711.
2026-06-29 10:00:30 -04:00
8f04833b6a Update ajax_inscr_gestion.php to improve error handling and include necessary function dependencies
This commit adds a required file inclusion for the `fxEveAccesResolveEveIdFromParId` function to ensure it is available during the execution of the 'par_statut_course' case. Additionally, it simplifies the error response for invalid actions by removing detailed diagnostic information, streamlining the error handling process while maintaining essential feedback for AJAX requests.
2026-06-29 09:47:57 -04:00
61bf838108 Implement error handling and JSON response for ajax_inscr_gestion.php
This commit enhances the error handling in the `ajax_inscr_gestion.php` file by introducing a shutdown function that captures fatal errors and returns a structured JSON response instead of a generic 500 error. Additionally, it adds diagnostic information for invalid actions, improving the debugging process. These changes aim to provide clearer feedback to users and developers when issues arise during AJAX requests.
2026-06-29 09:46:05 -04:00
6ab8c8c61f Enhance promoteur hub functionality by defining a threshold for unlimited events
This commit introduces a new constant, `PHUB_SEUIL_ILLIMITE`, to define the quantity threshold at which an event is considered "unlimited." The `fxPromoteurHubGetEpreuveStats` function has been updated to incorporate this threshold, allowing for better handling of event statistics and improving the accuracy of the sold tickets and remaining quantities display. These changes aim to enhance the clarity and functionality of the promoteur hub.
2026-06-27 17:41:30 -04:00
bedc53df7f chore: stop tracking v3_ci4/vendor (Composer artifacts)
Le dossier vendor/ avait ete committe par accident (milliers de fichiers), ce qui faisait geler l'IDE. Ajoute au .gitignore et retire du suivi (fichiers conserves sur disque). composer.lock reste versionne.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-27 17:38:03 -04:00
91bf29089e Add Composer artifacts to .gitignore to prevent vendor directory commits 2026-06-27 17:37:45 -04:00
ca452f9ade Update promoteur hub statistics display and refine CSS for better user experience
This commit enhances the `fxPromoteurHubGetEpreuveStats` function to improve the aggregation and presentation of event statistics, including sold tickets and check-ins. Additionally, it refines the CSS styles for the promoteur hub, ensuring a more cohesive and visually appealing layout for statistics and event tiles. These updates aim to provide users with clearer insights and a more organized interface for event management.
2026-06-27 17:35:37 -04:00
c47d826f8e Refactor promoteur hub rendering and update CSS for improved layout
This commit refines the rendering functions for the promoteur hub, introducing `fxPromoteurHubRenderFlash` and `fxPromoteurHubRenderEpreuvesGrid` to separate the display of the flash summary and the grid of event tiles. The CSS has been updated to enhance the layout, including new styles for the flash section and action buttons, ensuring a more organized and visually appealing interface. Additionally, the version code has been incremented to 4.72.710.
2026-06-27 17:12:59 -04:00
3e8112ec0b Implement statistics and layout enhancements for promoteur hub
This commit introduces a new function, `fxPromoteurHubGetEpreuveStats`, to aggregate and return statistics for events, including sold tickets, check-ins, and waiting list details. Additionally, it adds extensive CSS styles for the promoteur hub, enhancing the layout and visual presentation of statistics, alerts, and event tiles. These changes improve the user experience by providing clearer information and a more organized interface for event management.
2026-06-27 17:03:26 -04:00
67d7675e11 Add fxLegalLabelIsDraft function to improve label validation
This commit introduces the `fxLegalLabelIsDraft` function, which checks if a given label is a draft based on its content. The function trims the label and evaluates conditions such as emptiness and the presence of underscores, enhancing the clarity and maintainability of the cookie consent management system. This addition supports better label handling in the context of legal page requirements.
2026-06-27 15:00:03 -04:00
f32a4300f0 Enhance footer social links styling for improved layout and accessibility
This commit updates the CSS styles for the footer's social links, ensuring a more consistent and user-friendly layout. Changes include adjustments to the display properties and margins of social link elements, enhancing their alignment and visual appeal. Additionally, the font size of the cookie management icon has been refined for better integration within the footer design.
2026-06-27 14:58:07 -04:00
1b30f7e0ea Refactor cookie consent management to improve label handling
This commit updates the `fxEnsureLegalPage` function to utilize the `fxLegalLabelIsDraft` function for checking and updating French and English labels. This change enhances the clarity and maintainability of the code by ensuring that labels are only updated when they are in draft status, aligning with the overall improvements in cookie consent management.
2026-06-27 14:57:01 -04:00
33f5c3d0dd Update version code to 4.72.708 in inc_settings.php 2026-06-27 14:55:13 -04:00
58342532cd Enhance cookie management UI and functionality
This commit updates the cookie management interface in the footer by replacing the existing cookie icon with a new SVG representation, improving visual consistency. It also refines the associated CSS styles for better user interaction and accessibility. Additionally, the cookie consent text retrieval function has been simplified to ensure clearer language and compliance with privacy regulations. These changes collectively enhance the user experience and maintainability of the cookie consent system.
2026-06-27 14:54:52 -04:00
06d8a065dc Update cookie consent language and policy details for clarity and compliance
This commit enhances the cookie consent management by updating the language used in the consent messages and policy sections. The changes provide clearer explanations of cookie usage, including essential, analytics, and marketing cookies. Additionally, the policy now includes details on managing cookie preferences, cross-border data transfers, and user rights under Quebec Law 25, ensuring better compliance with privacy regulations and improving user understanding.
2026-06-27 14:49:52 -04:00
ce7c430046 Enhance cookie consent management and UI improvements
This commit refines the cookie consent management system by introducing a new function for handling cookie preferences and updating the UI elements in `inc_footer.php` and `inc_header.php`. It ensures that analytics and marketing scripts are executed based on user consent, improving compliance with privacy regulations. Additionally, it enhances the cookie consent interface, providing users with clearer options for managing their cookie preferences, and updates related functions for better maintainability.
2026-06-27 14:48:48 -04:00
19679875df Implement cookie consent management across various scripts
This commit introduces a comprehensive cookie consent management system by integrating the `fxCookiesAllowMarketing()` function across multiple files. It ensures that marketing-related scripts, such as Facebook Pixel and Google Analytics, are only executed if the user has consented to cookies. Additionally, it enhances the cookie consent UI in `inc_footer.php` and `inc_header.php`, improving user experience and compliance with privacy regulations. The changes also include refactoring of cookie handling functions for better maintainability.
2026-06-27 14:44:44 -04:00
4c08938403 Update promoteur hub layout and SQL for gestion integration
This commit modifies the layout of the promoteur hub by swapping the content of the left and right columns, ensuring that the management section is displayed correctly alongside the reports. Additionally, it updates the SQL script to include safety measures for missing labels and redefines the "Gestion" header in the promoteur hub, ensuring a consistent user experience across the interface.
2026-06-27 12:30:43 -04:00
6d885caedf Update inscriptions management to transition from mobile to gestion
This commit replaces references to mobile inscriptions with gestion inscriptions across multiple SQL files. It includes the addition of new button labels, search prompts, and QR code handling for the gestion interface, ensuring a consistent user experience. The changes enhance the overall structure and maintainability of the event management system while providing clearer instructions and improved functionality for users.
2026-06-27 12:25:36 -04:00
8f428a5c18 Refactor event management system to transition from mobile to gestion
This commit updates the event management system by replacing references to mobile inscriptions with gestion inscriptions. It includes changes to AJAX endpoints, helper functions, and UI elements to reflect this transition. The updates enhance the overall structure and maintainability of the codebase while ensuring that user access and permissions are properly managed for the new gestion interface.
2026-06-27 12:10:19 -04:00
9acc8102e8 Add AJAX functionality for event registration management and QR code scanning
This commit introduces new AJAX endpoints for managing event registrations, including a dedicated script for handling QR code scans. The new functionality validates user sessions and event access, providing JSON responses for both successful and error states. Additionally, it enhances the overall structure of the event management system by integrating various helper functions for improved maintainability and user experience.
2026-06-27 12:09:56 -04:00
116347036f Enhance mobile event registration search functionality with new prompts and criteria handling
This commit introduces a search prompt for the mobile event registration interface, guiding users on how to display registrations. It also implements logic to manage search criteria, ensuring that the search state is preserved during pagination and navigation. These improvements enhance user experience by providing clearer instructions and maintaining context during searches.
2026-06-27 11:54:40 -04:00
c4199c5557 Enhance mobile event management interface with new labels and version update
This commit introduces new labels for displaying invoice and participant modification actions in the mobile event management interface, improving clarity for users. Additionally, the version code has been updated to reflect these changes. These enhancements contribute to a more user-friendly experience in managing event registrations.
2026-06-27 11:47:41 -04:00
98ed9f6f8e Refactor mobile event management actions and update version code
This commit removes the visual grouping for event management actions on mobile and simplifies the button layout. The buttons for editing, refunding, canceling, and restoring registrations have been streamlined for better usability. Additionally, the version code has been updated to reflect the latest changes. These modifications enhance the overall clarity and functionality of the mobile event management interface.
2026-06-27 11:40:53 -04:00
236601fcf6 Implement new mobile event management actions layout and enhance button functionality
This commit introduces a visually distinct group for managing event actions on mobile, including buttons for editing, refunding, canceling, and restoring registrations. Each button now features icons for improved clarity and user experience. Additionally, a new button for sending invoice details via email is added, enhancing the overall functionality of the mobile event management interface. Version and date constants are also updated.
2026-06-27 11:37:52 -04:00
373483b4eb Enhance refund display in mobile event management by introducing a collapsible summary panel for invoice details. This update improves user experience by providing bilingual labels for the transaction visibility toggle and a visually distinct layout for better clarity during the refund process. 2026-06-27 11:27:46 -04:00
95536edec5 Enhance refund process in mobile event management by integrating a summary of the invoice alongside the refund form. This update includes the addition of a resume display for the invoice, ensuring users can view the details of what they are refunding. The changes improve the user experience by providing clearer context during the refund request process. 2026-06-27 11:21:21 -04:00
e486c22bfc Refactor refund process by removing the inc_tableau_remboursement.php file and updating the refund handling logic in ajax_refund.php and inc_fx_paypal.php. The changes include enhanced authorization checks for promoters, improved session management for refund tokens, and better integration of user information in refund records. This update streamlines the refund workflow and enhances security and maintainability. 2026-06-27 11:14:41 -04:00
bc407a7c20 Add refund functionality to mobile event management
This commit introduces a new refund feature in the mobile event management system. It includes an AJAX-based refund form that allows users to request refunds directly from the event management interface. The refund button is conditionally displayed based on user permissions, and the form is dynamically loaded to enhance user experience. Additionally, the refund processing logic is integrated with existing PayPal functions to ensure seamless transaction handling. This update improves the overall usability and functionality of the event management system.
2026-06-26 14:45:09 -04:00
bc26c37315 Refactor refund form rendering in commandes.php to integrate author information directly into the display. The previous debug output has been removed and replaced with a structured information panel that includes the order number and the name of the person processing the refund. This change enhances the clarity and usability of the refund interface. 2026-06-26 14:34:47 -04:00
74ee34634e Refactor PayPal refund process by introducing a modular approach for refund context and rendering. This update consolidates refund logic into dedicated functions in inc_fx_paypal.php, enhancing maintainability and clarity. The commandes.php file is updated to utilize these new functions, improving the overall structure and readability of the refund handling code. 2026-06-26 14:26:06 -04:00
f1e43c911b Update access permissions from 'inscriptions_mobile' to 'inscriptions_gestion' across multiple files
This commit refactors the access control logic to replace references to 'inscriptions_mobile' with 'inscriptions_gestion', aligning the permission structure with the new management system. Changes include updates to SQL scripts, PHP functions, and documentation comments to ensure consistency in permission handling for event management. This enhances clarity and maintains the integrity of the access control system.
2026-06-26 14:16:09 -04:00
8570b73257 Refactor access control in documentation.php to restrict POST actions and display messages based on user login status. This change enhances security by ensuring only logged-in users can perform actions and receive appropriate feedback on the page. 2026-06-25 21:55:35 -04:00
c97478ba8a Add documentation section to inc_droite.php with navigation links 2026-06-25 20:15:38 -04:00
6f03e76540 Add WYSIWYG editor integration using Trumbowyg in doc_edition.php
This commit introduces the Trumbowyg WYSIWYG editor specifically for the `doc_edition.php` page, enhancing the document editing experience. It includes the necessary CSS and JavaScript files from CDN, along with custom styles and initialization scripts for the editor. This change does not affect other editors like TinyMCE or CKEditor used elsewhere on the site, ensuring a focused enhancement for this page.
2026-06-25 19:30:37 -04:00
365c51f6c8 Enhance event search functionality by prioritizing event ID matches
This commit updates the SQL query in `ajax_newpromoteur.php` to prioritize matches based on event IDs when searching for events. It introduces a new integer conversion for the search query to facilitate this matching. Additionally, the event name display in `inc_footer.php` is modified to include the event ID for better user context. These changes aim to improve the event search experience in the promoteur hub.
2026-06-25 18:29:51 -04:00
953775d9ca Refactor event access management by replacing SQL views with inline queries
This commit removes the deprecated SQL views `v_eve_acces_actif` and `v_eve_acces_permissions`, replacing them with inline SQL queries in the PHP code. This change avoids issues with non-copiable DEFINER attributes in views and enhances the flexibility of access management. The new functions `fxEveAccesActifBody`, `fxEveAccesActifSql`, and `fxEveAccesPermSql` are introduced to encapsulate the SQL logic directly within the application, improving maintainability and performance.
2026-06-25 18:24:39 -04:00
683e2d69d0 Update promoteur hub styles and enhance event section rendering
This commit modifies the CSS for the promoteur hub, introducing new styles for card elements, including updated colors, borders, and shadows for improved aesthetics. It also enhances the event section rendering by adding a specific class to the event title, ensuring consistent styling across the promoteur hub. Additionally, the version code is incremented to reflect these changes.
2026-06-25 17:15:37 -04:00
a45be35d6c Enhance promoteur hub card styling and update event card structure
This commit introduces new CSS styles for the promoteur hub card, including a background color, border, and shadow effects to improve visual presentation. It also updates the HTML structure of the event card, incorporating a new card icon and enhancing the event title styling for better readability. Additionally, it modifies the logic for displaying document triggers in the promoteur hub announcements section, ensuring a more cohesive user experience.
2026-06-25 17:06:21 -04:00
9a0ea70034 Refactor event modal to event panel and enhance UI interactions
This commit renames the `fxPromoteurHubRenderEventModal` function to `fxPromoteurHubRenderEventPanel`, transitioning from a modal to a collapsible panel for event details. It updates the HTML structure accordingly, improving user interaction with a toggle button and a rotating caret icon for better visual feedback. Additionally, the CSS is enhanced to support the new panel design, ensuring a more cohesive user experience in the promoteur hub.
2026-06-25 17:02:48 -04:00
d95f84bff7 Add event management links and enhance SQL entries
This commit introduces a new function, `fxPromoteurHubGetEventLinks`, to generate management and report links for events, improving the event management interface. It also updates the SQL script to include new entries for the 'Gestion' section in both French and English, enhancing the localization of the promoteur hub. These changes aim to streamline event management and improve user navigation within the promoteur hub.
2026-06-25 16:59:01 -04:00
7cee99e8e0 Add legacy event ID retrieval and merge with access events
This commit introduces a new function, `fxPromoteurHubGetLegacyEventIds`, to retrieve legacy event IDs for a given account. It updates the `fxPromoteurHubGetEvents` and `fxShowPromoteurHubMenuLink` functions to merge legacy event IDs with current access event IDs, ensuring comprehensive event access management. Additionally, it modifies SQL insert statements to update participant space labels for clarity. These changes aim to enhance the event management functionality and improve user experience.
2026-06-25 16:54:12 -04:00
ade9608e72 Refactor promoteur hub logic and introduce new checks
This commit refines the promoteur hub integration by replacing the existing function `fxPromoteurHubComUsesV2` with `fxPromoteurHubShouldUse`, which combines access checks and page existence validation. It updates the account management logic to ensure proper redirects based on the new checks, enhancing the user experience for promoteur hub accounts. Additionally, it introduces a new function to verify the existence of the promoteur hub page, streamlining the access management process.
2026-06-25 16:49:03 -04:00
0dcdcfc829 Implement promoteur hub integration and UI enhancements
This commit introduces the promoteur hub functionality by adding new checks and redirects based on the user's account type. It updates the account management logic to handle promoteur hub users, ensuring proper navigation and access. Additionally, it enhances the CSS for promoteur hub elements, improving the visual presentation. These changes aim to streamline user experience for promoteur hub accounts and ensure proper access management.
2026-06-25 16:45:03 -04:00
a5b680c273 Enhance JIRA integration with improved category handling and error logging
This commit refines the JIRA request handling by restructuring the `jira_resolve_categorie_interne` function to better manage the internal category based on the provided input. It introduces a new function, `jira_set_categorie_interne`, to update the category for JIRA issues, enhancing the integration's functionality. Additionally, it improves error logging for user creation and ticket submission processes, providing clearer feedback on failures. These changes aim to enhance the robustness and user experience of the JIRA integration.
2026-06-25 15:55:00 -04:00
2d8fe939a3 Update JIRA ticket creation and error handling in AJAX scripts
This commit modifies the JIRA ticket creation function to structure the 'categorie_interne' field as an associative array, improving data formatting. Additionally, it enhances error handling in the AJAX script by adding localized error messages for better user feedback during ticket submission failures. These changes aim to improve the robustness and user experience of the JIRA integration.
2026-06-25 15:43:07 -04:00
1f8616e4d6 Update event access management UI with enhanced table layout and styling
This commit improves the event access management interface by updating the table structure to include hover effects and a dark header for better visibility. It refines the display of role information, including icons and labels, and adjusts the layout for permissions, assignments, and status indicators to enhance clarity and usability. These changes aim to provide a more intuitive experience for managing event access.
2026-06-23 16:09:30 -04:00
5d686b28a4 Enhance event access management with email search functionality and URL adjustments
This commit introduces a new email search feature in the event access management interface, allowing users to find accounts by email for access assignment. It updates the `fxEveAccesCompteAdminUrl` function to include an optional event focus parameter, improving URL handling. Additionally, the AJAX endpoint is modified to return user names along with emails, enhancing the data available for selection. These changes aim to streamline the process of assigning event access and improve overall usability.
2026-06-23 15:18:43 -04:00
c047e36a62 Add event access management functions and UI integration
This commit introduces new functions for managing event access, including `fxEveAccesCompteAdminUrl` and `fxEveAccesEventFormGroupByCom`, which group active kits and extras by event ID. Additionally, it integrates the event access panel into the admin interface, enhancing the user experience with a structured display of access assignments and improved navigation for managing event permissions.
2026-06-23 15:15:48 -04:00
2855967e81 Enhance event access management with validation and UI improvements
This commit adds validation to ensure at least one kit is assigned before granting individual rights, improving the logic for event access management. It also updates the UI to provide clearer options for assigning kits and permissions, enhances the display of assigned kits and rights, and improves the overall layout for better usability.
2026-06-23 15:10:43 -04:00
439a52eb0f Refactor event access display and enhance UI layout
This commit updates the event access management interface by replacing the previous block layout with a structured table format for better clarity and usability. It simplifies the role selection process and improves the visual presentation of event kits. Additionally, it includes conditional messaging for cases with no access, ensuring a more intuitive user experience.
2026-06-23 15:05:05 -04:00
6a4d1761f3 Enhance event access management UI with improved layout and functionality
This commit updates the event access management interface by introducing a more user-friendly layout for displaying event access assignments. It adds dynamic role and permission selection options, enhances the visual presentation of event kits, and improves the overall usability of the form elements. Additionally, it includes conditional messaging for cases with no events or kits, ensuring a clearer user experience.
2026-06-23 15:01:05 -04:00
77a2508c71 Refactor event access management UI and logic
This commit enhances the event access management interface by updating the layout and improving the usability of form elements. It introduces a clearer display of event access assignments and refines the logic for handling active kits. Additionally, it conditionally displays the legacy promoter section based on the current context, ensuring a smoother transition towards the new access management system.
2026-06-23 14:43:58 -04:00
fd26c90098 Implement event access grouping and enhance UI for event management
This commit introduces a new function, `fxEveAccesCompteFormGroupByEvent`, to group active kits and extras by event ID for improved access management in the super admin interface. It also updates the UI to provide clearer information on event access, including a new section for legacy promoter events, and enhances the form elements for better usability. Additionally, the logic for retrieving promoter events is refined to handle empty cases more gracefully.
2026-06-23 14:41:03 -04:00
4e6eb3191b Add access management section to sidebar in admin interface
This commit introduces a new section in the admin sidebar for managing access, specifically adding a link for "Kits d'accès". The logic determines the active state based on the current script and query parameters, enhancing navigation for event access management.
2026-06-23 14:32:52 -04:00
2cf8f5e7ec restructurer les acces 2026-06-23 14:19:57 -04:00
bc8bc86b74 Refactor access control and UI elements for event management
This commit updates the event management access control logic in `inc_fx_eve_acces.php` to ensure proper permission checks for managing bibs. It also simplifies the UI in `inc_tableau_promoteur.php` by removing conditional rendering for the management link, ensuring it is always displayed. Additionally, the comment in `inc_tableau_gestion_epreuves.php` is updated for clarity.
2026-06-23 12:36:56 -04:00
0b39f08ec7 Update .htaccess for improved routing and PHP handler configuration
This commit modifies the .htaccess file to implement a redirect for non-www domains to www for production clients, while ensuring compatibility with development environments. It also refines the routing rules for CI4 and legacy routes, and adds a PHP handler configuration for version 8.1, enhancing server-side processing capabilities.
2026-06-23 11:44:52 -04:00
100accfe34 Remove unused 'variables' table definition from static synchronization configuration 2026-06-23 10:10:06 -04:00
b3b24b08c4 Refactor database connection settings and introduce profile management functions
This commit adds structured database connection profiles for client and CRM environments, enhancing the management of database connections in different environments (preprod and prod). It introduces new functions, `fxSettingsUseDbProfiles` and `fxSettingsUseCrmProfile`, to streamline the assignment of connection parameters, improving code maintainability and readability. Additionally, it removes hardcoded connection details from the main logic, promoting a more dynamic configuration approach.
2026-06-23 10:08:10 -04:00
74da043805 Remove unused static synchronization table definitions from configuration 2026-06-23 09:39:17 -04:00
8eda3d40c3 Refactor static database synchronization configuration and connection handling
This commit updates the static database synchronization logic by removing hardcoded connection details from the `fxStaticSyncClientDatabaseConfigs` function. It now relies on configurations defined in `inc_settings.php` for client connections. The `fxStaticSyncEnvironmentDefinitions` and `fxStaticSyncOpenConnections` functions are modified to utilize these new configurations, enhancing flexibility and maintainability in managing database connections for different environments.
2026-06-23 09:37:49 -04:00
d74d06a3b0 Enhance static database connection handling and synchronization logic
This commit updates the `fxStaticSyncClientDatabaseConfigs` function to return detailed database connection configurations for both preprod and prod clients. It introduces a new class, `clsMysqlStaticSync`, to manage MySQL connections with a short timeout, improving connection handling. The `fxStaticSyncConnect` function is refined to utilize this new class, and the synchronization UI is updated to reflect the new client configurations. These changes streamline database interactions and enhance the overall synchronization process.
2026-06-23 09:20:28 -04:00
1797e0c91e Refactor static database connection handling and update synchronization functions
This commit removes hardcoded database connection details from the `fxStaticSyncClientDatabaseConfigs` function, returning an empty array instead. It also refines the `fxStaticSyncConnect` function to utilize `mysqli_init` for improved connection management and introduces a new `fxStaticSyncOpenConnections` function to reuse existing connections. Additionally, the script execution time limit is increased in `sync_static_db.php` to accommodate longer operations.
2026-06-23 09:17:25 -04:00
a23f492621 Refactor static database synchronization access and update UI elements
This commit enhances the static database synchronization feature by refining the `fxSuperadmStaticSyncAllowed` function to improve access control. The UI is updated to conditionally display synchronization options in both the header and sidebar, ensuring that only authorized users can access these tools. This change streamlines the user experience while maintaining security in the admin interface.
2026-06-23 09:12:20 -04:00
02384d3404 Remove unused bib input focus handling in mobile registration and increment version code to 4.72.699 2026-06-22 15:45:11 -04:00
547390286b Enhance mobile registration feedback and fallback text handling
This commit improves the mobile registration module by refining the feedback messages displayed during the bib scanning process. It introduces additional conditions for fallback text to ensure users receive appropriate guidance based on their input. The version code is incremented to 4.72.698 to reflect these updates.
2026-06-22 15:43:48 -04:00
596f3a55c1 Enhance mobile registration UI and functionality for QR and bib scanning
This commit introduces improvements to the mobile registration module by adding a new parameter for QR scanning and enhancing the UI for bib input sections. It includes new functions for rendering collapsible sections in the mobile interface, improving user interaction and accessibility. The CSS is updated to accommodate new styles and layout adjustments, ensuring a better user experience. The version code is incremented to 4.72.697 to reflect these changes.
2026-06-22 15:42:22 -04:00
30b17be43e Refactor mobile bib scanning functionality and enhance UI feedback
This commit refines the mobile bib scanning process by introducing a new capture element and updating the UI to provide clearer feedback during scanning. It removes the unused OCR interval and enhances the event handling for capture actions. Additionally, new messages are added for user guidance, and the version code is incremented to 4.72.696 to reflect these changes. The SQL file is updated to include new translation keys for improved user instructions.
2026-06-22 15:12:34 -04:00
adcb663925 Enhance OCR functionality and UI feedback for mobile registration
This commit improves the Optical Character Recognition (OCR) process in the mobile registration module by introducing a global OCR worker for better management and performance. It updates the UI to include a new loading message during the initialization phase and refines the scanning process. The version code is incremented to 4.72.695 to reflect these enhancements. Additionally, the SQL file is updated to include the new loading message in multiple languages.
2026-06-22 15:07:01 -04:00
e8883cfbf8 Refactor mobile registration QR scanning and update UI elements
This commit streamlines the mobile registration module by removing unused QR mode functionalities and updating the UI for the bib scanning process. It introduces new feedback messages and adjusts the layout for improved user experience. The version code is updated to 4.72.694 to reflect these changes. Additionally, the SQL file is modified to align with the new bib scan terminology.
2026-06-22 15:02:54 -04:00
fd469bdc22 Enhance mobile registration with OCR for dossier scanning and update UI elements
This commit introduces Optical Character Recognition (OCR) functionality for scanning dossier numbers in the mobile registration module. New functions are added to manage the OCR process, including starting and stopping the camera, and updating the UI with relevant feedback messages. Additionally, the version code is updated to 4.72.693 to reflect these enhancements. Unused SQL files related to previous QR code modes have been removed to streamline the codebase.
2026-06-22 14:56:54 -04:00
22534b4cbf Implement OCR functionality for dossier scanning in mobile registration
This commit enhances the mobile registration module by introducing Optical Character Recognition (OCR) capabilities for scanning dossier numbers. New functions are added to manage the OCR process, including preloading the OCR worker, starting and stopping the OCR loop, and updating the UI with scanning status messages. Additionally, the UI is improved with new elements for photo input and feedback messages. The version code is updated to 4.72.692 to reflect these changes.
2026-06-22 14:50:34 -04:00
b5c979058f Enhance QR code scanning functionality and UI for mobile registration
This commit introduces significant updates to the QR code scanning feature in the mobile registration module. It adds new functions for managing QR modes (participant and dossier), improving user interaction with the QR scanner. The UI is enhanced with new elements for mode selection and feedback messages, ensuring a clearer user experience. Additionally, the version code is updated to 4.72.691 to reflect these changes.
2026-06-22 14:36:57 -04:00
6e751b552f Refactor static sync access control and update UI elements
This commit replaces the `fxAdminStaticSyncAllowed` function with `fxSuperadmStaticSyncAllowed` to streamline access control for static database synchronization tools. The UI is updated accordingly to reflect this change, ensuring that only authorized users in development and pre-production environments can access these tools. This enhancement improves the overall user experience and maintains security in the admin interface.
2026-06-22 14:10:24 -04:00
3e3a0f1de7 Add static sync functionality and update UI for development tools
This commit introduces the `fxAdminStaticSyncAllowed` function to control access to static database synchronization tools, ensuring only authorized users in development and pre-production environments can access them. The UI is updated to display a new section for these tools in the admin interface, enhancing the overall user experience and access management for development tasks. Additionally, the `.gitignore` file is updated to exclude the `/output/` directory from version control.
2026-06-22 14:08:16 -04:00
87783ad99c Add reactivation functions for event access and update UI for revoked permissions
This commit introduces new functions `fxEveAccesReactivate` and `fxEveAccesReactivateExtra` to handle the reactivation of revoked event access and extra permissions. It also enhances the UI to display a history of revoked permissions, allowing users to reactivate them easily. Additionally, the logic for checking active access has been streamlined, improving overall user experience and access management.
2026-06-19 19:46:45 -04:00
f881b42f7f Update text editing access control and enhance environment checks
This commit modifies the `fxAdminIsDevPreprod` function to improve environment validation for text editing tools, ensuring access is restricted to the appropriate development and pre-production environments. Additionally, it introduces a 403 Forbidden response in `traduction.php` for unauthorized access attempts, enhancing security and user feedback.
2026-06-19 14:37:08 -04:00
d118f5bce7 Add error handling for missing bib assignments and enhance anomaly reporting
This commit introduces new functions to manage errors related to missing bib assignments, specifically for the 'bib_auto_miss' type. It includes logic to resolve open errors when a bib is manually assigned and adds a new function to retrieve and display anomalies for registrations without assigned bibs. Additionally, the UI is updated to provide users with actionable links for resolving these anomalies, improving overall user experience and error management.
2026-06-19 13:39:39 -04:00
c12cca2de9 Enhance mobile registration search functionality and update version code
This commit introduces new functions for resetting search parameters and building search reset URLs in the mobile registration module. It improves the search logic by allowing for more flexible filtering options, including updates to the SQL query conditions for team names and order numbers. Additionally, the version code is incremented to 4.72.690 to reflect these changes.
2026-06-19 13:35:23 -04:00
9c290a66ca Update mobile registration access functions and enhance event permission checks
This commit modifies the mobile registration access logic to utilize the new `fxEveAccesHasInscrMobileWebAccess` function, improving permission handling for mobile event registrations. Additionally, it updates the `fxInscrMobileGetPromoteurEveIds` and `fxInscrMobileCanAccess` functions to streamline access checks. New functions for retrieving event IDs with mobile access permissions are introduced, enhancing overall access control and user experience.
2026-06-19 13:31:00 -04:00
5d99fef4ed Refactor event access functions and streamline mobile registration logic
This commit updates the `fxEveAccesCanQrTest` function to clarify permission handling for QR testing. It also simplifies the `fxInscrMobileCanScan` function by directly returning the permission check result. Additionally, redundant code related to rendering mobile registration fields is removed, enhancing the overall efficiency and readability of the mobile registration logic.
2026-06-19 13:26:49 -04:00
9310c12536 Add event access management functions and enhance mobile permissions
This commit introduces several new functions for managing event access, including `fxEveAccesComHasV2ForEvent`, `fxEveAccesGrantExtra`, and `fxEveAccesRevokeExtra`, which facilitate the granting and revocation of permissions for events. Additionally, the mobile registration logic is updated with the new `fxInscrMobileCanDo` function to streamline permission checks for various actions. These enhancements improve the overall access control and user experience across the application.
2026-06-19 13:20:52 -04:00
e9ad3d8a92 Enhance event access control and integrate new permission checks
This commit introduces the `fxEveAccesCanManageBibV4` and `fxEveAccesCanQrTest` functions to improve event access management. It updates various files to utilize these new permission checks, ensuring that only authorized users can manage event registrations and access QR testing features. Additionally, redundant session checks are removed, and redirection logic is refined for better user experience across the application.
2026-06-19 12:49:19 -04:00
d5fdb227dd Update mobile registration menu labels and enhance SQL entries for V2
This commit modifies the mobile registration menu labels to reflect the new versioning, changing the text to "Gestion des inscriptions V2" in French and "Registration management V2" in English. Additionally, it introduces a new function, `fxInscrMobilePromoteurMenuLabel`, to standardize the label retrieval process. The icon for the mobile registration link is also updated to improve visual consistency across the interface.
2026-06-19 12:43:54 -04:00
460d5df27f Implement mobile login enhancements and UI updates
This commit introduces a mobile login feature with validation messages for username and password fields, improving user experience. It also updates the header and footer to conditionally render elements based on mobile entry status, ensuring a cohesive design. Additionally, CSS styles are added to enhance the mobile interface, and the version code is incremented to 4.72.689 to reflect these changes.
2026-06-19 12:39:47 -04:00
fb4f74d48f Update mobile registration links and enhance access logic
This commit modifies the mobile registration links to point to the correct routes, ensuring proper navigation for users. Additionally, it refines the logic for displaying mobile access based on user permissions, introducing a new function to check for legacy promoteur menu access. These changes improve the user experience and maintain consistency in the mobile module's functionality.
2026-06-19 12:32:43 -04:00
52f9b632fb Enhance mobile text management and internationalization support
This commit introduces several new functions for managing text localization in the mobile module, including `fxInscrMobileInitTextes`, `fxInscrMobileLoadTextes`, and `fxInscrMobileResolveTexte`. These changes improve the handling of multilingual text by loading and resolving localized strings based on the selected language. Additionally, the mobile page initialization logic is updated to ensure proper text loading, and existing text retrieval methods are refactored for consistency and clarity.
2026-06-19 12:29:58 -04:00
af2b0a7faa Refactor mobile registration access logic to utilize new permission functions
This commit updates the mobile registration access checks across multiple files to use the newly implemented `fxInscrMobileCanAccess` and `fxInscrMobileCanScan` functions. The changes enhance the permission handling for mobile event registrations, ensuring a more robust access control mechanism. Additionally, the mobile URL generation is updated for consistency, and new functions for managing event access permissions are introduced to streamline the codebase.
2026-06-19 11:55:00 -04:00
7de632d9fc gestion des droit 2026-06-19 11:45:53 -04:00
6403f371fe Enhance batch and auto selection UI with new prompts and visibility logic; increment version code to 4.72.688
This commit introduces a new prompt for sequence selection in batch and auto modes, improving user guidance. The visibility logic for various action groups is refined based on selection states, ensuring a clearer interface. Additionally, CSS styles are updated to support the new prompt, enhancing the overall user experience in the bib management system.
2026-06-19 11:43:57 -04:00
717f11e3d5 Refactor fxBibTexte function to include fallback for missing keys and add new fxBibStaticFallback function for bilingual support
This commit modifies the `fxBibTexte` function to utilize a new fallback mechanism via `fxBibStaticFallback`, which provides default translations for missing or incorrectly entered keys in both French and English. Additionally, the error handling in the bib view is updated to ensure proper display of messages for unassigned and duplicate bibs. These changes enhance the robustness and user experience of the bib management system.
2026-06-19 11:32:09 -04:00
026473dd59 Implement cancellation label function and update SQL for bilingual support in bib view
This commit introduces a new function, `fxBibViewCancelledLabel`, to handle the display of cancellation labels in both French and English. Additionally, SQL updates ensure that the correct cancellation text is set for both languages, enhancing the user experience in the bib view. These changes improve the clarity and accessibility of cancellation information for participants.
2026-06-19 11:12:20 -04:00
16b8637192 Enhance cancellation handling in bib view and update SQL queries for participant retrieval; increment version code to 4.72.687
This commit improves the handling of cancelled participants in the bib view by refining the logic for displaying cancellation status and updating the associated SQL queries for participant retrieval. The version code is incremented to reflect these changes, ensuring consistency in versioning.
2026-06-19 11:09:47 -04:00
f9e1715de1 Update cancellation styling in bib view and increment version code to 4.72.686
This commit enhances the visual representation of cancelled participants in the bib view by applying a new background color and adding a label for cancelled status. The version code is updated to reflect these changes, ensuring consistency in versioning.
2026-06-19 11:04:56 -04:00
e045ff3f98 Implement cancellation styling in bib view and update SQL queries for participant filtering; increment version code to 4.72.685
This commit introduces a new CSS class for cancelled bib view rows, enhancing visual distinction with a gradient background and color adjustments. Additionally, SQL queries in the participant filtering logic are updated to remove the cancellation condition, allowing for more flexible data retrieval. The version code is incremented to reflect these changes.
2026-06-19 11:03:28 -04:00
fd1d136842 Add SQL functions for duplicate unit count and anomaly detection in bib management
This commit introduces two new SQL functions: `fxBibSqlDuplicateUnitCount`, which counts the number of units sharing a `no_bib` for an event, and `fxBibSqlIsBibDuplicateAnomaly`, which checks for duplicate anomalies excluding legitimate team sharing. These functions enhance the existing bib management logic by improving the accuracy of duplicate detection. Additionally, the existing SQL queries in `fxInfosBibRange` and `fxBibAnomalyDuplicateBibs` are updated to utilize the new functions for better clarity and efficiency.
2026-06-19 10:56:09 -04:00
a8bcd26cdd Update spacing in summary headers of fxBibEpreuveHeaderSummaries function for improved readability 2026-06-19 10:22:55 -04:00
9c4c11efc9 Enhance batch processing interface by updating visibility logic and adding new UI elements; increment version code to 4.72.684
This commit refines the batch processing interface by improving the visibility logic for various action groups based on selection states. New UI elements, including a prompt for batch picking and a warning for reset actions, are introduced to enhance user guidance. CSS styles are updated to support these changes, ensuring a clearer and more interactive experience. The version code is incremented to reflect these updates.
2026-06-19 10:17:00 -04:00
d405f2aa99 Update CSS styles for mobile registration interface and increment version code to 4.72.683
This commit modifies the CSS for the mobile registration interface, enhancing the visual hierarchy and readability by adjusting font weights and colors. The version code is updated to reflect these changes, ensuring consistency in versioning.
2026-06-19 10:09:46 -04:00
09f57dcce0 Refactor mobile registration profile rendering and update CSS for improved layout
This commit enhances the mobile registration interface by refactoring the profile rendering logic to include a flag for captains and updating the associated CSS for better layout and responsiveness. The version code is incremented to 4.72.682 to reflect these changes, further improving the user experience in mobile registrations.
2026-06-19 10:07:26 -04:00
3a3ebec2bf Enhance mobile registration interface by introducing key-value list rendering functions and updating CSS for improved layout. This commit adds new functions to render participant profile details in a structured format, enhancing readability and organization. The CSS is updated to support the new layout, including adjustments to padding and grid structure. The version code is incremented to 4.72.680 to reflect these changes, further improving the user experience in mobile registrations. 2026-06-19 10:03:01 -04:00
6bffbba726 Enhance mobile registration interface by adding profile details rendering functionality; update CSS for improved layout
This commit introduces new functions to fetch and render participant profile details within the mobile registration interface, including role, name, email, birthdate, and custom questions. The CSS is updated to improve the layout of the profile section. The version code is incremented to 4.72.679 to reflect these changes, enhancing the overall user experience in mobile registrations.
2026-06-19 09:59:10 -04:00
7bb57ded70 Fix bib display logic in mobile registration interface to remove unnecessary prefix; improve clarity of rendered output 2026-06-19 09:29:26 -04:00
36280f0235 Implement QR code scanning functionality in mobile registration interface; enhance backend processing and update styles
This commit introduces a new QR code scanning feature within the mobile registration interface, allowing users to scan QR codes for event registration. The JavaScript logic for handling QR code detection and feedback is added, along with corresponding PHP functions for processing scanned data. CSS styles are updated to improve the layout of the QR scanning panel. The version code is incremented to 4.72.678 to reflect these changes, enhancing the overall user experience in mobile registrations.
2026-06-19 09:19:31 -04:00
b068157f87 Enhance mobile registration interface with new section title and panel summary rendering functions; update CSS for improved layout and styling
This commit introduces new functions for rendering section titles and panel summaries in the mobile registration interface, improving code organization and readability. CSS styles are updated to enhance the layout, including flexbox adjustments for better alignment and spacing. The version code is incremented to 4.72.677 to reflect these changes.
2026-06-19 09:15:09 -04:00
164e26af91 Enhance mobile registration interface with new navigation and page head rendering functions; update styles for improved layout and responsiveness
This commit introduces new functions for rendering navigation and page headers in the mobile registration interface, improving code organization and readability. CSS styles are updated to enhance the layout and responsiveness of the mobile interface, including adjustments to list items and section management. The version code is incremented to 4.72.676 to reflect these changes.
2026-06-19 09:12:09 -04:00
0157f34949 Refactor translation function calls in inc_fx_promoteur.php for improved readability; remove unnecessary echo statements 2026-06-19 09:00:26 -04:00
b64cee25d6 Refactor visibility logic for reset actions in footer scripts; improve user interaction in mobile registration
This commit updates the visibility logic for the reset action group in the mobile registration interface. The new implementation consolidates conditions into a single variable, enhancing readability and maintainability. This change aims to improve user interaction by ensuring the correct display of reset options based on the current state of the registration process.
2026-06-19 08:54:47 -04:00
cdf4e5188a Refactor mobile registration status management; enhance AJAX handling and update version code
This commit encapsulates the refactoring of the mobile registration status management by encapsulating the logic within an immediately invoked function expression (IIFE) for better organization. The AJAX handling for status changes has been improved, ensuring smoother user interactions and error handling. The version code is incremented to 4.72.675 to reflect these updates, further enhancing the mobile user experience.
2026-06-18 16:36:43 -04:00
514fae950c Update SQL query in inc_manager.php to include participant status; enhance data retrieval for mobile registration 2026-06-18 16:31:45 -04:00
d64b18c9fb Refactor mobile registration status management to use select elements; update version code
This commit modifies the mobile registration interface to replace radio buttons with a select dropdown for participant status management. The JavaScript and CSS are updated accordingly to enhance user interaction and maintain layout consistency. The version code is incremented to 4.72.674 to reflect these changes, further improving the mobile user experience.
2026-06-18 16:24:24 -04:00
2cb6a59c96 Implement participant status management in mobile registration; update version code
This commit introduces a new feature for managing participant status within the mobile registration interface. It includes updates to the PHP functions for handling status changes, new JavaScript for dynamic updates, and corresponding CSS styles for improved layout. The version code is incremented to 4.72.673 to reflect these enhancements, further optimizing the user experience in managing participant registrations.
2026-06-18 16:22:02 -04:00
c0b6f0c696 Enhance mobile registration interface with new editable fields and status display; update version code
This commit introduces new CSS styles for the mobile registration interface, including editable fields and a status display section. The PHP code is updated to render these new elements, improving user interaction and information clarity. Additionally, the version code is incremented to 4.72.672 to reflect these enhancements, further optimizing the mobile user experience.
2026-06-18 16:16:14 -04:00
146f692a46 Enhance mobile registration interface and update version code
This commit introduces several improvements to the mobile registration functionality, including the addition of a new function for parsing event IDs and updates to the CSS for a cleaner layout. The logic for displaying event information has been refined, and new JavaScript features have been added to improve user navigation. The version code is incremented to 4.72.671 to reflect these enhancements, further optimizing the mobile user experience.
2026-06-18 13:48:01 -04:00
b7fa26d1e9 Implement mobile registration functionality and update version code
This commit introduces new routing rules in .htaccess for mobile access, adds mobile-specific PHP includes and logic in compte.php, and enhances the footer scripts for mobile page handling. Additionally, new CSS styles are added to support the mobile registration interface. The version code is incremented to 4.72.670 to reflect these changes, improving the overall user experience for mobile users.
2026-06-18 13:35:36 -04:00
a1c8fc787e Enhance AJAX functionality for bib management by introducing a centralized access control function and updating existing actions to utilize it. The version code is incremented to 4.72.669 to reflect these changes, improving security and consistency in AJAX requests. 2026-06-18 13:04:06 -04:00
ef2275c54d Update CSS for improved layout and button styles; enhance PHP functions for translation handling; increment version code 2026-06-18 11:56:34 -04:00
0495ded5c2 Enhance bib management with translation buttons and CSS updates; increment version code
This commit introduces new functions for handling translation buttons in the bib management system, improving user interaction with translated content. The CSS is updated to style these buttons appropriately within the bib summary sections. Additionally, the version code is incremented to 4.72.667 to reflect these enhancements, further improving the user experience in managing bib assignments.
2026-06-18 11:48:10 -04:00
1d86258926 Refactor page visibility logic and update version code
This commit modifies the JavaScript to ensure that the 'hidden' class is removed when a page is displayed, improving the visibility handling of document pages. Additionally, the PHP code is updated to streamline the generation of page elements by adjusting the placement of the 'hidden' attribute. The version code is incremented to 4.72.666 to reflect these changes, enhancing the overall user experience in document navigation.
2026-06-18 10:50:44 -04:00
8258e7ad29 Implement dashboard anomalies feature for bib management
This commit introduces a new functionality to handle dashboard anomalies related to bibs, including duplicate and orphaned bibs. The PHP code is updated to include a new AJAX action for collecting anomalies, while JavaScript functions are added to refresh the anomalies panel dynamically. Additionally, new CSS styles are introduced to enhance the layout and presentation of the anomalies information. The version code is incremented to 4.72.665 to reflect these changes, improving the overall user experience in managing bib assignments.
2026-06-18 10:42:27 -04:00
42bd7c36de Refactor SQL queries to handle 'no_bib' as unsigned integers
This commit introduces a new function, fxBibSqlNoBibUnsigned, to ensure that 'no_bib' values are treated as unsigned integers in SQL queries. The existing SQL statements in ajax_bib_range.php and inc_fx_promoteur.php are updated to utilize this function, enhancing data integrity and comparison accuracy. These changes improve the overall reliability of bib number handling in the application.
2026-06-18 10:37:04 -04:00
0c1fc53331 Implement auto assignment checks and enhance footer scripts; increment version code
This commit adds new JavaScript functions to manage pending bib assignments and alert users when automatic activation is blocked due to unassigned registrations. The PHP code is updated to include a new function that verifies all bibs are assigned before allowing auto activation. Additionally, the version code is incremented to 4.72.664 to reflect these changes, improving the overall functionality and user experience in the bib assignment process.
2026-06-18 10:21:36 -04:00
2b117cdea4 Refactor footer scripts and CSS for auto assignment actions; increment version code
This commit updates the footer scripts to enhance the visibility logic for the auto assignment actions, ensuring the appropriate elements are displayed based on the current state. Additionally, new CSS styles are introduced for the auto configuration layout, improving the organization and alignment of action buttons. The version code is incremented to 4.72.663 to reflect these changes, enhancing the user interface and experience.
2026-06-18 10:13:59 -04:00
77564c0f46 Update CSS styles for grid layout and button design; increment version code
This commit modifies the CSS to adjust grid column widths for better alignment and updates the button styles for improved visual consistency. The version code is incremented to 4.72.660, reflecting these changes. These updates enhance the user interface by providing a more organized layout and refined button aesthetics.
2026-06-18 09:59:06 -04:00
27e581c40a Implement locking mechanism for bib ranges with AJAX support
This commit introduces a locking feature for bib ranges, allowing users to lock or unlock sequences via a new AJAX endpoint. The PHP code is updated to check for locked statuses before performing actions, enhancing data integrity. Additionally, new JavaScript functions are added to handle user interactions for locking and unlocking, along with corresponding CSS styles for visual feedback. This improves the user experience by preventing modifications to locked sequences and providing clear confirmation messages.
2026-06-18 09:55:49 -04:00
33a5e0a410 Add functions for auto assignment availability checks and update SQL for error messaging
This commit introduces two new PHP functions: fxBibRangeDispoFromRow, which calculates available bibs in a sequence, and fxBibIsRangeAvailableForAuto, which checks if a sequence is available for automatic assignment. Additionally, the fxSaveBibAutoConfig function is updated to utilize these checks, enhancing error handling for full sequences. SQL scripts are also updated to include localized error messages for cases when a selected sequence is full, improving user feedback in both French and English.
2026-06-18 09:48:42 -04:00
2a9e38fdea Refactor CSS and PHP for badge layout and rendering improvements
This commit updates the CSS to enhance the layout of auto badges, positioning them to the right and adjusting the grid structure. In the PHP code, the badge description is modified for clarity, and the rendering logic is streamlined by removing unnecessary elements. These changes improve the user interface by providing a more organized and visually appealing display of sequence information.
2026-06-18 09:41:22 -04:00
71d850531d Update CSS and PHP for badge rendering and layout adjustments in batch mode
This commit modifies the CSS to introduce a new layout for auto badges in batch mode, ensuring they are hidden when not needed. The PHP functions are updated to include an optional parameter for displaying the auto badge, enhancing the summary information for sequences. These changes improve the overall user interface and experience by providing clearer visual cues and better organization of sequence data.
2026-06-18 09:38:44 -04:00
2fb6db9bd4 Remove unused auto badge visibility logic and introduce new badge rendering function for active sequences
This commit eliminates the visibility logic for the auto badge in the footer scripts, streamlining the code. Additionally, a new function, fxBibRenderRangeAutoBadge, is introduced to render the auto badge for active sequences, enhancing the display of sequence information. CSS styles are also added for the new badge, improving the visual presentation in the interface. No version code change is included in this update.
2026-06-18 09:33:26 -04:00
d0db73624e Enhance fxBibEpreuveDisplayName function to include inactive status and improve display logic
This commit updates the fxBibEpreuveDisplayName function to provide a more comprehensive display name for events by incorporating both type and category information. It also adds logic to append an "(inactive)" label when the event is not active. Additionally, new SQL entries are introduced to support localized messages for inactive events in both French and English, improving user feedback in the interface. No version code change is included in this update.
2026-06-18 09:24:25 -04:00
b71d1c300b Implement overlap detection and detailed error messaging for bib sequences in AJAX responses
This commit introduces a new function, fxBibGetOverlappingRanges, to check for overlapping bib sequences within a specified event. The validation logic in ajax_bib_range.php is updated to utilize this function, enhancing the accuracy of overlap detection. Additionally, detailed error messages are formatted for both single and multiple overlaps, improving user feedback in the interface. SQL scripts are updated to include new localized messages for overlap conflicts in both French and English. No version code change is included in this update.
2026-06-18 09:19:01 -04:00
bdb33801c9 Refactor button styles and enhance action group layout in event management interface
This commit updates button classes for the auto action feature, replacing success and secondary styles with a new 'btn-bib-auto' class for better visual distinction. It also introduces new CSS styles for the action group layout, ensuring buttons are aligned correctly. The version code has been incremented to 4.72.659 to reflect these changes.
2026-06-17 16:15:57 -04:00
7a3e9395e6 Enhance fxBibAideButton functionality and introduce segment helper functions
This commit modifies the fxBibAideButton function to include an optional parameter for controlling the display of translation buttons. Additionally, new helper functions fxBibBtnAideSegmentOpen and fxBibBtnAideSegmentClose are introduced to streamline the rendering of action segments in the interface. These changes improve the organization and flexibility of the button rendering logic, enhancing the overall user experience. No version code change is included in this update.
2026-06-17 16:13:07 -04:00
8d78de304b Refactor localization handling in event management interface
This commit streamlines the localization process by removing unnecessary parameters from functions and consolidating the logic for retrieving localized strings. The changes enhance the clarity and maintainability of the code, ensuring that the interface dynamically reflects the selected language. No version code change is included in this update.
2026-06-17 16:08:03 -04:00
9c105565d1 Implement localization for JavaScript messages in the event management interface
This commit introduces a new function to inject data attributes for JavaScript localization in the #module-bib element. It also updates various text strings used in the interface to utilize localized messages, enhancing user experience by providing contextually relevant feedback in both French and English. Additionally, a new SQL script is added to populate the database with these localized strings, ensuring they are available for dynamic use. No version code change is included in this update.
2026-06-17 15:54:35 -04:00
69f360eae8 Implement dynamic language handling and improve error messaging in AJAX responses
This commit enhances the AJAX functionality by introducing a new language handling mechanism, allowing for dynamic language selection based on user input. Additionally, error messages throughout the AJAX responses have been updated to utilize the new fxBibMsg function for better localization and clarity. This improves user experience by providing contextually relevant messages in the selected language. No version code change is included in this update.
2026-06-17 15:49:47 -04:00
5e31ea1129 Enhance tooltip functionality in event management interface with new fxBibAideTippyAttr function
This commit introduces the fxBibAideTippyAttr function to provide dynamic tooltips for buttons in the event management interface. The function retrieves contextual help text, improving user experience by offering additional information on hover. Updates to button elements ensure tooltips are displayed where relevant, enhancing clarity and usability. No version code change is included in this update.
2026-06-17 15:45:51 -04:00
8a13fbfc0d Update event management interface for improved localization and error handling
This commit enhances the event management interface by replacing static text with localized strings using the fxBibTexte function. Changes include updates to button labels, error messages, and section headers to ensure better language support and clarity for users. No version code change is included in this update.
2026-06-17 15:41:17 -04:00
5e64d0ecf6 Refactor team mode labels and improve language handling in event management interface
This commit updates the team mode option labels to utilize the fxBibTexte function for better localization support. The changes enhance the clarity of the interface by ensuring that the labels are dynamically generated based on the selected language. No version code change is included in this update.
2026-06-17 15:37:38 -04:00
3b8c867c72 Fix URL formatting in event management navigation link for better readability 2026-06-17 15:11:15 -04:00
e55d30d61b Enhance event management interface with new quantity input feature and update version code to 4.72.657
This commit introduces a new compact quantity input feature for managing event quantities, improving user interaction. Additionally, CSS styles have been refined for better layout consistency across the interface. The version code has been incremented to reflect these updates.
2026-06-17 15:01:05 -04:00
57bceba425 Remove diagnostic script and update version code to 4.72.656
This commit deletes the temporary diagnostic script used for troubleshooting includes and connections, as it is no longer needed. Additionally, the version code has been incremented to reflect this change.
2026-06-17 14:56:19 -04:00
04925de3dc Refactor CSS layout for identity blocks and update version code to 4.72.655
This commit enhances the CSS styles for the event management interface, specifically refining the layout of identity blocks with improved grid settings, padding, and border styles. Additionally, the version code has been incremented to reflect these updates.
2026-06-17 14:51:03 -04:00
8fa390a5e6 Update CSS styles for improved layout and increment version code to 4.72.654
This commit modifies the CSS styles to enhance the layout of the event management interface, including adjustments to flex properties and padding for better responsiveness. The version code has been updated to reflect these changes.
2026-06-17 14:46:07 -04:00
a26de022e2 Enhance event management interface with new identity header and summary features
This commit introduces new CSS styles and PHP functions to improve the event management interface. It adds a visually cohesive identity header for events, displaying relevant summaries such as team/individual status and registration counts. The version code has been incremented to 4.72.653 to reflect these updates.
2026-06-17 14:41:33 -04:00
05fd4d35ff Add new header summary features for event management interface
This commit introduces new CSS styles and PHP functions to enhance the event management interface with summary badges for available quantities and sequence counts. The collapsible blocks now display relevant summaries, improving user navigation and information visibility. The version code has been incremented to 4.72.652 to reflect these updates.
2026-06-17 14:35:58 -04:00
b44a100bad Add collapsible functionality for management blocks in event interface
This commit introduces a collapsible feature for the management blocks in the event interface, allowing users to expand or collapse sections for better navigation. New CSS styles have been added to support this functionality, and a button for toggling the visibility of the content has been implemented. The version code has been incremented to 4.72.651 to reflect these changes.
2026-06-17 14:32:49 -04:00
ff5533bee1 Add session handling for navigation context in waiting list management
This commit enhances the waiting list management functionality by preserving the original navigation context through session variables. It introduces logic to store and retrieve the 'from' parameter, ensuring users return to the correct page after actions. Additionally, the back navigation URL generation has been updated to utilize the new context-aware functions, improving user experience and code maintainability.
2026-06-17 14:27:09 -04:00
af07182682 Refactor back navigation URL handling in event management interface
This commit introduces a new function, fxGetPromoteurBackUrl, to streamline the generation of back navigation URLs based on the event ID and the 'from' parameter. The event management interface has been updated to utilize this function, improving code maintainability and enhancing user navigation experience. The changes ensure that the correct return URL is generated based on the context of the user's navigation.
2026-06-17 14:20:15 -04:00
3f04015669 Add conditional navigation item for managing event epreuves based on quantity modifiability
This commit introduces a new navigation item in the promoteur panel that appears only if the event's quantity is modifiable. The SQL query to check the modifiability has been added, and the corresponding UI element is displayed conditionally. Additionally, the icon for the existing management link has been updated for consistency. These changes enhance the user experience by providing relevant options based on event settings.
2026-06-17 14:16:27 -04:00
b773f767b7 Refactor event management interface by replacing V2 with new gestion epreuves functionality
This commit updates the event management interface by replacing the previous V2 implementation with a new gestion epreuves section. The associated PHP files have been modified to include the new functionality, and the old V2 files have been removed. CSS styles have been cleaned up to reflect these changes. The version code has been incremented to 4.72.650 to signify the updates made.
2026-06-17 14:13:08 -04:00
8f49173141 Add new functionality for managing event V2 in promoteur panel
This commit introduces a new section in the promoteur panel for managing event V2, including a dedicated function to display event details and readonly quantities. The UI has been updated with new CSS styles for better layout and presentation. Additionally, a new navigation item has been added for accessing the V2 management interface. The version code has been incremented to 4.72.649 to reflect these changes.
2026-06-17 14:03:58 -04:00
6e2f357f14 Update readonly notice text in event management panel for improved clarity and increment version code to 4.72.648 in inc_settings.php. 2026-06-17 13:38:19 -04:00
be68e4f21e Add MySQL connection diagnostics to diag.php
This commit introduces a new section in diag.php to test MySQL connections for different environments (Main, Prod, PreProd) and outputs the connection status along with error messages if applicable. Additionally, it removes the previous promoteur file check and enhances the overall diagnostic output for better clarity. This aims to improve troubleshooting capabilities for database connectivity issues.
2026-06-17 13:36:39 -04:00
d532b767fb Refactor event management panel to improve layout and enhance user experience with updated CSS styles and functionality. Increment version code to 4.72.648 to reflect these changes. 2026-06-17 13:32:00 -04:00
2c6bf1ad5c Implement event management panel with new readonly statistics and waiting list display
This commit introduces a new function to render the event management panel, showcasing readonly quantities and waiting list statistics. The associated CSS styles have been added to enhance the layout and presentation of the panel. The version code has been updated to 4.72.647 to reflect these changes, aiming to improve user experience in managing event parameters.
2026-06-17 13:21:23 -04:00
cf6a71f5e2 Update event management UI to reveal management block with new content and improved comments for clarity. Adjustments made to enhance user understanding of quantities and parameters for events. 2026-06-17 13:19:18 -04:00
aed8aa7e8f Enhance documentation functionality by adding fxDocSchemaReady function and updating fxBibBlockHeader to support optional documentation overlay. Increment version code to 4.72.646 in inc_settings.php. 2026-06-17 13:14:51 -04:00
f3ed7d6ea7 Remove unused CSS styles for event management and update version code to 4.72.645 in inc_settings.php 2026-06-17 13:11:08 -04:00
1b08f543b0 Refactor event management UI by simplifying header functions and hiding management block
This commit streamlines the event management UI by simplifying the `fxBibBlockHeader` function and hiding the management block for the event. The management block is now set to display as hidden, with a placeholder for future content. Additionally, the comments have been updated to reflect the next steps in the UI development. These changes aim to enhance the clarity and maintainability of the code while preparing for upcoming features.
2026-06-17 13:10:44 -04:00
4b5f6a3b1e Implement event management panel with new CSS styles and PHP function for readonly quantities
This commit introduces a new event management panel in the PHP code, displaying readonly quantities and waiting list statistics. The associated CSS styles have been added to enhance the layout and presentation of the panel. Additionally, translations for the readonly notice have been included in the SQL script. The version code has been updated to 4.72.644 to reflect these changes, aiming to improve user experience in managing race parameters.
2026-06-17 12:58:06 -04:00
c791d44ac0 Update event management UI and documentation with new styles and translations
This commit modifies the CSS for the event management block, changing its display properties to enhance visibility. The PHP files have been updated to reflect these changes, including the introduction of new translations for the event management section. Additionally, the version code has been incremented to 4.72.642 to reflect these updates. This aims to improve user experience and accessibility in managing race parameters and statistics.
2026-06-17 11:48:16 -04:00
030a331cd5 Add new CSS styles for educational button blocks and introduce SQL script for bib assignment documentation
This commit enhances the documentation interface by adding new CSS styles for educational button blocks, improving layout and readability. Additionally, a new SQL script is introduced to manage the bib assignment documentation, including structured content for various sections and buttons, ensuring a comprehensive guide for users. This update aims to improve user interaction and accessibility within the documentation.
2026-06-17 07:10:43 -04:00
7719737db7 Enhance documentation interface with new edit functionality and improved styles
This commit introduces a new JavaScript function to open a document editing popup, enhancing user interaction with documentation links. The CSS has been updated to improve the layout and styling of the documentation panel, including adjustments to padding and font sizes for better readability. Additionally, new PHP functions have been added to generate edit links for documentation, integrating them into the header actions for improved accessibility. The version code has been incremented to 4.72.641.
2026-06-17 07:05:32 -04:00
180b00e8ed Refactor documentation trigger and styles for improved user interaction
This commit updates the CSS for the documentation overlay, consolidating styles for the help button and ensuring it aligns with the existing button styles. The PHP files have been modified to enhance the integration of the documentation trigger within the block headers, allowing for optional documentation links. Additionally, the SQL file has been updated to improve the content of the documentation pages, providing clearer instructions and enhancing user experience.
2026-06-17 07:01:02 -04:00
7814deb02d Increment version code to 4.72.639 in inc_settings.php 2026-06-17 06:57:58 -04:00
b3f38d5d98 Add styling for document assignment header and integrate document trigger
This commit introduces new CSS styles for the document assignment header, enhancing its appearance with a blue background and hover effects. Additionally, the PHP file has been updated to include a document trigger in the assignment section, improving user interaction and access to related documentation.
2026-06-17 06:54:47 -04:00
7db84a060a debut de doc 2026-06-17 06:51:48 -04:00
48de0b0168 Restore site-wide translation pencils via afficheTexte markers (revert e2ab4e5).
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-16 18:26:15 -04:00
e2ab4e58fe Revert site-wide translation markers; keep bib v4 pencils only.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-16 18:20:55 -04:00
02bbca628f Add fxAdminTradLinkClass function and update translation button handling
This commit introduces the fxAdminTradLinkClass function to centralize CSS class management for translation links. The fxAdminTradButton function has been updated to utilize this new function, ensuring consistent styling. Additionally, a new JavaScript function is added to handle translation link clicks, opening a popup for editing translations. This enhances the user experience and maintains code clarity.
2026-06-16 18:16:09 -04:00
5816a53b11 Refactor translation handling and update CSS styles for admin buttons
This commit enhances the translation functionality by introducing a new invisible marker for translation links and updating the existing button structure to use anchor tags instead of buttons. The afficheTexte function has been modified to include the new marker, improving the integration of translation links in the HTML context. Additionally, CSS styles for the translation button have been updated for better alignment and display. The version code has been incremented to 4.72.638.
2026-06-16 18:10:02 -04:00
4779493fad Add afficheTextePage function for improved translation handling in HTML context
This commit introduces the afficheTextePage function, which enhances the translation functionality by allowing the display of translation buttons in HTML context. The existing afficheTexte function has been updated to remove the translation button display option, ensuring clearer usage. This change aims to streamline translation handling and improve maintainability of the codebase.
2026-06-16 17:55:57 -04:00
ebe2022164 Enhance translation functionality in bib management interface by introducing new helper functions for admin translation buttons and improving button visibility. Update existing functions to streamline translation handling and ensure consistent UI elements. Adjust HTML structure for better clarity and maintainability. 2026-06-16 17:52:51 -04:00
9faae13e7d Fix HTML structure in fxAdminTextEditSwitchHtml function by removing unnecessary text in the label for the text edit mode switch. This improves clarity and maintains consistency in the user interface. 2026-06-16 17:45:42 -04:00
196367adc1 fin pour aujourdhui pour assignation de dossard 2026-06-16 17:43:13 -04:00
96ffc48f84 Implement text editing mode for superadmin in dev/preprod environments. Introduce new functions for handling text edit requests and rendering environment notices. Update CSS styles for the text edit switch and related elements. Increment version code to 4.72.638 in inc_settings.php. 2026-06-16 17:39:18 -04:00
afe27bc8ec Refactor translation handling in traduction.php by introducing new helper functions for managing translation fields and improving data processing logic. Enhanced HTML structure and CSS styles for better layout and responsiveness. Updated version code to reflect changes. 2026-06-16 17:31:33 -04:00
7a8dafa49b Refactor bib management interface to enhance translation functionality and improve button visibility. Introduced new helper functions for managing translation pop-ups and updated CSS styles for the new translation button. Incremented version code to 4.72.637 in inc_settings.php. 2026-06-16 17:26:18 -04:00
b7ec814231 Increment version code to 4.72.636 in inc_settings.php. 2026-06-16 17:09:52 -04:00
597cdf4a52 Refactor bib management interface to support empty labels in column headers and enhance layout for statistics display. Updated CSS styles for improved responsiveness and adjusted translation handling in Python scripts for better clarity in the bib management system. 2026-06-16 17:09:35 -04:00
a48617db7d Enhance bib management functions by adding support for empty labels in column headers and improving fallback logic in the fxBibTexte function. Updated column header rendering to include default labels for better clarity. Adjusted layout for statistics display in the bib range header. 2026-06-16 17:09:07 -04:00
506f999920 Refactor bib management interface to improve action group visibility and layout. Introduced new helper functions for managing UI states and enhanced button visibility logic. Updated CSS for better responsiveness and incremented version code to 4.72.635 in inc_settings.php. 2026-06-16 16:59:31 -04:00
8fc0370f70 Refactor bib management interface to enhance button visibility and layout. Updated CSS for better responsiveness and added new functionality for batch action groups. Incremented version code to 4.72.634 in inc_settings.php. 2026-06-16 16:43:57 -04:00
780a1c140b Enhance bib management functionality by adding sequence detection and improving UI state synchronization. Introduced a new helper function to check for saved sequences and updated button visibility logic based on sequence presence. Adjusted rendering of action buttons in the assignment interface to reflect the current state more accurately. 2026-06-16 16:36:06 -04:00
bea4db8386 MSIN-4379: Update bib management interface with new 'View' column and improved layout. Adjusted CSS for better responsiveness and added dynamic button for viewing bib details. Incremented version code to 4.72.633 in inc_settings.php. 2026-06-16 16:31:37 -04:00
55ba3f154b Enhance CSS styles for inline help elements and update column header structure in PHP. Adjusted grid layout in style.css and wrapped header content in a span for better styling. Updated version code to 4.72.632 in inc_settings.php. 2026-06-16 16:28:01 -04:00
e43cff9762 Update version code to 4.72.63 in inc_settings.php 2026-06-16 16:23:32 -04:00
f9011a3dc2 MSIN-4379: bump version 4.72.63
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-16 16:15:17 -04:00
28ce5f929e MSIN-4379: Refactor bib help system with dynamic inline assistance and improved tooltip handling. Updated JavaScript selectors and added new CSS styles for inline help elements. Introduced new Python scripts for generating translation files in both Excel and SQL formats. 2026-06-16 16:08:57 -04:00
0bfe3aa264 MSIN-4379: aide Tippy multi-lignes et fallback traductions legacy.
Les pop-ups (?) respectent les retours à la ligne de info_aide ; lecture seule des clés info créées sous ajax_bib_range.php en attendant nettoyage BD.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-16 15:24:06 -04:00
d6cd0df5ec Refactor AJAX context in bib range handling to ensure translations are consistently linked to compte.php. Simplified language initialization and removed unnecessary conditionals for improved clarity. 2026-06-16 15:12:44 -04:00
08a4ba52f8 Refactor bib management functions for improved dynamic text handling and AJAX context. Updated text retrieval methods to enhance user experience and ensure consistency across components. Adjusted JavaScript selectors for better element targeting in the UI. 2026-06-16 15:06:10 -04:00
e55665e0f3 Amélioration de l'interface utilisateur pour la gestion des dossards : ajout de textes d'aide dynamiques, refonte des boutons d'action et ajustement des styles CSS pour une meilleure présentation. Mise à jour des sélecteurs pour les éléments d'information et réorganisation des colonnes dans le layout. 2026-06-16 14:50:30 -04:00
797e1e10ba changement visuel dossard 2026-06-16 14:37:43 -04:00
cb28d1b553 Retirer les identifiants SQLTools du dépôt et ajouter un fichier exemple
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-16 14:28:22 -04:00
909162d49f Merge branch 'dev' of http://git.progiweb.ca/stephan/ms1inscription-v5 into dev 2026-06-16 14:10:33 -04:00
f987d6e7b6 changement de visul dossard 2026-06-16 14:09:53 -04:00
4a548b2278 Merge remote-tracking branch 'gitea/dev' into dev 2026-06-16 13:07:23 -04:00
14303963b9 recu don test 2026-06-16 13:05:18 -04:00
e79d2d9e91 correction contact MSIN-4380 2026-06-04 10:42:17 -04:00
60c51e6d94 correction contact MSIN-4380 2026-06-04 10:36:53 -04:00
c5f004b7af Merge branch 'dev' of http://git.progiweb.ca/stephan/ms1inscription-v5 into dev 2026-06-04 09:08:16 -04:00
888e265a73 correction fiche contact 2026-06-04 09:08:08 -04:00
fada0b7595 Merge remote-tracking branch 'gitea/dev' into dev 2026-06-04 08:45:45 -04:00
c534a7f846 patch cookie prod et excel pour federation nat 2026-06-04 08:45:30 -04:00
5a3ce23a4b test 2026-06-03 15:09:00 -04:00
2ee8ee21d2 Fix: Added array checks for $rows in fxGetRangesForBatchAssign and fxLoadUsedBibsForBatch functions to prevent potential errors. 2026-06-03 14:14:56 -04:00
162052a2d1 MSIN-4379 — prêt prod 2026-06-02 15:24:55 -04:00
dabb797060 MSIN-4379 2026-06-02 15:04:03 -04:00
54d5363f09 MSIN-3821 2026-06-02 12:06:16 -04:00
85e030b93a MSIN-3821 2026-06-02 12:05:09 -04:00
1af97e7bb6 test 2026-06-02 12:01:05 -04:00
49d8bd23f4 Merge branch 'dev' of http://git.progiweb.ca/stephan/ms1inscription-v5 into dev 2026-06-02 11:59:57 -04:00
2f699ed10d MSIN-3821 ajout de v2 pour les admin 2026-06-02 11:59:53 -04:00
a30f887489 Merge remote-tracking branch 'gitea/dev' into dev 2026-06-01 11:32:47 -04:00
bdf5df0923 MSIN-4378 2026-06-01 11:32:27 -04:00
79bea0f9c7 test 2026-06-01 10:15:28 -04:00
088cfefee2 ms1.ca 2026-05-28 15:33:51 -04:00
1baefb5f9c code qr vehicule 2026-05-28 11:58:05 -04:00
504f245eb5 code qr vehicule 2026-05-28 11:55:39 -04:00
088fd91617 code qr vehicule 2026-05-28 11:52:30 -04:00
238c09357e code qr vehicule 2026-05-28 11:49:40 -04:00
c2b5b68568 test 2026-05-28 11:43:12 -04:00
789ce4947d code qr vehicule 2026-05-28 11:31:57 -04:00
ef8304c617 code qr vehicule 2026-05-28 11:26:29 -04:00
3ed9f8e1fc code qr vehicule 2026-05-28 11:18:58 -04:00
d9775ab315 MSIN-4378 2026-05-27 11:10:20 -04:00
8717130865 MSIN-4378 2026-05-27 11:09:49 -04:00
044aca6026 MSIN-4378 2026-05-27 11:09:01 -04:00
97e554fc20 MSIN-4378 2026-05-27 11:06:49 -04:00
b5ca510f1f MSIN-4378 2026-05-27 11:05:15 -04:00
4d7ebeba85 MSIN-4378 2026-05-27 11:02:20 -04:00
382b83e690 MSIN-4378 2026-05-27 10:59:27 -04:00
d016eacb57 MSIN-4378 2026-05-27 10:56:35 -04:00
8001389e39 MSIN-4378 2026-05-27 10:52:55 -04:00
0a944bd549 MSIN-4378 2026-05-27 10:52:22 -04:00
804a8bba18 MSIN-4378 2026-05-27 10:50:38 -04:00
f6398c5035 MSIN-4378 2026-05-27 10:49:07 -04:00
025e145f6f Merge branch 'dev' of http://git.progiweb.ca/stephan/ms1inscription-v5 into dev 2026-05-27 10:48:18 -04:00
d006d75e53 ajout switch qrcode MSIN-4376 2026-05-27 10:46:01 -04:00
f83bc634a3 Clic sur un œil MSIN-4370 2026-05-27 10:08:44 -04:00
b6f0b85140 trie 2026-05-26 10:36:35 -04:00
517ff7dff6 Enhance bib assignment logic in batch processing functions to support team modes and prevent duplicates. Add detailed documentation for clarity. 2026-05-26 10:11:11 -04:00
9690f1884c Refactor batch processing functions to include epr_id in summary calculations and improve bib assignment logic 2026-05-26 09:45:29 -04:00
35b5a2a605 ajout switch qrcode MSIN-4376 2026-05-25 15:59:00 -04:00
27e1ce326c ajout switch qrcode MSIN-4376 2026-05-25 15:57:50 -04:00
ed1b5b5bd2 ajout switch qrcode MSIN-4376 2026-05-25 15:49:37 -04:00
f97ee2485a ajout switch qrcode MSIN-4376 2026-05-25 15:42:04 -04:00
8cfd6f2e35 ajout switch qrcode MSIN-4376 2026-05-25 15:38:16 -04:00
4cdd814945 ajout switch qrcode MSIN-4376 2026-05-25 15:35:26 -04:00
dab41e4857 ajout switch qrcode MSIN-4376 2026-05-25 15:34:33 -04:00
a762f5e599 ajout switch qrcode MSIN-4376 2026-05-25 15:20:46 -04:00
9d0e14b74b fin MSIN-4375 2026-05-21 08:52:44 -04:00
bd0da2651b fin MSIN-4375 2026-05-20 17:58:33 -04:00
889ceddf79 test MSIN-4375 2026-05-20 17:56:59 -04:00
bba116e7b6 test MSIN-4375 2026-05-20 17:56:35 -04:00
525ddba26f test MSIN-4375 2026-05-20 17:54:01 -04:00
5d35f38985 test MSIN-4375 2026-05-20 17:53:32 -04:00
b4864a2278 test MSIN-4375 2026-05-20 17:49:43 -04:00
a0d879502a test MSIN-4375 2026-05-20 17:42:06 -04:00
b2ed26a0aa test MSIN-4375 2026-05-20 17:39:22 -04:00
7a1685e7fa test MSIN-4375 2026-05-20 17:36:52 -04:00
f7b1a18365 test MSIN-4375 2026-05-20 17:36:01 -04:00
181b3f7d1c test MSIN-4375 2026-05-20 17:35:46 -04:00
13d6f0752a test MSIN-4375 2026-05-20 17:35:13 -04:00
98109fa0ab test MSIN-4375 2026-05-20 17:26:28 -04:00
0293ce9099 test MSIN-4375 2026-05-20 15:44:49 -04:00
36dc2f6ab0 test MSIN-4375 2026-05-20 15:44:24 -04:00
2efb46fc59 test MSIN-4375 2026-05-20 15:43:12 -04:00
ac21e4aa81 correction suite a la mep 2026-05-20 15:42:01 -04:00
509619520d enlever lancien MSIN-4370 2026-05-16 16:18:15 -04:00
de7e5ebe7f ajout undossard equipe meme participent MSIN-4370 2026-05-16 14:51:54 -04:00
891fed7c66 ajout undossard equipe meme participent MSIN-4370 2026-05-16 12:12:11 -04:00
f7f38fa752 ajout undossard equipe meme participent MSIN-4370 2026-05-16 12:09:55 -04:00
7b87f18d1a ajout undossard equipe meme participent MSIN-4370 2026-05-16 12:06:26 -04:00
4bd33db912 ajout undossard equipe meme participent MSIN-4370 2026-05-16 12:04:33 -04:00
41ecfd0756 ajout undossard equipe meme participent MSIN-4370 2026-05-16 12:01:31 -04:00
afc71f0d58 ajout undossard equipe meme participent MSIN-4370 2026-05-16 11:57:10 -04:00
a5d85d28fb ajout undossard equipe meme participent MSIN-4370 2026-05-16 11:55:07 -04:00
f55852a14f ajout undossard equipe meme participent MSIN-4370 2026-05-16 11:54:19 -04:00
3eff5b3c3f ajout undossard equipe meme participent MSIN-4370 2026-05-16 11:48:24 -04:00
5c35dd3c1b ajout undossard equipe meme participent MSIN-4370 2026-05-16 11:46:26 -04:00
1f5cbcc8ba ajout undossard equipe meme participent MSIN-4370 2026-05-16 11:42:46 -04:00
175d7e11db ajout undossard equipe meme participent MSIN-4370 2026-05-16 11:39:43 -04:00
95204ed0f5 ajout undossard equipe meme participent MSIN-4370 2026-05-16 11:16:50 -04:00
bb62c4c8b6 ajout undossard equipe meme participent MSIN-4370 2026-05-16 11:10:53 -04:00
30bdcd1423 ajout undossard equipe meme participent MSIN-4370 2026-05-16 11:08:09 -04:00
734f9a899d ajout undossard equipe meme participent MSIN-4370 2026-05-16 11:05:07 -04:00
688f864c95 ajout undossard equipe meme participent MSIN-4370 2026-05-16 11:01:32 -04:00
57ffc59524 correction MSIN-4370 2026-05-16 10:40:38 -04:00
714b0e11ed correction MSIN-4370 2026-05-16 10:27:40 -04:00
b3ebb337c4 correction MSIN-4370 2026-05-16 10:26:35 -04:00
cc897bcaaa correction MSIN-4370 2026-05-16 10:23:34 -04:00
722488006d correction MSIN-4370 2026-05-16 10:21:16 -04:00
182083b6ce correction MSIN-4370 2026-05-16 10:08:15 -04:00
0dae4463ca correction MSIN-4370 2026-05-16 10:01:08 -04:00
0cf57c81fd correction MSIN-4370 2026-05-16 09:58:24 -04:00
95e4e7bb1a Merge remote-tracking branch 'gitea/dev' into dev
# Conflicts:
#	php/inc_settings.php
2026-05-16 09:57:31 -04:00
b9f1b7d550 correction MSIN-4370 2026-05-16 09:57:11 -04:00
3b5a82b01c correction MSIN-4370 2026-05-16 09:54:01 -04:00
623895be87 correction MSIN-4370 2026-05-16 09:49:26 -04:00
911fbc788c correction MSIN-4370 2026-05-16 09:47:10 -04:00
62a6f4b7f6 Add runtime folders to gitignore 2026-05-16 09:43:22 -04:00
6d5dc43a5a Add runtime folders to gitignore 2026-05-16 09:39:09 -04:00
93e198372d Add runtime folders to gitignore 2026-05-16 09:36:40 -04:00
4d5836b0ad Add runtime folders to gitignore 2026-05-16 09:26:06 -04:00
fb2525dfa4 Add runtime folders to gitignore 2026-05-13 18:18:41 -04:00
db13175202 Add runtime folders to gitignore 2026-05-13 18:01:59 -04:00
1d850d035b Add runtime folders to gitignore 2026-05-13 17:56:46 -04:00
96f78ebcb0 Add runtime folders to gitignore 2026-05-13 17:43:18 -04:00
5957c72884 Add runtime folders to gitignore 2026-05-13 17:01:54 -04:00
16cda56e02 Add runtime folders to gitignore 2026-05-13 16:38:06 -04:00
5fa2bc3db5 Add runtime folders to gitignore 2026-05-13 16:16:10 -04:00
f90347c5ba Add runtime folders to gitignore 2026-05-13 16:05:01 -04:00
9b36733035 Add runtime folders to gitignore 2026-05-13 13:54:05 -04:00
230 changed files with 58756 additions and 3555 deletions

View File

@ -0,0 +1,21 @@
---
description: Checklist agent — monter un nouveau projet sur le modèle Progiweb
alwaysApply: true
---
# Checklist nouveau projet (agent)
Quand l'utilisateur demande de reproduire le modèle Reader / Inscription / CRM :
1. Créer la structure `.cursor/rules/{entreprise,projet,ops}/` (copier `entreprise/` depuis un projet déjà aligné).
2. Remplir `ops/20-parametres.mdc` (URLs Gitea, Bitbucket, chemins serveur, webhook, SSH).
3. Remplir `ops/20-git-deploy-playbook.mdc` (layout git root : `$HOME` vs `public_html`).
4. Remplir `projet/` (stack, conventions, préfixe JIRA).
5. Windows : remote Gitea, branche `dev`, `.gitignore` secrets / runtime.
6. Versionner `deploy_dev.php` au bon endroit (docroot) **avant** d'activer le webhook.
7. Serveur : `~/deploy/deploy_dev.sh` (chemins absolus), git init/fetch sur le bon root, tester `curl` webhook.
8. Gitea : webhook push → URL `deploy_dev.php` ; miroir Bitbucket avec sync **à chaque soumission** + intervalle filet (ex. `1h`).
9. Test bout en bout : push `dev` → commit visible sur Gitea, serveur, Bitbucket.
10. Secrets uniquement dans 1Password (référencer le titre de l'item dans `ops/20-parametres.mdc`).
Ne pas inventer Bitbucket SSH sur le serveur : le serveur tire depuis Gitea local (`127.0.0.1:3000`) en général.

View File

@ -0,0 +1,43 @@
---
description: Progiweb — modèle Git / Gitea / Bitbucket / serveur (entreprise)
alwaysApply: true
---
# Modèle Git entreprise (Progiweb)
Référentiel agent. Détails par projet → `ops/20-parametres.mdc` et `ops/20-git-deploy-playbook.mdc`.
## Rôles
| Système | Rôle |
|---|---|
| **Gitea** (`git.progiweb.ca`) | Source de vérité — push depuis Windows |
| **Bitbucket** | Miroir / backup opérationnel hors serveurs |
| **Serveur (cPanel)** | Déploiement uniquement — **jamais** de commits sur le serveur |
## Branches
- Travail quotidien : **`dev`**
- Prod : **`master`** (quand le projet l'utilise ; ne pas inventer le flux prod)
- Ne pas pousser `master` sans demande explicite
## Flux nominal
```
Windows --push--> Gitea(dev) --webhook--> serveur (deploy_dev)
|
+--miroir push--> Bitbucket(dev)
```
## Déploiement (règles absolues)
- Script shell hors webroot : typiquement `~/deploy/deploy_dev.sh`
- Déclencheur versionné dans le **docroot** : `deploy_dev.php` (sinon `git clean -fd` le détruit)
- Le script fait : `git fetch` → `git reset --hard FETCH_HEAD` → `git clean` **ciblé** (jamais un clean aveugle sur tout le home cPanel)
- Via PHP/Apache, **`HOME` est souvent absent** → chemins **absolus** dans le `.sh`
- Auth Gitea Windows : token (éviter OAuth HTTP « Bad Request »)
- Auth miroir Bitbucket : user `x-token-auth` + Repository Access Token (1Password) — pas le mot de passe web
## Nouveau projet
Suivre `entreprise/00-checklist-nouveau-projet.mdc` et recopier `entreprise/` + remplir `ops/` + `projet/`.

View File

@ -0,0 +1,30 @@
---
description: Progiweb — workflow agent (entreprise). Toujours s'y référer.
alwaysApply: true
---
# Workflow entreprise (Progiweb)
Référentiel agent — pas une doc humaine. S'appliquer à tous les projets Progiweb montés sur ce modèle.
## Avant de modifier du code
- Demander le **numéro JIRA** si l'utilisateur ne l'a pas fourni (préfixe selon le projet, voir `projet/`).
- Ne pas bloquer une simple question ou une lecture seule.
## Portée
- **Changement minimal** : uniquement ce qui est demandé.
- Pas de refactor large, renommage massif ou nettoyage opportuniste sans accord explicite.
- Cohérence locale (fichier / module) plutôt que modernisation globale.
## Incohérences voisines
1. Signaler brièvement.
2. Proposer une correction ciblée.
3. Attendre confirmation avant de sortir du périmètre.
## Fin de tâche
- Résumer : quoi, où, déploiement / SQL / étapes manuelles restantes.
- Ne pas créer de README ou docs hors demande explicite (les règles agent vivent dans `.cursor/rules/`).

View File

@ -0,0 +1,23 @@
---
description: MS1 Inscription — playbook Git/deploy (ops)
alwaysApply: true
---
# Playbook Git / deploy — MS1 Inscription
## Layout
- Le dépôt Git **est** le site (racine web = racine git), modèle historique MS1.
- Différent de Reader (git root = `$HOME` avec `ci4app` + `public_html` frères).
## Deploy
1. `~/deploy/deploy_dev.sh` : fetch `dev` → reset --hard → clean (adapter les chemins au docroot Inscription).
2. `deploy_dev.php` versionné à la racine — obligatoire avant webhook (sinon `git clean` l'efface).
3. Chemins absolus dans le `.sh` (PHP sans `HOME`).
## À vérifier / aligner si besoin
- Miroir Bitbucket : case « Synchroniser quand les révisions sont soumis » + intervalle `1h`.
- Webhook Gitea → URL HTTPS du `deploy_dev.php` d'environnement dev.
- Même philosophie entreprise que Reader (voir `entreprise/00-git-modele.mdc`).

View File

@ -0,0 +1,41 @@
---
description: MS1 Inscription — paramètres figés (ops)
alwaysApply: true
---
# Paramètres — MS1 Inscription
Secrets → 1Password uniquement.
## Git / remotes
| Clé | Valeur |
|---|---|
| Gitea (Windows) | `http://git.progiweb.ca/stephan/ms1inscription-v5.git` |
| Branche travail | `dev` |
| Branche prod | `master` |
| Bitbucket miroir | `https://bitbucket.org/progiwebinc/ms1inscription-v5.git` |
## Miroir
| Clé | Valeur |
|---|---|
| Modèle | Comme Reader : sync **à chaque soumission** + intervalle filet (ex. `1h`) |
| Auth Bitbucket | `x-token-auth` + access token (item 1Password dédié Inscription si distinct) |
## Déploiement
| Clé | Valeur |
|---|---|
| User / home typique | `devinscription` / `/home/devinscription` |
| Git root serveur | **Docroot du site** (pas le home entier — layout ≠ Reader) |
| Script | `/home/devinscription/deploy/deploy_dev.sh` |
| Webhook PHP | `deploy_dev.php` à la **racine du dépôt** (versionné) |
| Contenu PHP | `shell_exec('/home/devinscription/deploy/deploy_dev.sh 2>&1')` |
## SSH / SQLTools
| Clé | Valeur |
|---|---|
| Port SSH | **7822** |
| Exemple | `.vscode/settings.json.example` |

View File

@ -0,0 +1,32 @@
---
description: Incrémenter _VERSION_CODE après modification CSS/JS servis par Minify
globs: css/**/*.css,js/**/*.js,min/groupsConfig.php,inc_footer_scripts.php,inc_header.php
alwaysApply: false
---
# Cache busting CSS / JS (MS1 inscription)
Les feuilles de style et scripts du site public passent par Minify :
- CSS : `/min/g=css&version=v{_VERSION_CODE}` (`inc_header.php`)
- JS : `/min/index.php?g=js&version=v{_VERSION_CODE}`
`_VERSION_CODE` est défini dans **`php/inc_settings.php`** (environnement local/dev).
## Obligation
Après **toute modification** dun fichier inclus dans `min/groupsConfig.php` (ex. `css/style.css`, `css/ms1-doc.css`, JS du groupe `js`) **ou** dun asset chargé avec `?version=v{_VERSION_CODE}` :
1. Incrémenter le **dernier segment** de `_VERSION_CODE` dans `php/inc_settings.php`
Ex. `4.72.659` → `4.72.660`
2. Mettre à jour `_DATE_CODE` à la date du jour (YYYY-MM-DD).
Ne pas oublier cette étape dans le même changement que le CSS/JS — sinon le navigateur garde lancien bundle en cache.
## Vérification rapide
Le pied de page affiche : `code (v{_VERSION_CODE})` — utile pour confirmer que la nouvelle version est déployée.
## Autres environnements
Fichiers séparés si déploiement ciblé : `php/inc_settingsprod.php`, `inc_settings_baloo.php`, etc. En dev local, cest **`php/inc_settings.php`** qui compte.

View File

@ -0,0 +1,59 @@
---
description: Documentation du code et des changements MS1
alwaysApply: true
---
# Documentation MS1
## Commentaires dans le code
- Documenter la **logique métier non évidente** (règles d'inscription, paiement, permissions, états annulés, etc.).
- Pour une nouvelle fonction ou un bloc important : courte description du **pourquoi**, pas seulement du quoi.
- Référencer le ticket : `// MSIN-4405` ou bloc PHPDoc `* MSIN-4405 — …`.
- Langue : **français** pour les commentaires métier (comme le reste du projet), sauf si le fichier est entièrement en anglais.
## Libellés UI (table `info`)
- Tout texte affiché à lutilisateur (FR/EN) passe par **Info** (`afficheTexte` / `fxInscrGestionT` / clés `info_clef`), **jamais** codé en dur dans le PHP/JS.
- Ça inclut aussi : en-têtes de colonnes, compteurs (« 12 inscriptions »), pastilles, `title`/`aria-label`, messages dinvite — **même si `aria-hidden`**.
- Interdit : `if ($strLangue === 'fr') { echo '…'; } else { echo '…'; }`, ternaires FR/EN, chaînes collées dans un `echo` HTML.
- Nouvelle clé = script SQL `sql/MSIN-xxxx-….sql` (INSERT dans `info`, fr + en) **dans le même lot** que le PHP.
- Les fallbacks PHP FR/EN (`fxInscrGestionFallbackTexte`, etc.) = filet de secours uniquement ; **pas** la source de vérité, et ne remplacent **pas** le script SQL.
Avant de terminer une tâche UI : rechercher dans les fichiers touchés les motifs `=== 'fr'`, `=== 'en'`, `$blnFr ?`, et les libellés français littéraux hors commentaires.
## AJAX multi-pages (Info)
Tout endpoint AJAX qui lit/écrit des libellés Info et peut être appelé depuis plusieurs pages (compte, hub, `/mobile`) doit **forcer le contexte `compte.php`** avant les includes utiles :
```php
define('MS1_…_AJAX', true); // optionnel, pattern existant
$_SERVER['PHP_SELF'] = '/compte.php';
$_SERVER['SCRIPT_NAME'] = '/compte.php';
$vPage = 'compte.php';
$vtexte_page = obtenirTextepage($vPage, $strLangue, 2);
```
Objectif : éviter les clés créées sous `info_prg=ajax_….php` et les libellés manquants selon la page appelante. Références : `ajax_promoteur_hub.php`, `ajax_bib_range.php`, `ajax_inscr_gestion.php`.
## Scripts SQL
En-tête obligatoire en tête de fichier :
```sql
-- MSIN-xxxx — Titre court
-- Contexte / prérequis (scripts à exécuter avant)
-- Notes prod (idempotent, safe pilote, etc.)
```
- Indiquer l'ordre d'exécution si plusieurs scripts forment une migration.
- Préférer les scripts **idempotents** quand c'est possible (pattern déjà utilisé dans `sql/`).
## Résumé des changements (agent)
À la fin d'une implémentation, fournir :
1. **Quoi** — comportement ajouté ou corrigé.
2. **Où** — fichiers principaux touchés.
3. **Déploiement** — SQL, `_VERSION_CODE`, config, étapes manuelles.
Ne pas créer de fichiers README ou docs hors demande explicite.

View File

@ -0,0 +1,38 @@
---
description: Conventions PHP legacy MS1 inscription (site + superadm)
globs: *.php, php/**/*.php, superadm/**/*.php, ajax*.php, auto_*.php
alwaysApply: false
---
# Conventions PHP MS1
Code applicatif legacy (racine, `php/`, `superadm/`). Ne pas imposer ces règles à `v3_ci4/`, `rapports/libs/`, vendors ou librairies tierces.
## Style à respecter
- **Suivre le fichier ouvert** : indentation, `array()`, guillemets, style des `if` — ne pas « moderniser » PHP dans un diff parallèle.
- **Préfixes de variables** du projet : `$str`, `$int`, `$tab`, `$bln`, `$mem`, `$v…` pour config globale.
- **Fonctions métier** : préfixe `fx` (`fxNbItemsPanier`, `inc_fonctions.php`). Réutiliser les helpers existants avant d'en créer.
- **Includes** : `php/inc_settings.php`, `php/inc_fonctions.php`, patterns `inc_*` / `inc_fx_*` déjà en place.
## Qualité
- Code **propre et lisible** dans la zone modifiée ; pas de code mort ni de `var_dump` laissé en prod.
- **Pas de sur-ingénierie** : pas d'abstraction pour un seul appel, pas de classes si le module voisin est procédural.
- Sécurité : échapper les sorties HTML, requêtes préparées / fonctions BD existantes — ne pas introduire de concat SQL brute.
## Version et assets
- CSS/JS servis par Minify : voir la règle `css-js-version-bump` — incrémenter `_VERSION_CODE` et `_DATE_CODE` dans `php/inc_settings.php`.
## Exemple de référence ticket
```php
// MSIN-4290
define('QR_SECRET_KEY', '…');
/**
* MSIN-4405 — Détermine, en LECTURE SEULE, pourquoi une inscription est annulée.
*/
function fx…()
```

View File

@ -0,0 +1,11 @@
---
description: MS1 Inscription — stack et préfixe JIRA (projet)
alwaysApply: true
---
# Projet MS1 Inscription — stack
- Stack : PHP legacy (racine / `php/` / `superadm/`) + modules CI4 optionnels (`v3_ci4/`).
- Préfixe JIRA : **`MSIN-xxxx`**.
- Scripts SQL : `sql/MSIN-xxxx-description-courte.sql`.
- Workspace local typique : `C:\dev_v4\ms1inscription4`.

View File

@ -0,0 +1,45 @@
---
description: MS1 modules v2 — bib v4 et gestion inscriptions (hors legacy)
globs: js/v2/**,php/inc_v2_assets.php,inc_tableau_gestion_epreuves.php,inc_tableau_inscriptions_gestion.php,ajax_bib_range.php,ajax_inscr_gestion.php,php/inc_fx_promoteur.php,php/inc_fx_inscriptions_gestion.php
alwaysApply: false
---
# Modules v2 MS1
## Périmètre v2 (non prod, développement actif)
- **Bib assignments v4** — `#module-bib`, `fxShowBibTool4`, `ajax_bib_range.php`
- **Gestion inscriptions v2** — `#module-inscr-gestion`, `inc_fx_inscriptions_gestion.php`, `ajax_inscr_gestion.php`
## Règle JS
- **Ne jamais** ajouter de code v2 dans `inc_footer_scripts.php`.
- Nouveau JS v2 → `js/v2/` (fichier existant ou nouveau selon le module).
- Enregistrer le script sur la page concernée : `fxV2RegisterScript('bib-assignments')` ou `'inscr-gestion'`.
- Config PHP → JS : `data-*` sur le module (bib) ou `window.MS1_V2_INSCR_GESTION` (inscr).
## Legacy (ne pas toucher sauf pont minimal)
- `.btn_bib` jQuery, `fxShowBibTool2/3`, `inc_tableau_dossards.php`
## Fichiers de référence
| Rôle | Fichier |
|------|---------|
| Enregistrement / rendu scripts | `php/inc_v2_assets.php` |
| Bib v4 client | `js/v2/bib-assignments.js` |
| Inscr gestion client | `js/v2/inscr-gestion.js` |
## Visuel panneaux v2 (standard)
Réutiliser le même langage couleur que la **fiche** (`inscr-gestion-fiche-section--*`) et le **détail dossards** (`#module-bib .bib-tool-detail`) — pas inventer une nouvelle palette par écran.
| Rôle | Classes | En-tête / accent | Corps |
|------|---------|------------------|-------|
| Info / lecture | `--details`, panneau `--search`, **liste résultats** | fond `#e7f1ff`, bordure gauche `#007bff`, texte `#004085` | `#f5f9ff` / hover ligne `#f0f6ff` |
| Gestion / attention | `--gestion` | `#fff8e6` / `#e0a800` / `#664d03` | `#fffdf5` |
| Édition / ok | `--editable` | `#e8f5e9` / `#28a745` / `#155724` | `#f4faf5` |
| Outil caméra | panneau `--qr` | `#f3eef9` / `#6f42c1` | `#faf8fd` |
| Détail bib | `.bib-tool-detail` | bordure `#2f5fd0`, titre bleu | corps `#f4f8fc` |
Conventions communes : `border-radius` 68px, bordure gauche 4px sur les en-têtes de section/panneau, labels uppercase discrets (`1112px`, `#6c757d` / `#9aa0a6`) quand on est en mode « carte ». Ne pas aplatir en gris `#f8f9fa` seul sur un panneau métier important.

View File

@ -0,0 +1,32 @@
---
description: Workflow MS1 — JIRA, validation avant gros changements, code legacy
alwaysApply: true
---
# Workflow MS1 inscription
## Numéro JIRA (MSIN-xxxx)
- Au début d'une tâche de développement (nouvelle feature, correctif, refactor), **demander le numéro JIRA** si l'utilisateur ne l'a pas fourni.
- Ne pas bloquer une simple question ou une lecture du code — seulement quand on s'apprête à modifier des fichiers.
- Référencer le ticket dans le code et les scripts SQL : `// MSIN-4290` ou `-- MSIN-4379 — description`.
- Nommer les nouveaux scripts SQL : `sql/MSIN-xxxx-description-courte.sql`.
## Portée des changements
- **Changement minimal** : corriger uniquement ce qui est demandé.
- Pas de refactor large, renommage massif ou « nettoyage » opportuniste sans accord explicite.
- Pour le code legacy (~10 ans), privilégier la cohérence locale (fichier / module touché) plutôt qu'une modernisation globale.
## Incohérences repérées
Quand du code voisin contredit le pattern du fichier ou du module :
1. **Signaler** brièvement l'incohérence à l'utilisateur.
2. **Proposer** une correction ciblée ou l'alignement sur le pattern dominant du module.
3. **Attendre confirmation** avant de corriger du code hors périmètre de la tâche.
## Fin de tâche
- Résumer ce qui a changé (fichiers, comportement, SQL à exécuter).
- Mentionner les étapes manuelles restantes (déploiement, import SQL, cache).

52
.gitignore vendored
View File

@ -7,6 +7,12 @@
node_modules/
dist/
# Composer artifacts (ne jamais committer vendor/)
/v3_ci4/vendor/
# IDE — ne jamais committer les connexions SQL (copier settings.json.example)
.vscode/settings.json
# Compiled Java class files
*.class
@ -51,22 +57,32 @@ Thumbs.db
/data/
/pdf/dons/
/images/evenements/
v3_ci4/writable/
/v3_ci4/writable/
*.log
error_log
webhooks/paypaldevclient/paypal_incoming.json
webhooks/paypaldevtest/paypal_incoming.json
preprod/dons
preprod/images/evenements
preprod/pdf/dons/
preprod/pdf/questions/
preprod/images/backgrounds/
preprod/pdf/cartes/
preprod/pdf/template_carte/
preprod/pdf/template_dons/
upload/
test/
tpp/
qr/
bp/
fnq/
/error_log
/webhooks/paypaldevclient/paypal_incoming.json
/webhooks/paypaldevtest/paypal_incoming.json
/preprod/dons
/preprod/images/evenements
/preprod/pdf/dons/
/preprod/pdf/questions/
/preprod/images/backgrounds/
/preprod/pdf/cartes/
/preprod/pdf/template_carte/
/preprod/pdf/template_dons/
/upload/
/test/
/tpp/
/qr/
/output/
/bp/
/fnq/
/images/backgrounds/
/pdf/questions/
/preprod/images/backgrounds/
/preprod/images/evenements/
/v3_ci4/writable/debugbar/
/webhooks/paypaldevclient/
/webhooks/paypaldevtest/
webhooks/paypal/
paypal_advanced/error_log

View File

@ -1,24 +1,21 @@
RewriteEngine On
RewriteBase /
# ====================================
# CI4 ROUTES PRIORITAIRES
# ====================================
# Redirect www — prod client seulement (en dev le redirect etait commente pour la meme raison)
RewriteCond %{HTTP_HOST} !^www\. [NC]
RewriteCond %{HTTP_HOST} !^preprod\. [NC]
RewriteCond %{HTTP_HOST} !\.progiweb\.net$ [NC]
RewriteRule ^(.*)$ https://www.%{HTTP_HOST}/$1 [R=302,L]
# CI4 (dev / v3) — inoffensif en prod si v3 absent
RewriteRule ^test/?$ v3/index.php/test [E=IS_CI4:1,L]
RewriteRule ^admin-dashboard/?$ v3/index.php/admin-dashboard [E=IS_CI4:1,L]
RewriteRule ^resultats-nouveaux/?$ v3/index.php/resultats-nouveaux [E=IS_CI4:1,L]
# ====================================
# STOP SI CI4
# ====================================
RewriteCond %{ENV:IS_CI4} =1
RewriteRule ^ - [L]
# ====================================
# LEGACY ROUTES
# ====================================
RewriteCond %{SCRIPT_FILENAME} !-d
RewriteCond %{SCRIPT_FILENAME} !-f
RewriteRule ^kc/([\w\-]+)/([\w\-]+)/?$ ./kc.php?param1=$1&param2=$2 [NC,QSA]
@ -28,6 +25,9 @@ RewriteRule ^cart/([\w\-]+)/([\w\-]+)/?$ ./panier.php?lang=en&step=$1&code=$2 [N
RewriteRule ^compte/?([\w\-]+)?/?$ ./compte.php?lang=fr&code=$1 [NC,QSA]
RewriteRule ^account/?([\w\-]+)?/?$ ./compte.php?lang=en&code=$1 [NC,QSA]
RewriteRule ^mobile/?$ ./mobile.php?lang=fr [NC,QSA]
RewriteRule ^mobile/en/?$ ./mobile.php?lang=en [NC,QSA]
RewriteRule ^reserver/([\w\-]+)/(\d+)/?$ ./reserver.php?lang=fr&code=$1&id=$2 [NC,QSA]
RewriteRule ^book/([\w\-]+)/(\d+)/?$ ./reserver.php?lang=en&code=$1&id=$2 [NC,QSA]
@ -36,6 +36,12 @@ RewriteRule ^donate/([\w\-]+)/([\w\-]+)/?/?([\w\-]+)?/?$ ./dons.php?lang=en&code
RewriteRule ^page/([\w\-]+)/?(fr|en)?/?$ ./index.php?code=$1&lang=$2 [NC,QSA]
RewriteCond %{REQUEST_URI} !^/(rapports_js|superadm)(/|$) [NC]
RewriteRule ^([\w\-]+)/?(fr|en)?/?([\w\-]+)?/?$ ./evenements.php?code=$1&lang=$2&tab=$3 [NC,QSA]
RewriteCond %{REQUEST_URI} !^/(rapports_js|superadm/.*)$
# php -- BEGIN cPanel-generated handler, do not edit
# Set the "ea-php81" package as the default "PHP" programming language.
<IfModule mime_module>
AddHandler application/x-httpd-ea-php81___lsphp .php .php8 .phtml
</IfModule>
# php -- END cPanel-generated handler, do not edit

26
.vscode/settings.json.example vendored Normal file
View File

@ -0,0 +1,26 @@
{
// Copier vers settings.json (gitignored) et renseigner vos identifiants locaux.
"sqltools.connections": [
{
"mysqlOptions": {
"authProtocol": "default",
"enableSsl": "Disabled"
},
"ssh": "Enabled",
"previewLimit": 50,
"server": "localhost",
"port": 3306,
"sshOptions": {
"port": 7822,
"host": "YOUR_SSH_HOST",
"username": "YOUR_SSH_USER",
"password": "YOUR_SSH_PASSWORD"
},
"driver": "MariaDB",
"name": "local dev connection",
"username": "YOUR_DB_USER",
"password": "YOUR_DB_PASSWORD",
"database": "YOUR_DATABASE"
}
]
}

Binary file not shown.

File diff suppressed because it is too large Load Diff

334
ajax_inscr_gestion.php Normal file
View File

@ -0,0 +1,334 @@
<?php
/**
* Actions AJAX reservees a la fiche de gestion des inscriptions (v2).
* N'altere pas ajax_promoteur.php ni enregistrer.php (legacy promoteur).
*/
session_start();
// MSIN-4401 — Traductions toujours au nom de compte.php (jamais ajax_inscr_gestion.php).
// Evite les doublons info_prg=ajax_*.php et les libelles manquants selon la page appelante
// (hub, compte, /mobile). Meme pattern que ajax_promoteur_hub / ajax_bib_range.
define('MS1_INSCR_GESTION_AJAX', true);
$_SERVER['PHP_SELF'] = '/compte.php';
$_SERVER['SCRIPT_NAME'] = '/compte.php';
header('Content-Type: application/json; charset=utf-8');
// Filet de securite : transforme tout fatal (include manquant, fonction
// indefinie, parse error...) en reponse JSON exploitable au lieu d'un 500 muet.
$GLOBALS['__inscr_gestion_done'] = false;
register_shutdown_function(function () {
if (!empty($GLOBALS['__inscr_gestion_done'])) {
return;
}
$err = error_get_last();
$strDetail = ($err && in_array($err['type'], array(E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR), true))
? ($err['message'] . ' @ ' . $err['file'] . ':' . $err['line'])
: 'fatal inconnu';
error_log('[ajax_inscr_gestion] ' . $strDetail);
if (!headers_sent()) {
http_response_code(200);
}
echo json_encode(array('state' => 'error', 'message' => 'fatal', 'detail' => $strDetail));
});
require_once 'php/inc_fonctions.php';
require_once 'php/inc_fx_promoteur.php';
require_once 'php/inc_fx_inscriptions_gestion.php';
// Necessaire pour fxEveAccesResolveEveIdFromParId() appelee dans le case
// 'par_statut_course' : ce fichier n'est sinon charge que paresseusement
// (fxInscrGestionLoadEveAcces) APRES ce premier appel -> fatal undefined function.
require_once 'php/inc_fx_eve_acces.php';
$intComId = intval($_SESSION['com_id'] ?? 0);
$strAction = trim((string)($_REQUEST['a'] ?? ''));
$strLangue = trim((string)($_REQUEST['lang'] ?? 'fr'));
if ($strLangue === '' || !in_array($strLangue, array('fr', 'en'), true)) {
$strLangue = 'fr';
}
$vPage = 'compte.php';
$vtexte_page = obtenirTextepage($vPage, $strLangue, 2);
fxInscrGestionInitTextes($strLangue);
if ($intComId <= 0) {
$GLOBALS['__inscr_gestion_done'] = true;
// MSIN-4401 — code session : le client recharge pour afficher le login.
echo json_encode(array('state' => 'error', 'code' => 'session', 'message' => 'session'));
exit;
}
$tabRetour = array('state' => 'error');
try {
switch ($strAction) {
case 'par_statut_course':
$intParId = intval($_REQUEST['par_id'] ?? 0);
$strStatut = trim((string)($_REQUEST['par_statut_course'] ?? ''));
$intEveId = fxEveAccesResolveEveIdFromParId($intParId);
if ($intEveId <= 0 || !fxInscrGestionCanDo($intComId, $intEveId, 'inscriptions_gestion.statut_edit')) {
$tabRetour = array('state' => 'error', 'message' => 'unauthorized');
break;
}
$tabRetour = fxSetParStatutCourse($intParId, $strStatut, $strLangue);
break;
case 'refund_form':
// Charge le formulaire de remboursement mutualise pour la fiche de gestion (panneau repliable).
$strNoPanier = trim((string)($_REQUEST['no_panier'] ?? ''));
if ($strNoPanier === '') {
$tabRetour = array('state' => 'error', 'message' => 'no_panier_manquant');
break;
}
// Meme source que le Super Admin : la commande vient de inscriptions_panier_acheteurs.
$sqlCmd = "SELECT *, IF(a.com_id <> 0, (SELECT c.com_login FROM inscriptions_comptes c WHERE a.com_id = c.com_id), '') AS usager"
. " FROM inscriptions_panier_acheteurs a, inscriptions_panier_statuts s"
. " WHERE s.sta_id = a.sta_id AND a.no_panier = '" . $objDatabase->fxEscape($strNoPanier) . "'";
$recCommandes = $objDatabase->fxGetRow($sqlCmd);
if (!$recCommandes) {
$tabRetour = array('state' => 'error', 'message' => 'commande_introuvable');
break;
}
// L'eve_id de contexte = evenement de la fiche (participant), pas seulement celui du checkout.
require_once $_SERVER['DOCUMENT_ROOT'] . '/superadm/php/inc_fx_paypal.php';
$intEveContext = intval($_REQUEST['eve_id'] ?? 0);
if ($intEveContext <= 0) {
$intEveContext = intval($recCommandes['eve_id']);
}
if ($intEveContext <= 0 || !fxInscrGestionCanDo($intComId, $intEveContext, 'inscriptions_gestion.refund')) {
$tabRetour = array('state' => 'error', 'message' => 'unauthorized');
break;
}
if (!fxRefundPanierLinkedToEvent($recCommandes['no_panier'], $intEveContext, intval($recCommandes['eve_id']))) {
$tabRetour = array('state' => 'error', 'message' => 'panier_evenement_incompatible');
break;
}
$strRefundToken = fxRefundEnsureToken();
$arrRefundCtx = fxRefundComputeContext($recCommandes, $intEveContext);
// Auteur = promoteur connecte (alimente l'encart "Ajoute par" du formulaire).
$strAuteur = trim((string)($_SESSION['com_info']['com_prenom'] ?? '') . ' '
. (string)($_SESSION['com_info']['com_nom'] ?? ''));
// Markup seul (sans <script>, dernier param false) car injecte via innerHTML :
// le comportement (submit AJAX) est gere par le handler delegue dans inc_footer_scripts.php.
// NB : le resume de la transaction n'est plus inclus ici (MSIN) ; il est desormais
// accessible via le bouton "Afficher la transaction" de la barre Gestion (action transaction_resume).
$strDocAside = '';
if (function_exists('fxDocRenderRefundPromoteurAside')) {
global $vPage;
$memPage = $vPage ?? null;
$vPage = 'compte.php';
$strDocAside = fxDocRenderRefundPromoteurAside();
$vPage = $memPage;
}
$strForm = fxRenderRefundForm($recCommandes, $strRefundToken, $arrRefundCtx, $vDomaine, $strAuteur, null, false, $strDocAside);
$tabRetour = array('state' => 'ok', 'html' => $strForm);
break;
case 'bib_ocr':
// MSIN-4401 — Lecture numero dossard via Google Cloud Vision.
// Image : fichier multipart (prefere) ou base64 POST (legacy).
if (!fxInscrGestionBibOcrCloudEnabled()) {
$tabRetour = array('state' => 'error', 'message' => 'ocr_not_configured');
break;
}
$strImage = '';
if (!empty($_FILES['image']['tmp_name']) && is_uploaded_file($_FILES['image']['tmp_name'])) {
if ((int)$_FILES['image']['error'] === UPLOAD_ERR_OK && (int)$_FILES['image']['size'] > 0
&& (int)$_FILES['image']['size'] <= 4 * 1024 * 1024) {
$strBin = (string)file_get_contents($_FILES['image']['tmp_name']);
$strImage = base64_encode($strBin);
} else {
$tabRetour = array('state' => 'error', 'message' => 'image_invalid');
break;
}
} else {
$strImage = (string)($_POST['image'] ?? '');
}
if ($strImage === '') {
$tabRetour = array('state' => 'error', 'message' => 'image_invalid');
break;
}
$arrOcr = fxInscrGestionVisionReadBibNumber($strImage);
if (!empty($arrOcr['ok']) && !empty($arrOcr['number'])) {
$tabRetour = array(
'state' => 'ok',
'number' => (string)$arrOcr['number'],
);
break;
}
$strSeen = '';
if (!empty($arrOcr['text'])) {
$strSeen = preg_replace('/\D+/', '', (string)$arrOcr['text']);
}
$tabRetour = array(
'state' => 'error',
'message' => (string)($arrOcr['message'] ?? 'no_number'),
'seen' => $strSeen,
'detail' => (string)($arrOcr['detail'] ?? ''),
);
break;
case 'bib_duplicate_check':
// Controle "dossard en double" dans l'evenement (avant enregistrement legacy).
// Ne modifie rien : renvoie qui porte deja ce dossard + la politique a appliquer.
$intParId = intval($_REQUEST['par_id'] ?? 0);
$strNoBib = trim((string)($_REQUEST['no_bib'] ?? ''));
$blnTeam = (trim((string)($_REQUEST['type'] ?? '')) === 'no_equipe');
$intEveId = fxEveAccesResolveEveIdFromParId($intParId);
if ($intEveId <= 0 || !fxInscrGestionCanDo($intComId, $intEveId, 'inscriptions_gestion.bib_edit')) {
$tabRetour = array('state' => 'error', 'message' => 'unauthorized');
break;
}
$strPolicy = fxInscrGestionBibDuplicatePolicy($intEveId);
if ($strNoBib === '') {
$tabRetour = array('state' => 'ok', 'duplicate' => false, 'policy' => $strPolicy, 'who' => array());
break;
}
// Commande courante a exclure du controle (un participant ne se "double" pas lui-meme).
$intPecOrig = intval($objDatabase->fxGetVar(
'SELECT e.pec_id_original FROM resultats_epreuves_commandees e, resultats_participants p'
. ' WHERE p.par_id = ' . $intParId . ' AND p.pec_id = e.pec_id_original LIMIT 1'
));
$intCurrentEprId = (int)$objDatabase->fxGetVar(
'SELECT epr_id FROM resultats_participants WHERE par_id = ' . $intParId . ' LIMIT 1'
);
$arrDup = fxInscrGestionFindBibDuplicates($intEveId, $strNoBib, $intPecOrig, $blnTeam, $intCurrentEprId, $intParId);
$arrWho = array();
foreach ($arrDup as $rowDup) {
$arrWho[] = array(
'name' => fxInscrGestionEsc(fxUnescape($rowDup['par_nom']) . ', ' . fxUnescape($rowDup['par_prenom'])),
'epreuve' => fxInscrGestionEsc(fxInscrGestionEpreuveLabel($rowDup['epr_id'], $strLangue)),
);
}
$tabRetour = array(
'state' => 'ok',
'duplicate' => count($arrWho) > 0,
'policy' => $strPolicy,
'who' => $arrWho,
);
break;
case 'transaction_resume':
// Resume de la transaction (meme rendu que facture.php cote promoteur) + detail des
// remboursements PayPal, affichable a tout moment via le panneau repliable de la barre Gestion.
$strNoPanier = trim((string)($_REQUEST['no_panier'] ?? ''));
if ($strNoPanier === '') {
$tabRetour = array('state' => 'error', 'message' => 'no_panier_manquant');
break;
}
// Commande complete (meme source que refund_form / Super Admin) : alimente fxRefundComputeContext.
$sqlCmd = "SELECT *, IF(a.com_id <> 0, (SELECT c.com_login FROM inscriptions_comptes c WHERE a.com_id = c.com_id), '') AS usager"
. " FROM inscriptions_panier_acheteurs a, inscriptions_panier_statuts s"
. " WHERE s.sta_id = a.sta_id AND a.no_panier = '" . $objDatabase->fxEscape($strNoPanier) . "'";
$recCommandes = $objDatabase->fxGetRow($sqlCmd);
if (!$recCommandes) {
$tabRetour = array('state' => 'error', 'message' => 'commande_introuvable');
break;
}
// L'eve_id fait foi cote serveur (jamais le client). Droit granulaire transaction.
$intEveId = intval($recCommandes['eve_id']);
if ($intEveId <= 0 || !fxInscrGestionCanDo($intComId, $intEveId, 'inscriptions_gestion.transaction')) {
$tabRetour = array('state' => 'error', 'message' => 'unauthorized');
break;
}
require_once $_SERVER['DOCUMENT_ROOT'] . '/php/inc_fx_panier.php';
$strResume = fxShowPanier($strNoPanier, 0, 'web', 1, $strLangue);
// Sommaire des remboursements PayPal (vue compacte, pas la grande table Super Admin).
// N'apparait que s'il existe une capture PayPal exploitable ; sinon rien (les cas
// gratuits/non-PayPal sont deja couverts par la facture ci-dessus).
$strRefunds = '';
require_once $_SERVER['DOCUMENT_ROOT'] . '/superadm/php/inc_fx_paypal.php';
$arrRefundCtx = fxRefundComputeContext($recCommandes);
if (!empty($arrRefundCtx['ok'])) {
// renderPayPalRefundSummaryHtml() renvoie '' s'il n'y a aucun remboursement :
// dans ce cas on n'ajoute meme pas le separateur/encadre (rien a montrer).
$strSummary = renderPayPalRefundSummaryHtml($arrRefundCtx['paypaldetails']);
if (trim($strSummary) !== '') {
$strRefunds = '<div style="margin-top:14px;border-top:1px solid #cfe0ff;padding-top:12px;">'
. $strSummary
. '</div>';
}
}
// Conteneur encadre (sans bascule interne : le bouton de la barre Gestion fait office de toggle).
$strHtml =
'<div class="ms1-transaction-resume"'
. ' style="border:1px solid #2d6cdf;border-radius:10px;background:#f4f7fc;overflow:hidden;box-shadow:0 1px 4px rgba(0,0,0,.12);">'
. '<div style="padding:14px;">' . $strResume . $strRefunds . '</div>'
. '</div>';
$tabRetour = array('state' => 'ok', 'html' => $strHtml);
break;
case 'fiche_audit_history':
// MSIN-4464 — derniers changements d'un champ whitelist (popup « ! » fiche).
require_once 'php/inc_fx_fiche_audit.php';
$intParId = intval($_REQUEST['par_id'] ?? 0);
$strField = preg_replace('/[^a-z0-9_]/i', '', (string)($_REQUEST['field'] ?? ''));
$intEveId = fxEveAccesResolveEveIdFromParId($intParId);
if ($intParId <= 0 || $strField === '' || $intEveId <= 0
|| !fxInscrGestionCanDo($intComId, $intEveId, 'inscriptions_gestion.audit_view')) {
$tabRetour = array('state' => 'error', 'message' => 'unauthorized');
break;
}
$arrItems = fxFicheAuditGetRecentForField($intParId, $strField);
$arrOut = array();
if (is_array($arrItems)) {
foreach ($arrItems as $row) {
if (!is_array($row)) {
continue;
}
$arrOut[] = array(
'when' => (string)($row['created_at'] ?? ''),
'who' => (string)($row['changed_by_label'] ?? ''),
'old' => (string)($row['old_value'] ?? ''),
'new' => (string)($row['new_value'] ?? ''),
'source' => (string)($row['source'] ?? ''),
);
}
}
$tabRetour = array(
'state' => 'success',
'field' => $strField,
'items' => $arrOut,
);
break;
default:
$tabRetour = array('state' => 'error', 'message' => 'invalid_action');
break;
}
} catch (\Throwable $e) {
error_log('[ajax_inscr_gestion] ' . $e->getMessage() . ' @ ' . $e->getFile() . ':' . $e->getLine());
$tabRetour = array(
'state' => 'error',
'message' => 'exception',
'detail' => $e->getMessage() . ' @ ' . $e->getFile() . ':' . $e->getLine(),
);
}
$GLOBALS['__inscr_gestion_done'] = true;
echo json_encode($tabRetour);

49
ajax_inscr_gestion_qr.php Normal file
View File

@ -0,0 +1,49 @@
<?php
session_start();
// MSIN-4401 — Traductions toujours au nom de compte.php (jamais ajax_inscr_gestion_qr.php).
define('MS1_INSCR_GESTION_AJAX', true);
$_SERVER['PHP_SELF'] = '/compte.php';
$_SERVER['SCRIPT_NAME'] = '/compte.php';
require_once 'php/inc_fonctions.php';
require_once 'php/inc_fx_inscriptions_gestion.php';
global $vDomaine;
$strLangue = 'fr';
if (!empty($_REQUEST['lang']) && in_array($_REQUEST['lang'], array('fr', 'en'), true)) {
$strLangue = $_REQUEST['lang'];
}
$vPage = 'compte.php';
$vtexte_page = obtenirTextepage($vPage, $strLangue, 2);
fxInscrGestionInitTextes($strLangue);
header('Content-Type: application/json; charset=utf-8');
if (!isset($_SESSION['com_id'])) {
// MSIN-4401 — session morte : pas un faux "QR invalide".
echo json_encode(array('state' => 'error', 'code' => 'session', 'message' => 'session'));
exit;
}
$intEveId = 0;
if (!empty($_REQUEST['promoteur_eve_id'])) {
$intEveId = fxInscrGestionParseEveId($_REQUEST['promoteur_eve_id']);
}
if ($intEveId <= 0 || !fxInscrGestionCanScan($_SESSION['com_id'], $intEveId)) {
echo json_encode(array('state' => 'error', 'message' => fxInscrGestionT('inscr_gestion_qr_invalid')));
exit;
}
$strRaw = '';
if (!empty($_REQUEST['qr'])) {
$strRaw = $_REQUEST['qr'];
} elseif (!empty($_REQUEST['t'])) {
$strRaw = $_REQUEST['t'];
}
echo json_encode(fxInscrGestionResolveQrScan($strRaw, $intEveId, $strLangue));

View File

@ -30,7 +30,7 @@ $strAction = $intEvenement = $intNewNoBib = $intEpreuveCommandee = $intEpreuve =
//$_POST['typedemande']="test sl";
//$_POST['summary']="sujet";
$typedemande = (int)($_POST['typedemande'] ?? 4);
const JIRA_REQUEST_TYPE_SOUTIEN_TECHNIQUE = '4';
if (isset($_POST['request_type'] )) {
$strAction = $_POST['request_type'];
@ -40,82 +40,7 @@ $tabRetour = array();
$tabRetour['success']=false;
switch($strAction) {
case 'check_email':
$result=jira_recherche_email($_POST['email']);
if($result['success']==false){
//cree le user
$result=jira_creer_user($_POST['email'],$_POST['nom']);
if($result['success']==true){
$tabRetour['success']=true;
$tabRetour['accountId']=$result['statut']['accountId'] ;
}else{
$tabRetour['success']=false;
$tabRetour['accountId']="";
};
}else{
//retourne le id
$tabRetour['success']=true;
$tabRetour['accountId']= $result['jira']['accountId'];
}
if($tabRetour['success']==true){
// creer le jira
$data['serviceDeskId'] = 2; // Remplacez par l'ID réel de votre Service Desk
$data['requestTypeId'] = (string)$typedemande; // Remplacez par l'ID réel du type de demande
$data['summary']=$_POST['summary'];
$data['description']=$_POST['description'];
$data['typedemande']=$_POST['typedemande'];
$data['raiseOnBehalfOf']= $_POST['email'] ;
$tabRetourtemp=jira_create_ticket($data);
$tabRetour['success']=true;
}else
{
$tabRetour['success']=false;
$tabRetour['accountId']= "";
}
break;
case 'check_emailold':
$result=jira_recherche_email($_POST['email']);
if($result['success']==false){
//cree le user
$result=jira_creer_user($_POST['email'],$_POST['nom']);
if($result['success']==true){
$tabRetour['success']='success';
$tabRetour['accountId']=$result['statut']['accountId'] ;
}else{
$tabRetour['success']=false;
$tabRetour['accountId']="";
};
}else{
//retourne le id
$tabRetour['success']=true;
$tabRetour['accountId']= $result['jira']['accountId'];
}
if($tabRetour['success']==true){
// creer le jira
$data['serviceDeskId'] = 2; // Remplacez par l'ID réel de votre Service Desk
$data['requestTypeId'] = (string)$typedemande; // Remplacez par l'ID réel du type de demande
$data['summary']=utf8_encode($_POST['summary']);
$data['description']=utf8_encode($_POST['description']);
$data['typedemande']=utf8_encode($_POST['typedemande']);
$data['raiseOnBehalfOf']= $_POST['email'] ;
$tabRetourtemp=jira_create_ticket($data);
$tabRetour['success']=true;
}else
{
$tabRetour['success']=false;
$tabRetour['accountId']= "";
}
$tabRetour = jira_submit_contact_ticket($_POST);
break;
case 'no_equipe':
case 'no_bib':
@ -129,6 +54,149 @@ header('Content-Type: application/json; charset=utf-8');
echo json_encode($tabRetour);
exit;
function jira_get_type_demande_row(array $post)
{
global $objDatabase;
if (empty($post['typedemande']) || !isset($objDatabase)) {
return null;
}
$sqlCol = "SELECT info_texte, info_option2, info_option3 FROM info WHERE info_actif = 1 AND info_clef = 'type_de_demande' AND info_option2 = '" . $objDatabase->fxEscape($post['typedemande']) . "' LIMIT 1";
$row = $objDatabase->fxGetResults($sqlCol);
return !empty($row) ? reset($row) : null;
}
function jira_resolve_request_type_id(array $post)
{
$row = jira_get_type_demande_row($post);
if (!empty($row['info_texte']) && fxJiraIsAutreDemande($row['info_texte'])) {
return (string)($post['typedemande'] ?? JIRA_REQUEST_TYPE_SOUTIEN_TECHNIQUE);
}
return JIRA_REQUEST_TYPE_SOUTIEN_TECHNIQUE;
}
function jira_resolve_categorie_interne(array $post)
{
$strCategorie = '';
if (isset($post['categorie_interne'])) {
$strCategorie = trim((string)$post['categorie_interne']);
} else {
$row = jira_get_type_demande_row($post);
if (!empty($row['info_option3'])) {
$strCategorie = trim((string)$row['info_option3']);
} elseif (!empty($row['info_texte'])) {
$strCategorie = fxJiraMapCategorieInterne($row['info_texte']);
}
}
// Seul « Vente » est géré pour l'instant ; les autres choix n'envoient pas de catégorie interne.
return ($strCategorie === 'Vente') ? 'Vente' : '';
}
function jira_log_contact_error($context, $detail = '')
{
$line = 'jira contact [' . $context . ']';
if ($detail !== '') {
$line .= ': ' . (is_string($detail) ? $detail : json_encode($detail, JSON_UNESCAPED_UNICODE));
}
error_log($line);
}
function jira_wait_for_account_id($email, $maxAttempts = 4)
{
for ($i = 0; $i < $maxAttempts; $i++) {
if ($i > 0) {
usleep(500000);
}
$result = jira_recherche_email($email);
if (!empty($result['success']) && !empty($result['jira']['accountId'])) {
return $result['jira']['accountId'];
}
}
return '';
}
function jira_submit_contact_ticket(array $post)
{
$tabRetour = array('success' => false, 'accountId' => '');
$email = trim((string)($post['email'] ?? ''));
$nom = trim((string)($post['nom'] ?? ''));
$userJustCreated = false;
$result = jira_recherche_email($email);
$accountId = !empty($result['success']) ? ($result['jira']['accountId'] ?? '') : '';
if ($accountId === '') {
$result = jira_creer_user($email, $nom);
if (empty($result['success'])) {
jira_log_contact_error('user_creation', $result['statut'] ?? '');
return $tabRetour;
}
$userJustCreated = true;
if (is_array($result['statut'])) {
$accountId = $result['statut']['accountId'] ?? '';
}
if ($accountId === '') {
$accountId = jira_wait_for_account_id($email);
}
}
$tabRetour['accountId'] = $accountId;
$data = array(
'serviceDeskId' => 2,
'requestTypeId' => jira_resolve_request_type_id($post),
'summary' => $post['summary'] ?? '',
'description' => $post['description'] ?? '',
'categorie_interne' => jira_resolve_categorie_interne($post),
'raiseOnBehalfOf' => $accountId !== '' ? $accountId : $email,
);
$ticket = jira_create_ticket($data);
if (empty($ticket['success']) && ($userJustCreated || $accountId === '')) {
$accountId = jira_wait_for_account_id($email, 3);
if ($accountId !== '') {
$tabRetour['accountId'] = $accountId;
$data['raiseOnBehalfOf'] = $accountId;
$ticket = jira_create_ticket($data);
}
}
if (empty($ticket['success'])) {
jira_log_contact_error('ticket_creation', $ticket['statut'] ?? '');
return $tabRetour;
}
$categorieInterne = $data['categorie_interne'] ?? '';
if ($categorieInterne === '') {
$tabRetour['success'] = true;
return $tabRetour;
}
$issueKey = '';
if (is_array($ticket['statut']) && !empty($ticket['statut']['issueKey'])) {
$issueKey = $ticket['statut']['issueKey'];
}
if ($issueKey === '') {
jira_log_contact_error('categorie_interne', 'issueKey manquant après création');
return $tabRetour;
}
$update = jira_set_categorie_interne($issueKey, $categorieInterne);
if (empty($update['success'])) {
jira_log_contact_error('categorie_interne', $update['statut'] ?? '');
return $tabRetour;
}
$tabRetour['success'] = true;
return $tabRetour;
}
function jira_recherche_email($param)
{
// recherche user
@ -263,6 +331,43 @@ function jira_creer_user($email,$displayName)
return($sortie);
}
function jira_set_categorie_interne($issueKey, $categorieInterne)
{
$jiraDomain = 'https://progiwebinc.atlassian.net';
$userEmail = 'stephan@progiweb.ca';
$apiToken = 'ATATT3xFfGF0Z91qESp8j7uO783qK0E4BWsY8rhPWEmexSa4Rw3c21Spg6hYIkmVgy6Vwzb34RxqFl3sR00Kby_DuY1NdcUJyfPQYZ0_JbeEjAk9ybFgNY8-MBSABMCxzaDk4szTbWupfjxHT8sJxJMvSNxgHPds5FxXaGo2DnZICZu5dXuvWbk=F73E15AE';
$payload = json_encode(array(
'fields' => array(
'customfield_11179' => array(
'value' => $categorieInterne,
),
),
));
$ch = curl_init($jiraDomain . '/rest/api/3/issue/' . rawurlencode($issueKey));
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
'Authorization: Basic ' . base64_encode($userEmail . ':' . $apiToken),
'Content-Type: application/json',
));
$response = curl_exec($ch);
$httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$sortie = array('success' => false, 'statut' => '');
if ($httpcode === 204) {
$sortie['success'] = true;
return $sortie;
}
$sortie['statut'] = ($response !== false && $response !== '') ? $response : ('HTTP ' . $httpcode);
return $sortie;
}
function jira_create_ticket( $param)
{
@ -278,7 +383,6 @@ function jira_create_ticket( $param)
'requestFieldValues' => [
'summary' => $param['summary'],
'description' => $param['description'],
'customfield_11143' => $param['typedemande'],
],
'raiseOnBehalfOf' => $param['raiseOnBehalfOf'], // Remplacez par l'accountId du client (facultatif)
] ;

View File

@ -7,7 +7,7 @@ require_once "php/inc_fx_promoteur.php";
require_once "php/inc_fx_messages.php";
global $vDomaine;
$strAction = $intEvenement = $intNewNoBib = $intEpreuveCommandee = $intEpreuve = $intPartipcipant = $intQuantite = $strLangue = '';
$strAction = $intEvenement = $intNewNoBib = $intEpreuveCommandee = $intEpreuve = $intPartipcipant = $intQuantite = $strLangue = $strStatutCourse = '';
if (isset($_REQUEST['a'])) {
$strAction = $_REQUEST['a'];
@ -33,6 +33,9 @@ if (isset($_REQUEST['qte'])) {
if (isset($_REQUEST['lang'])) {
$strLangue = $_REQUEST['lang'];
}
if (isset($_REQUEST['par_statut_course'])) {
$strStatutCourse = $_REQUEST['par_statut_course'];
}
$vPage = 'compte.php';
$vtexte_page = obtenirTextepage($vPage, $strLangue, 2);
@ -47,6 +50,9 @@ switch($strAction) {
case 'no_bib':
$tabRetour = fxSetBibManuel($strAction, $intEvenement, $intNewNoBib, $intEpreuveCommandee, $intEpreuve, $intPartipcipant, $strLangue);
break;
case 'par_statut_course':
$tabRetour = fxSetParStatutCourse($intPartipcipant, $strStatutCourse, $strLangue);
break;
case 'teammates':
$tabRetour = fxShowTeammates($intEvenement, $intEpreuve, $intEpreuveCommandee);
break;

71
ajax_promoteur_hub.php Normal file
View File

@ -0,0 +1,71 @@
<?php
session_start();
// Traductions toujours au nom de compte.php (jamais ajax_promoteur_hub.php).
define('MS1_PROMOTEUR_HUB_AJAX', true);
$_SERVER['PHP_SELF'] = '/compte.php';
$_SERVER['SCRIPT_NAME'] = '/compte.php';
require_once __DIR__ . '/php/inc_fonctions.php';
require_once __DIR__ . '/php/inc_fx_promoteur_hub.php';
header('Content-Type: application/json; charset=UTF-8');
$strAction = $_REQUEST['a'] ?? '';
$strLangue = $_REQUEST['lang'] ?? ($_SESSION['lang'] ?? 'fr');
if (!in_array($strLangue, array('fr', 'en'), true)) {
$strLangue = 'fr';
}
$vPage = 'compte.php';
$vtexte_page = obtenirTextepage($vPage, $strLangue, 2);
$intEveId = intval($_REQUEST['eve_id'] ?? 0);
if (empty($_SESSION['com_id'])) {
// MSIN-4401 — Le JS doit renvoyer au login, pas afficher un message mort.
echo json_encode(array(
'ok' => false,
'code' => 'session',
'msg' => fxPromoteurHubTexte('promoteur_hub_ajax_session', 0),
));
exit;
}
$intComId = intval($_SESSION['com_id']);
if (!in_array($strAction, array('event_stats', 'event_links'), true) || $intEveId <= 0) {
echo json_encode(array('ok' => false, 'msg' => fxPromoteurHubTexte('promoteur_hub_ajax_invalid', 0)));
exit;
}
if (!fxPromoteurHubComHasEvent($intComId, $intEveId)) {
echo json_encode(array('ok' => false, 'msg' => fxPromoteurHubTexte('promoteur_hub_ajax_denied', 0)));
exit;
}
if ($strAction === 'event_stats') {
if (!fxPromoteurHubCanViewDashboard($intComId, $intEveId)) {
echo json_encode(array('ok' => false, 'msg' => fxPromoteurHubTexte('promoteur_hub_ajax_denied', 0)));
exit;
}
$arrBundle = fxPromoteurHubRenderEventStatsBundle($intEveId, $strLangue);
echo json_encode(array(
'ok' => true,
'flash' => $arrBundle['flash'],
'html' => $arrBundle['html'],
));
exit;
}
$arrItem = fxPromoteurHubGetEventItemForLinks($intComId, $intEveId);
if ($arrItem === null) {
echo json_encode(array('ok' => false, 'msg' => fxPromoteurHubTexte('promoteur_hub_ajax_not_found', 0)));
exit;
}
echo json_encode(array(
'ok' => true,
'html' => fxPromoteurHubRenderEventPanelContent($arrItem, $strLangue),
));

50
auto_chronotrack_sync.php Normal file
View File

@ -0,0 +1,50 @@
<?php
/**
* MSIN-4328 — Cron sync auto ChronoTrack (différentiel).
*
* Même pattern que auto_liste_attente.php :
* /usr/bin/curl -s https://VOTRE-DOMAINE/auto_chronotrack_sync.php > /dev/null 2>&1
*
* Minute=* Hour=* … → le script décide selon fréquence/fin de chaque event.
*/
ini_set('display_errors', 0);
ini_set('log_errors', 1);
ini_set('error_log', __DIR__ . '/log/php_log.txt');
@set_time_limit(300);
$strLockPath = rtrim(sys_get_temp_dir(), '/\\') . DIRECTORY_SEPARATOR . 'ms1_chronotrack_auto_sync.lock';
$fpLock = @fopen($strLockPath, 'c');
if ($fpLock === false || !flock($fpLock, LOCK_EX | LOCK_NB)) {
if (is_resource($fpLock)) {
fclose($fpLock);
}
header('Content-Type: text/plain; charset=utf-8');
echo 'busy';
exit;
}
session_start();
try {
require_once __DIR__ . '/php/inc_fonctions.php';
require_once __DIR__ . '/php/chronotrack_api/inc_bootstrap.php';
$arrResult = fxChronotrackApiSyncRunAutoCron();
$strLine = 'auto_chronotrack_sync [' . date('Y-m-d H:i:s') . '] '
. ($arrResult['message'] ?? 'done');
// Même style que liste dattente : une ligne dans log/php_log.txt
error_log($strLine);
header('Content-Type: text/plain; charset=utf-8');
echo $strLine . "\n";
} catch (Throwable $ex) {
$strErr = 'auto_chronotrack_sync ERROR [' . date('Y-m-d H:i:s') . '] ' . $ex->getMessage();
error_log($strErr);
header('Content-Type: text/plain; charset=utf-8');
echo $strErr . "\n";
}
flock($fpLock, LOCK_UN);
fclose($fpLock);

46
bib_dist_print.php Normal file
View File

@ -0,0 +1,46 @@
<?php
// MSIN-4433 — Génération PDF distribution des dossards (POST + session promoteur).
session_start();
$_SERVER['PHP_SELF'] = '/compte.php';
$_SERVER['SCRIPT_NAME'] = '/compte.php';
require_once('php/inc_fonctions.php');
require_once('php/inc_fx_promoteur.php');
$strLangue = $_SESSION['lang'] ?? 'fr';
$reqLang = $_REQUEST['lang'] ?? $_REQUEST['lng'] ?? '';
if (!empty($reqLang) && in_array($reqLang, ['fr', 'en'], true)) {
$strLangue = $reqLang;
}
$vPage = 'compte.php';
$vtexte_page = obtenirTextepage($vPage, $strLangue, 2);
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
exit;
}
$int_eve_id = (int)($_POST['eve_id'] ?? 0);
$_REQUEST['eve_id'] = $int_eve_id;
$_POST['eve_id'] = $int_eve_id;
fxBibRequireDistPrintAccess();
$tabSelections = fxBibParseDistPrintSelectionsFromPost($_POST);
$tabSave = fxBibSaveDistPrintSelections($int_eve_id, $tabSelections, $strLangue);
if (!$tabSave['success']) {
http_response_code(400);
header('Content-Type: text/plain; charset=utf-8');
echo $tabSave['message'] ?? 'Invalid selection';
exit;
}
$strPrintedBy = fxBibGetCurrentPromoteurDisplayName();
$intComId = (int)($_SESSION['com_id'] ?? 0);
fxBibSaveDistPrintMeta($int_eve_id, $intComId, $strPrintedBy);
fxBibOutputDistPrintPdf($int_eve_id, $strLangue, $strPrintedBy);
exit;

View File

@ -21,6 +21,10 @@ require_once('php/inc_logs_course.php');
require_once('superadm/php/inc_fx_excell.php');
require_once('php/inc_fx_liste_attente.php');
require_once('php/inc_fx_inscriptions_gestion.php');
require_once('php/inc_v2_assets.php');
require_once('php/inc_fx_eve_acces.php');
require_once('php/inc_fx_promoteur_hub.php');
global $objDatabase, $vPage, $vDomaine;
// vérifier les paramètres
@ -51,11 +55,34 @@ else
if (!empty($_GET['code']))
$strCode = 'compte_' . $_GET['code'];
else {
if (isset($_SESSION['com_id']))
$strCode = "compte_accueil";
else
$strCode = "compte_login";
if (isset($_SESSION['com_id'])) {
// Compte v2 : atterrissage hub (Mes événements). Sinon accueil participant.
if (fxPromoteurHubShouldUse($_SESSION['com_id'])) {
$strCode = 'compte_promoteur_hub';
} else {
$strCode = 'compte_accueil';
}
} else {
$strCode = 'compte_login';
}
}
if ($strCode === 'compte_promoteur_hub' && (!isset($_SESSION['com_id']) || !fxPromoteurHubIsAvailable($_SESSION['com_id']))) {
header('Location: ' . $vDomaine . '/' . ($strLangue === 'fr' ? 'compte' : 'account'));
exit;
}
// Ancien slug renomme (retrait du nom "mobile") : redirection 301 vers le nouveau slug "gestion".
// Conserve les parametres utiles (promoteur_eve_id, pec_id, lng...) pour ne pas casser favoris/QR.
if (($_GET['code'] ?? '') === 'inc_tableau_inscriptions_mobile') {
$strNewPath = ($strLangue === 'en') ? '/account/inc_tableau_inscriptions_gestion' : '/compte/inc_tableau_inscriptions_gestion';
parse_str($_SERVER['QUERY_STRING'] ?? '', $arrOldQs);
unset($arrOldQs['code'], $arrOldQs['lang']);
$strQsClean = http_build_query($arrOldQs);
header('Location: ' . $vDomaine . $strNewPath . ($strQsClean !== '' ? '?' . $strQsClean : ''), true, 301);
exit;
}
$recPage = fxGetPage($strCode);
if (count($recPage) < 1) {
@ -72,16 +99,17 @@ if (!empty($_GET['a']) && $_GET['code'] != 'bib_assignment') {
$strCode = $_GET['a'];
}
// empêcher d'accéder à une page où il faut être loggé et qu'on ne l'est pas
if (($strCode != "compte_login" && $strCode != "compte_motdepasse" && $strCode != "compte_nouveau" && $strCode != "compte_reset" && !isset($_SESSION['com_id'])) || (($strCode == "compte_login" || $strCode == "compte_motdepasse" || $strCode == "compte_nouveau" || $strCode == "compte_reset") && isset($_SESSION['com_id']))) {
/// MSIN-4339 Sauvegarder l'URL demandée
// Page protégée sans session : sauver la cible puis formulaire de login (MSIN-4339).
if ($strCode != 'compte_login' && $strCode != 'compte_motdepasse' && $strCode != 'compte_nouveau' && $strCode != 'compte_reset' && !isset($_SESSION['com_id'])) {
$_SESSION['redirect_after_login'] = $_SERVER['REQUEST_URI'];
header('Location: ' . $vDomaine . '/' . $strPage);
exit;
}
// Déjà connecté sur login / mot de passe / inscription : accueil compte, sans polluer redirect_after_login.
// MSIN-4401 — compte_reset exclu : géré dans fxShowFormReset (conflit autre session).
if (($strCode == 'compte_login' || $strCode == 'compte_motdepasse' || $strCode == 'compte_nouveau') && isset($_SESSION['com_id'])) {
header('Location: ' . $vDomaine . '/' . $strPage);
exit;
}
@ -226,7 +254,6 @@ if (!empty($_POST['form_action'])) {
default:
switch ($strAction) {
case 'login':
fxLoginCompte($_POST, $strLangue);
@ -308,10 +335,26 @@ if (isset($_GET['promoteur_eve_id'])) {
$intEveId = base64_decode(urldecode($_GET['promoteur_eve_id']));
$intEveIdbase = $intEveId;
$blnIsPromoteur = fxIsPromoteur( $_SESSION['com_id'],$intEveIdbase );
if (!$blnIsPromoteur) {
if ($strCode === 'compte_inc_tableau_inscriptions_gestion') {
if (!fxInscrGestionCanAccess($_SESSION['com_id'], $intEveIdbase)) {
header('Location: ' . $vDomaine . '/' . $strPage);
exit;
}
} elseif ($strCode === 'compte_inc_tableau_gestion_epreuves') {
if (empty($_SESSION['usa_id'])
&& !fxEveAccesHasAnyPermission($_SESSION['com_id'] ?? 0, $intEveIdbase, array('dossards.manage', 'epreuves.edit_qte'))) {
header('Location: ' . $vDomaine . '/' . $strPage);
exit;
}
} elseif ($strCode === 'compte_inc_tableau_eve_acces') {
require_once('php/inc_fx_eve_acces.php');
if (!fxEveAccesCanTeamInvite($_SESSION['com_id'] ?? 0, $intEveIdbase)) {
header('Location: ' . $vDomaine . '/' . $strPage);
exit;
}
} elseif (empty($_SESSION['usa_id'])
&& !fxIsPromoteur($_SESSION['com_id'], $intEveIdbase)
&& !fxEveAccesComHasV2ForEvent($_SESSION['com_id'], $intEveIdbase)) {
header('Location: ' . $vDomaine . '/' . $strPage);
exit;
}
@ -334,6 +377,69 @@ $strMetaTitle = fxRemoveHtml(fxUnescape($recPage['pag_titre_' . $strLangue]));
$strMetaDescription = fxRemoveHtml(fxUnescape($recPage['pag_description_' . $strLangue]));
$strMetaKeywords = fxRemoveHtml(fxUnescape($recPage['pag_keywords_' . $strLangue]));
$blnBoutonRetour = true;
$blnHideCompteTitle = false;
// MSIN-4401 — POST invite/revoke AVANT le HTML (sinon header Location → page blanche).
if ($strCode === 'compte_inc_tableau_eve_acces'
&& ($_SERVER['REQUEST_METHOD'] ?? '') === 'POST'
&& isset($_GET['promoteur_eve_id'])) {
require_once('php/inc_fx_eve_acces_promoteur.php');
$intEveIdPost = intval(base64_decode(urldecode($_GET['promoteur_eve_id'])));
$intActorPost = intval($_SESSION['com_id'] ?? 0);
if (!empty($_SESSION['usa_id'])) {
$intActorPost = intval($_SESSION['usa_id']);
}
$strLanguePost = ($strLangue === 'en') ? 'en' : 'fr';
$strBasePathPost = ($strLanguePost === 'en') ? '/account' : '/compte';
$strSelfUrlPost = $vDomaine . $strBasePathPost . '/inc_tableau_eve_acces?promoteur_eve_id='
. urlencode(base64_encode($intEveIdPost)) . '&lng=' . $strLanguePost;
if ($intEveIdPost > 0 && fxEveAccesCanTeamInvite($intActorPost, $intEveIdPost)) {
$strEveAccesAction = (string)($_POST['eve_acces_action'] ?? '');
if ($strEveAccesAction === 'invite') {
$arrEveAccesResult = fxEveAccesPromoteurInvite($intEveIdPost, $intActorPost, $_POST, $strLanguePost);
if (($arrEveAccesResult['state'] ?? '') === 'success') {
header('Location: ' . $strSelfUrlPost . '&ok=' . urlencode($arrEveAccesResult['message']));
exit;
}
header('Location: ' . $strSelfUrlPost . '&err=' . urlencode(
$arrEveAccesResult['message'] ?? fxEveAccesPromoteurT('eve_acces_promoteur_err_generic', $strLanguePost)
));
exit;
}
if ($strEveAccesAction === 'revoke') {
$arrEveAccesResult = fxEveAccesPromoteurRevoke(intval($_POST['ea_id'] ?? 0), $intActorPost, $intEveIdPost);
if (($arrEveAccesResult['state'] ?? '') === 'success') {
header('Location: ' . $strSelfUrlPost . '&ok=' . urlencode(
fxEveAccesPromoteurT('eve_acces_promoteur_ok_revoked', $strLanguePost)
));
exit;
}
header('Location: ' . $strSelfUrlPost . '&err=' . urlencode(
fxEveAccesPromoteurT('eve_acces_promoteur_err_revoke', $strLanguePost)
));
exit;
}
if ($strEveAccesAction === 'resend') {
$arrEveAccesResult = fxEveAccesPromoteurResendMail(
intval($_POST['ea_id'] ?? 0),
$intActorPost,
$intEveIdPost,
$strLanguePost
);
if (($arrEveAccesResult['state'] ?? '') === 'success') {
header('Location: ' . $strSelfUrlPost . '&ok=' . urlencode(
$arrEveAccesResult['message'] ?? fxEveAccesPromoteurT('eve_acces_promoteur_ok_resent', $strLanguePost)
));
exit;
}
header('Location: ' . $strSelfUrlPost . '&err=' . urlencode(
$arrEveAccesResult['message'] ?? fxEveAccesPromoteurT('eve_acces_promoteur_err_resend', $strLanguePost)
));
exit;
}
}
}
require_once("inc_header.php");
@ -343,7 +449,7 @@ require_once("inc_header.php");
<div class="container bg-white p-3 p-xl-5">
<?php
if (isset($recPage['pag_titre_' . $strLangue]) != '') {
if (isset($recPage['pag_titre_' . $strLangue]) != '' && empty($blnHideCompteTitle)) {
?>
@ -360,7 +466,7 @@ require_once("inc_header.php");
}
if (isset($_SESSION['msg'])) {
echo fxMessage($_SESSION['msg'], '', $strLangue);
} else {
} else if (empty($blnHideCompteTitle)) {
?>
<br>
<?php
@ -465,9 +571,13 @@ require_once("inc_header.php");
case 'compte_annuler_inscription':
include_once('annuler_inscription.php');
break;
case 'compte_promoteur_hub':
$blnBoutonRetour = false;
echo fxShowPromoteurHub($_SESSION['com_id'], $strLangue);
break;
case 'compte_accueil':
//MSIN-4371
if (isset($_SESSION['usa_id'])) { ?>
if (fxEveAccesCanQrTest($_SESSION['com_id'])) { ?>
<a href="qr_test.php" target="_blank" class="btn btn-primary">
QR Test
@ -688,19 +798,18 @@ require_once("inc_header.php");
$footer_script = true;
include("inc_tableau_rabais.php");
break;
// MSIN-3679
case 'compte_inc_tableau_remboursement':
$blnBoutonRetour = false;
$footer_script = true;
include("inc_tableau_remboursement.php");
break;
case 'compte_inc_tableau_inscriptions':
$blnBoutonRetour = false;
$footer_script = true;
include("inc_tableau_inscriptions.php");
break;
case 'compte_inc_tableau_inscriptions_gestion':
$blnBoutonRetour = false;
$footer_script = true;
$blnHideCompteTitle = true;
fxInscrGestionInitTextes($strLangue);
include("inc_tableau_inscriptions_gestion.php");
break;
case 'compte_inc_tableau_mail_entraineurs':
$blnBoutonRetour = false;
$footer_script = true;
@ -713,13 +822,24 @@ require_once("inc_header.php");
break;
case 'compte_inc_tableau_dossards':
$blnBoutonRetour = false;
include("inc_tableau_dossards.php");
include("inc_tableau_dossards_v2.php");
break;
case 'compte_inc_tableau_epreuves':
$blnBoutonRetour = false;
include("inc_tableau_epreuves.php");
break;
case 'compte_inc_tableau_gestion_epreuves':
$blnBoutonRetour = false;
include("inc_tableau_gestion_epreuves.php");
break;
case 'compte_inc_tableau_eve_acces':
// MSIN-4401
$blnBoutonRetour = false;
$footer_script = true;
$blnHideCompteTitle = true;
include("inc_tableau_eve_acces.php");
break;
case 'compte_inc_tableau_liste_attente':

324
css/ms1-doc.css Normal file
View File

@ -0,0 +1,324 @@
body.ms1-doc-open {
overflow: hidden;
}
/**
* MS1 — Module documentation (overlay paginé).
* Le bouton ? réutilise .btn-aide-bib (style.css) — pas de style rond dupliqué ici.
*/
.btn-aide-bib.ms1-doc-trigger {
flex-shrink: 0;
}
/* Overlay plein écran */
.ms1-doc-overlay {
position: fixed;
inset: 0;
z-index: 10050;
display: flex;
align-items: center;
justify-content: center;
padding: 16px;
}
.ms1-doc-overlay[hidden] {
display: none !important;
}
.ms1-doc-backdrop {
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.55);
cursor: pointer;
}
/* Panneau central — plus large pour le guide */
.ms1-doc-panel {
position: relative;
z-index: 1;
width: 100%;
max-width: 840px;
max-height: 92vh;
display: flex;
flex-direction: column;
background: #fff;
border-radius: 10px;
border: 1px solid #dee2e6;
overflow: hidden;
}
.ms1-doc-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
padding: 16px 20px;
border-bottom: 1px solid #dee2e6;
background: #f8f9fa;
}
.ms1-doc-header-actions {
display: flex;
align-items: center;
gap: 8px;
flex-shrink: 0;
}
.ms1-doc-title {
margin: 0;
font-size: 1.25rem;
font-weight: 600;
color: #212529;
line-height: 1.3;
}
.ms1-doc-subtitle {
margin: 4px 0 0;
font-size: 0.85rem;
color: #6c757d;
line-height: 1.35;
}
.ms1-doc-close {
flex-shrink: 0;
width: 32px;
height: 32px;
padding: 0;
border: none;
border-radius: 4px;
background: transparent;
color: #6c757d;
font-size: 24px;
line-height: 1;
cursor: pointer;
}
.ms1-doc-close:hover {
background: #e9ecef;
color: #212529;
}
.ms1-doc-body {
flex: 1;
overflow: auto;
padding: 20px 22px;
min-height: 160px;
}
.ms1-doc-page[hidden] {
display: none !important;
}
/* Typographie contenu HTML admin */
.ms1-doc-format,
.ms1-doc-content {
font-size: 0.95rem;
line-height: 1.6;
color: #212529;
}
.ms1-doc-format h2,
.ms1-doc-format h3,
.ms1-doc-content h2,
.ms1-doc-content h3 {
margin: 14px 0 8px;
font-size: 0.95rem;
font-weight: 600;
color: #212529;
}
.ms1-doc-format h2:first-child,
.ms1-doc-format h3:first-child,
.ms1-doc-content h2:first-child,
.ms1-doc-content h3:first-child {
margin-top: 0;
}
.ms1-doc-format p,
.ms1-doc-content p {
margin: 0 0 10px;
}
.ms1-doc-format ol,
.ms1-doc-format ul,
.ms1-doc-content ol,
.ms1-doc-content ul {
margin: 0 0 10px;
padding-left: 1.35rem;
}
.ms1-doc-format li,
.ms1-doc-content li {
margin-bottom: 4px;
}
.ms1-doc-format blockquote,
.ms1-doc-content blockquote {
margin: 12px 0;
padding: 10px 12px;
border-left: 3px solid #007bff;
background: #f8f9fa;
font-size: 0.85rem;
}
.ms1-doc-format table,
.ms1-doc-content table {
width: 100%;
border-collapse: collapse;
margin-bottom: 10px;
font-size: 0.85rem;
}
.ms1-doc-format th,
.ms1-doc-format td,
.ms1-doc-content th,
.ms1-doc-content td {
border: 1px solid #dee2e6;
padding: 6px 8px;
text-align: left;
vertical-align: top;
}
.ms1-doc-format th,
.ms1-doc-content th {
background: #f1f3f5;
font-weight: 600;
}
.ms1-doc-format strong,
.ms1-doc-content strong {
font-weight: 600;
}
/* Bloc pédagogique par bouton / contrôle */
.ms1-doc-bouton {
margin: 16px 0;
padding: 12px 14px;
border: 1px solid #e9ecef;
border-radius: 6px;
background: #fafbfc;
}
.ms1-doc-bouton h3,
.ms1-doc-bouton h4 {
margin: 0 0 10px;
font-size: 0.92rem;
font-weight: 600;
color: #2f5fd0;
}
.ms1-doc-bouton dl {
margin: 0;
}
.ms1-doc-bouton dt {
font-size: 0.8rem;
font-weight: 600;
color: #495057;
margin-top: 8px;
}
.ms1-doc-bouton dt:first-child {
margin-top: 0;
}
.ms1-doc-bouton dd {
margin: 2px 0 0;
font-size: 0.88rem;
line-height: 1.5;
color: #212529;
}
/* Pied — navigation */
.ms1-doc-footer {
padding: 10px 14px 12px;
border-top: 1px solid #dee2e6;
background: #f8f9fa;
}
.ms1-doc-nav-dots {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 8px;
margin-bottom: 8px;
}
.ms1-doc-dot {
width: 36px;
height: 36px;
padding: 0;
border: 2px solid #dee2e6;
border-radius: 6px;
background: #fff;
color: #495057;
font-size: 13px;
font-weight: 600;
cursor: pointer;
}
.ms1-doc-dot.is-active,
.ms1-doc-dot:hover {
border-color: #007bff;
background: #e7f1ff;
color: #0056b3;
}
.ms1-doc-nav-arrows {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
}
.ms1-doc-prev,
.ms1-doc-next {
padding: 4px 12px;
border: 1px solid #6c757d;
border-radius: 4px;
background: #fff;
color: #495057;
font-size: 0.8rem;
cursor: pointer;
}
.ms1-doc-prev:hover:not(:disabled),
.ms1-doc-next:hover:not(:disabled) {
background: #e9ecef;
}
.ms1-doc-prev:disabled,
.ms1-doc-next:disabled {
opacity: 0.45;
cursor: not-allowed;
}
.ms1-doc-counter {
font-size: 0.8rem;
color: #6c757d;
min-width: 48px;
text-align: center;
}
/* Métadonnées JSON — jamais affichées */
script.ms1-doc-meta {
display: none !important;
}
@media (max-width: 576px) {
.ms1-doc-overlay {
padding: 8px;
}
.ms1-doc-panel {
max-height: 95vh;
border-radius: 8px;
}
.ms1-doc-dot {
width: 32px;
height: 32px;
font-size: 12px;
}
}

100
css/ms1-loader.css Normal file
View File

@ -0,0 +1,100 @@
/**
* MS1 — Indicateur de chargement (coureur animé).
* Reutilisable en overlay plein ecran (#preloader) ou inline (panneaux AJAX).
*/
.ms1-loader {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0.75rem;
text-align: center;
}
.ms1-loader__runner {
width: 72px;
height: 72px;
object-fit: contain;
flex-shrink: 0;
}
.ms1-loader__label {
font-size: 1rem;
font-weight: 600;
color: #333;
line-height: 1.35;
}
.ms1-loader--inline {
padding: 1.25rem 1rem;
min-height: 120px;
}
.ms1-loader--inline .ms1-loader__runner {
width: 64px;
height: 64px;
}
.ms1-loader--inline .ms1-loader__label {
font-size: 0.9rem;
font-weight: 500;
color: #666;
}
#preloader #spinner.ms1-loader--overlay {
background: #fff;
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.18);
padding: 1.25rem 1.75rem;
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
margin: 0;
width: auto;
min-width: 200px;
font-size: inherit;
color: inherit;
border: none;
}
#preloader #spinner.ms1-loader--overlay .ms1-loader__runner {
width: 80px;
height: 80px;
}
#preloader #spinner.ms1-loader--overlay #preloader_text,
#preloader #spinner.ms1-loader--overlay .ms1-loader__label {
font-size: 1.1rem;
font-weight: 600;
}
.ms1-loader__progress {
width: 100%;
min-width: 220px;
margin-top: 0.25rem;
}
.ms1-loader__progress-track {
width: 100%;
height: 8px;
background: #e8e8e8;
border-radius: 4px;
overflow: hidden;
}
.ms1-loader__progress-fill {
height: 100%;
width: 0;
background: linear-gradient(90deg, #2e7d32, #43a047);
border-radius: 4px;
transition: width 0.25s ease;
}
.ms1-loader__progress-pct {
display: block;
margin-top: 0.35rem;
font-size: 0.85rem;
font-weight: 600;
color: #555;
}

File diff suppressed because it is too large Load Diff

View File

@ -5,7 +5,7 @@
* Date: 2020-05-26
* Time: 16:51
*/
require_once("PHPColors/Color.php"); // inclure la class
require_once(__DIR__ . '/../PHPColors/Color.php'); // inclure la class
use Mexitek\PHPColors\Color; // utiliser le namepace de la class
global $vDomaine, $vRepertoireFichiers;

3
deploy_dev.php Normal file
View File

@ -0,0 +1,3 @@
<?php
echo shell_exec('/home/devinscription/deploy/deploy_dev.sh 2>&1');

View File

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--!Font Awesome Free 6.6.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M247.2 17c-22.1-3.1-44.6 .9-64.4 11.4l-74 39.5C89.1 78.4 73.2 94.9 63.4 115L26.7 190.6c-9.8 20.1-13 42.9-9.1 64.9l14.5 82.8c3.9 22.1 14.6 42.3 30.7 57.9l60.3 58.4c16.1 15.6 36.6 25.6 58.7 28.7l83 11.7c22.1 3.1 44.6-.9 64.4-11.4l74-39.5c19.7-10.5 35.6-27 45.4-47.2l36.7-75.5c9.8-20.1 13-42.9 9.1-64.9l-14.6-82.8c-3.9-22.1-14.6-42.3-30.7-57.9L388.9 57.5c-16.1-15.6-36.6-25.6-58.7-28.7L247.2 17zM208 144a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM144 336a32 32 0 1 1 64 0 32 32 0 1 1 -64 0zm224-64a32 32 0 1 1 0 64 32 32 0 1 1 0-64z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--!Font Awesome Free 6.6.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path fill="currentColor" d="M247.2 17c-22.1-3.1-44.6 .9-64.4 11.4l-74 39.5C89.1 78.4 73.2 94.9 63.4 115L26.7 190.6c-9.8 20.1-13 42.9-9.1 64.9l14.5 82.8c3.9 22.1 14.6 42.3 30.7 57.9l60.3 58.4c16.1 15.6 36.6 25.6 58.7 28.7l83 11.7c22.1 3.1 44.6-.9 64.4-11.4l74-39.5c19.7-10.5 35.6-27 45.4-47.2l36.7-75.5c9.8-20.1 13-42.9 9.1-64.9l-14.6-82.8c-3.9-22.1-14.6-42.3-30.7-57.9L388.9 57.5c-16.1-15.6-36.6-25.6-58.7-28.7L247.2 17zM208 144a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM144 336a32 32 0 1 1 64 0 32 32 0 1 1 -64 0zm224-64a32 32 0 1 1 0 64 32 32 0 1 1 0-64z"/></svg>

Before

Width:  |  Height:  |  Size: 752 B

After

Width:  |  Height:  |  Size: 772 B

BIN
images/ms1-kc-logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 348 KiB

View File

@ -59,7 +59,8 @@ if (isset($tabEvenement) && $tabEvenement['general']['eve_commanditaires'] == "1
}
}
?>
<footer>
<footer<?php echo (defined('MS1_INSCR_GESTION_ENTRY') && MS1_INSCR_GESTION_ENTRY) ? ' class="inscr-gestion-entry-footer"' : ''; ?>>
<?php if (!(defined('MS1_INSCR_GESTION_ENTRY') && MS1_INSCR_GESTION_ENTRY)) { ?>
<div class="ms1-footer-top">
<div class="container">
<div class="row py-4">
@ -113,14 +114,20 @@ if (isset($tabEvenement) && $tabEvenement['general']['eve_commanditaires'] == "1
<div class="social">
<a href="http://www.facebook.com/pages/Ms1timing/387594717971765" target="_blank"><i class="fa fa-facebook-square"></i></a>
<a href="https://twitter.com/ms1timing" target="_blank"><i class="fa fa-twitter-square"></i></a>
<a href="/index.php?cookies" ><img style="border: none;" width="30" src="<?php echo $vDomaine; ?>/images/cookie.png" alt="cookies" /></a>
<a href="#" class="cookie-manage-link" data-cookie-manage title="<?php echo ($strLangue === 'fr') ? 'Gérer les témoins' : 'Manage cookies'; ?>">
<i class="fa cookie-manage-icon" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" focusable="false">
<path d="M247.2 17c-22.1-3.1-44.6 .9-64.4 11.4l-74 39.5C89.1 78.4 73.2 94.9 63.4 115L26.7 190.6c-9.8 20.1-13 42.9-9.1 64.9l14.5 82.8c3.9 22.1 14.6 42.3 30.7 57.9l60.3 58.4c16.1 15.6 36.6 25.6 58.7 28.7l83 11.7c22.1 3.1 44.6-.9 64.4-11.4l74-39.5c19.7-10.5 35.6-27 45.4-47.2l36.7-75.5c9.8-20.1 13-42.9 9.1-64.9l-14.6-82.8c-3.9-22.1-14.6-42.3-30.7-57.9L388.9 57.5c-16.1-15.6-36.6-25.6-58.7-28.7L247.2 17zM208 144a32 32 0 1 1 0 64 32 32 0 1 1 0-64zM144 336a32 32 0 1 1 64 0 32 32 0 1 1 -64 0zm224-64a32 32 0 1 1 0 64 32 32 0 1 1 0-64z"/>
</svg>
</i>
<span class="sr-only"><?php echo ($strLangue === 'fr') ? 'Gérer les témoins' : 'Manage cookies'; ?></span>
</a>
</div>
</div>
</div>
</div>
</div>
<?php } ?>
<div class="ms1-footer-bottom">
<div class="container">
<div class="row align-items-center">
@ -167,6 +174,48 @@ if (isset($tabEvenement) && $tabEvenement['general']['eve_commanditaires'] == "1
</div>
<!-- /MSIN-3860 -->
<style>
footer .social {
display: inline-flex;
align-items: center;
line-height: 1;
}
footer .social > a {
display: inline-flex;
align-items: center;
margin: 10px 0 10px 5px;
}
footer .social > a > i {
margin: 0;
}
footer .social a.cookie-manage-link {
color: #fff;
text-decoration: none;
}
footer .social a.cookie-manage-link:hover {
color: #f3954a;
}
footer .social a.cookie-manage-link .cookie-manage-icon {
font-size: 30px;
line-height: 1;
width: 1em;
height: 1em;
display: inline-flex;
align-items: center;
justify-content: center;
font-style: normal;
}
footer .social a.cookie-manage-link .cookie-manage-icon svg {
display: block;
width: 1em;
height: 1em;
fill: currentColor;
}
/* /MSIN-3860 */
.cookie-consent {
@ -181,12 +230,40 @@ if (isset($tabEvenement) && $tabEvenement['general']['eve_commanditaires'] == "1
}
.cookie-consent-container {
max-width: 1000px;
margin: 0 auto;
display: flex;
justify-content: center;
align-items: center;
flex-wrap: wrap;
gap: 12px;
text-align: center;
}
.cookie-consent-container p {
flex: 1 1 100%;
max-width: 900px;
margin: 0 auto 8px;
font-size: 14px;
line-height: 1.5;
}
.cookie-consent-actions {
display: flex;
gap: 10px;
flex-wrap: wrap;
justify-content: center;
}
.cookie-consent-links {
margin-top: 10px;
text-align: center;
font-size: 13px;
}
.cookie-consent-links a {
color: #0066cc;
text-decoration: underline;
}
#acceptCookies, #rejectCookies {
@ -212,117 +289,92 @@ if (isset($tabEvenement) && $tabEvenement['general']['eve_commanditaires'] == "1
background-color: #e53935;
}
#cookiePreferencesPanel {
max-width: 640px;
margin: 12px auto 0;
padding: 12px 16px;
background: #fff;
border: 1px solid #ddd;
border-radius: 4px;
text-align: left;
}
.cookie-pref-row {
display: flex;
align-items: flex-start;
gap: 10px;
margin-bottom: 10px;
}
.cookie-pref-row label {
margin: 0;
font-weight: normal;
font-size: 13px;
}
.cookie-pref-row input[type="checkbox"] {
margin-top: 3px;
}
#customizeCookies, #saveCookiePreferences {
background: #666;
border-radius: 5px;
color: white;
border: none;
padding: 10px 20px;
cursor: pointer;
font-size: 12px;
}
#saveCookiePreferences {
background: #333;
}
</style>
<!-- MSIN-3860 -->
<div id="cookieConsent" class="cookie-consent">
<p><?php echo afficheTexte('experience_cookies', 0, 1, 1); ?></p>
<div class="cookie-consent-container">
<button id="acceptCookies"><?php echo afficheTexte('bouton_acceper_cookies', 0, 1, 1); ?></button>
<button id="rejectCookies"><?php echo afficheTexte('bouton_refuser_cookies', 0, 1, 1); ?></button>
<p class="mb-2 mb-md-0"><?php echo fxCookieConsentText('experience_cookies', $strLangue); ?></p>
<div class="cookie-consent-actions">
<button type="button" id="acceptCookies"><?php echo fxCookieConsentText('bouton_acceper_cookies', $strLangue); ?></button>
<button type="button" id="rejectCookies"><?php echo fxCookieConsentText('bouton_refuser_cookies', $strLangue); ?></button>
<button type="button" id="customizeCookies"><?php echo fxCookieConsentText('bouton_personnaliser_cookies', $strLangue); ?></button>
</div>
</div>
<div id="cookiePreferencesPanel" class="cookie-consent-container d-none">
<p><strong><?php echo fxCookieConsentText('cookie_pref_titre', $strLangue); ?></strong></p>
<div class="cookie-pref-row">
<input type="checkbox" id="cookiePrefEssential" checked disabled>
<label for="cookiePrefEssential"><?php echo fxCookieConsentText('cookie_pref_essentiels', $strLangue); ?></label>
</div>
<div class="cookie-pref-row">
<input type="checkbox" id="cookiePrefAnalytics">
<label for="cookiePrefAnalytics"><?php echo fxCookieConsentText('cookie_pref_analytique', $strLangue); ?></label>
</div>
<div class="cookie-pref-row">
<input type="checkbox" id="cookiePrefMarketing">
<label for="cookiePrefMarketing"><?php echo fxCookieConsentText('cookie_pref_marketing', $strLangue); ?></label>
</div>
<div class="cookie-consent-actions">
<button type="button" id="saveCookiePreferences"><?php echo fxCookieConsentText('bouton_enregistrer_cookies', $strLangue); ?></button>
</div>
</div>
<div class="cookie-consent-links">
<a href="<?php echo $vDomaine; ?>/page/cookie/<?php echo $strLangue; ?>"><?php echo fxCookieConsentText('politique_cookie', $strLangue); ?></a>
&nbsp;|&nbsp;
<a href="<?php echo $vDomaine; ?>/page/confidentialite/<?php echo $strLangue; ?>"><?php echo fxCookieConsentText('politique_confidentialite', $strLangue); ?></a>
</div>
</br>
<a href="/page/cookie/<?php if ($strLangue != 'fr') { echo ("/en"); }?> "><?php echo afficheTexte('politique_cookie', 0, 1, 1); ?></a>&nbsp &nbsp &nbsp &nbsp
</div>
</body>
</html>
<script>
// Function to disable all clicks within a specific div
function disableClicks(divId) {
document.getElementById(divId).addEventListener('click', blockClicks, true);
}
// Function to block the click event
function blockClicks(event) {
event.stopPropagation(); // Stop the event from reaching its target
event.preventDefault(); // Prevent the default action (like navigating a link)
}
// Function to re-enable clicks within a specific div
function enableClicks(divId) {
document.getElementById(divId).removeEventListener('click', blockClicks, true);
}
//MSIN-3860
if (!getCookie('cookiesAccepted') && !getCookie('cookiesRejected')) {
$('#cookieConsent').fadeIn();
}
// Handle the acceptance of cookies
$('#acceptCookies').click(function() {
setCookie('cookiesAccepted', 'true', 365);
$('#cookieConsent').fadeOut();
});
// Handle the rejection of cookies
$('#rejectCookies').click(function() {
setCookie('cookiesRejected', 'true', 365);
$('#cookieConsent').fadeOut();
enableClicks('page');
});
// Function to set a cookie
function setCookie(cname, cvalue, exdays) {
const d = new Date();
d.setTime(d.getTime() + (exdays*24*60*60*1000));
let expires = "expires="+ d.toUTCString();
document.cookie = cname + "=" + cvalue + ";" + expires + ";path=/";
}
// Function to get a cookie
function getCookie(cname) {
let name = cname + "=";
let decodedCookie = decodeURIComponent(document.cookie);
let ca = decodedCookie.split(';');
for(let i = 0; i < ca.length; i++) {
let c = ca[i];
while (c.charAt(0) == ' ') {
c = c.substring(1);
}
if (c.indexOf(name) == 0) {
return c.substring(name.length, c.length);
}
}
return "";
}
</script>
<?php require_once 'inc_footer_scripts.php'; ?>
<?php
require_once __DIR__ . '/php/inc_v2_assets.php';
fxV2RenderFooterScripts();
?>
if ((isset($_GET['cookies']))){
?>
<script>
setCookie('cookiesAccepted', '', { expires: -1 });
setCookie('cookiesRejected', '', { expires: -1 });
window.location = window.location.href.split("?")[0];
</script>
<?php
}
require_once 'inc_footer_scripts.php'; ?>
<script src="<?php echo $vDomaine; ?>/js/cookie-consent.js?version=v<?php echo _VERSION_CODE; ?>"></script>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,10 @@
<?php
global $vPaypal_devise,$vblnEnvironementDev,$vDomaine, $vClient, $strCode, $strLangue, $vRepertoireFichiers,$vPaypaladv_SANDBOX,$favicon;
fxAdminTextEditModeHandleRequest();
fxAdminPermInspectModeHandleRequest();
$strQueryString = "";
if ($_SERVER['QUERY_STRING'] != "") { // sortir les données du query string
@ -134,26 +138,38 @@ if (!isset($strOGDescription)) {
-->
<!-- apres migration des info paypal pour MSIN-4216
affaire remplacer echo PAYPAL_CLIENT_ID; -->
<?php if (fxPageNeedsPaypal()) { ?>
<script src="https://www.paypal.com/sdk/js?components=card-fields,buttons,googlepay&client-id=<?php echo PAYPAL_CLIENT_ID; ?>&disable-funding=paylater,credit&currency=CAD<?php echo $strPaypalLangue; ?>"></script>
<?php } ?>
<script type="text/javascript" src="<?php echo $vDomaine; ?>/min/index.php?g=js&version=v<?php echo _VERSION_CODE; ?>"></script>
<?php
require_once($_SERVER['DOCUMENT_ROOT']."/php/inc_facebook_pixel.php");
if (fxCookiesAllowMarketing()) {
require_once($_SERVER['DOCUMENT_ROOT'] . "/php/inc_facebook_pixel.php");
}
?>
</head>
<?php include_once('css/theme.php'); ?>
<body>
<?php
$blnInscrGestionShell = defined('MS1_INSCR_GESTION_ENTRY') && MS1_INSCR_GESTION_ENTRY;
// MSIN-4436 — Preprod/dev : overlay au chargement des pages lourdes (gestion qtés/dossards v2).
$blnPreloaderOnLoad = ($vblnEnvironementDev !== true);
if (!$blnPreloaderOnLoad && !empty($strCode) && $strCode === 'compte_inc_tableau_gestion_epreuves') {
$blnPreloaderOnLoad = true;
}
?>
<body<?php echo $blnInscrGestionShell ? ' class="inscr-gestion-entry-body"' : ''; ?>>
<?php if( $vblnEnvironementDev !== true){ ?>
<?php if ($blnPreloaderOnLoad) { ?>
<div id="preloader">
<div id="spinner">
<i class="fa fa-spinner fa-pulse"></i>
<div id="spinner" class="ms1-loader ms1-loader--overlay">
<img class="ms1-loader__runner" src="<?php echo $vDomaine; ?>/images/ms1-runner-loader.gif?v=<?php echo _VERSION_CODE; ?>" alt="" role="presentation" decoding="async">
<span id="preloader_text"><?php afficheTexte('preloader_load',1,0,1); ?></span>
</div>
</div>
@ -163,13 +179,19 @@ if (!isset($strOGDescription)) {
</script>
<?php
}else{
?>
<div id="preloader" style="display:none;">
<div id="spinner" class="ms1-loader ms1-loader--overlay">
<img class="ms1-loader__runner" src="<?php echo $vDomaine; ?>/images/ms1-runner-loader.gif?v=<?php echo _VERSION_CODE; ?>" alt="" role="presentation" decoding="async">
<span id="preloader_text"><?php afficheTexte('preloader_wait',1,0,1); ?></span>
</div>
</div>
<?php
} ?>
<?php echo $vEnvNotice; ?>
<header>
<?php fxRenderEnvNoticeBar(); ?>
<header<?php echo $blnInscrGestionShell ? ' class="inscr-gestion-entry-header"' : ''; ?>>
<?php if (!$blnInscrGestionShell) { ?>
<div class="ms1-header-top">
<div class="container">
<div class="row align-items-center">
@ -276,13 +298,18 @@ if (!isset($strOGDescription)) {
</div>
</div>
</div>
<?php } ?>
<div class="ms1-header-bottom py-4">
<div class="container">
<div class="row align-items-center">
<div class="col-lg-6 text-center text-lg-left">
<?php if ($blnInscrGestionShell) { ?>
<img class="mb-3 mb-lg-0" src="<?php echo $vDomaine; ?>/images/logoms1inscription.png" alt="<?php echo $vClient; ?>">
<?php } else { ?>
<a href="<?php echo $vDomaine; ?>">
<img class="mb-3 mb-lg-0" src="<?php echo $vDomaine; ?>/images/logoms1inscription.png" alt="<?php echo $vClient; ?>">
</a>
<?php } ?>
</div>
<div class="col-lg-6 text-center text-lg-right">
@ -298,6 +325,11 @@ if (!isset($strOGDescription)) {
$ref_no_panier = "0";
}
if (!isset($_SESSION['com_id']) && !isset($_SESSION['affiche_pas_connexion'])) {
if ($blnInscrGestionShell) {
?>
<p class="mb-0 text-muted small"><?php afficheTexte('gauche_placeholder_login', 1, 1, 1); ?></p>
<?php
} else {
?>
<form action="<?php echo $vDomaine; ?>/<?php if ($strLangue == 'fr') { ?>compte<?php } else { ?>account<?php } ?>" id="frm_login_top" name="frm_login_top" method="post">
<input type="hidden" name="form_action" value="<?php echo urlencode(base64_encode('login')); ?>">
@ -329,7 +361,40 @@ if (!isset($strOGDescription)) {
<?php echo fxShowCartLink($intNbItems, $strEvenementURL, $strLangue); ?>
</ul>
<?php
}
} else {
if ($blnInscrGestionShell && isset($_SESSION['com_id'])) {
if (!function_exists('fxInscrGestionRenderFullAccountLink')) {
require_once __DIR__ . '/php/inc_fx_inscriptions_gestion.php';
}
?>
<p class="mb-2"><strong><?php afficheTexte('gauche_bonjour_user',1,1,1); ?> <?php echo $_SESSION['com_info']['com_prenom']; ?></strong></p>
<ul class="list-inline mb-0">
<?php
// MSIN-4401 — Lien hub / Mon compte à côté de Déconnexion (pas dans le contenu).
if (function_exists('fxInscrGestionShouldShowFullAccountLink')
&& fxInscrGestionShouldShowFullAccountLink(intval($_SESSION['com_id']))) {
$strFullUrl = fxInscrGestionFullAccountUrl($strLangue);
$blnHubExit = function_exists('fxPromoteurHubIsAvailable')
&& fxPromoteurHubIsAvailable(intval($_SESSION['com_id']));
$strFullLbl = $blnHubExit
? fxInscrGestionT('inscr_gestion_back_full_account')
: fxInscrGestionT('inscr_gestion_back_mon_compte');
?>
<li class="list-inline-item">
<i class="fa fa-th-large mr-1" aria-hidden="true"></i>
<a href="<?php echo htmlspecialchars($strFullUrl, ENT_QUOTES, 'UTF-8'); ?>"><?php echo htmlspecialchars($strFullLbl, ENT_QUOTES, 'UTF-8'); ?></a>
</li>
<?php
}
?>
<li class="list-inline-item">
<i class="fa fa-sign-out mr-1"></i>
<a class="btn_logout" href="<?php echo $vDomaine; ?>/logout.php?lang=<?php echo $strLangue; ?>&amp;redirect=mobile"><?php afficheTexte('gauche_deconnextion',1,1,1); ?></a>
</li>
</ul>
<?php
} elseif (!$blnInscrGestionShell) {
?>
<p class="mb-2">
@ -371,6 +436,7 @@ if (!isset($strOGDescription)) {
</ul>
<?php
}
}
?>
</div>
</div>
@ -379,7 +445,7 @@ if (!isset($strOGDescription)) {
</div>
</header>
<?php
if (isset($_SESSION['com_id'])) {
if (isset($_SESSION['com_id']) && !$blnInscrGestionShell) {
$intCompte = $_SESSION['com_id'];
// liste attente
if (fx_liste_attente_compte_dispo_inscrire($intCompte) > 0) {

View File

@ -67,7 +67,9 @@ if ($intEveId != 0) {
</li> </ul>
<?php
/* MSIN-4379 — Ancienne UI dossards (Tool3). La v4 complète : inc_tableau_gestion_epreuves (superadm). */
fxShowBibTool3($strBibCode, $intEveId, $strLangue);
?>
<a class="btn btn-primary rounded-pill" id="link_back" href="<?php echo $vDomaine . $strPage . "/inc_tableau_promoteur?promoteur_eve_id=" . urlencode(base64_encode($intEveId)); ?>">

View File

@ -1,86 +0,0 @@
<?php
// MSIN-3880 nouveau tableau
include_once("php/inc_fx_modifierinscriptions.php");
include_once("php/inc_fx_memberships.php");
include_once("php/inc_fx_promoteur.php");
require_once('superadm/php/inc_manager.php');
global $objDatabase, $vDomaine, $strLangue;
$intEveId = 0;
$blnDon = false;
if ($strLangue == 'fr') {
$strPage = '/compte';
} else {
$strPage = '/account';
}
if($strLangue == 'fr')
$strCode = 'compte/';
else
$strCode = 'account/';
$strBibCode = $strCode;
if (!empty($_GET['code'])) {
$code = $_GET['code'];
} else {
$code = 'bib_assignment';
}
if (isset($_GET['promoteur_eve_id'])) {
$intEveId = base64_decode(urldecode($_GET['promoteur_eve_id']));
$arrEvenement = fxGetEvenementsId($intEveId);
//$tabEvenement['general'] = $arrEvenement;
}
?>
<a class="btn btn-primary rounded-pill" id="link_back" href="<?php echo $vDomaine . $strPage . "/inc_tableau_promoteur?promoteur_eve_id=" . urlencode(base64_encode($intEveId)); ?>">
<i class="fa fa-chevron-left mr-2" aria-hidden="true"></i>
<?php afficheTexte('promoteur_back'); ?>
</a>
<br> <br>
<?php
// recalculer les catégorie
if (isset($_REQUEST['resetcategorie'])) {
fxregles_niveau_updateall($intEveId);
}
// Annuler/retablir une inscriptions dans le tableau du promoteur
if (isset($_POST['btn_cancel_event_promo'])) {
$int_pec_id = $_POST['pec_id_original'];
$int_epr_id = $_POST['epr_id'];
$tabEpreuve = fxGetEpreuve($int_epr_id);
fxAnnulerRetablirInscription($tabEpreuve, $int_pec_id);
}
if ($intEveId != 0) {
$arrEvenement = fxGetEvenementsId($intEveId);
$tabRapports = fxGetMenussuperadm(0, 0, 1, $intEveId);
//$tabEvenement['general'] = $arrEvenement;
?> <ul class="nav flex-column ms1-menu-compte">
<li class="nav-item">
<a class="nav-list" href="<?php echo $vDomaine . "/" . $strCode; ?>bib_assignment?a=show_bib_dupes&promoteur_eve_id=<?php echo urlencode(base64_encode($intEveId)); ?>"> <i class="fa fa-clone" aria-hidden="true"></i> <?php echo afficheTexte('tableau_dossards_doublons', 0) ;?>
</a>
</li> </ul>
V2
<?php
fxShowBibTool4($strBibCode, $intEveId, $strLangue);
?>
<a class="btn btn-primary rounded-pill" id="link_back" href="<?php echo $vDomaine . $strPage . "/inc_tableau_promoteur?promoteur_eve_id=" . urlencode(base64_encode($intEveId)); ?>">
<i class="fa fa-chevron-left mr-2" aria-hidden="true"></i>
<?php afficheTexte('promoteur_back'); ?>
</a>
<?php
} else {
if ($strLangue == 'fr')
echo "Une erreur quelconque est survenue.";
else
echo "A generic error occurred.";
}

56
inc_tableau_eve_acces.php Normal file
View File

@ -0,0 +1,56 @@
<?php
/**
* MSIN-4401 — Gestion des accès V2 (promoteur, par événement).
* Les POST invite/revoke sont traités dans compte.php AVANT inc_header (évite page blanche).
*/
include_once('php/inc_fx_promoteur.php');
require_once('php/inc_fx_eve_acces.php');
require_once('php/inc_fx_eve_acces_promoteur.php');
require_once('php/inc_v2_assets.php');
global $objDatabase, $vDomaine, $strLangue;
$intEveId = 0;
if (isset($_GET['promoteur_eve_id'])) {
$intEveId = intval(base64_decode(urldecode($_GET['promoteur_eve_id'])));
}
$strLangue = ($strLangue === 'en') ? 'en' : 'fr';
$strBasePath = ($strLangue === 'en') ? '/account' : '/compte';
// MSIN-4399 — deny : hub + focus si dispo, sinon ancien tableau.
if (!function_exists('fxPromoteurHubOrLegacyBackUrl')) {
require_once __DIR__ . '/php/inc_fx_promoteur_hub.php';
}
$strRedirect = ($intEveId > 0)
? fxPromoteurHubOrLegacyBackUrl($intEveId, $strLangue)
: ($vDomaine . $strBasePath);
$intActor = intval($_SESSION['com_id'] ?? 0);
if (!empty($_SESSION['usa_id'])) {
$intActor = intval($_SESSION['usa_id']);
}
if ($intEveId <= 0
|| (empty($_SESSION['com_id']) && empty($_SESSION['usa_id']))
|| !fxEveAccesCanTeamInvite($intActor, $intEveId)) {
// Headers déjà envoyés (include après layout) : message + lien, pas de header Location.
echo '<div class="alert alert-warning">';
echo fxEveAccesPromoteurEsc(fxEveAccesPromoteurT('eve_acces_promoteur_access_denied', $strLangue));
echo ' <a href="' . htmlspecialchars($strRedirect, ENT_QUOTES, 'UTF-8') . '">';
echo fxEveAccesPromoteurEsc(fxEveAccesPromoteurT('eve_acces_promoteur_btn_back', $strLangue));
echo '</a></div>';
return;
}
$strFlash = '';
$strError = '';
if (!empty($_GET['ok'])) {
$strFlash = (string)$_GET['ok'];
}
if (!empty($_GET['err'])) {
$strError = (string)$_GET['err'];
}
fxEveAccesPromoteurRenderPage($intEveId, $strLangue, $strFlash, $strError);

View File

@ -0,0 +1,76 @@
<?php
// MSIN-4379 — Gestion quantités + dossards v4
include_once("php/inc_fx_modifierinscriptions.php");
include_once("php/inc_fx_memberships.php");
include_once("php/inc_fx_promoteur.php");
require_once('php/inc_v2_assets.php');
require_once('php/inc_fx_eve_acces.php');
require_once('superadm/php/inc_manager.php');
global $objDatabase, $vDomaine, $strLangue;
$intEveId = 0;
if ($strLangue == 'fr') {
$strPage = '/compte';
} else {
$strPage = '/account';
}
if ($strLangue == 'fr') {
$strCode = 'compte/';
} else {
$strCode = 'account/';
}
$strBibCode = $strCode;
if (isset($_GET['promoteur_eve_id'])) {
$intEveId = base64_decode(urldecode($_GET['promoteur_eve_id']));
}
if ($intEveId <= 0 || (empty($_SESSION['com_id']) && empty($_SESSION['usa_id']))
|| (empty($_SESSION['usa_id'])
&& !fxEveAccesHasAnyPermission($_SESSION['com_id'] ?? 0, $intEveId, array('dossards.manage', 'epreuves.edit_qte')))) {
// MSIN-4399 — même retour que le bouton back (hub + focus si dispo).
$strRedirect = fxGetPromoteurBackUrl($intEveId > 0 ? $intEveId : 0, $strPage);
header('Location: ' . $strRedirect);
exit;
}
$strBackUrl = fxGetPromoteurBackUrl($intEveId, $strPage);
?>
<a class="btn btn-primary rounded-pill" id="link_back" href="<?php echo $strBackUrl; ?>">
<i class="fa fa-chevron-left mr-2" aria-hidden="true"></i>
<?php afficheTexte('promoteur_back'); ?>
</a>
<br><br>
<?php
if (isset($_REQUEST['resetcategorie'])) {
fxregles_niveau_updateall($intEveId);
}
if (isset($_POST['btn_cancel_event_promo'])) {
$int_pec_id = $_POST['pec_id_original'];
$int_epr_id = $_POST['epr_id'];
$tabEpreuve = fxGetEpreuve($int_epr_id);
fxAnnulerRetablirInscription($tabEpreuve, $int_pec_id);
}
?>
<?php
fxV2RegisterScript('bib-assignments');
fxShowBibTool4($strBibCode, $intEveId, $strLangue);
?>
<link rel="stylesheet" href="<?php echo $vDomaine; ?>/libs/tippy.js-6.3.7/tippy.css">
<script src="<?php echo $vDomaine; ?>/libs/tippy.js-6.3.7/popper.min.js"></script>
<script src="<?php echo $vDomaine; ?>/libs/tippy.js-6.3.7/tippy-bundle.umd.min.js"></script>
<a class="btn btn-primary rounded-pill mt-3" href="<?php echo $strBackUrl; ?>">
<i class="fa fa-chevron-left mr-2" aria-hidden="true"></i>
<?php afficheTexte('promoteur_back'); ?>
</a>
<?php

View File

@ -0,0 +1,30 @@
<?php
include_once('php/inc_fx_modifierinscriptions.php');
include_once('php/inc_fx_memberships.php');
include_once('php/inc_fx_promoteur.php');
include_once('php/inc_fx_inscriptions_gestion.php');
require_once('php/inc_v2_assets.php');
require_once('superadm/php/inc_manager.php');
global $objDatabase, $vDomaine, $strLangue;
$intEveId = 0;
if (isset($_GET['promoteur_eve_id'])) {
$intEveId = fxInscrGestionParseEveId($_GET['promoteur_eve_id']);
}
if ($intEveId === 0) {
if ($strLangue === 'fr') {
echo 'Une erreur quelconque est survenue.';
} else {
echo 'A generic error occurred.';
}
return;
}
echo '<div class="inscr-gestion-page">';
fxV2RegisterScript('inscr-gestion');
fxInscrGestionRun($intEveId, $strLangue);
echo '</div>';

View File

@ -1,6 +1,7 @@
<?php
require_once('php/inc_fx_liste_attente.php');
include_once('php/inc_fx_promoteur.php');
global $objDatabase, $vDomaine, $strLangue;
$intEveId = 0;
@ -54,6 +55,19 @@ if (isset($_GET['promoteur_eve_id'])) {
//$tabEvenement['general'] = $arrEvenement;
}
// Conserver la page d'origine (gestion v4 vs ancien tableau épreuves), y compris après POST.
if (isset($_GET['from']) && trim($_GET['from']) !== '') {
$_SESSION['liste_attente_from'] = trim($_GET['from']);
} elseif ($_SERVER['REQUEST_METHOD'] !== 'POST') {
$_SESSION['liste_attente_from'] = 'epreuves';
}
if (!empty($_SESSION['liste_attente_from'])) {
$_GET['from'] = $_SESSION['liste_attente_from'];
}
$strBackUrl = fxGetPromoteurBackUrl($intEveId, $strPage, 'epreuves');
$strhomeclientannuler = fxGetPromoteurBackPath($strPage, 'epreuves');
?>
<h3> <?php echo $tabEpreuve["epr_type_" . $strLangue] . " / " . $tabEpreuve["epr_nom_" . $strLangue];; ?></h3>
@ -61,7 +75,6 @@ if (isset($_GET['promoteur_eve_id'])) {
<?php
$_GET['id'] = $inteprId;
$strhomeclient = "/compte/inc_tableau_liste_attente";
$strhomeclientannuler = "/compte/inc_tableau_epreuves";
fxShowForm("mod", base64_encode(urlencode(250)), $strLangue, $intEveId, $inteprId, 0);
@ -155,7 +168,7 @@ fxShowList($tabResults, $intmenu, '', $strwhere, '', 1, $strLangue, $inteprId);
?>
<a class="btn btn-primary rounded-pill" id="link_back"
href="<?php echo $vDomaine . $strPage . "/inc_tableau_epreuves?promoteur_eve_id=" . urlencode(base64_encode($intEveId)); ?>">
href="<?php echo $strBackUrl; ?>">
<i class="fa fa-chevron-left mr-2" aria-hidden="true"></i>
<?php afficheTexte('promoteur_back'); ?>
</a>

View File

@ -218,10 +218,6 @@ if ($intEveId != 0) {
</a>
</li>
<?php
$strBibCode = $strPage;
// afficher l'outil de no. equipe/no. dossard
$sqlQteLimitee = "SELECT eve_qte_modifiable FROM inscriptions_evenements WHERE eve_id = " . intval($intEveId);
$qryQteLimitee = $objDatabase->fxGetVar($sqlQteLimitee);
@ -236,6 +232,7 @@ if ($intEveId != 0) {
</a>
</li><?php
}
// MSIN-4401 — menus V2 (qté/dossards, inscriptions, accès) : hub uniquement, pas le Legacy.
}

View File

@ -1,294 +0,0 @@
<?php
// MSIN-3679 nouveau tableau
ini_set('display_errors', true);
error_reporting(E_ALL);
require_once $_SERVER["DOCUMENT_ROOT"] . "/superadm/php/inc_fx_paypal.php";
global $objDatabase, $vDomaine, $strLangue;
$intEveId = 0;
$blnDon = false;
if ($strLangue == 'fr') {
$strPage = '/compte';
} else {
$strPage = '/account';
}
if($strLangue == 'fr')
$strCode = 'compte/';
else
$strCode = 'account/';
$strBibCode = $strCode;
if (!empty($_GET['code'])) {
$code = $_GET['code'];
} else {
$code = 'bib_assignment';
}
if (isset($_GET['promoteur_eve_id'])) {
$intEveId = base64_decode(urldecode($_GET['promoteur_eve_id']));
$arrEvenement = fxGetEvenementsId($intEveId);
//$tabEvenement['general'] = $arrEvenement;
}
// a metre dimamique
$sqlCommandes = "SELECT *, IF(a.com_id <> 0, (SELECT c.com_login FROM inscriptions_comptes c WHERE a.com_id = c.com_id), '') AS usager FROM inscriptions_panier_acheteurs a, inscriptions_panier_statuts s WHERE s.sta_id = a.sta_id AND a.no_panier = '" . $_GET['no_panier']."'";
$recCommandes = $objDatabase->fxGetRow($sqlCommandes);
?>
<a class="btn btn-primary rounded-pill" id="link_back" href="<?php echo $vDomaine . $strPage . "/inc_tableau_inscriptions?promoteur_eve_id=" . urlencode(base64_encode($intEveId)); ?>">
<i class="fa fa-chevron-left mr-2" aria-hidden="true"></i>
<?php afficheTexte('promoteur_back'); ?>
</a>
<br>
<h3><img src="images_blue/ico_commandes_big.png" alt="">Détails de la commande</h3>
<?php echo fxShowPanier($recCommandes['no_panier'], 0, 'web', 1, 'fr'); ?>
<p>&nbsp;</p>
<br>
<h3><img src="images_blue/ico_commandes_big.png" alt="">PAYPAL</h3>
<?php
// ************************************************ rembourcement
/****************************************************
* Page: remboursement.php
* Rôle : affiche le formulaire de remboursement + prépare
* les variables serveur nécessaires au front (AJAX).
****************************************************/
// Démarrer la session
//session_start();
// Jeton anti double clic / re-soumission
if (empty($_SESSION['refund_token'])) {
$_SESSION['refund_token'] = bin2hex(random_bytes(16));
}
$refundToken = $_SESSION['refund_token'];
// Récupération des infos commande (présentes dans ton contexte)
$CAPTURE_ID = $recCommandes["TransactionID"] ?? null;
if (!isset($CAPTURE_ID) || !is_string($CAPTURE_ID) || $CAPTURE_ID === '') {
//echo '<div style="background:#feecec;border:1px solid #f2b8b5;padding:10px;border-radius:8px;">
// ⚠️ CAPTURE_ID non fourni côté serveur.
// </div>';
return;
}
// Charger ta classe PayPal
require_once $_SERVER["DOCUMENT_ROOT"] . "/paypal_advanced/PaypalCheckout.class.php";
$paypal = new PayPalCheckout(); // adapte si nécessaire
// Préparation
$errors = [];
$successMsg = null;
// Devise par défaut si jamais les détails PayPal nen donnent pas
$currencyUI = 'CAD';
// ====== Calculs initiaux côté serveur ======
// Solde dispo promoteur (ton helper existant)
$solde_dispo_ms1 = statut_payement($recCommandes["eve_id"]);
// Récupérer les détails PayPal de la capture
$paypaldetails = $paypal->paypaldetails($CAPTURE_ID);
// Harmoniser avec tes remboursements locaux si besoin
$paypaldetails = reconcileRefundsWithLocal($paypaldetails);
// Déterminer le max remboursable PayPal
$max_remboursement_paypal = 0.0;
// Cas habituel : clé remaining
if (!empty($paypaldetails['refunds']['totals']['remaining'])) {
$max_remboursement_paypal = (float)$paypaldetails['refunds']['totals']['remaining'];
}
// Fallback : montant total capturé (certains retours utilisent "details", dautres ont eu une coquille "delails")
elseif (!empty($paypaldetails['details']['amount']['value'])) {
$max_remboursement_paypal = (float)$paypaldetails['details']['amount']['value'];
} elseif (!empty($paypaldetails['delails']['amount']['value'])) { // compat. si ton code historique renvoie "delails"
$max_remboursement_paypal = (float)$paypaldetails['delails']['amount']['value'];
}
// Déterminer la devise daffichage si fournie par PayPal
if (!empty($paypaldetails['refunds']['totals']['currency'])) {
$currencyUI = (string)$paypaldetails['refunds']['totals']['currency'];
} elseif (!empty($paypaldetails['details']['amount']['currency_code'])) {
$currencyUI = (string)$paypaldetails['details']['amount']['currency_code'];
} elseif (!empty($paypaldetails['delails']['amount']['currency_code'])) {
$currencyUI = (string)$paypaldetails['delails']['amount']['currency_code'];
}
// Frais MS1 (selon ta structure)
$frais_ms1 = (float)($recCommandes['fra_total'] ?? 0);
// Max remboursable à lécran (ta logique actuelle)
$maxUI = $max_remboursement_paypal - $frais_ms1;
// maxblock = limite stricte dinput côté front, selon ton choix actuel
if ($max_remboursement_paypal < $solde_dispo_ms1) {
$maxblock = $max_remboursement_paypal;
} else {
$maxblock = 0;
$maxUI = 0;
}
// ******************** variable info
//print_rsl($recCommandes);
echosl( "no_commande : ".$recCommandes["no_commande"]);
// echosl( "rem_ajoute_par : ".$_SESSION["usa_info"]["com_prenom"]." ".$_SESSION["usa_info"]["com_nom"]);
echosl( "eve_id : ".$recCommandes["eve_id"]) ;
echosl( "Montant remboursable : " . number_format($max_remboursement_paypal, 2) . " " . ($result['refunds']['totals']['currency'] ?? $result['delails']['amount']['currency_code'] ?? ''));
echo("solde dispo ms1 :");
echosl($solde_dispo_ms1);
echo("frais ms1 :");
$frais_ms1=$recCommandes['fra_total'];
echosl($frais_ms1);
echo("max Remboursement :");
// $maxUI le max Remboursement
$maxUI=$max_remboursement_paypal-$frais_ms1;
echosl($maxUI);
?>
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8">
<title>Remboursement PayPal</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body style="font-family:system-ui,Arial,Helvetica;">
<div style="max-width:560px;border:1px solid #ddd;border-radius:10px;padding:14px;">
<div style="font-weight:700;font-size:16px;margin-bottom:8px;">Remboursement PayPal (Checkout)</div>
<!-- Zone messages AJAX -->
<div id="refund-msg" style="margin-bottom:10px;display:none;padding:10px;border-radius:8px;"></div>
<?php if ($maxUI > 0): ?>
<div style="background:#f9fbff;border:1px solid #dbe7ff;padding:10px;border-radius:8px;margin-bottom:10px;">
<div><strong>Capture:</strong> <?= h($CAPTURE_ID) ?></div>
<div><strong>Max remboursable (affiché):</strong> <span id="pp_ref_max"><?= h(money2($maxUI)) ?></span> <?= h($currencyUI) ?></div>
</div>
<form id="refund-form" method="post" style="display:grid;grid-template-columns:1fr 1fr;gap:10px;">
<input type="hidden" name="refund_token" value="<?= h($refundToken) ?>">
<input type="hidden" name="capture_id" value="<?= h($CAPTURE_ID) ?>">
<input type="hidden" name="currency" value="<?= h($currencyUI) ?>">
<input type="hidden" name="eve_id" value="<?= h($recCommandes["eve_id"]) ?>">
<input type="hidden" name="no_commande" value="<?= h($recCommandes["no_commande"]) ?>">
<div style="grid-column:1/2;">
<label style="display:block;font-size:12px;color:#333;margin-bottom:4px;">
Montant à rembourser (<?= h($currencyUI) ?>)
</label>
<input id="pp_ref_amount" type="number" name="refund_amount" step="0.01" min="0.01"
max="<?= h(money2($maxblock)) ?>"
value="<?= h(money2($maxUI)) ?>"
style="width:100%;padding:8px;border:1px solid #ccc;border-radius:8px;">
</div>
<div style="grid-column:2/3;">
<label style="display:block;font-size:12px;color:#333;margin-bottom:4px;">Note (optionnelle)</label>
<input type="text" name="refund_note" placeholder="Raison du remboursement"
style="width:100%;padding:8px;border:1px solid #ccc;border-radius:8px;">
</div>
<div style="grid-column:1/3;text-align:right;margin-top:6px;">
<button type="submit"
style="padding:10px 14px;border:1px solid #2d6cdf;border-radius:8px;background:#2d6cdf;color:#fff;cursor:pointer;">
Rembourser
</button>
</div>
</form>
<?php else: ?>
<div style="background:#fff8e6;border:1px solid #ffe1a6;padding:10px;border-radius:8px;">
Aucun montant remboursable pour cette capture.
</div>
<?php endif; ?>
</div>
<script>
// Petit helper visuel
function showMsg(text, ok) {
const box = document.getElementById('refund-msg');
box.style.display = 'block';
box.style.background = ok ? '#e9f8ef' : '#feecec';
box.style.border = ok ? '1px solid #b8e3c5' : '1px solid #f2b8b5';
box.innerText = text;
}
document.getElementById('refund-form')?.addEventListener('submit', async (e) => {
e.preventDefault();
showMsg('⏳ Traitement en cours...', false);
const data = new FormData(e.target);
const res = await fetch( '<?php echo $vDomaine; ?>/superadm/ajax_refund.php', { method: 'POST', body: data });
// Défensif
if (!res.ok) {
showMsg('⚠️ Erreur réseau pendant le remboursement.', false);
return;
}
let json;
try { json = await res.json(); } catch (_) {
showMsg('⚠️ Réponse invalide du serveur.', false);
return;
}
if (json.success) {
showMsg('✅ ' + (json.message || 'Remboursement effectué.'), true);
// 👉 Ici, tu peux déclencher des actions UI :
// - recharger une grille des remboursements
// - rafraîchir un solde
// - etc.
// refreshTableRemboursements();
} else {
showMsg('⚠️ ' + (json.message || 'Erreur remboursement.'), false);
}
});
</script>
</body>
</html>
<h3><img src="images_blue/ico_commandes_big.png" alt="">Détails PAYPAL</h3>
<?php
// $sortie=paypaldetails($recCommandes["TransactionID"]);
echo(renderPayPalCaptureHtml($paypaldetails));
?>
<a class="btn btn-primary rounded-pill" id="link_back" href="<?php echo $vDomaine . $strPage . "/inc_tableau_promoteur?promoteur_eve_id=" . urlencode(base64_encode($intEveId)); ?>">
<i class="fa fa-chevron-left mr-2" aria-hidden="true"></i>
<?php afficheTexte('promoteur_back'); ?>
</a>
<?php

View File

@ -2,7 +2,9 @@
require_once('php/inc_start_time.php');
require_once('php/inc_fonctions.php');
require_once('php/inc_fx_messages.php');
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
global $vPage, $vDomaine;
$strLangue = 'fr';
$strCode = "accueil";
@ -63,8 +65,13 @@ require_once("inc_header.php");
<?php
switch ($strCode) {
case 'cookie':
$strCookieContent = trim((string) $recPage['pag_texte_' . $strLangue]);
echo $strCookieContent !== '' ? fxUnescape($recPage['pag_texte_' . $strLangue], 1) : fxCookiePolicyPageContent($strLangue);
break;
echo fxUnescape($recPage['pag_texte_' . $strLangue], 1);
case 'confidentialite':
$strPrivacyContent = trim((string) $recPage['pag_texte_' . $strLangue]);
echo $strPrivacyContent !== '' ? fxUnescape($recPage['pag_texte_' . $strLangue], 1) : fxPrivacyPolicyPageContent($strLangue);
break;
case 'evenements':

142
js/cookie-consent.js Normal file
View File

@ -0,0 +1,142 @@
(function (window, $) {
'use strict';
var STORAGE_KEY = 'cookiesPreferences';
var LEGACY_ACCEPT = 'cookiesAccepted';
var LEGACY_REJECT = 'cookiesRejected';
function setCookie(name, value, days) {
var expires = '';
if (typeof days === 'number') {
var date = new Date();
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
expires = ';expires=' + date.toUTCString();
}
document.cookie = name + '=' + encodeURIComponent(value) + expires + ';path=/;SameSite=Lax';
}
function deleteCookie(name) {
document.cookie = name + '=;expires=Thu, 01 Jan 1970 00:00:00 UTC;path=/;SameSite=Lax';
}
function getCookie(name) {
var prefix = name + '=';
var parts = decodeURIComponent(document.cookie).split(';');
for (var i = 0; i < parts.length; i++) {
var part = parts[i];
while (part.charAt(0) === ' ') {
part = part.substring(1);
}
if (part.indexOf(prefix) === 0) {
return part.substring(prefix.length);
}
}
return '';
}
function readPreferences() {
var raw = getCookie(STORAGE_KEY);
if (raw) {
try {
var parsed = JSON.parse(raw);
if (parsed && typeof parsed === 'object') {
return {
analytics: !!parsed.analytics,
marketing: !!parsed.marketing,
set: true
};
}
} catch (e) {
}
}
if (getCookie(LEGACY_ACCEPT) === 'true') {
return { analytics: true, marketing: true, set: true };
}
if (getCookie(LEGACY_REJECT) === 'true') {
return { analytics: false, marketing: false, set: true };
}
return { analytics: false, marketing: false, set: false };
}
function writePreferences(prefs, reload) {
deleteCookie(LEGACY_ACCEPT);
deleteCookie(LEGACY_REJECT);
setCookie(STORAGE_KEY, JSON.stringify({
analytics: prefs.analytics ? 1 : 0,
marketing: prefs.marketing ? 1 : 0,
v: 1
}), 365);
if (reload !== false) {
window.location.reload();
}
}
function syncPanel(prefs) {
$('#cookiePrefAnalytics').prop('checked', !!prefs.analytics);
$('#cookiePrefMarketing').prop('checked', !!prefs.marketing);
}
function openBanner() {
$('#cookieConsent').stop(true, true).fadeIn(200);
}
function closeBanner() {
$('#cookieConsent').stop(true, true).fadeOut(200);
$('#cookiePreferencesPanel').addClass('d-none');
}
window.ms1CookieConsent = {
getPreferences: readPreferences,
openManager: function () {
syncPanel(readPreferences());
$('#cookiePreferencesPanel').removeClass('d-none');
openBanner();
}
};
$(function () {
var prefs = readPreferences();
syncPanel(prefs);
if (!prefs.set) {
openBanner();
}
$('#acceptCookies').on('click', function () {
writePreferences({ analytics: true, marketing: true });
});
$('#rejectCookies').on('click', function () {
writePreferences({ analytics: false, marketing: false });
});
$('#customizeCookies').on('click', function () {
$('#cookiePreferencesPanel').removeClass('d-none');
});
$('#saveCookiePreferences').on('click', function () {
writePreferences({
analytics: $('#cookiePrefAnalytics').is(':checked'),
marketing: $('#cookiePrefMarketing').is(':checked')
});
});
$('[data-cookie-manage]').on('click', function (event) {
event.preventDefault();
window.ms1CookieConsent.openManager();
});
});
}(window, jQuery));

188
js/ms1-doc.js Normal file
View File

@ -0,0 +1,188 @@
/**
* MS1 — Module documentation overlay (navigation + ouverture/fermeture).
*/
(function () {
'use strict';
function findOverlayForTrigger(trigger) {
var anchor = trigger.getAttribute('data-doc-anchor');
if (!anchor) {
return null;
}
var overlays = document.querySelectorAll('.ms1-doc-overlay[data-doc-anchor="' + anchor + '"]');
return overlays.length ? overlays[0] : null;
}
function getMeta(overlay) {
var anchor = overlay.getAttribute('data-doc-anchor');
var script = document.querySelector('script.ms1-doc-meta[data-doc-anchor="' + anchor + '"]');
if (!script) {
return [];
}
try {
return JSON.parse(script.textContent || '[]');
} catch (e) {
return [];
}
}
function setPage(overlay, index) {
var pages = overlay.querySelectorAll('.ms1-doc-page');
var dots = overlay.querySelectorAll('.ms1-doc-dot');
var meta = getMeta(overlay);
var total = pages.length;
if (total === 0) {
return;
}
if (index < 0) {
index = 0;
}
if (index >= total) {
index = total - 1;
}
pages.forEach(function (page, i) {
if (i === index) {
page.removeAttribute('hidden');
page.classList.remove('hidden');
} else {
page.setAttribute('hidden', 'hidden');
}
});
dots.forEach(function (dot, i) {
if (i === index) {
dot.classList.add('is-active');
} else {
dot.classList.remove('is-active');
}
});
var titleEl = overlay.querySelector('.ms1-doc-title');
var subtitleEl = overlay.querySelector('.ms1-doc-subtitle');
var currentEl = overlay.querySelector('.ms1-doc-counter-current');
var prevBtn = overlay.querySelector('.ms1-doc-prev');
var nextBtn = overlay.querySelector('.ms1-doc-next');
if (meta[index]) {
if (titleEl) {
titleEl.textContent = meta[index].titre || '';
}
if (subtitleEl) {
subtitleEl.textContent = meta[index].sous_titre || '';
}
}
if (currentEl) {
currentEl.textContent = String(index + 1);
}
if (prevBtn) {
prevBtn.disabled = index <= 0;
}
if (nextBtn) {
nextBtn.disabled = index >= total - 1;
}
overlay.setAttribute('data-doc-current-index', String(index));
}
function openOverlay(overlay) {
overlay.removeAttribute('hidden');
document.body.classList.add('ms1-doc-open');
var idx = parseInt(overlay.getAttribute('data-doc-current-index') || '0', 10);
setPage(overlay, idx);
}
function closeOverlay(overlay) {
overlay.setAttribute('hidden', 'hidden');
if (!document.querySelector('.ms1-doc-overlay:not([hidden])')) {
document.body.classList.remove('ms1-doc-open');
}
}
function getCurrentIndex(overlay) {
return parseInt(overlay.getAttribute('data-doc-current-index') || '0', 10);
}
function openDocEditPopup(url) {
var w = window.open(
url,
'ms1DocEdit' + Date.now(),
'width=1120,height=820,scrollbars=yes,resizable=yes'
);
if (w) {
var timer = setInterval(function () {
if (w.closed) {
clearInterval(timer);
window.location.reload();
}
}, 400);
}
}
document.addEventListener('click', function (e) {
var docEditLink = e.target.closest('a.ms1-doc-edit-link');
if (docEditLink && docEditLink.href) {
e.preventDefault();
e.stopPropagation();
openDocEditPopup(docEditLink.href);
return;
}
var trigger = e.target.closest('.ms1-doc-trigger, .btn-doc-trigger');
if (trigger) {
e.preventDefault();
var overlay = findOverlayForTrigger(trigger);
if (overlay) {
openOverlay(overlay);
}
return;
}
var closeEl = e.target.closest('[data-ms1-doc-close]');
if (closeEl) {
var overlayClose = closeEl.closest('.ms1-doc-overlay');
if (overlayClose) {
closeOverlay(overlayClose);
}
return;
}
var dot = e.target.closest('.ms1-doc-dot');
if (dot) {
var overlayDot = dot.closest('.ms1-doc-overlay');
if (overlayDot) {
setPage(overlayDot, parseInt(dot.getAttribute('data-doc-page-index') || '0', 10));
}
return;
}
if (e.target.closest('[data-ms1-doc-prev]')) {
var overlayPrev = e.target.closest('.ms1-doc-overlay');
if (overlayPrev) {
setPage(overlayPrev, getCurrentIndex(overlayPrev) - 1);
}
return;
}
if (e.target.closest('[data-ms1-doc-next]')) {
var overlayNext = e.target.closest('.ms1-doc-overlay');
if (overlayNext) {
setPage(overlayNext, getCurrentIndex(overlayNext) + 1);
}
}
});
document.addEventListener('keydown', function (e) {
if (e.key !== 'Escape') {
return;
}
var open = document.querySelector('.ms1-doc-overlay:not([hidden])');
if (open) {
closeOverlay(open);
}
});
})();

113
js/ms1-loader.js Normal file
View File

@ -0,0 +1,113 @@
/**
* MS1 — Loader coureur (overlay + inline).
* Usage : Ms1Loader.show() / Ms1Loader.hide() / Ms1Loader.inlineHtml(message)
* Norme MS1 : tout message d'attente AJAX/panel doit utiliser Ms1Loader.inlineHtml().
*/
(function (global, $) {
'use strict';
var config = {
src: '/images/ms1-runner-loader.gif',
labelWait: 'Chargement...'
};
function escHtml(str) {
return String(str == null ? '' : str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
function runnerImg() {
return '<img class="ms1-loader__runner" src="' + escHtml(config.src) + '" alt="" role="presentation" decoding="async">';
}
function init(options) {
if (options) {
$.extend(config, options);
}
}
function inlineHtml(message) {
var msg = message == null || message === '' ? config.labelWait : message;
return '<div class="ms1-loader ms1-loader--inline" role="status" aria-live="polite" aria-busy="true">'
+ runnerImg()
+ '<span class="ms1-loader__label">' + escHtml(msg) + '</span>'
+ '</div>';
}
function show(message) {
var $preloader = $('#preloader');
if (!$preloader.length) {
return;
}
clearProgress();
if (message != null && message !== '') {
var $text = $('#preloader_text');
if ($text.length) {
$text.html(message);
}
}
$preloader.show();
}
function clearProgress() {
$('#spinner .ms1-loader__progress').remove();
}
/**
* MSIN-4433 — Overlay avec coureur + barre de progression (% réel).
* @param {number} current
* @param {number} total
* @param {string} [message]
*/
function showProgress(current, total, message) {
var $preloader = $('#preloader');
if (!$preloader.length) {
return;
}
var intCurrent = Math.max(0, parseInt(current, 10) || 0);
var intTotal = Math.max(1, parseInt(total, 10) || 1);
var intPct = Math.min(100, Math.round((intCurrent / intTotal) * 100));
var $spinner = $('#spinner');
var $bar = $spinner.find('.ms1-loader__progress');
if (!$bar.length) {
$bar = $('<div class="ms1-loader__progress" role="progressbar" aria-valuemin="0" aria-valuemax="100">'
+ '<div class="ms1-loader__progress-track">'
+ '<div class="ms1-loader__progress-fill"></div>'
+ '</div>'
+ '<span class="ms1-loader__progress-pct"></span>'
+ '</div>');
$('#preloader_text').after($bar);
}
$bar.attr('aria-valuenow', intPct);
$bar.find('.ms1-loader__progress-fill').css('width', intPct + '%');
$bar.find('.ms1-loader__progress-pct').text(intPct + '%');
if (message != null && message !== '') {
var $text = $('#preloader_text');
if ($text.length) {
$text.html(message);
}
}
$preloader.show();
}
function hide() {
clearProgress();
$('#preloader').fadeOut('slow');
}
global.Ms1Loader = {
init: init,
inlineHtml: inlineHtml,
show: show,
showProgress: showProgress,
hide: hide
};
}(window, jQuery));

4746
js/v2/bib-assignments.js Normal file

File diff suppressed because it is too large Load Diff

1555
js/v2/inscr-gestion.js Normal file

File diff suppressed because it is too large Load Diff

155
js/v2/promoteur-hub.js Normal file
View File

@ -0,0 +1,155 @@
/**
* Hub promoteur — chargement AJAX stats + menu fonctionnalités.
* Chargé uniquement sur compte_promoteur_hub.
*/
(function () {
'use strict';
var cfg = window.MS1_V2_PROMOTEUR_HUB || {};
var $ = window.jQuery;
if (!$) {
return;
}
var strAjaxUrl = cfg.ajaxUrl || '';
var strWaitMsg = cfg.preloaderWait || '';
var i18n = cfg.i18n || {};
var strAjaxError = i18n.ajaxError || '';
var intHubLoaderPending = 0;
// MSIN-4401 — Session expiree : recharger la page → formulaire de login.
// Detection par code serveur (pas egalite de message FR).
function fxHubRedirectIfSessionExpired(r) {
if (r && r.code === 'session') {
window.location.reload();
return true;
}
return false;
}
function fxHubShowLoader() {
intHubLoaderPending++;
if (intHubLoaderPending !== 1) {
return;
}
if (window.Ms1Loader) {
Ms1Loader.show(strWaitMsg);
return;
}
var $preloader = $('#preloader');
if ($preloader.length) {
$('#preloader_text').html(strWaitMsg);
$preloader.show();
}
}
function fxHubHideLoader() {
intHubLoaderPending = Math.max(0, intHubLoaderPending - 1);
if (intHubLoaderPending !== 0) {
return;
}
if (window.Ms1Loader) {
Ms1Loader.hide();
return;
}
$('#preloader').fadeOut('slow');
}
function fxHubLoadStats($collapse) {
if ($collapse.data('hub-loaded') || !strAjaxUrl) {
return;
}
var intEve = $collapse.data('hub-eve-id');
var strLang = $collapse.data('hub-lang') || 'fr';
fxHubShowLoader();
$.getJSON(strAjaxUrl, { a: 'event_stats', eve_id: intEve, lang: strLang })
.done(function (r) {
if (fxHubRedirectIfSessionExpired(r)) {
return;
}
if (!r || !r.ok) {
$collapse.find('.promoteur-hub-stats-placeholder').text(r && r.msg ? r.msg : strAjaxError);
return;
}
$collapse.find('.promoteur-hub-stats-placeholder').replaceWith(r.html || '');
if (r.flash) {
$collapse.closest('.promoteur-hub-card').find('.promoteur-hub-card-flash-wrap').html(r.flash);
}
$collapse.data('hub-loaded', 1);
})
.fail(function () {
$collapse.find('.promoteur-hub-stats-placeholder').text(strAjaxError);
})
.always(function () {
fxHubHideLoader();
});
}
function fxHubLoadLinks($collapse) {
if ($collapse.data('hub-links-loaded') || !strAjaxUrl) {
return;
}
var intEve = $collapse.data('hub-eve-id');
var strLang = $collapse.data('hub-lang') || 'fr';
fxHubShowLoader();
$.getJSON(strAjaxUrl, { a: 'event_links', eve_id: intEve, lang: strLang })
.done(function (r) {
if (fxHubRedirectIfSessionExpired(r)) {
return;
}
if (!r || !r.ok) {
$collapse.find('.promoteur-hub-links-placeholder').text(r && r.msg ? r.msg : strAjaxError);
return;
}
$collapse.find('.promoteur-hub-links-placeholder').replaceWith(r.html || '');
$collapse.data('hub-links-loaded', 1);
})
.fail(function () {
$collapse.find('.promoteur-hub-links-placeholder').text(strAjaxError);
})
.always(function () {
fxHubHideLoader();
});
}
$(document).on('show.bs.collapse', '.promoteur-hub-epreuves-collapse', function () {
var $card = $(this).closest('.promoteur-hub-card');
$card.find('.promoteur-hub-epreuves-collapse.show, .promoteur-hub-functions.show').not(this).collapse('hide');
fxHubLoadStats($(this));
});
$(document).on('show.bs.collapse', '.promoteur-hub-functions', function () {
var $card = $(this).closest('.promoteur-hub-card');
$card.find('.promoteur-hub-epreuves-collapse.show, .promoteur-hub-functions.show').not(this).collapse('hide');
fxHubLoadLinks($(this));
});
// MSIN-4399 — Retour depuis une page événement : scroll + ouvrir le menu fonctionnalités.
function fxHubFocusEvent(intEve) {
intEve = parseInt(intEve, 10) || 0;
if (intEve <= 0) {
return;
}
var $card = $('#hub-eve-' + intEve);
if (!$card.length) {
return;
}
if ($card[0].scrollIntoView) {
$card[0].scrollIntoView({ behavior: 'smooth', block: 'start' });
}
var $fn = $('#hub-fn-' + intEve);
var $epr = $('#hub-epr-' + intEve);
if ($fn.length) {
$fn.collapse('show');
} else if ($epr.length) {
$epr.collapse('show');
}
}
$(function () {
fxHubFocusEvent(cfg.focusEveId);
});
})();

6
libs/tippy.js-6.3.7/popper.min.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
.tippy-box[data-animation=fade][data-state=hidden]{opacity:0}[data-tippy-root]{max-width:calc(100vw - 10px)}.tippy-box{position:relative;background-color:#333;color:#fff;border-radius:4px;font-size:14px;line-height:1.4;white-space:normal;outline:0;transition-property:transform,visibility,opacity}.tippy-box[data-placement^=top]>.tippy-arrow{bottom:0}.tippy-box[data-placement^=top]>.tippy-arrow:before{bottom:-7px;left:0;border-width:8px 8px 0;border-top-color:initial;transform-origin:center top}.tippy-box[data-placement^=bottom]>.tippy-arrow{top:0}.tippy-box[data-placement^=bottom]>.tippy-arrow:before{top:-7px;left:0;border-width:0 8px 8px;border-bottom-color:initial;transform-origin:center bottom}.tippy-box[data-placement^=left]>.tippy-arrow{right:0}.tippy-box[data-placement^=left]>.tippy-arrow:before{border-width:8px 0 8px 8px;border-left-color:initial;right:-7px;transform-origin:center left}.tippy-box[data-placement^=right]>.tippy-arrow{left:0}.tippy-box[data-placement^=right]>.tippy-arrow:before{left:-7px;border-width:8px 8px 8px 0;border-right-color:initial;transform-origin:center right}.tippy-box[data-inertia][data-state=visible]{transition-timing-function:cubic-bezier(.54,1.5,.38,1.11)}.tippy-arrow{width:16px;height:16px;color:#333}.tippy-arrow:before{content:"";position:absolute;border-color:transparent;border-style:solid}.tippy-content{position:relative;padding:5px 9px;z-index:1}

13
min/.htaccess Normal file
View File

@ -0,0 +1,13 @@
<IfModule mod_rewrite.c>
RewriteEngine on
# You may need RewriteBase on some servers
#RewriteBase /min
# rewrite URLs like "/min/f=..." to "/min/?f=..."
RewriteRule ^([bfg]=.*) index.php?$1 [L,NE]
</IfModule>
<IfModule mod_env.c>
# In case AddOutputFilterByType has been added
SetEnv no-gzip
</IfModule>

View File

@ -25,7 +25,9 @@ return array(
'//plugins/kartik-v-bootstrap-popover-x-36c3827/css/bootstrap-popover-x.min.css',
'//plugins/select2-4.0.13/dist/css/select2.min.css',
'//plugins/select2-bootstrap4-theme-1.5.2/dist/select2-bootstrap4.min.css',
'//css/style.css'
'//css/style.css',
'//css/ms1-doc.css',
'//css/ms1-loader.css'
),
'js' => array(
'//libs/jquery/jquery-3.4.1.min.js',
@ -43,6 +45,8 @@ return array(
'//plugins/flatpickr-4.6.13/dist/flatpickr.min.js',
'//plugins/bootbox-5.4/bootbox.min.js',
'//plugins/select2-4.0.13/dist/js/select2.full.min.js',
'//plugins/kartik-v-bootstrap-popover-x-36c3827/js/bootstrap-popover-x.min.js'
'//plugins/kartik-v-bootstrap-popover-x-36c3827/js/bootstrap-popover-x.min.js',
'//js/ms1-doc.js',
'//js/ms1-loader.js'
)
);

216
mobile.php Normal file
View File

@ -0,0 +1,216 @@
<?php
define('MS1_INSCR_GESTION_ENTRY', true);
require_once('php/inc_start_time.php');
require_once('php/inc_fonctions.php');
require_once('php/inc_fx_messages.php');
require_once('php/inc_fx_compte.php');
require_once('php/inc_fx_promoteur.php');
require_once('php/inc_fx_inscriptions_gestion.php');
require_once('php/inc_fx_eve_acces_promoteur.php');
require_once('php/inc_v2_assets.php');
global $objDatabase, $vDomaine, $vPage, $vtexte_page;
if (!empty($_GET['lang'])) {
$strLangue = $_GET['lang'];
} else {
$strLangue = 'fr';
}
fxInscrGestionInitTextes($strLangue);
fxV2RegisterScript('inscr-gestion');
$vPage = 'mobile.php';
$strCode = 'mobile';
if ($_SERVER['REQUEST_METHOD'] === 'POST' && !empty($_POST['form_action']) && !isset($_SESSION['com_id'])) {
$strFormAction = base64_decode(urldecode($_POST['form_action']), true);
if ($strFormAction === 'login') {
fxLoginCompte($_POST, $strLangue);
exit;
}
}
if (!isset($_SESSION['com_id'])) {
$_SESSION['redirect_after_login'] = $_SERVER['REQUEST_URI'];
$_SESSION['affiche_pas_connexion'] = 'true';
$strMetaTitle = fxInscrGestionT('inscr_gestion_mobile_title');
$strMetaDescription = '';
$strMetaKeywords = '';
$blnBoutonRetour = false;
$footer_script = true;
require_once('inc_header.php');
?>
<div id="page">
<div id="main">
<div class="container bg-white p-3 p-xl-4">
<?php
if (isset($_SESSION['msg'])) {
echo fxMessage($_SESSION['msg'], '', $strLangue);
}
fxInscrGestionRenderLoginPage($strLangue);
?>
</div>
</div>
</div>
<?php
require_once('inc_footer.php');
exit;
}
// MSIN-4401 — Lien d'invite destiné à un autre compte : forcer déconnexion.
$intInviteCom = intval($_GET['invite_com'] ?? 0);
$strInviteSig = (string)($_GET['invite_sig'] ?? '');
$intEveForInvite = 0;
if (!empty($_GET['promoteur_eve_id'])) {
$intEveForInvite = fxInscrGestionParseEveId($_GET['promoteur_eve_id']);
}
if ($intInviteCom > 0 && $intEveForInvite > 0
&& function_exists('fxEveAccesPromoteurInviteSigValid')
&& fxEveAccesPromoteurInviteSigValid($intInviteCom, $intEveForInvite, $strInviteSig)
&& intval($_SESSION['com_id']) !== $intInviteCom) {
global $objDatabase;
$arrInviteCompte = $objDatabase->fxGetRow(
'SELECT com_id, com_prenom, com_nom, com_courriel, com_login FROM inscriptions_comptes WHERE com_id = '
. $intInviteCom . ' AND com_actif = 1 LIMIT 1'
);
$strSessionLabel = trim(
(string)($_SESSION['com_info']['com_prenom'] ?? $_SESSION['com_prenom'] ?? '')
. ' '
. (string)($_SESSION['com_info']['com_nom'] ?? '')
);
if ($strSessionLabel === '') {
$strSessionLabel = (string)($_SESSION['com_info']['com_courriel'] ?? ('#' . intval($_SESSION['com_id'])));
}
$strTargetLabel = $arrInviteCompte
? trim(($arrInviteCompte['com_prenom'] ?? '') . ' ' . ($arrInviteCompte['com_nom'] ?? ''))
: '';
if ($strTargetLabel === '' && $arrInviteCompte) {
$strTargetLabel = (string)($arrInviteCompte['com_courriel'] ?? $arrInviteCompte['com_login'] ?? ('#' . $intInviteCom));
}
if ($strTargetLabel === '') {
$strTargetLabel = '#' . $intInviteCom;
}
$strTargetMail = $arrInviteCompte ? (string)($arrInviteCompte['com_courriel'] ?? '') : '';
$strReturnPath = (string)(parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) ?: '/mobile');
$strQs = (string)(parse_url($_SERVER['REQUEST_URI'], PHP_URL_QUERY) ?: '');
if ($strQs !== '') {
$strReturnPath .= '?' . $strQs;
}
$strLogoutUrl = $vDomaine . '/logout.php?lang=' . urlencode($strLangue)
. '&return=' . rawurlencode($strReturnPath);
$strStayUrl = $vDomaine . ($strLangue === 'en' ? '/account' : '/compte');
$strMetaTitle = fxInscrGestionT('inscr_gestion_mobile_title');
$strMetaDescription = '';
$strMetaKeywords = '';
$blnBoutonRetour = false;
$footer_script = false;
$strMailPart = ($strTargetMail !== '')
? ' (' . htmlspecialchars($strTargetMail, ENT_QUOTES, 'UTF-8') . ')'
: '';
$strSignedAs = str_replace(
'%name%',
'<strong>' . htmlspecialchars($strSessionLabel, ENT_QUOTES, 'UTF-8') . '</strong>',
fxInscrGestionT('mobile_invite_conflict_signed_as')
);
$strInviteFor = str_replace(
array('%name%', '%mail_part%'),
array(
'<strong>' . htmlspecialchars($strTargetLabel, ENT_QUOTES, 'UTF-8') . '</strong>',
$strMailPart,
),
fxInscrGestionT('mobile_invite_conflict_for')
);
require_once('inc_header.php');
?>
<div id="page">
<div id="main">
<div class="container bg-white p-3 p-xl-4">
<div class="alert alert-warning" role="alert">
<p class="mb-2"><?php echo $strSignedAs; ?></p>
<p class="mb-3"><?php echo $strInviteFor; ?></p>
<a class="btn btn-primary rounded-0 mr-2" href="<?php echo htmlspecialchars($strLogoutUrl, ENT_QUOTES, 'UTF-8'); ?>">
<?php echo htmlspecialchars(fxInscrGestionT('mobile_invite_conflict_btn_logout'), ENT_QUOTES, 'UTF-8'); ?>
</a>
<a class="btn btn-outline-secondary rounded-0" href="<?php echo htmlspecialchars($strStayUrl, ENT_QUOTES, 'UTF-8'); ?>">
<?php echo htmlspecialchars(fxInscrGestionT('mobile_invite_conflict_btn_stay'), ENT_QUOTES, 'UTF-8'); ?>
</a>
</div>
</div>
</div>
</div>
<?php
require_once('inc_footer.php');
exit;
}
unset($_SESSION['affiche_pas_connexion']);
$intEveId = 0;
if (!empty($_GET['promoteur_eve_id'])) {
$intEveId = fxInscrGestionParseEveId($_GET['promoteur_eve_id']);
}
$tabEveIds = fxInscrGestionGetPromoteurEveIds($_SESSION['com_id']);
if ($intEveId > 0 && !fxInscrGestionCanAccess($_SESSION['com_id'], $intEveId)) {
header('Location: ' . $vDomaine . '/mobile');
exit;
}
if ($intEveId === 0 && count($tabEveIds) === 1) {
header('Location: ' . $vDomaine . '/mobile?promoteur_eve_id=' . urlencode(base64_encode($tabEveIds[0])));
exit;
}
$strMetaTitle = fxInscrGestionT('inscr_gestion_mobile_title');
$strMetaDescription = '';
$strMetaKeywords = '';
$blnBoutonRetour = false;
$footer_script = ($intEveId > 0);
$strSousTitre = '';
if ($intEveId > 0) {
$arrEvenement = fxGetEvenementsId($intEveId);
$strSousTitre = '<br><small class="text-muted">' . fxUnescape($arrEvenement['eve_nom_' . $strLangue]) . '</small>';
}
require_once('inc_header.php');
?>
<div id="page">
<div id="main">
<div class="container bg-white p-3 p-xl-4">
<?php
if (isset($_SESSION['msg'])) {
echo fxMessage($_SESSION['msg'], '', $strLangue);
}
if ($intEveId === 0) {
if (count($tabEveIds) === 0) {
echo '<div class="alert alert-warning" role="alert">';
echo fxInscrGestionEsc(fxInscrGestionT('mobile_no_acces_v2'));
echo '</div>';
} else {
fxInscrGestionRenderEventPicker($tabEveIds, $strLangue, fxInscrGestionBaseUrl($strLangue, true));
}
} else {
include('inc_tableau_inscriptions_gestion.php');
}
?>
</div>
</div>
</div>
<?php
require_once('inc_footer.php');

134
ms1_kc_qr_batch.php Normal file
View File

@ -0,0 +1,134 @@
<?php
/**
* Génération par lot de QR codes key_chain (kc.php?param1=...).
*
* CLI:
* php ms1_kc_qr_batch.php --start=500 --count=100
*
* Web (protégé par KEYCHAIN_HTTP_EDIT_SECRET):
* /ms1_kc_qr_batch.php?k=<secret>&start=500&count=100
*/
$blnNoMaintenance = true;
function kc_qr_batch_is_cli()
{
return (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg');
}
if (kc_qr_batch_is_cli()) {
require_once __DIR__ . '/php/inc_settings.php';
} else {
require_once __DIR__ . '/php/inc_fonctions.php';
header('X-Robots-Tag: noindex, nofollow');
}
require_once __DIR__ . '/php/inc_fx_kc_qr.php';
function kc_qr_batch_params()
{
$source = kc_qr_batch_is_cli() ? kc_qr_batch_cli_args() : $_GET;
return [
'start' => isset($source['start']) ? intval($source['start']) : 500,
'count' => isset($source['count']) ? intval($source['count']) : 100,
'base_url' => isset($source['base_url']) ? trim((string) $source['base_url']) : 'https://ms1inscription.com',
'module_size' => isset($source['module_size']) ? intval($source['module_size']) : 18,
'with_logo' => !isset($source['with_logo']) || (string) $source['with_logo'] !== '0',
'output_dir' => isset($source['output_dir']) ? trim((string) $source['output_dir']) : (__DIR__ . '/output/kc_qr'),
];
}
function kc_qr_batch_cli_args()
{
global $argv;
$params = [];
if (!is_array($argv)) {
return $params;
}
foreach (array_slice($argv, 1) as $arg) {
if (strpos($arg, '--') !== 0) {
continue;
}
$pair = explode('=', substr($arg, 2), 2);
$params[$pair[0]] = $pair[1] ?? '1';
}
return $params;
}
function kc_qr_batch_web_authorized()
{
$strSecret = isset($_GET['k']) ? (string) $_GET['k'] : '';
return ($strSecret !== '' && defined('KEYCHAIN_HTTP_EDIT_SECRET') && hash_equals(KEYCHAIN_HTTP_EDIT_SECRET, $strSecret));
}
if (!kc_qr_batch_is_cli()) {
if (!kc_qr_batch_web_authorized()) {
http_response_code(403);
echo 'Accès refusé.';
exit;
}
}
$params = kc_qr_batch_params();
if ($params['count'] < 1 || $params['count'] > 500) {
$msg = 'Le paramètre count doit être entre 1 et 500.';
if (kc_qr_batch_is_cli()) {
fwrite(STDERR, $msg . PHP_EOL);
exit(1);
}
http_response_code(400);
echo $msg;
exit;
}
$result = fxKcQrGenerateBatch($params);
if (kc_qr_batch_is_cli()) {
echo 'QR codes générés: ' . $result['count'] . PHP_EOL;
echo 'Plage param1: ' . $result['start'] . ' → ' . $result['end'] . PHP_EOL;
echo 'Dossier: ' . $result['output_dir'] . PHP_EOL;
echo 'Manifeste: ' . $result['manifest'] . PHP_EOL;
exit(0);
}
header('Content-Type: text/html; charset=UTF-8');
?>
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="robots" content="noindex, nofollow">
<title>QR key_chain — génération</title>
<style>
body { font-family: "Segoe UI", sans-serif; max-width: 720px; margin: 2rem auto; padding: 0 1rem; color: #1a1a2e; }
.ok { background: #e6f4ed; border: 1px solid #b8e6d0; padding: 1rem; border-radius: 12px; }
code { background: #f3f4f6; padding: 0.15rem 0.35rem; border-radius: 4px; }
ul { padding-left: 1.2rem; }
</style>
</head>
<body>
<h1>QR codes générés</h1>
<div class="ok">
<p><strong><?php echo (int) $result['count']; ?></strong> fichiers créés
(param1 <?php echo (int) $result['start']; ?> → <?php echo (int) $result['end']; ?>).</p>
<p>Dossier serveur : <code><?php echo htmlspecialchars($result['output_dir'], ENT_QUOTES, 'UTF-8'); ?></code></p>
<p>Manifeste CSV : <code><?php echo htmlspecialchars(basename($result['manifest']), ENT_QUOTES, 'UTF-8'); ?></code></p>
</div>
<h2>Exemples</h2>
<ul>
<?php
$preview = array_slice($result['files'], 0, 6);
foreach ($preview as $file) {
$name = basename($file);
echo '<li><code>' . htmlspecialchars($name, ENT_QUOTES, 'UTF-8') . '</code></li>';
}
if (count($result['files']) > 6) {
echo '<li>… et ' . (count($result['files']) - 6) . ' autres</li>';
}
?>
</ul>
<p>Relancer avec dautres paramètres :
<code>?k=…&amp;start=<?php echo (int) $params['start']; ?>&amp;count=<?php echo (int) $params['count']; ?></code></p>
</body>
</html>

682
ms1_kc_set.php Normal file
View File

@ -0,0 +1,682 @@
<?php
/**
* Outil temporaire : modifier key_chain_http pour un pk_key_chain.
* Accès 1 : /ms1_kc_set.php?k=<KEYCHAIN_HTTP_EDIT_SECRET>
* Accès 2 : compte connecté dont com_id est dans KEYCHAIN_HTTP_EDIT_ALLOWED_COM_IDS
*/
$blnNoMaintenance = true;
session_start();
require_once __DIR__ . '/php/inc_fonctions.php';
header('X-Robots-Tag: noindex, nofollow');
header('Cache-Control: no-store, no-cache, must-revalidate');
global $objDatabase, $vDomaine;
function kc_set_allowed_com_ids()
{
if (!defined('KEYCHAIN_HTTP_EDIT_ALLOWED_COM_IDS') || KEYCHAIN_HTTP_EDIT_ALLOWED_COM_IDS === '') {
return [];
}
$arr = array_map('intval', explode(',', KEYCHAIN_HTTP_EDIT_ALLOWED_COM_IDS));
return array_values(array_filter($arr, function ($id) {
return $id > 0;
}));
}
function kc_set_com_id_authorized()
{
if (!isset($_SESSION['com_id']) || (int) $_SESSION['com_id'] <= 0) {
return false;
}
return in_array((int) $_SESSION['com_id'], kc_set_allowed_com_ids(), true);
}
$strSecret = isset($_GET['k']) ? (string) $_GET['k'] : '';
$blnAuthSecret = ($strSecret !== '' && hash_equals(KEYCHAIN_HTTP_EDIT_SECRET, $strSecret));
if ($blnAuthSecret) {
$_SESSION['kc_edit_auth_key'] = 1;
}
$blnAuthUser = kc_set_com_id_authorized();
$blnAuth = $blnAuthSecret || $blnAuthUser || !empty($_SESSION['kc_edit_auth_key']);
if (!$blnAuth) {
if (!isset($_SESSION['com_id']) && kc_set_allowed_com_ids() !== []) {
$_SESSION['redirect_after_login'] = $_SERVER['REQUEST_URI'];
header('Location: ' . $vDomaine . '/compte');
exit;
}
http_response_code(403);
echo '<!DOCTYPE html><html lang="fr"><head><meta charset="UTF-8"><title>Accès refusé</title></head><body>';
echo '<p>Accès refusé.</p>';
if (kc_set_allowed_com_ids() !== []) {
echo '<p><a href="' . htmlspecialchars($vDomaine . '/compte', ENT_QUOTES, 'UTF-8') . '">Se connecter</a></p>';
}
echo '</body></html>';
exit;
}
if (empty($_SESSION['kc_edit_csrf'])) {
$_SESSION['kc_edit_csrf'] = bin2hex(random_bytes(16));
}
$strCsrf = $_SESSION['kc_edit_csrf'];
$strBasePath = $_SERVER['PHP_SELF'];
function kc_set_page_url(array $params = [])
{
global $strBasePath;
$query = $params;
if (!empty($_SESSION['kc_edit_auth_key']) && !isset($query['k'])) {
$query = array_merge(['k' => KEYCHAIN_HTTP_EDIT_SECRET], $query);
}
$qs = http_build_query($query);
return $strBasePath . ($qs !== '' ? '?' . $qs : '');
}
$strPageUrl = kc_set_page_url();
$strBaseUrlHtml = htmlspecialchars($strPageUrl, ENT_QUOTES, 'UTF-8');
$blnCanShareLink = !empty($_SESSION['kc_edit_auth_key']);
$strShareUrl = '';
if ($blnCanShareLink) {
$scheme = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
$strShareUrl = $scheme . '://' . $_SERVER['HTTP_HOST'] . kc_set_page_url(['k' => KEYCHAIN_HTTP_EDIT_SECRET]);
}
$strUserName = '';
if ($blnAuthUser && !empty($_SESSION['com_prenom'])) {
$strUserName = trim((string) $_SESSION['com_prenom']);
} elseif ($blnAuthUser && !empty($_SESSION['com_info']['com_prenom'])) {
$strUserName = trim((string) $_SESSION['com_info']['com_prenom']);
}
$strMessage = '';
$strMessageClass = '';
$intPk = 0;
$strCurrentHttp = '';
$blnShowUrlForm = false;
$strVehiculeSaisi = '';
if (!empty($_GET['ok'])) {
$strMessage = 'Le lien de votre code QR a été enregistré. Vous pouvez modifier un autre véhicule ci-dessous.';
$strMessageClass = 'ok';
}
function kc_set_parse_vehicule_no($input)
{
$input = trim((string) $input);
if ($input === '' || !ctype_digit($input)) {
return 0;
}
$no = intval($input);
if ($no <= 0) {
return 0;
}
return $no + KEYCHAIN_HTTP_EDIT_SHIFT;
}
function kc_set_validate_url($url)
{
$url = trim($url);
if ($url === '') {
return false;
}
if (!filter_var($url, FILTER_VALIDATE_URL)) {
return false;
}
$parts = parse_url($url);
if (empty($parts['scheme']) || !in_array(strtolower($parts['scheme']), ['http', 'https'], true)) {
return false;
}
return $url;
}
function kc_set_save_lien_for_pk($intPk, $strVehiculeSaisi, $strNewHttp)
{
global $objDatabase, $strMessage, $strMessageClass, $strCurrentHttp, $blnShowUrlForm;
if ($strVehiculeSaisi !== '' && kc_set_parse_vehicule_no($strVehiculeSaisi) !== $intPk) {
$intPk = 0;
}
if ($intPk <= 0) {
$strMessage = 'Veuillez recommencer avec votre numéro de véhicule.';
$strMessageClass = 'err';
return false;
}
$sqlCheck = 'SELECT pk_key_chain FROM key_chain WHERE pk_key_chain = ' . $intPk;
if ($objDatabase->fxGetRow($sqlCheck) === null) {
$strMessage = 'Code QR introuvable. Vérifiez votre numéro de véhicule.';
$strMessageClass = 'err';
return false;
}
$sqlUpdate = "UPDATE key_chain SET key_chain_http = '" . $objDatabase->fxEscape($strNewHttp) . "' WHERE pk_key_chain = " . $intPk;
if ($objDatabase->fxQuery($sqlUpdate) === false) {
$strMessage = 'Impossible denregistrer pour le moment. Réessayez plus tard.';
$strMessageClass = 'err';
$blnShowUrlForm = true;
$strCurrentHttp = $strNewHttp;
return false;
}
header('Location: ' . kc_set_page_url(['ok' => '1']));
exit;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$strPostedCsrf = isset($_POST['csrf']) ? (string) $_POST['csrf'] : '';
if (!hash_equals($strCsrf, $strPostedCsrf)) {
$strMessage = 'Veuillez recharger la page et réessayer.';
$strMessageClass = 'err';
} else {
$strAction = isset($_POST['action']) ? (string) $_POST['action'] : '';
if ($strAction === 'load') {
$strVehiculeSaisi = isset($_POST['vehicule_no']) ? trim((string) $_POST['vehicule_no']) : '';
$intPk = kc_set_parse_vehicule_no($strVehiculeSaisi);
if ($intPk <= 0) {
$strMessage = 'Veuillez entrer un numéro de véhicule valide.';
$strMessageClass = 'err';
} else {
$sql = 'SELECT key_chain_http FROM key_chain WHERE pk_key_chain = ' . $intPk;
$row = $objDatabase->fxGetRow($sql);
if ($row === null || !isset($row['key_chain_http'])) {
$strMessage = 'Aucun code QR trouvé pour ce numéro de véhicule.';
$strMessageClass = 'err';
} else {
$strCurrentHttp = $row['key_chain_http'];
$blnShowUrlForm = true;
}
}
} elseif ($strAction === 'save') {
$intPk = intval(isset($_POST['pk']) ? $_POST['pk'] : 0);
$strVehiculeSaisi = isset($_POST['vehicule_no']) ? trim((string) $_POST['vehicule_no']) : '';
$strLienPost = isset($_POST['lien_qr']) ? trim((string) $_POST['lien_qr']) : '';
$strNewHttp = kc_set_validate_url($strLienPost);
if ($strNewHttp === false) {
$strMessage = 'Veuillez entrer une adresse Web valide (commençant par http:// ou https://).';
$strMessageClass = 'err';
$blnShowUrlForm = true;
$strCurrentHttp = $strLienPost;
} else {
kc_set_save_lien_for_pk($intPk, $strVehiculeSaisi, $strNewHttp);
}
} elseif ($strAction === 'save_ms1') {
$intPk = intval(isset($_POST['pk']) ? $_POST['pk'] : 0);
$strVehiculeSaisi = isset($_POST['vehicule_no']) ? trim((string) $_POST['vehicule_no']) : '';
kc_set_save_lien_for_pk($intPk, $strVehiculeSaisi, 'https://ms1.ca');
}
}
}
?>
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="robots" content="noindex, nofollow">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Code QR — lien de redirection</title>
<style>
:root {
--ms1-orange: #ff8c00;
--ms1-orange-dark: #e67a00;
--ms1-ink: #1a1a2e;
--ms1-card: #ffffff;
--ms1-muted: #5c6370;
--ms1-ok: #0d7a4a;
--ms1-err: #b42318;
--radius: 16px;
--shadow: 0 12px 40px rgba(26, 26, 46, 0.12);
}
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
font-family: "Segoe UI", system-ui, -apple-system, sans-serif;
color: var(--ms1-ink);
background:
radial-gradient(ellipse 80% 50% at 50% -10%, rgba(255, 140, 0, 0.25), transparent),
linear-gradient(160deg, #f8f4ef 0%, #eef2f7 45%, #e8edf5 100%);
padding: 1.5rem 1rem 3rem;
}
.page {
max-width: 640px;
margin: 0 auto;
animation: fadeUp 0.5s ease-out;
}
@keyframes fadeUp {
from { opacity: 0; transform: translateY(12px); }
to { opacity: 1; transform: translateY(0); }
}
.hero {
text-align: center;
margin-bottom: 1.5rem;
}
.qr-icon {
width: 56px;
height: 56px;
margin: 0 auto 0.75rem;
background: var(--ms1-ink);
border-radius: 12px;
padding: 10px;
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 4px;
box-shadow: 0 6px 20px rgba(26, 26, 46, 0.2);
}
.qr-icon span {
background: var(--ms1-orange);
border-radius: 2px;
}
.qr-icon span:nth-child(5) { background: #fff; }
h1 {
margin: 0;
font-size: 1.75rem;
font-weight: 700;
letter-spacing: -0.02em;
}
.user-bar {
margin-top: 0.5rem;
color: var(--ms1-muted);
font-size: 1rem;
}
.intro {
text-align: center;
font-size: 1.08rem;
line-height: 1.6;
color: var(--ms1-muted);
margin: 0 0 1.5rem;
padding: 0 0.5rem;
}
.steps {
display: flex;
justify-content: center;
gap: 0.5rem;
margin-bottom: 1.25rem;
}
.step {
width: 2.5rem;
height: 2.5rem;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 0.95rem;
background: #dde3eb;
color: var(--ms1-muted);
transition: transform 0.2s, background 0.2s;
}
.step.active {
background: var(--ms1-orange);
color: #fff;
transform: scale(1.08);
box-shadow: 0 4px 14px rgba(255, 140, 0, 0.45);
}
.step.done {
background: var(--ms1-ok);
color: #fff;
}
.step-line {
width: 2rem;
height: 3px;
align-self: center;
background: #dde3eb;
border-radius: 2px;
}
.card {
background: var(--ms1-card);
border-radius: var(--radius);
box-shadow: var(--shadow);
padding: 1.75rem 1.5rem 1.5rem;
border: 1px solid rgba(255, 255, 255, 0.8);
}
.field-label {
display: block;
font-weight: 700;
font-size: 1.05rem;
margin-bottom: 0.6rem;
color: var(--ms1-ink);
}
.field-wrap {
position: relative;
margin-bottom: 0.25rem;
}
.input-vehicule {
width: 100%;
padding: 1rem 1.15rem;
font-size: 1.75rem;
font-weight: 700;
text-align: center;
letter-spacing: 0.08em;
border: 2px solid #e2e8f0;
border-radius: 12px;
transition: border-color 0.2s, box-shadow 0.2s;
}
.input-vehicule:focus {
outline: none;
border-color: var(--ms1-orange);
box-shadow: 0 0 0 4px rgba(255, 140, 0, 0.2);
}
.input-lien {
width: 100%;
padding: 1rem 1.15rem;
font-size: 1.05rem;
line-height: 1.45;
border: 2px solid #e2e8f0;
border-radius: 12px;
transition: border-color 0.2s, box-shadow 0.2s;
}
.input-lien:focus {
outline: none;
border-color: var(--ms1-orange);
box-shadow: 0 0 0 4px rgba(255, 140, 0, 0.2);
}
.input-lien::placeholder {
color: #9aa3af;
font-family: inherit;
}
.actions {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.75rem 1rem;
margin-top: 1.25rem;
}
.btn-enter {
display: inline-flex;
align-items: center;
gap: 0.55rem;
padding: 0.85rem 1.5rem;
font-size: 1.05rem;
font-weight: 700;
color: #fff;
background: linear-gradient(180deg, var(--ms1-orange) 0%, var(--ms1-orange-dark) 100%);
border: none;
border-radius: 999px;
cursor: pointer;
box-shadow: 0 6px 20px rgba(255, 140, 0, 0.4);
transition: transform 0.15s, box-shadow 0.15s;
}
.btn-enter:hover {
transform: translateY(-2px);
box-shadow: 0 10px 28px rgba(255, 140, 0, 0.5);
}
.btn-enter:active {
transform: translateY(0);
}
.btn-ms1 {
display: inline-flex;
align-items: center;
padding: 0.85rem 1.25rem;
font-size: 1rem;
font-weight: 700;
color: var(--ms1-ink);
background: #fff;
border: 2px solid var(--ms1-ink);
border-radius: 999px;
cursor: pointer;
transition: background 0.15s, color 0.15s;
}
.btn-ms1:hover {
background: var(--ms1-ink);
color: #fff;
}
.btn-enter:disabled {
opacity: 0.85;
cursor: wait;
transform: none;
}
.btn-enter .key-cap {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 2rem;
height: 1.75rem;
padding: 0 0.4rem;
background: rgba(255, 255, 255, 0.25);
border-radius: 6px;
font-size: 0.9rem;
border: 1px solid rgba(255, 255, 255, 0.35);
}
.hint {
color: var(--ms1-muted);
font-size: 0.9rem;
margin: 0;
}
.msg {
margin: 0 0 1.25rem;
padding: 1rem 1.15rem;
border-radius: 12px;
font-size: 1rem;
font-weight: 500;
animation: fadeUp 0.35s ease-out;
}
.msg.ok {
background: #e6f4ed;
color: var(--ms1-ok);
border: 1px solid #b8e6d0;
}
.msg.err {
background: #fdecea;
color: var(--ms1-err);
border: 1px solid #f5c4c0;
}
.back {
text-align: center;
margin-top: 1.5rem;
}
.back a {
color: var(--ms1-muted);
text-decoration: none;
font-weight: 600;
padding: 0.5rem 1rem;
border-radius: 8px;
transition: color 0.2s, background 0.2s;
}
.back a:hover {
color: var(--ms1-orange-dark);
background: rgba(255, 140, 0, 0.1);
}
.share-box {
background: #fff8ee;
border: 1px dashed var(--ms1-orange);
border-radius: 12px;
padding: 1rem 1.15rem;
margin-bottom: 1.25rem;
}
.share-box strong {
display: block;
margin-bottom: 0.5rem;
font-size: 0.95rem;
}
.share-row {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.share-url {
flex: 1 1 12rem;
min-width: 0;
padding: 0.55rem 0.65rem;
font-size: 0.82rem;
border: 1px solid #e2e8f0;
border-radius: 8px;
background: #fff;
}
.btn-copy {
padding: 0.55rem 1rem;
font-weight: 600;
font-size: 0.9rem;
border: none;
border-radius: 8px;
background: var(--ms1-ink);
color: #fff;
cursor: pointer;
}
.btn-copy:hover { background: #2d2d44; }
@media (max-width: 480px) {
.actions { flex-direction: column; align-items: stretch; }
.btn-enter { justify-content: center; }
}
</style>
</head>
<body>
<div class="page">
<header class="hero">
<div class="qr-icon" aria-hidden="true">
<span></span><span></span><span></span>
<span></span><span></span><span></span>
<span></span><span></span><span></span>
</div>
<h1>Code QR</h1>
<?php if ($strUserName !== '') { ?>
<p class="user-bar">Bonjour <?php echo htmlspecialchars($strUserName, ENT_QUOTES, 'UTF-8'); ?>.</p>
<?php } ?>
</header>
<p class="intro">Entrez votre numéro de véhicule. Vous pourrez ainsi changer le lien de votre code QR.</p>
<?php if ($blnCanShareLink && $strShareUrl !== '') { ?>
<div class="share-box">
<strong>Lien à envoyer aux autres</strong>
<p class="hint" style="margin:0 0 0.65rem;">Chaque personne ouvre ce lien et entre <em>son</em> numéro de véhicule.</p>
<div class="share-row">
<input type="text" class="share-url" id="share_url" readonly value="<?php echo htmlspecialchars($strShareUrl, ENT_QUOTES, 'UTF-8'); ?>">
<button type="button" class="btn-copy" id="btn_copy_link">Copier le lien</button>
</div>
</div>
<?php } ?>
<div class="steps" aria-hidden="true">
<span class="step <?php echo !$blnShowUrlForm ? 'active' : 'done'; ?>">1</span>
<span class="step-line"></span>
<span class="step <?php echo $blnShowUrlForm ? 'active' : ''; ?>">2</span>
</div>
<?php if ($strMessage !== '') { ?>
<div class="msg <?php echo htmlspecialchars($strMessageClass, ENT_QUOTES, 'UTF-8'); ?>" role="status"><?php
echo htmlspecialchars($strMessage, ENT_QUOTES, 'UTF-8');
?></div>
<?php } ?>
<div class="card">
<?php if (!$blnShowUrlForm) { ?>
<form method="post" action="<?php echo $strBaseUrlHtml; ?>" id="frm_load">
<input type="hidden" name="csrf" value="<?php echo htmlspecialchars($strCsrf, ENT_QUOTES, 'UTF-8'); ?>">
<input type="hidden" name="action" value="load">
<label class="field-label" for="vehicule_no">Numéro de véhicule</label>
<div class="field-wrap">
<input type="text" inputmode="numeric" pattern="[0-9]+" class="input-vehicule" id="vehicule_no" name="vehicule_no" required autofocus placeholder="ex. 8" value="<?php echo htmlspecialchars($strVehiculeSaisi, ENT_QUOTES, 'UTF-8'); ?>">
</div>
<div class="actions">
<button type="submit" class="btn-enter">
<span class="key-cap">↵</span>
<span>Continuer</span>
</button>
<p class="hint">Remplissez le numéro, puis cliquez ou appuyez sur Entrée.</p>
</div>
</form>
<?php } else { ?>
<?php if ($strVehiculeSaisi !== '') { ?>
<p class="hint" style="margin:0 0 1rem;">Véhicule <strong><?php echo htmlspecialchars($strVehiculeSaisi, ENT_QUOTES, 'UTF-8'); ?></strong></p>
<?php } ?>
<form method="post" action="<?php echo $strBaseUrlHtml; ?>" id="frm_save">
<input type="hidden" name="csrf" value="<?php echo htmlspecialchars($strCsrf, ENT_QUOTES, 'UTF-8'); ?>">
<input type="hidden" name="action" value="save">
<input type="hidden" name="pk" value="<?php echo (int) $intPk; ?>">
<input type="hidden" name="vehicule_no" value="<?php echo htmlspecialchars($strVehiculeSaisi, ENT_QUOTES, 'UTF-8'); ?>">
<label class="field-label" for="lien_qr">Lien de votre code QR</label>
<div class="field-wrap">
<input type="url" class="input-lien" id="lien_qr" name="lien_qr" required autofocus placeholder="https://..." value="<?php echo htmlspecialchars($strCurrentHttp, ENT_QUOTES, 'UTF-8'); ?>">
</div>
<div class="actions">
<button type="submit" class="btn-enter">
<span class="key-cap">↵</span>
<span>Enregistrer</span>
</button>
<p class="hint">Ou enregistrer directement le lien par défaut :</p>
</div>
</form>
<form method="post" action="<?php echo $strBaseUrlHtml; ?>" style="margin-top:0.75rem;">
<input type="hidden" name="csrf" value="<?php echo htmlspecialchars($strCsrf, ENT_QUOTES, 'UTF-8'); ?>">
<input type="hidden" name="action" value="save_ms1">
<input type="hidden" name="pk" value="<?php echo (int) $intPk; ?>">
<input type="hidden" name="vehicule_no" value="<?php echo htmlspecialchars($strVehiculeSaisi, ENT_QUOTES, 'UTF-8'); ?>">
<button type="submit" class="btn-ms1">Mettre le lien vers ms1.ca</button>
</form>
<p class="hint" style="margin-top:0.75rem;">Sinon modifiez ladresse ci-dessus et appuyez sur Entrée pour enregistrer.</p>
<?php } ?>
</div>
<?php if ($blnShowUrlForm) { ?>
<p class="back"><a href="<?php echo $strBaseUrlHtml; ?>">← Autre numéro de véhicule</a></p>
<?php } ?>
</div>
<?php if ($blnCanShareLink && $strShareUrl !== '') { ?>
<script>
(function () {
var btn = document.getElementById('btn_copy_link');
var input = document.getElementById('share_url');
if (!btn || !input) return;
btn.addEventListener('click', function () {
input.select();
input.setSelectionRange(0, 99999);
var ok = false;
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(input.value).then(function () { ok = true; });
}
try { ok = document.execCommand('copy') || ok; } catch (e) {}
btn.textContent = ok ? 'Copié !' : 'Sélectionné — Ctrl+C';
setTimeout(function () { btn.textContent = 'Copier le lien'; }, 2000);
});
})();
</script>
<?php } ?>
</body>
</html>

View File

@ -1,526 +0,0 @@
<?php
// mets les totaux dans un tableau (JSP)
// param : id de l'vévénement, no panier, langue
// créé : 2021-05-12
function fxUpdateFinancesMembership($intEvenement) {
global $objDatabase;
$strOutput = '';
// SQL
$sqlTypeEvenement = "
SELECT
te_id
FROM
inscriptions_evenements
WHERE
eve_id = " . intval($intEvenement) . "
";
$intTypeEvenement = $objDatabase->fxGetVar($sqlTypeEvenement);
if (intval($intTypeEvenement) != 0) {
if (intval($intTypeEvenement) == 13) {
$sqlAcheteurs = "
SELECT
a.*, " . intval($intEvenement) . " AS eve_id_membership
FROM
inscriptions_panier_acheteurs a
WHERE
a.sta_id = 3
AND a.eve_id <> " . intval($intEvenement) . "
AND a.no_panier IN(
SELECT no_panier FROM inscriptions_panier_produits_new p WHERE p.eve_id = " . intval($intEvenement) . "
UNION
SELECT no_panier FROM inscriptions_panier_epreuves_commandees ec WHERE ec.eve_id = " . intval($intEvenement) . "
)
AND a.no_panier NOT IN(
SELECT no_panier FROM rapport_finances_membership
)
";
} else {
$sqlAcheteurs = "
SELECT
*
FROM
inscriptions_panier_acheteurs a
JOIN inscriptions_evenements e ON e.eve_id = a.eve_id
WHERE
a.sta_id = 3
AND a.eve_id = " . intval($intEvenement) . "
AND a.no_panier IN(
SELECT no_panier FROM inscriptions_panier_produits_new p WHERE p.eve_id = e.eve_id_membership
UNION
SELECT no_panier FROM inscriptions_panier_epreuves_commandees ec WHERE ec.eve_id = e.eve_id_membership
)
";
}
$tabAcheteurs = $objDatabase->fxGetResults($sqlAcheteurs);
if ($tabAcheteurs != null) {
foreach ($tabAcheteurs as $acheteur) {
//$strOutput .= '<h1>no_commande: ' . $acheteur['no_commande'] . '</h1>';
$arrTotalsMembership = fxTotalPanierMembership($acheteur['eve_id'], $acheteur['eve_id_membership'], $acheteur['no_panier']);
//$strOutput .= '<pre>' . print_r($arrTotals, true) . '</pre>';
// insérer les totaux dans la table de jonction (NOTE: no_commande et no_panier onnt des INDEX UNIQUE donc, aucune chance de doublons)
$sqlInsert = "INSERT INTO rapport_finances_membership SET no_commande = '" . $objDatabase->fxEscape($acheteur['no_commande']) . "', no_panier = '" . $objDatabase->fxEscape($acheteur['no_panier']) . "', eve_id = " . intval($acheteur['eve_id']) . ", eve_id_membership = " . intval($acheteur['eve_id_membership']) . ", ach_total = " . floatval($arrTotalsMembership['total']) . ", ach_taxe1 = " . floatval($arrTotalsMembership['taxes'][1]['montant']) . ", ach_taxe2 = " . floatval($arrTotalsMembership['taxes'][2]['montant']) . ", ach_taxe3 = " . floatval($arrTotalsMembership['taxes'][3]['montant']) . ", rabais_total = " . floatval($arrTotalsMembership['rabais_total']);
$strOutput = $objDatabase->fxQuery($sqlInsert);
// mettre à jour les totaux de la facture originale
$arrTotalsOriginal = fxTotalPanier($acheteur['no_panier'], 'fr');
$sqlUpdate = "UPDATE inscriptions_panier_acheteurs SET ach_total = " . floatval(floatval($arrTotalsOriginal['total']) - floatval($arrTotalsMembership['total'])) . ", ach_taxe1 = " . floatval(floatval($arrTotalsOriginal['taxes'][1]['montant']) - floatval($arrTotalsMembership['taxes'][1]['montant'])) . ", ach_taxe2 = " . floatval(floatval($arrTotalsOriginal['taxes'][2]['montant']) - floatval($arrTotalsMembership['taxes'][2]['montant'])) . ", ach_taxe3 = " . floatval(floatval($arrTotalsOriginal['taxes'][3]['montant']) - floatval($arrTotalsMembership['taxes'][3]['montant'])) . ", rabais_total = " . floatval(floatval($arrTotalsOriginal['rabais_total']) - floatval($arrTotalsMembership['rabais_total'])) . " WHERE no_panier = '" . $objDatabase->fxEscape($acheteur['no_panier']) . "'";
//$strOutput .= $objDatabase->fxQuery($sqlUpdate);
}
}
}
return $strOutput;
}
function fxTotalPanierMembership($intEvenement, $intEvenementMembership, $strNoPanier) {
global $objDatabase;
$fltTotalTaxes = $fltTotalTaxesFrais = $fltMontantTaxable = $fltSousTotal = $fltSousTotal2 = $fltTotal = 0;
// info événements
$sqlEvenements = "SELECT * FROM inscriptions_panier_evenements WHERE eve_id = " . intval($intEvenement);
$tabEvenements = $objDatabase->fxGetRow($sqlEvenements);
// info épreuves
$sqlEpreuves = "SELECT pec_id FROM inscriptions_panier_epreuves_commandees WHERE no_panier = '" . $objDatabase->fxEscape($strNoPanier) . "' AND eve_id = " . intval($intEvenementMembership) . " ORDER BY pec_id";
$tabEpreuves = $objDatabase->fxGetResults($sqlEpreuves);
// Calculer le sous-total
$sqlTotal = "
SELECT
(SELECT SUM(pec_prix) FROM inscriptions_panier_epreuves_commandees WHERE no_panier = '" . $objDatabase->fxEscape($strNoPanier) . "' AND eve_id = " . intval($intEvenementMembership) . ") AS prix_epreuves,
(SELECT SUM(e.rab_montant) FROM inscriptions_panier_epreuves_commandees e, inscriptions_panier_rabais r WHERE r.pra_id = e.pra_id AND r.pra_depot = 0 AND e.no_panier = '" . $objDatabase->fxEscape($strNoPanier) . "' AND eve_id = " . intval($intEvenementMembership) . ") AS rabais_epreuves,
(SELECT SUM(c.pec_prix - c.rab_montant) FROM inscriptions_panier_epreuves_commandees c, inscriptions_panier_epreuves e WHERE c.epr_id = e.epr_id AND e.epr_taxes_incluses = 0 AND c.no_panier = '" . $objDatabase->fxEscape($strNoPanier) . "' AND eve_id = " . intval($intEvenementMembership) . ") AS prix_epreuves_taxable,
(SELECT SUM(pro_prix) FROM inscriptions_panier_produits_new WHERE no_panier = '" . $objDatabase->fxEscape($strNoPanier) . "' AND eve_id = " . intval($intEvenementMembership) . ") AS prix_produits,
(SELECT SUM(p.rab_montant) FROM inscriptions_panier_produits_new p, inscriptions_panier_rabais r WHERE r.pra_id = p.pra_id AND r.pra_depot = 0 AND p.no_panier = '" . $objDatabase->fxEscape($strNoPanier) . "' AND eve_id = " . intval($intEvenementMembership) . ") AS rabais_produits,
(SELECT SUM(pro_prix - rab_montant) FROM inscriptions_panier_produits_new WHERE pro_taxable = 1 AND no_panier = '" . $objDatabase->fxEscape($strNoPanier) . "' AND eve_id = " . intval($intEvenementMembership) . ") AS prix_produits_taxable,
(SELECT SUM(r.rab_montant) FROM inscriptions_panier_rabais r, inscriptions_panier_rabais_acheteurs a WHERE r.pra_id = a.pra_id AND r.pra_depot = 0 AND a.no_panier = '" . $objDatabase->fxEscape($strNoPanier) . "' AND r.rab_pourcent = 0 AND r.rab_epreuves = '' AND r.rab_produits = '') AS rabais_panier_montant,
(SELECT SUM(r.rab_pourcent) FROM inscriptions_panier_rabais r, inscriptions_panier_rabais_acheteurs a WHERE r.pra_id = a.pra_id AND r.pra_depot = 0 AND a.no_panier = '" . $objDatabase->fxEscape($strNoPanier) . "' AND r.rab_montant = 0 AND r.rab_epreuves = '' AND r.rab_produits = '') AS rabais_panier_pourcent
FROM DUAL
";
$tabTotal = $objDatabase->fxGetRow($sqlTotal);
$fltRegistrationTotal = $tabTotal['prix_epreuves'] + $tabTotal['prix_produits'];
// calculer les rabais
$fltRabaisMontant = $tabTotal['rabais_panier_montant'] + $tabTotal['rabais_epreuves'] + $tabTotal['rabais_produits'];
$intRabaisPourcent = $tabTotal['rabais_panier_pourcent'];
if ($intRabaisPourcent > 100)
$intRabaisPourcent = 100;
// calculer le rabais %
$fltRabaisPourcent = ($fltRegistrationTotal - $fltRabaisMontant) * ($intRabaisPourcent / 100);
// calculer le total après rabais pourcent
$fltRabaisTotal = $fltRabaisMontant + $fltRabaisPourcent;
$fltRegistrationTotal -= ($fltRabaisTotal);
if ($fltRegistrationTotal < 0)
$fltRegistrationTotal = 0;
$fltMontantTaxable = ($tabTotal['prix_epreuves_taxable'] + $tabTotal['prix_produits_taxable']) - ($fltRabaisPourcent + $tabTotal['rabais_panier_montant']);
if ($fltMontantTaxable < 0)
$fltMontantTaxable = 0;
// calculer le sous total (registration + option)
$fltSousTotal = $fltRegistrationTotal;
// calculer les taxes
$tabTaxes[1] = $tabTaxes[2] = $tabTaxes[3] = array('label_fr' => '', 'label_en' => '', 'montant' => 0, 'taux' => 0, 'numero' => '');
// récupérer les infos des taxes
if ($tabEvenements['tt_id'] > 1) {
$sqlTaux = "SELECT t.tau_nom_fr, t.tau_nom_en, t.tau_taux, t.tau_numero_client, t.tau_numero_ms1 FROM inscriptions_taux t, inscriptions_taux_types_taux ttt WHERE t.tau_id = ttt.tau_id AND ttt.tt_id = " . intval($tabEvenements['tt_id']) . " ORDER BY ttt.ttt_tri";
$tabTaux = $objDatabase->fxGetResults($sqlTaux);
if ($tabTaux != null) {
for ($intCtr = 1; $intCtr <= count($tabTaux); $intCtr++) {
$fltMontantTaxes = round($fltMontantTaxable * ($tabTaux[$intCtr]['tau_taux'] / 100), 2);
$tabTaxes[$intCtr] = array('label_fr' => $tabTaux[$intCtr]['tau_nom_fr'] . ' (' . $tabTaux[$intCtr]['tau_taux'] . '%)', 'label_en' => $tabTaux[$intCtr]['tau_nom_en'] . ' (' . $tabTaux[$intCtr]['tau_taux'] . '%)', 'montant' => $fltMontantTaxes, 'taux' => $tabTaux[$intCtr]['tau_taux'], 'numero' => $tabTaux[$intCtr]['tau_numero_client']);
$fltTotalTaxes += $fltMontantTaxes;
}
} else {
fxcreer_log("Il y a eu une erreur lors de la récupération des taxes:\n" . $sqlTaux);
}
}
// calculer le total après taxes
$fltSousTotal2 = $fltSousTotal + $fltTotalTaxes;
// calculer le total avec frais d'administration
$fltTotal = $fltSousTotal2;
// affiche le sous-total, les taxes, rabais et le total complet
$tabRetour['taxes'] = $tabTaxes;
$tabRetour['total_taxes'] = $fltTotalTaxes;
$tabRetour['total'] = abs($fltTotal);
$tabRetour['stotal'] = $fltSousTotal;
$tabRetour['stotal2'] = $fltSousTotal2;
$tabRetour['rabais_total'] = $fltRabaisTotal;
if ($tabEpreuves != null)
$tabRetour['pec_id'] = implode(',', array_map(function($tab) { return $tab['pec_id']; }, $tabEpreuves));
else
$tabRetour['pec_id'] = 0;
$tabRetour['eve_id'] = $tabEvenements['eve_id'];
$tabRetour['eve_id_membership'] = $intEvenementMembership;
return $tabRetour;
}
function fxGetTermes($intEvenement, $strNoPanier, $blnMembership, $strLangue) {
$strSuffixe = $strTermesText = $strTermesHTML = $strTermesModal = '';
$arrRetour = $arrTermesModalButtons = $arr_epreuves_id = array();
// faire un tableau avec tout les epr_id (sl)
$epreuves = fxListPanier($strNoPanier);
foreach ($epreuves[3] as $key => $value) {
$arr_epreuves_id[$key]= $value['epr_id'] ;
}
$tabEvenement = fxGetEvenements(fxGetEvenementsUrl($intEvenement), $strLangue);
if ($blnMembership) {
$strSuffixe = '_mem';
$tabEvenement = fxGetEvenements(fxGetEvenementsUrl($tabEvenement['general']['eve_id_membership']), $strLangue);
}
if ($tabEvenement['general']['eve_waiver_' . $strLangue] != '') {
$strTermesText .= '<p><strong>' . afficheTexte('terme_waiver', 0, 1, 1) . ' :</strong></p>';
$strTermesText .= $tabEvenement['general']['eve_waiver_' . $strLangue] . '<br>';
$strTermesHTML = '
<div class="card-text">
' . afficheTexte('terme_jaccepte', 0, 1, 1) . '
<strong><a href="#div_waiver' . $strSuffixe . '" id="link_waiver' . $strSuffixe . '" data-toggle="modal">' . afficheTexte('terme_waiver', 0, 1, 1) .'</a>.</strong>
<div class="form-check d-inline-block ml-2">
<input class="form-check-input position-static chk_termes" type="checkbox" id="chk_waiver' . $strSuffixe . '" name="chk_waiver' . $strSuffixe . '" value="1" aria-label="..." readonly>
</div>
</div>
';
$strTermesModal .= '
<div class="modal fade" id="div_waiver' . $strSuffixe . '" data-backdrop="static" data-keyboard="false" tabindex="-1" aria-labelledby="div_waiver_label' . $strSuffixe . '" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-centered modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="div_waiver_label' . $strSuffixe . '">' . afficheTexte('terme_waiver_titre', 0, 1, 1) . '</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="' . afficheTexte('btn_terme_close', 0, 1, 1) . '">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
' . $tabEvenement['general']['eve_waiver_' . $strLangue] . '
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary rounded-0" data-dismiss="modal">' . afficheTexte('btn_terme_close', 0, 1, 1) . '</button>
<button id="btn_accept_waiver' . $strSuffixe . '" type="button" class="btn btn-primary rounded-0">' . afficheTexte('btn_terme_jaccepte', 0, 1, 1) . '</button>
</div>
</div>
</div>
</div>
';
$arrTermesModalButtons[] = 'btn_accept_waiver' . $strSuffixe;
}
if ($tabEvenement['general']['eve_policy_' . $strLangue] != '') {
$strTermesText .= '<p><strong>' . afficheTexte('terme_politique_achat', 0, 1, 1) . ' :</strong></p>';
$strTermesText .= $tabEvenement['general']['eve_policy_' . $strLangue] . '<br>';
$strTermesHTML .= '
<div class="card-text">
' . afficheTexte('terme_jaccepte', 0, 1, 1) . '
<strong><a href="#div_policy' . $strSuffixe . '" id="link_policy' . $strSuffixe . '" data-toggle="modal">' . afficheTexte('terme_politique_achat', 0, 1, 1) . '</a>.</strong>
<div class="form-check d-inline-block ml-2">
<input class="form-check-input position-static chk_termes" type="checkbox" id="chk_policy' . $strSuffixe . '" name="chk_policy' . $strSuffixe . '" value="1" aria-label="..." readonly>
</div>
</div>
';
$strTermesModal .= '
<div class="modal fade" id="div_policy' . $strSuffixe . '" data-backdrop="static" data-keyboard="false" tabindex="-1" aria-labelledby="div_policy_label' . $strSuffixe . '" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-centered modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="div_policy_label' . $strSuffixe . '">' . afficheTexte('terme_policy_titre', 0, 1, 1) . '</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="' . afficheTexte('btn_terme_close', 0, 1, 1) . '">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
' . $tabEvenement['general']['eve_policy_' . $strLangue] . '
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary rounded-0" data-dismiss="modal">' . afficheTexte('btn_terme_close', 0, 1, 1) . '</button>
<button id="btn_accept_policy' . $strSuffixe . '" type="button" class="btn btn-primary rounded-0">' . afficheTexte('btn_terme_jaccepte', 0, 1, 1) . '</button>
</div>
</div>
</div>
</div>
';
$arrTermesModalButtons[] = 'btn_accept_policy' . $strSuffixe;
}
if ($tabEvenement['general']['eve_policy2_' . $strLangue] != '') {
$strTermesText .= '<p><strong>' . afficheTexte('terme_location_politique_achat', 0, 1, 1) . ' :</strong></p>';
$strTermesText .= $tabEvenement['general']['eve_policy2_' . $strLangue] . '<br>';
$strTermesHTML .= '
<div class="card-text">
' . afficheTexte('terme_jaccepte', 0, 1, 1) . '
<strong><a href="#div_location_policy' . $strSuffixe . '" id="link_location_policy' . $strSuffixe . '" data-toggle="modal">' . afficheTexte('terme_location_politique_achat', 0, 1, 1) . '</a>.</strong>
<div class="form-check d-inline-block ml-2">
<input class="form-check-input position-static chk_termes" type="checkbox" id="chk_location_policy' . $strSuffixe . '" name="chk_location_policy' . $strSuffixe . '" value="1" aria-label="..." readonly>
</div>
</div>
';
$strTermesModal .= '
<div class="modal fade" id="div_location_policy' . $strSuffixe . '" data-backdrop="static" data-keyboard="false" tabindex="-1" aria-labelledby="div_location_policy_label' . $strSuffixe . '" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-centered modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="div_location_policy_label' . $strSuffixe . '">' . afficheTexte('terme_location_policy_titre', 0, 1, 1) . '</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="' . afficheTexte('btn_terme_close', 0, 1, 1) . '">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
' . $tabEvenement['general']['eve_policy2_' . $strLangue] . '
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary rounded-0" data-dismiss="modal">' . afficheTexte('btn_terme_close', 0, 1, 1) . '</button>
<button id="btn_accept_location_policy' . $strSuffixe . '" type="button" class="btn btn-primary rounded-0">' . afficheTexte('btn_terme_jaccepte', 0, 1, 1) . '</button>
</div>
</div>
</div>
</div>
';
$arrTermesModalButtons[] = 'btn_accept_location_policy' . $strSuffixe;
}
// valide les waiver de 0 a 6 (sl)
for ($x = 0; $x <= 6; $x++) {
$mem_waive="eve_waiver".$x;
$array_list = explode(',',$tabEvenement['general'][$mem_waive.'_list_epreuves' ]);
$result = array_intersect($array_list, $arr_epreuves_id);
if (count($result) > 0) {
$strTermesText .= '<p><strong>' . $tabEvenement['general'][$mem_waive.'_titre_' . $strLangue] . ' :</strong></p>';
$strTermesText .= $tabEvenement['general'][$mem_waive . '_' . $strLangue] . '<br>';
$strTermesHTML .= '
<div class="card-text">
' . afficheTexte('terme_jaccepte', 0, 1, 1) . '
<strong><a href="#div_' . $mem_waive . $strSuffixe . '" id="link_' . $mem_waive . $strSuffixe . '" data-toggle="modal">' . $tabEvenement['general'][$mem_waive.'_titre_' . $strLangue] . '</a>.</strong>
<div class="form-check d-inline-block ml-2">
<input class="form-check-input position-static chk_termes" type="checkbox" id="chk_' . $mem_waive . $strSuffixe . '" name="chk_' . $mem_waive . $strSuffixe . '" value="1" aria-label="..." readonly>
</div>
</div>
';
$strTermesModal .= '
<div class="modal fade" id="div_'.$mem_waive.'' . $strSuffixe . '" data-backdrop="static" data-keyboard="false" tabindex="-1" aria-labelledby="div_' . $mem_waive . '_label' . $strSuffixe . '" aria-hidden="true">
<div class="modal-dialog modal-lg modal-dialog-centered modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="div_'.$mem_waive.'_label' . $strSuffixe . '">' .$tabEvenement['general'][$mem_waive.'_titre_' . $strLangue]. '</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="' . afficheTexte('btn_terme_close', 0, 1, 1) . '">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
' . $tabEvenement['general'][$mem_waive.'_' . $strLangue] . '
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary rounded-0" data-dismiss="modal">' . afficheTexte('btn_terme_close', 0, 1, 1) . '</button>
<button id="btn_accept_' . $mem_waive . $strSuffixe . '" type="button" class="btn btn-primary rounded-0">' . afficheTexte('btn_terme_jaccepte', 0, 1, 1) . '</button>
</div>
</div>
</div>
</div>
';
$arrTermesModalButtons[] = 'btn_accept_' . $mem_waive . $strSuffixe;
}
}
$arrRetour['html'] = $strTermesHTML;
$arrRetour['modal'] = $strTermesModal;
$arrRetour['text'] = $strTermesText;
$arrRetour['buttons'] = $arrTermesModalButtons;
return $arrRetour;
}
function fxShowBlocPaiement($tabEvenement, $fltMontant, $strLangue) {
global $vDomaine, $vblnEnvironementDev;
$strCCNumber = ($vblnEnvironementDev) ? ' value="4528038032834793"' : '';
$strCVV = ($vblnEnvironementDev) ? ' value="123"' : '';
$blnIsPromoteur = $blnAdmin = false;
$strOutput = '';
if (isset($tabEvenement) && intval($tabEvenement['general']['eve_gratuit']) != 1) { // si l'événement n'est pas gratuite, afficher le bloc de paiement
$strOutput .= '
<div id="paiement" class="card box_participant mb-3">
<div class="card-header">
<h2 class="box_paiement">' . afficheTexte('mode_paiement', 0) . '</h2>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-2 col-lg-3">
</div>
<div class="col-12 col-md-8 col-lg-6">
<div class="form-group text-center">
';
if (floatval($fltMontant) > 0) {
if (isset($_SESSION['com_info']) && $_SESSION['com_info'] != '') {
$blnIsPromoteur = fxIsPromoteur($_SESSION['com_info']['com_id'], $tabEvenement['general']['eve_id']);
}
// si en mode admin, récupérer les mode de paiement réservés aux admin
if (isset($_SESSION['usa_id'])) {
$blnAdmin = true;
}
$tabPaiements = fxGetPaiements($blnAdmin, $blnIsPromoteur, $strLangue);
if ($tabPaiements != false) { // afficher le formulaire de la carte de crédit
for ($intCtr = 1; $intCtr <= count($tabPaiements); $intCtr++) {
if ($tabPaiements[$intCtr]['pai_id'] == 2 || $tabPaiements[$intCtr]['pai_id'] == 6) {
$strDetailsPaiement = $strChecked = '';
if ($tabPaiements[$intCtr]['pai_id'] == 2) {
$strChecked = ' checked';
}
if ($tabPaiements[$intCtr]['pai_id'] == 6) {
//$strDetailsPaiement = ' <em>' . afficheTexte('paiement_cheque', 0) . '</em>';
}
$strOutput .= '
<div class="form-check">
<input class="form-check-input" type="radio" id="chk_pai_' . $tabPaiements[$intCtr]['pai_id'] . '" name="chk_paiement" value="' . $tabPaiements[$intCtr]['pai_id'] . '"' . $strChecked . '>
<label class="form-check-label" for="chk_pai_' . $tabPaiements[$intCtr]['pai_id'] . '">
' . $tabPaiements[$intCtr]['pai_nom_' . $strLangue] . '
</label>
</div>
';
}
}
} else {
$strOutput .= '
<div class="form-check">
<input class="form-check-input" type="radio" id="chk_pai_1" name="chk_paiement" value="1" checked>
<label class="form-check-label" for="chk_pai_1">
' . afficheTexte('paypal', 0) . '
</label>
</div>
';
}
} else {
$strOutput .= '
<div class="form-check">
<input class="form-check-input" type="radio" id="chk_pai_1" name="chk_paiement" value="1" checked>
<label class="form-check-label" for="chk_pai_1">
' . afficheTexte('gratuit', 0) . '
</label>
</div>
';
}
if (floatval($fltMontant) > 0) {
$strMonth = ($strLangue == 'fr') ? '-- Mois --' : '-- Month --';
$strYear = ($strLangue == 'fr') ? '-- Année --' :'-- Year --';
$strOutput .= '
</div>
<div id="creditcard">
<div class="form-message"></div>
<div class="img_cartes text-center">
<img id="card_mc" src="' . $vDomaine . '/images/mastercard.gif" width="55" height="34" alt="MasterCard">
<img id="card_visa" src="' . $vDomaine . '/images/visa.gif" width="59" height="34" alt="Visa">
</div>
<br>
<input type="hidden" id="card_type" name="card_type" value="">
<div class="form-group row justify-content-center">
<label for="ccnumber" class="control-label col-12">' . afficheTexte('checkout_ccnumber', 0) . ':</label>
<div class="col-11">
<input class="form-control" type="text" id="ccnumber" name="ccnumber" size="20" maxlength="16" autocomplete="off" placeholder="1234567890123456"' . $strCCNumber . '>
</div>
<small class="col text-muted">*</small>
</div>
<div class="form-group row justify-content-center">
<label class="control-label col-12">' . afficheTexte('checkout_ccexp', 0) . ':</label>
<div class="col-5">
<select class="form-control" id="ccmonth" name="ccmonth">
<label class="sr-only" for="ccmonth">' . afficheTexte('month', 0, 1, 1) . '</label>
<option value="" selected="selected">' . $strMonth . '</option>
';
for ($intCtr = 1; $intCtr <= 12; $intCtr++) {
$strSelected = ($vblnEnvironementDev && $intCtr == 3) ? ' selected' : '';
$strOutput .= '<option value="' . fxAddLeadingZero($intCtr) . '"' . $strSelected . '>' . fxMois('2013-' . $intCtr . '-01', $strLangue) . ' (' . $intCtr . ')' . '</option>';
}
$strOutput .= '
</select>
</div>
<small class="col text-muted">*</small>
<div class="col-5">
<label class="sr-only" for="ccyear">' . afficheTexte('year', 0, 1, 1) . '</label>
<select class="form-control" id="ccyear" name="ccyear">
<option value="" selected="selected">' . $strYear . '</option>
';
for ($intCtr = date('Y', strtotime(fxGetDate())); $intCtr <= date('Y', strtotime(fxGetDate()))+ 10; $intCtr++) {
$strSelected = ($vblnEnvironementDev && $intCtr == 2025) ? ' selected' : '';
$strOutput .= '<option value="' . $intCtr . '"' . $strSelected . '>' . $intCtr . '</option>';
}
$strOutput .= '
</select>
</div>
<small class="col text-muted">*</small>
</div>
<div class="form-group row justify-content-center">
<label class="control-label col-12" for="cvv">' . afficheTexte('checkout_cvv', 0) . ':</label>
<div class="col-11">
<input class="form-control" type="text" id="cvv" name="cvv" size="3" maxlength="3" autocomplete="off" placeholder="123"' . $strCVV . '>
</div>
<small class="col text-muted">*</small>
</div>
</div>
</div>
<div class="col-md-2 col-lg-3">
</div>
</div>
</div>
</div>
';
} else {
$strOutput .= '
</div>
</div>
</div>
</div>
</div>
';
}
} else { // si événement gratuit
$strOutput .= '
<input type="hidden" name="chk_paiement" value="1">
';
}
return $strOutput;
}
function fxShowTermesAcceptes($strNoPanier, $strLangue) {
$strRetour = '';
$tabPanier = fxListPanier($strNoPanier);
$tabAcheteur = $tabPanier[0];
$strRetour .= '
<div style="padding: 10px;">
<div style="color: #666666; font-size: 0.75rem;">
' . ($tabAcheteur['ach_termes_' . $strLangue]) . '
</div>
</div>
';
return $strRetour;}

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,364 @@
<?php
/**
* MSIN API ChronoTrack — config par eve_id (lien event, statut, mapping épreuves).
*/
function fxChronotrackApiConfigGet($intEveId) {
global $objDatabase;
$intEveId = intval($intEveId);
if ($intEveId <= 0) {
return null;
}
$sql = "SELECT * FROM api_chronotrack_config WHERE eve_id = " . $intEveId . " LIMIT 1";
return $objDatabase->fxGetRow($sql);
}
function fxChronotrackApiConfigStatusLabel($strStatus) {
switch ($strStatus) {
case MSIN_API_CHRONOTRACK_SYNC_ACTIF:
return 'Actif';
case MSIN_API_CHRONOTRACK_SYNC_FERME:
return 'Fermé';
case MSIN_API_CHRONOTRACK_SYNC_LIE:
default:
return 'Lié';
}
}
function fxChronotrackApiConfigSaveLink($intEveId, $intCtEventId, $strCtEventName, $strCtEventDate, $intUsaId) {
global $objDatabase;
$intEveId = intval($intEveId);
$intCtEventId = intval($intCtEventId);
if ($intEveId <= 0 || $intCtEventId <= 0) {
return array('state' => 'error', 'message' => 'Identifiants invalides');
}
$arrExisting = fxChronotrackApiConfigGet($intEveId);
if ($arrExisting !== null && $arrExisting['sync_status'] === MSIN_API_CHRONOTRACK_SYNC_FERME) {
return array('state' => 'error', 'message' => 'Événement fermé — réouverture non autorisée depuis cette interface.');
}
$strName = $objDatabase->fxEscape(trim($strCtEventName));
$strDateSql = 'NULL';
if (trim($strCtEventDate) !== '') {
$intTs = strtotime($strCtEventDate);
if ($intTs !== false) {
$strDateSql = "'" . $objDatabase->fxEscape(date('Y-m-d H:i:s', $intTs)) . "'";
}
}
if ($arrExisting === null) {
$sql = "INSERT INTO api_chronotrack_config SET eve_id = " . $intEveId
. ", ct_event_id = " . $intCtEventId
. ", ct_event_name = '" . $strName . "'"
. ", ct_event_date = " . $strDateSql
. ", sync_status = '" . MSIN_API_CHRONOTRACK_SYNC_LIE . "'"
. ", linked_at = '" . fxGetDateTime() . "'"
. ", linked_by_usa_id = " . intval($intUsaId)
. ", updated_at = '" . fxGetDateTime() . "'";
} else {
$sql = "UPDATE api_chronotrack_config SET ct_event_id = " . $intCtEventId
. ", ct_event_name = '" . $strName . "'"
. ", ct_event_date = " . $strDateSql
. ", updated_at = '" . fxGetDateTime() . "'"
. " WHERE eve_id = " . $intEveId
. " AND sync_status <> '" . MSIN_API_CHRONOTRACK_SYNC_FERME . "'";
}
if (!$objDatabase->fxQuery($sql)) {
return array('state' => 'error', 'message' => 'Erreur SQL enregistrement lien');
}
return array('state' => 'ok');
}
function fxChronotrackApiConfigSetStatus($intEveId, $strStatus, $intUsaId) {
global $objDatabase;
$intEveId = intval($intEveId);
$arrExisting = fxChronotrackApiConfigGet($intEveId);
if ($arrExisting === null) {
return array('state' => 'error', 'message' => 'Aucun lien ChronoTrack pour cet événement');
}
if ($arrExisting['sync_status'] === MSIN_API_CHRONOTRACK_SYNC_FERME) {
if ($strStatus !== MSIN_API_CHRONOTRACK_SYNC_LIE) {
return array('state' => 'error', 'message' => 'Événement fermé — seule la réouverture (Lié) est permise.');
}
}
$strStatus = trim($strStatus);
if (!in_array($strStatus, array(MSIN_API_CHRONOTRACK_SYNC_LIE, MSIN_API_CHRONOTRACK_SYNC_ACTIF, MSIN_API_CHRONOTRACK_SYNC_FERME), true)) {
return array('state' => 'error', 'message' => 'Statut invalide');
}
$strExtra = '';
if ($strStatus === MSIN_API_CHRONOTRACK_SYNC_ACTIF) {
$strExtra = ", activated_at = '" . fxGetDateTime() . "'";
}
if ($strStatus === MSIN_API_CHRONOTRACK_SYNC_FERME) {
$strExtra = ", closed_at = '" . fxGetDateTime() . "', closed_by_usa_id = " . intval($intUsaId);
}
if ($strStatus === MSIN_API_CHRONOTRACK_SYNC_LIE && $arrExisting['sync_status'] === MSIN_API_CHRONOTRACK_SYNC_FERME) {
$strExtra = ", closed_at = NULL, closed_by_usa_id = NULL";
}
$sql = "UPDATE api_chronotrack_config SET sync_status = '" . $objDatabase->fxEscape($strStatus) . "'"
. $strExtra
. ", updated_at = '" . fxGetDateTime() . "'"
. " WHERE eve_id = " . $intEveId;
if (!$objDatabase->fxQuery($sql)) {
return array('state' => 'error', 'message' => 'Erreur SQL changement statut');
}
return array('state' => 'ok');
}
/**
* MSIN-4328 — mémorise la fin dun push réussi (référence pour par_maj).
*/
function fxChronotrackApiConfigSetLastPush($intEveId, $intOkCount) {
global $objDatabase;
$intEveId = intval($intEveId);
if ($intEveId <= 0) {
return false;
}
$sql = "UPDATE api_chronotrack_config SET last_push_at = '" . fxGetDateTime() . "'"
. ", last_push_ok_count = " . max(0, intval($intOkCount))
. ", updated_at = '" . fxGetDateTime() . "'"
. " WHERE eve_id = " . $intEveId;
return (bool)$objDatabase->fxQuery($sql);
}
/**
* MSIN-4328 — fréquences autorisées (minutes), phase A.
*/
function fxChronotrackApiConfigAutoSyncIntervals() {
return array(1, 5, 15, 30, 60);
}
/**
* MSIN-4328 — enregistre la config sync auto (UI). Le cron (phase B) lira ces champs.
*/
function fxChronotrackApiConfigSaveAutoSync($intEveId, $blnEnabled, $intIntervalMin, $strUntil) {
global $objDatabase;
$intEveId = intval($intEveId);
$arrExisting = fxChronotrackApiConfigGet($intEveId);
if ($arrExisting === null) {
return array('state' => 'error', 'message' => 'Événement non lié à ChronoTrack');
}
if (($arrExisting['sync_status'] ?? '') === MSIN_API_CHRONOTRACK_SYNC_FERME) {
return array('state' => 'error', 'message' => 'Événement fermé — sync auto impossible.');
}
$blnEnabled = $blnEnabled ? 1 : 0;
$intIntervalMin = intval($intIntervalMin);
if (!in_array($intIntervalMin, fxChronotrackApiConfigAutoSyncIntervals(), true)) {
$intIntervalMin = 15;
}
$strUntilSql = 'NULL';
$strUntil = trim((string)$strUntil);
if ($strUntil !== '') {
// Accepte "YYYY-MM-DDTHH:MM" (input datetime-local) ou "YYYY-MM-DD HH:MM[:SS]"
$strUntilNorm = str_replace('T', ' ', $strUntil);
if (preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/', $strUntilNorm)) {
$strUntilNorm .= ':00';
}
$intTs = strtotime($strUntilNorm);
if ($intTs === false) {
return array('state' => 'error', 'message' => 'Date / heure de fin invalide');
}
if ($blnEnabled && $intTs <= time()) {
return array('state' => 'error', 'message' => 'La date/heure de fin doit être dans le futur');
}
$strUntilSql = "'" . $objDatabase->fxEscape(date('Y-m-d H:i:s', $intTs)) . "'";
} elseif ($blnEnabled) {
return array('state' => 'error', 'message' => 'Indiquez une date et une heure de fin pour activer la sync auto');
}
$sql = "UPDATE api_chronotrack_config SET auto_sync_enabled = " . $blnEnabled
. ", auto_sync_interval_min = " . $intIntervalMin
. ", auto_sync_until = " . $strUntilSql
. ", updated_at = '" . fxGetDateTime() . "'"
. " WHERE eve_id = " . $intEveId;
if (!$objDatabase->fxQuery($sql)) {
return array('state' => 'error', 'message' => 'Erreur SQL enregistrement sync auto');
}
$arrFresh = fxChronotrackApiConfigGet($intEveId);
return array(
'state' => 'ok',
'message' => $blnEnabled
? ('Sync auto enregistrée (toutes les ' . $intIntervalMin . ' min jusquau '
. trim((string)($arrFresh['auto_sync_until'] ?? '')) . '). Cron : auto_chronotrack_sync.php')
: 'Sync auto désactivée.',
'auto_sync' => array(
'enabled' => intval($arrFresh['auto_sync_enabled'] ?? 0) === 1,
'interval_min' => intval($arrFresh['auto_sync_interval_min'] ?? 15),
'until' => ($arrFresh['auto_sync_until'] ?? null),
'last_run_at' => ($arrFresh['last_auto_run_at'] ?? null),
),
);
}
/**
* MSIN-4328 — events avec sync auto ON (pas fermés).
*/
function fxChronotrackApiConfigListAutoSyncEnabled() {
global $objDatabase;
$sql = "SELECT c.*"
. " FROM api_chronotrack_config c"
. " WHERE IFNULL(c.auto_sync_enabled, 0) = 1"
. " AND c.sync_status <> '" . $objDatabase->fxEscape(MSIN_API_CHRONOTRACK_SYNC_FERME) . "'"
. " ORDER BY c.eve_id ASC";
$arrRows = $objDatabase->fxGetResults($sql);
$tabOut = array();
if (!is_array($arrRows)) {
return $tabOut;
}
for ($i = 1; $i <= count($arrRows); $i++) {
$tabOut[] = $arrRows[$i];
}
return $tabOut;
}
/**
* MSIN-4328 — horodatage dernier passage cron auto.
*/
function fxChronotrackApiConfigTouchAutoRun($intEveId) {
global $objDatabase;
$intEveId = intval($intEveId);
if ($intEveId <= 0) {
return false;
}
$sql = "UPDATE api_chronotrack_config SET last_auto_run_at = '" . fxGetDateTime() . "'"
. ", updated_at = '" . fxGetDateTime() . "'"
. " WHERE eve_id = " . $intEveId;
return (bool)$objDatabase->fxQuery($sql);
}
/**
* MSIN-4328 — coupe la sync auto (échéance atteinte).
*/
function fxChronotrackApiConfigDisableAutoSync($intEveId) {
global $objDatabase;
$intEveId = intval($intEveId);
if ($intEveId <= 0) {
return false;
}
$sql = "UPDATE api_chronotrack_config SET auto_sync_enabled = 0"
. ", updated_at = '" . fxGetDateTime() . "'"
. " WHERE eve_id = " . $intEveId;
return (bool)$objDatabase->fxQuery($sql);
}
function fxChronotrackApiRaceMapGetForEvent($intEveId) {
global $objDatabase;
$sql = "SELECT * FROM api_chronotrack_race_map WHERE eve_id = " . intval($intEveId);
$arrRows = $objDatabase->fxGetResults($sql);
$arrMap = array();
if ($arrRows !== null) {
for ($i = 1; $i <= count($arrRows); $i++) {
$arrMap[intval($arrRows[$i]['epr_id'])] = $arrRows[$i];
}
}
return $arrMap;
}
function fxChronotrackApiMs1EpreuvesForEvent($intEveId, $strLangue = 'fr') {
global $objDatabase;
$strLangue = ($strLangue === 'en') ? 'en' : 'fr';
$sql = "SELECT epr_id, epr_nom_" . $strLangue . " AS epr_nom, epr_type_" . $strLangue . " AS epr_type, epr_date"
. " FROM inscriptions_epreuves WHERE eve_id = " . intval($intEveId)
. " ORDER BY epr_date, epr_nom_" . $strLangue;
return $objDatabase->fxGetResults($sql);
}
function fxChronotrackApiRaceMapSave($intEveId, $arrMapInput) {
global $objDatabase;
$intEveId = intval($intEveId);
$arrExisting = fxChronotrackApiConfigGet($intEveId);
if ($arrExisting === null) {
return array('state' => 'error', 'message' => 'Lier d\'abord un événement ChronoTrack');
}
if ($arrExisting['sync_status'] === MSIN_API_CHRONOTRACK_SYNC_FERME) {
return array('state' => 'error', 'message' => 'Événement fermé — mapping en lecture seule');
}
if (!is_array($arrMapInput)) {
return array('state' => 'error', 'message' => 'Mapping invalide');
}
foreach ($arrMapInput as $intEprId => $arrRace) {
$intEprId = intval($intEprId);
$intCtRaceId = intval($arrRace['ct_race_id'] ?? 0);
$strCtRaceName = trim((string)($arrRace['ct_race_name'] ?? ''));
if ($intEprId <= 0) {
continue;
}
if ($intCtRaceId <= 0) {
$sqlDel = "DELETE FROM api_chronotrack_race_map WHERE eve_id = " . $intEveId . " AND epr_id = " . $intEprId;
$objDatabase->fxQuery($sqlDel);
continue;
}
$sqlExists = "SELECT map_id FROM api_chronotrack_race_map WHERE eve_id = " . $intEveId
. " AND epr_id = " . $intEprId . " LIMIT 1";
if ($objDatabase->fxGetRow($sqlExists) !== null) {
$sql = "UPDATE api_chronotrack_race_map SET ct_race_id = " . $intCtRaceId
. ", ct_race_name = '" . $objDatabase->fxEscape($strCtRaceName) . "'"
. ", updated_at = '" . fxGetDateTime() . "'"
. " WHERE eve_id = " . $intEveId . " AND epr_id = " . $intEprId;
} else {
$sql = "INSERT INTO api_chronotrack_race_map SET eve_id = " . $intEveId
. ", epr_id = " . $intEprId
. ", ct_race_id = " . $intCtRaceId
. ", ct_race_name = '" . $objDatabase->fxEscape($strCtRaceName) . "'"
. ", updated_at = '" . fxGetDateTime() . "'";
}
$objDatabase->fxQuery($sql);
}
return array('state' => 'ok');
}
function fxChronotrackApiMs1EventRow($intEveId, $strLangue = 'fr') {
global $objDatabase;
$strLangue = ($strLangue === 'en') ? 'en' : 'fr';
$sql = "SELECT eve_id, eve_nom_" . $strLangue . " AS eve_nom, eve_date_debut, eve_date_fin"
. " FROM inscriptions_evenements WHERE eve_id = " . intval($intEveId) . " LIMIT 1";
return $objDatabase->fxGetRow($sql);
}
function fxChronotrackApiListActiveConfigs($arrStatuses = null) {
global $objDatabase;
if ($arrStatuses === null) {
$arrStatuses = array(MSIN_API_CHRONOTRACK_SYNC_LIE, MSIN_API_CHRONOTRACK_SYNC_ACTIF);
}
$arrEsc = array();
foreach ($arrStatuses as $strStatus) {
$arrEsc[] = "'" . $objDatabase->fxEscape($strStatus) . "'";
}
$sql = "SELECT c.*, e.eve_nom_fr AS eve_nom"
. " FROM api_chronotrack_config c"
. " INNER JOIN inscriptions_evenements e ON e.eve_id = c.eve_id"
. " WHERE c.sync_status IN (" . implode(',', $arrEsc) . ")"
. " ORDER BY c.sync_status DESC, c.updated_at DESC";
return $objDatabase->fxGetResults($sql);
}

View File

@ -0,0 +1,12 @@
<?php
/**
* MSIN API ChronoTrack — constantes métier du module.
*/
define('MSIN_API_CHRONOTRACK_SYNC_LIE', 'lie');
define('MSIN_API_CHRONOTRACK_SYNC_ACTIF', 'actif');
define('MSIN_API_CHRONOTRACK_SYNC_FERME', 'ferme');
if (!defined('MSIN_API_CHRONOTRACK_TOKEN_KEY')) {
define('MSIN_API_CHRONOTRACK_TOKEN_KEY', 'prod');
}

View File

@ -0,0 +1,809 @@
<?php
/**
* MSIN API ChronoTrack — lecture / filtrage des ressources CT (event, race).
*/
function fxChronotrackApiNormalizeEntityList($mixPayload, $strEntityKey) {
$arrOut = array();
if (!is_array($mixPayload)) {
return $arrOut;
}
if (isset($mixPayload[$strEntityKey]) && is_array($mixPayload[$strEntityKey])) {
$mixList = $mixPayload[$strEntityKey];
if (fxChronotrackApiIsAssocArray($mixList)) {
$arrOut[] = $mixList;
} else {
$arrOut = $mixList;
}
return $arrOut;
}
// Pluriel courant côté CT (ex. "races" pour clé "race")
$strPluralKey = $strEntityKey . 's';
if ($strEntityKey !== $strPluralKey && isset($mixPayload[$strPluralKey]) && is_array($mixPayload[$strPluralKey])) {
$mixList = $mixPayload[$strPluralKey];
if (fxChronotrackApiIsAssocArray($mixList)) {
$arrOut[] = $mixList;
} else {
$arrOut = $mixList;
}
return $arrOut;
}
// Clé CT Live pour GET event/{id}/race (ex. event_race, pas race)
if ($strEntityKey === 'race') {
foreach (array('event_race') as $strAltKey) {
if (!isset($mixPayload[$strAltKey]) || !is_array($mixPayload[$strAltKey])) {
continue;
}
$mixList = $mixPayload[$strAltKey];
if (fxChronotrackApiIsAssocArray($mixList)) {
$arrOut[] = $mixList;
} else {
$arrOut = $mixList;
}
return $arrOut;
}
}
// Clé CT Live pour GET event/{id}/entry (ex. event_entry, pas entry)
if ($strEntityKey === 'entry') {
foreach (array('event_entry', 'entries') as $strAltKey) {
if (!isset($mixPayload[$strAltKey]) || !is_array($mixPayload[$strAltKey])) {
continue;
}
$mixList = $mixPayload[$strAltKey];
if (fxChronotrackApiIsAssocArray($mixList)) {
$arrOut[] = $mixList;
} else {
$arrOut = $mixList;
}
return $arrOut;
}
}
if (fxChronotrackApiIsAssocArray($mixPayload) && (
isset($mixPayload['race_id']) || isset($mixPayload['reg_choice_id'])
|| isset($mixPayload['entry_id']) || isset($mixPayload['external_id'])
)) {
return array($mixPayload);
}
if (fxChronotrackApiIsAssocArray($mixPayload) && isset($mixPayload['id'])
&& !isset($mixPayload['event_id']) && !isset($mixPayload['event_name'])
) {
return array($mixPayload);
}
if (!fxChronotrackApiIsAssocArray($mixPayload)) {
return $mixPayload;
}
return $arrOut;
}
function fxChronotrackApiIsAssocArray($arr) {
if (!is_array($arr) || $arr === array()) {
return false;
}
return array_keys($arr) !== range(0, count($arr) - 1);
}
function fxChronotrackApiEntityId($arrEntity) {
foreach (array('entry_id', 'reg_choice_id', 'race_id', 'event_id', 'id') as $strKey) {
if (isset($arrEntity[$strKey]) && $arrEntity[$strKey] !== '') {
return (string)$arrEntity[$strKey];
}
}
return '';
}
function fxChronotrackApiEntityExternalId($arrEntity) {
foreach (array('external_id', 'entry_external_id', 'ext_id', 'external_entry_id') as $strKey) {
if (isset($arrEntity[$strKey]) && $arrEntity[$strKey] !== '') {
return (string)$arrEntity[$strKey];
}
}
return '';
}
function fxChronotrackApiEntityEntryLabel($arrEntity) {
$strFirst = fxChronotrackApiRaceField($arrEntity, array('first_name', 'entry_first_name', 'fname'));
$strLast = fxChronotrackApiRaceField($arrEntity, array('last_name', 'entry_last_name', 'lname'));
if ($strFirst !== '' || $strLast !== '') {
return trim($strFirst . ' ' . $strLast);
}
return fxChronotrackApiEntityName($arrEntity);
}
function fxChronotrackApiEntityName($arrEntity) {
foreach (array(
'reg_choice_name', 'registration_choice_name', 'choice_name',
'event_name', 'race_name', 'name', 'title',
'entry_name', 'athlete_name',
) as $strKey) {
if (!empty($arrEntity[$strKey])) {
return trim((string)$arrEntity[$strKey]);
}
}
return '';
}
function fxChronotrackApiRaceField($arrEntity, $arrKeys) {
foreach ($arrKeys as $strKey) {
if (!isset($arrEntity[$strKey])) {
continue;
}
$strVal = trim((string)$arrEntity[$strKey]);
if ($strVal !== '') {
return $strVal;
}
}
return '';
}
function fxChronotrackApiFormatRaceDistanceLabel($arrEntity) {
$strDist = fxChronotrackApiRaceField($arrEntity, array(
'race_course_distance', 'course_distance', 'race_distance', 'distance', 'race_length',
));
if ($strDist === '') {
return '';
}
$strUnit = fxChronotrackApiRaceField($arrEntity, array(
'race_pref_distance_unit', 'course_distance_type', 'course_distance_unit',
'distance_unit', 'race_distance_unit', 'distance_type',
));
if (is_numeric($strDist) && $strUnit !== '') {
$fltDist = floatval($strDist);
if ($strUnit === 'kilometers' && $fltDist >= 1000) {
$strDist = rtrim(rtrim(number_format($fltDist / 1000, 1, '.', ''), '0'), '.') . ' km';
return $strDist;
}
if ($strUnit === 'meters') {
$strDist = rtrim(rtrim(number_format($fltDist, 0, '.', ''), '0'), '.') . ' m';
return $strDist;
}
}
if ($strUnit !== '' && stripos($strDist, $strUnit) === false) {
return $strDist . ' ' . $strUnit;
}
return $strDist;
}
function fxChronotrackApiFormatRaceTeamLabel($arrEntity) {
foreach (array('team_race', 'is_team_race', 'team') as $strKey) {
if (!isset($arrEntity[$strKey])) {
continue;
}
$mixVal = $arrEntity[$strKey];
if ($mixVal === true || $mixVal === 1 || $mixVal === '1' || strtolower((string)$mixVal) === 'true' || strtolower((string)$mixVal) === 'yes') {
return 'Équipe';
}
if ($mixVal === false || $mixVal === 0 || $mixVal === '0' || strtolower((string)$mixVal) === 'false' || strtolower((string)$mixVal) === 'no') {
return 'Individuel';
}
}
$strSubtype = strtolower(fxChronotrackApiRaceField($arrEntity, array('race_subtype', 'subtype', 'participant_type')));
if ($strSubtype !== '') {
if (strpos($strSubtype, 'team') !== false) {
return 'Équipe';
}
if (strpos($strSubtype, 'individual') !== false || strpos($strSubtype, 'individ') !== false) {
return 'Individuel';
}
}
return '';
}
function fxChronotrackApiFormatRaceRegChoicesLabel($arrEntity) {
foreach (array('reg_choice', 'reg_choices', 'registration_choices', 'choices') as $strKey) {
if (!isset($arrEntity[$strKey]) || !is_array($arrEntity[$strKey])) {
continue;
}
$arrNames = array();
$arrList = $arrEntity[$strKey];
if (fxChronotrackApiIsAssocArray($arrList) && (
isset($arrList['reg_choice_name']) || isset($arrList['name']) || isset($arrList['label'])
)) {
$arrList = array($arrList);
}
foreach ($arrList as $arrChoice) {
if (!is_array($arrChoice)) {
continue;
}
$strChoice = fxChronotrackApiRaceField($arrChoice, array(
'reg_choice_name', 'registration_choice_name', 'choice_name', 'name', 'label',
));
if ($strChoice !== '') {
$arrNames[] = $strChoice;
}
}
if (count($arrNames) > 0) {
return implode(', ', array_values(array_unique($arrNames)));
}
}
return '';
}
function fxChronotrackApiFormatRaceLabel($arrEntity) {
$strName = fxChronotrackApiRaceField($arrEntity, array('race_name', 'display_name', 'name', 'title'));
if ($strName !== '') {
return $strName;
}
$strTag = fxChronotrackApiRaceField($arrEntity, array('race_tag', 'tag'));
$strType = fxChronotrackApiRaceField($arrEntity, array('race_type', 'sport', 'race_course_type'));
$strDist = fxChronotrackApiFormatRaceDistanceLabel($arrEntity);
$strTeam = fxChronotrackApiFormatRaceTeamLabel($arrEntity);
$strRegChoice = fxChronotrackApiRaceField($arrEntity, array(
'reg_choice_name', 'registration_choice_name', 'choice_name', 'reg_choice_label',
));
if ($strRegChoice === '') {
$strRegChoice = fxChronotrackApiFormatRaceRegChoicesLabel($arrEntity);
}
$arrExtras = array();
if ($strRegChoice !== '' && $strRegChoice !== $strName) {
$arrExtras[] = $strRegChoice;
}
if ($strTag !== '' && $strTag !== $strName && $strTag !== $strRegChoice) {
$arrExtras[] = 'tag ' . $strTag;
}
if ($strType !== '') {
$arrExtras[] = $strType;
}
if ($strDist !== '' && $strDist !== $strName) {
$arrExtras[] = $strDist;
}
if ($strTeam !== '') {
$arrExtras[] = $strTeam;
}
$arrExtras = array_values(array_unique($arrExtras));
if ($strName === '') {
$strName = $strDist !== '' ? $strDist : ('Race ' . fxChronotrackApiEntityId($arrEntity));
}
if (count($arrExtras) === 0) {
return $strName;
}
return $strName . ' — ' . implode(' · ', $arrExtras);
}
function fxChronotrackApiEntityStartEpoch($arrEntity) {
foreach (array(
'race_planned_start_time', 'race_start_time', 'event_start_time',
'start_time', 'start_date', 'event_date',
) as $strKey) {
if (!isset($arrEntity[$strKey]) || $arrEntity[$strKey] === '' || $arrEntity[$strKey] === null) {
continue;
}
$mixVal = $arrEntity[$strKey];
if (is_numeric($mixVal)) {
return intval($mixVal);
}
$intTs = strtotime((string)$mixVal);
if ($intTs !== false) {
return $intTs;
}
}
return 0;
}
function fxChronotrackApiFormatEventRow($arrEntity) {
$intStart = fxChronotrackApiEntityStartEpoch($arrEntity);
return array(
'ct_event_id' => fxChronotrackApiEntityId($arrEntity),
'name' => fxChronotrackApiEntityName($arrEntity),
'start_epoch' => $intStart,
'start_label' => $intStart > 0 ? date('Y-m-d H:i', $intStart) : '',
'is_past' => ($intStart > 0 && $intStart < strtotime('today')),
'raw' => $arrEntity,
);
}
function fxChronotrackApiFetchEventsRaw() {
$arrApi = fxChronotrackApiOAuthApiGet('event');
if ($arrApi['state'] !== 'ok') {
return $arrApi;
}
$arrList = fxChronotrackApiNormalizeEntityList($arrApi['json'], 'event');
$arrRows = array();
foreach ($arrList as $arrEntity) {
if (!is_array($arrEntity)) {
continue;
}
$arrRow = fxChronotrackApiFormatEventRow($arrEntity);
if ($arrRow['ct_event_id'] === '') {
continue;
}
$arrRows[] = $arrRow;
}
return array('state' => 'ok', 'events' => $arrRows);
}
function fxChronotrackApiFilterEvents($arrEvents, $strSearch = '', $blnShowPast = false) {
$strSearch = mb_strtolower(trim($strSearch), 'UTF-8');
$arrFiltered = array();
foreach ($arrEvents as $arrEvent) {
if (!$blnShowPast && !empty($arrEvent['is_past'])) {
continue;
}
if ($strSearch !== '') {
$strHay = mb_strtolower($arrEvent['name'] . ' ' . $arrEvent['ct_event_id'], 'UTF-8');
if (mb_strpos($strHay, $strSearch, 0, 'UTF-8') === false) {
continue;
}
}
$arrFiltered[] = $arrEvent;
}
usort($arrFiltered, function ($a, $b) {
$intA = intval($a['start_epoch']);
$intB = intval($b['start_epoch']);
if ($intA === $intB) {
return strcasecmp($a['name'], $b['name']);
}
if ($intA <= 0) {
return 1;
}
if ($intB <= 0) {
return -1;
}
return ($intA < $intB) ? -1 : 1;
});
return $arrFiltered;
}
function fxChronotrackApiFetchEventsFiltered($strSearch = '', $blnShowPast = false) {
$arrRaw = fxChronotrackApiFetchEventsRaw();
if ($arrRaw['state'] !== 'ok') {
return $arrRaw;
}
return array(
'state' => 'ok',
'events' => fxChronotrackApiFilterEvents($arrRaw['events'], $strSearch, $blnShowPast),
'total' => count($arrRaw['events']),
);
}
function fxChronotrackApiEntityBelongsToEvent($arrEntity, $intCtEventId) {
if ($intCtEventId <= 0) {
return true;
}
foreach (array('event_id', 'event') as $strKey) {
if (!isset($arrEntity[$strKey])) {
continue;
}
$mixVal = $arrEntity[$strKey];
if (is_array($mixVal)) {
$mixVal = $mixVal['event_id'] ?? $mixVal['id'] ?? null;
}
if ($mixVal !== null && $mixVal !== '' && intval($mixVal) !== $intCtEventId) {
return false;
}
}
return true;
}
function fxChronotrackApiFormatRegChoiceRow($arrEntity) {
$strName = fxChronotrackApiRaceField($arrEntity, array(
'reg_choice_name', 'registration_choice_name', 'choice_name', 'display_name', 'name', 'title',
));
$intParentRaceId = intval(fxChronotrackApiRaceField($arrEntity, array('race_id')));
$arrExtras = array();
if (isset($arrEntity['race']) && is_array($arrEntity['race'])) {
$strRaceName = fxChronotrackApiEntityName($arrEntity['race']);
$strDist = fxChronotrackApiFormatRaceDistanceLabel($arrEntity['race']);
if ($strRaceName !== '' && strcasecmp($strRaceName, $strName) !== 0) {
$arrExtras[] = $strRaceName;
} elseif ($strDist !== '' && stripos($strName, $strDist) === false) {
$arrExtras[] = $strDist;
}
$strTeam = fxChronotrackApiFormatRaceTeamLabel($arrEntity['race']);
if ($strTeam !== '') {
$arrExtras[] = $strTeam;
}
}
$strLabel = $strName;
if ($strLabel === '') {
$strLabel = 'Choix d\'inscription ' . fxChronotrackApiEntityId($arrEntity);
} elseif (count($arrExtras) > 0) {
$strLabel .= ' · ' . implode(' · ', array_values(array_unique($arrExtras)));
}
return array(
'ct_race_id' => fxChronotrackApiEntityId($arrEntity),
'kind' => 'reg_choice',
'name' => $strName !== '' ? $strName : $strLabel,
'label' => $strLabel,
'parent_race_id' => $intParentRaceId > 0 ? $intParentRaceId : null,
'start_label' => '',
'raw' => $arrEntity,
);
}
function fxChronotrackApiCollectRegChoiceEntities($mixPayload) {
$arrById = array();
if (!is_array($mixPayload)) {
return array();
}
$arrScopes = array($mixPayload);
if (isset($mixPayload['event']) && is_array($mixPayload['event'])) {
$arrScopes[] = $mixPayload['event'];
}
foreach ($arrScopes as $arrScope) {
if (!is_array($arrScope)) {
continue;
}
$arrList = fxChronotrackApiNormalizeEntityList($arrScope, 'reg_choice');
foreach ($arrList as $arrEntity) {
if (!is_array($arrEntity) || !isset($arrEntity['reg_choice_id'])) {
continue;
}
$strId = (string)$arrEntity['reg_choice_id'];
$arrById[$strId] = $arrEntity;
}
$arrRaces = fxChronotrackApiNormalizeEntityList($arrScope, 'race');
foreach ($arrRaces as $arrRace) {
if (!is_array($arrRace) || !isset($arrRace['race_id'])) {
continue;
}
$arrChoices = fxChronotrackApiNormalizeEntityList($arrRace, 'reg_choice');
foreach ($arrChoices as $arrChoice) {
if (!is_array($arrChoice) || !isset($arrChoice['reg_choice_id'])) {
continue;
}
$strId = (string)$arrChoice['reg_choice_id'];
if (!isset($arrChoice['race']) || !is_array($arrChoice['race'])) {
$arrChoice['race'] = $arrRace;
}
$arrById[$strId] = $arrChoice;
}
}
}
return array_values($arrById);
}
function fxChronotrackApiCollectRaceEntities($mixPayload) {
$arrById = array();
if (!is_array($mixPayload)) {
return array();
}
$arrScopes = array($mixPayload);
if (isset($mixPayload['event']) && is_array($mixPayload['event'])) {
$arrScopes[] = $mixPayload['event'];
}
foreach ($arrScopes as $arrScope) {
if (!is_array($arrScope)) {
continue;
}
$arrList = fxChronotrackApiNormalizeEntityList($arrScope, 'race');
foreach ($arrList as $arrEntity) {
if (!is_array($arrEntity) || !isset($arrEntity['race_id'])) {
continue;
}
$strId = (string)$arrEntity['race_id'];
$arrById[$strId] = $arrEntity;
}
}
return array_values($arrById);
}
function fxChronotrackApiParseRegChoiceRowsFromPayload($mixPayload, $intCtEventId = 0) {
$arrRows = array();
$arrEntities = fxChronotrackApiCollectRegChoiceEntities($mixPayload);
foreach ($arrEntities as $arrEntity) {
if ($intCtEventId > 0 && !fxChronotrackApiEntityBelongsToEvent($arrEntity, $intCtEventId)) {
continue;
}
$arrRow = fxChronotrackApiFormatRegChoiceRow($arrEntity);
if ($arrRow['ct_race_id'] === '') {
continue;
}
$arrRows[$arrRow['ct_race_id']] = $arrRow;
}
$arrRows = array_values($arrRows);
usort($arrRows, function ($a, $b) {
return strcasecmp($a['name'], $b['name']);
});
return $arrRows;
}
function fxChronotrackApiFetchRegChoicesForEvent($intCtEventId) {
$intCtEventId = intval($intCtEventId);
if ($intCtEventId <= 0) {
return array('state' => 'error', 'message' => 'event_id CT invalide');
}
$arrApi = fxChronotrackApiOAuthApiGet('event/' . $intCtEventId);
if ($arrApi['state'] !== 'ok') {
return array(
'state' => 'error',
'message' => $arrApi['message'] ?? 'Impossible de lire l\'événement ChronoTrack',
);
}
$arrRows = fxChronotrackApiParseRegChoiceRowsFromPayload($arrApi['json'], $intCtEventId);
if (count($arrRows) > 0) {
return array(
'state' => 'ok',
'races' => $arrRows,
'source' => 'event/' . $intCtEventId,
'kind' => 'reg_choice',
);
}
return array('state' => 'ok', 'races' => array(), 'kind' => 'reg_choice');
}
function fxChronotrackApiFormatRaceRow($arrEntity) {
$intStart = fxChronotrackApiEntityStartEpoch($arrEntity);
$strLabel = fxChronotrackApiFormatRaceLabel($arrEntity);
return array(
'ct_race_id' => fxChronotrackApiEntityId($arrEntity),
'kind' => 'race',
'name' => fxChronotrackApiEntityName($arrEntity),
'label' => $strLabel,
'race_tag' => fxChronotrackApiRaceField($arrEntity, array('race_tag', 'tag')),
'race_type' => fxChronotrackApiRaceField($arrEntity, array('race_type', 'sport')),
'distance' => fxChronotrackApiFormatRaceDistanceLabel($arrEntity),
'team_mode' => fxChronotrackApiFormatRaceTeamLabel($arrEntity),
'start_label' => $intStart > 0 ? date('Y-m-d H:i', $intStart) : '',
'raw' => $arrEntity,
);
}
function fxChronotrackApiParseRaceRowsFromPayload($mixPayload, $intCtEventId = 0) {
$arrRows = array();
$arrEntities = fxChronotrackApiCollectRaceEntities($mixPayload);
foreach ($arrEntities as $arrEntity) {
if ($intCtEventId > 0 && !fxChronotrackApiEntityBelongsToEvent($arrEntity, $intCtEventId)) {
continue;
}
$arrRow = fxChronotrackApiFormatRaceRow($arrEntity);
if ($arrRow['ct_race_id'] === '') {
continue;
}
$arrRows[$arrRow['ct_race_id']] = $arrRow;
}
$arrRows = array_values($arrRows);
usort($arrRows, function ($a, $b) {
return strcasecmp($a['name'], $b['name']);
});
return $arrRows;
}
function fxChronotrackApiFetchRacesForEvent($intCtEventId) {
$intCtEventId = intval($intCtEventId);
if ($intCtEventId <= 0) {
return array('state' => 'error', 'message' => 'event_id CT invalide');
}
if (!fxChronotrackApiSettingsConfigured()) {
return array('state' => 'error', 'message' => fxChronotrackApiSettingsErrorMessage());
}
$strRacePath = 'event/' . $intCtEventId . '/race';
$arrApi = fxChronotrackApiOAuthApiGet($strRacePath);
if ($arrApi['state'] !== 'ok') {
return array(
'state' => 'error',
'message' => $arrApi['message'] ?? 'Impossible de lire les courses ChronoTrack',
);
}
$arrRaceRows = fxChronotrackApiParseRaceRowsFromPayload($arrApi['json'], $intCtEventId);
if (count($arrRaceRows) > 0) {
return array(
'state' => 'ok',
'races' => $arrRaceRows,
'source' => $strRacePath,
'kind' => 'race',
);
}
return array(
'state' => 'ok',
'races' => array(),
'kind' => 'race',
'message' => 'Aucune course dans GET ' . $strRacePath
. ' (clé attendue : event_race).',
);
}
function fxChronotrackApiDiagnosticEntitySamples($mixPayload, $strEntityKey, $intMax = 8) {
if ($strEntityKey === 'reg_choice') {
$arrEntities = fxChronotrackApiCollectRegChoiceEntities($mixPayload);
} elseif ($strEntityKey === 'race') {
$arrEntities = fxChronotrackApiCollectRaceEntities($mixPayload);
} elseif ($strEntityKey === 'entry') {
$arrEntities = fxChronotrackApiNormalizeEntityList($mixPayload, 'entry');
} else {
$arrEntities = fxChronotrackApiNormalizeEntityList($mixPayload, $strEntityKey);
}
$arrSamples = array();
foreach ($arrEntities as $arrEntity) {
if (!is_array($arrEntity)) {
continue;
}
if ($strEntityKey === 'entry') {
$strId = fxChronotrackApiEntityId($arrEntity);
if ($strId === '') {
$strId = fxChronotrackApiEntityExternalId($arrEntity);
}
if ($strId === '') {
continue;
}
$arrSamples[$strId] = array(
'id' => $strId,
'external_id' => fxChronotrackApiEntityExternalId($arrEntity),
'name' => fxChronotrackApiEntityEntryLabel($arrEntity),
'bib' => fxChronotrackApiRaceField($arrEntity, array('bib', 'bib_number', 'entry_bib')),
'entry_status'=> fxChronotrackApiRaceField($arrEntity, array('entry_status', 'status')),
'race_id' => fxChronotrackApiRaceField($arrEntity, array('race_id', 'event_race_id')),
'country_code'=> fxChronotrackApiRaceField($arrEntity, array('country_code', 'country', 'country_iso')),
'state_code' => fxChronotrackApiRaceField($arrEntity, array('state_code', 'state', 'province_code')),
'keys' => array_slice(array_keys($arrEntity), 0, 32),
);
continue;
}
$strId = fxChronotrackApiEntityId($arrEntity);
if ($strId === '') {
continue;
}
$arrSamples[$strId] = array(
'id' => $strId,
'name' => fxChronotrackApiEntityName($arrEntity),
'keys' => array_slice(array_keys($arrEntity), 0, 24),
);
}
$arrSamples = array_values($arrSamples);
return array(
'count' => count($arrSamples),
'samples' => array_slice($arrSamples, 0, $intMax),
);
}
function fxChronotrackApiDiagnosticSummarizeEntryPayload($mixJson) {
if (!is_array($mixJson)) {
return array(
'top_keys' => array(),
'entry' => array('count' => 0, 'samples' => array(), 'field_keys' => array()),
);
}
$arrEntry = fxChronotrackApiDiagnosticEntitySamples($mixJson, 'entry', 5);
$arrFieldKeys = array();
if (!empty($arrEntry['samples'][0]['keys'])) {
$arrFieldKeys = $arrEntry['samples'][0]['keys'];
}
return array(
'top_keys' => array_slice(array_keys($mixJson), 0, 40),
'entry' => $arrEntry,
'field_keys' => $arrFieldKeys,
);
}
function fxChronotrackApiDiagnosticSummarizePayload($mixJson) {
if (!is_array($mixJson)) {
return array(
'top_keys' => array(),
'reg_choice' => array('count' => 0, 'samples' => array()),
'race' => array('count' => 0, 'samples' => array()),
);
}
return array(
'top_keys' => array_slice(array_keys($mixJson), 0, 40),
'event_race' => fxChronotrackApiDiagnosticEntitySamples(
is_array($mixJson) && isset($mixJson['event_race']) ? array('event_race' => $mixJson['event_race']) : $mixJson,
'race'
),
'reg_choice' => fxChronotrackApiDiagnosticEntitySamples($mixJson, 'reg_choice'),
'race' => fxChronotrackApiDiagnosticEntitySamples($mixJson, 'race'),
);
}
function fxChronotrackApiDiagnosticEncodeJson($mixJson) {
$strRaw = json_encode($mixJson, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
if (!is_string($strRaw)) {
return '(json_encode a échoué)';
}
if (strlen($strRaw) > 48000) {
return substr($strRaw, 0, 48000) . "\n\n... [tronqué — " . strlen($strRaw) . ' octets au total]';
}
return $strRaw;
}
function fxChronotrackApiRunEventDiagnostic($intCtEventId) {
$intCtEventId = intval($intCtEventId);
if ($intCtEventId <= 0) {
return array('state' => 'error', 'message' => 'ct_event_id manquant');
}
if (!fxChronotrackApiSettingsConfigured()) {
return array('state' => 'error', 'message' => fxChronotrackApiSettingsErrorMessage());
}
$arrToken = fxChronotrackApiOAuthFetchToken(false);
if ($arrToken['state'] !== 'ok') {
return array('state' => 'error', 'message' => 'OAuth : ' . ($arrToken['message'] ?? 'échec'));
}
$arrPaths = array(
array('path' => 'event/' . $intCtEventId . '/race'),
array('path' => 'event/' . $intCtEventId . '/entry', 'query' => array('page' => 1, 'page_size' => 5)),
array('path' => 'event/' . $intCtEventId),
array('path' => 'event/' . $intCtEventId . '/reg_choice'),
);
$arrProbes = array();
foreach ($arrPaths as $arrPathDef) {
$strPath = $arrPathDef['path'];
$arrQuery = isset($arrPathDef['query']) ? $arrPathDef['query'] : array();
$arrApi = fxChronotrackApiOAuthApiGet($strPath, $arrQuery);
$arrProbe = array(
'path' => $strPath,
'query' => $arrQuery,
'state' => $arrApi['state'] ?? 'error',
'http_code' => intval($arrApi['http_code'] ?? $arrApi['http']['http_code'] ?? 0),
'message' => $arrApi['message'] ?? '',
'summary' => array(),
'raw_json' => '',
);
if ($arrApi['state'] === 'ok') {
if (strpos($strPath, '/entry') !== false) {
$arrProbe['summary'] = fxChronotrackApiDiagnosticSummarizeEntryPayload($arrApi['json']);
} else {
$arrProbe['summary'] = fxChronotrackApiDiagnosticSummarizePayload($arrApi['json']);
}
$arrProbe['raw_json'] = fxChronotrackApiDiagnosticEncodeJson($arrApi['json']);
} elseif (isset($arrApi['http']['body']) && trim((string)$arrApi['http']['body']) !== '') {
$arrProbe['raw_json'] = substr(trim((string)$arrApi['http']['body']), 0, 8000);
}
$arrProbes[] = $arrProbe;
}
return array(
'state' => 'ok',
'ct_event_id' => $intCtEventId,
'oauth' => !empty($arrToken['from_cache']) ? 'token en cache' : 'token renouvelé',
'probes' => $arrProbes,
'generated_at'=> date('c'),
);
}

View File

@ -0,0 +1,111 @@
<?php
/**
* MSIN API ChronoTrack — client HTTP bas niveau (cURL).
*/
function fxChronotrackApiHttpRequest($strMethod, $strUrl, $arrOptions = array()) {
$arrResult = array(
'state' => 'error',
'http_code' => 0,
'body' => '',
'json' => null,
'curl_error' => '',
);
if (!function_exists('curl_init')) {
$arrResult['curl_error'] = 'cURL indisponible';
return $arrResult;
}
$ch = curl_init();
$arrHeaders = isset($arrOptions['headers']) && is_array($arrOptions['headers']) ? $arrOptions['headers'] : array();
curl_setopt($ch, CURLOPT_URL, $strUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 20);
curl_setopt($ch, CURLOPT_TIMEOUT, 180);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, $arrHeaders);
$strMethod = strtoupper(trim($strMethod));
if ($strMethod === 'GET') {
curl_setopt($ch, CURLOPT_HTTPGET, true);
} else {
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $strMethod);
if (isset($arrOptions['body'])) {
curl_setopt($ch, CURLOPT_POSTFIELDS, $arrOptions['body']);
} elseif ($strMethod === 'POST') {
curl_setopt($ch, CURLOPT_POSTFIELDS, '');
}
}
if (!empty($arrOptions['basic_user']) || !empty($arrOptions['basic_pass'])) {
curl_setopt($ch, CURLOPT_USERPWD, $arrOptions['basic_user'] . ':' . $arrOptions['basic_pass']);
}
$strBody = curl_exec($ch);
$arrResult['http_code'] = (int)curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($strBody === false) {
$arrResult['curl_error'] = curl_error($ch);
} else {
$arrResult['body'] = $strBody;
$tabJson = json_decode($strBody, true);
if (is_array($tabJson)) {
$arrResult['json'] = $tabJson;
}
}
curl_close($ch);
if ($arrResult['curl_error'] !== '') {
return $arrResult;
}
if ($arrResult['http_code'] >= 200 && $arrResult['http_code'] < 300) {
$arrResult['state'] = 'ok';
}
return $arrResult;
}
function fxChronotrackApiHttpGetJsonErrorMessage($arrHttp) {
if (!empty($arrHttp['curl_error'])) {
return $arrHttp['curl_error'];
}
$arrParts = array();
if (is_array($arrHttp['json'])) {
foreach (array('errorMessage', 'message', 'error_description', 'errorCode') as $strKey) {
if (!empty($arrHttp['json'][$strKey])) {
$arrParts[] = (string)$arrHttp['json'][$strKey];
}
}
if (!empty($arrHttp['json']['error'])) {
if (is_array($arrHttp['json']['error'])) {
foreach ($arrHttp['json']['error'] as $mixErr) {
if (is_string($mixErr) && trim($mixErr) !== '') {
$arrParts[] = $mixErr;
} elseif (is_array($mixErr)) {
if (!empty($mixErr['errorMessage'])) {
$arrParts[] = (string)$mixErr['errorMessage'];
} elseif (!empty($mixErr['message'])) {
$arrParts[] = (string)$mixErr['message'];
}
}
}
} elseif (is_string($arrHttp['json']['error'])) {
$arrParts[] = $arrHttp['json']['error'];
}
}
}
if (count($arrParts) > 0) {
return 'HTTP ' . intval($arrHttp['http_code']) . ' — ' . implode(' | ', array_unique($arrParts));
}
$strBody = trim((string)($arrHttp['body'] ?? ''));
if ($strBody !== '') {
return 'HTTP ' . intval($arrHttp['http_code']) . ' — ' . substr($strBody, 0, 400);
}
return 'HTTP ' . intval($arrHttp['http_code']);
}

View File

@ -0,0 +1,221 @@
<?php
/**
* MSIN API ChronoTrack — OAuth2 Password Flow + cache token.
*/
function fxChronotrackApiOAuthGetTokenRow() {
global $objDatabase;
$sql = "SELECT * FROM api_chronotrack_token WHERE token_key = '"
. $objDatabase->fxEscape(MSIN_API_CHRONOTRACK_TOKEN_KEY) . "' LIMIT 1";
return $objDatabase->fxGetRow($sql);
}
function fxChronotrackApiOAuthSaveToken($strAccessToken, $strRefreshToken, $intExpiresEpoch) {
global $objDatabase;
$strExpires = '';
if ($intExpiresEpoch > 0) {
$strExpires = date('Y-m-d H:i:s', $intExpiresEpoch);
}
$sqlExists = "SELECT token_key FROM api_chronotrack_token WHERE token_key = '"
. $objDatabase->fxEscape(MSIN_API_CHRONOTRACK_TOKEN_KEY) . "' LIMIT 1";
if ($objDatabase->fxGetRow($sqlExists) !== null) {
$sql = "UPDATE api_chronotrack_token SET access_token = '"
. $objDatabase->fxEscape($strAccessToken) . "', refresh_token = '"
. $objDatabase->fxEscape($strRefreshToken) . "', expires_at = "
. ($strExpires !== '' ? "'" . $objDatabase->fxEscape($strExpires) . "'" : 'NULL')
. ", updated_at = '" . fxGetDateTime() . "' WHERE token_key = '"
. $objDatabase->fxEscape(MSIN_API_CHRONOTRACK_TOKEN_KEY) . "'";
} else {
$sql = "INSERT INTO api_chronotrack_token SET token_key = '"
. $objDatabase->fxEscape(MSIN_API_CHRONOTRACK_TOKEN_KEY) . "', access_token = '"
. $objDatabase->fxEscape($strAccessToken) . "', refresh_token = '"
. $objDatabase->fxEscape($strRefreshToken) . "', expires_at = "
. ($strExpires !== '' ? "'" . $objDatabase->fxEscape($strExpires) . "'" : 'NULL')
. ", updated_at = '" . fxGetDateTime() . "'";
}
$objDatabase->fxQuery($sql);
}
function fxChronotrackApiOAuthTokenValid($arrRow) {
if ($arrRow === null || trim((string)($arrRow['access_token'] ?? '')) === '') {
return false;
}
if (empty($arrRow['expires_at'])) {
return true;
}
$intExpires = strtotime($arrRow['expires_at']);
if ($intExpires === false) {
return true;
}
return ($intExpires > (time() + 120));
}
function fxChronotrackApiOAuthFetchToken($blnForceRefresh = false) {
if (!fxChronotrackApiSettingsConfigured()) {
return array('state' => 'error', 'message' => fxChronotrackApiSettingsErrorMessage());
}
if (!$blnForceRefresh) {
$arrCached = fxChronotrackApiOAuthGetTokenRow();
if (fxChronotrackApiOAuthTokenValid($arrCached)) {
return array(
'state' => 'ok',
'access_token' => $arrCached['access_token'],
'from_cache' => true,
);
}
}
$strBase = fxChronotrackApiGetBaseUrl();
$strQuery = http_build_query(array(
'grant_type' => 'password',
'username' => fxChronotrackApiGetServiceUser(),
'password' => fxChronotrackApiGetServicePass(),
));
$strUrl = $strBase . '/oauth2/token?' . $strQuery;
$arrHttp = fxChronotrackApiHttpRequest('GET', $strUrl, array(
'basic_user' => fxChronotrackApiGetClientId(),
'basic_pass' => fxChronotrackApiGetClientSecret(),
));
if ($arrHttp['state'] !== 'ok' || !is_array($arrHttp['json'])) {
return array(
'state' => 'error',
'message' => fxChronotrackApiHttpGetJsonErrorMessage($arrHttp),
);
}
$arrJson = $arrHttp['json'];
if (!empty($arrJson['errorCode']) || !empty($arrJson['error'])) {
return array(
'state' => 'error',
'message' => fxChronotrackApiHttpGetJsonErrorMessage($arrHttp),
);
}
$strToken = trim((string)($arrJson['access_token'] ?? ''));
if ($strToken === '') {
return array(
'state' => 'error',
'message' => 'access_token absent — ' . fxChronotrackApiHttpGetJsonErrorMessage($arrHttp),
);
}
$strRefresh = trim((string)($arrJson['refresh_token'] ?? ''));
$intExpires = intval($arrJson['expires'] ?? 0);
if ($intExpires <= 0 && !empty($arrJson['expires_in'])) {
$intExpires = time() + intval($arrJson['expires_in']);
}
fxChronotrackApiOAuthSaveToken($strToken, $strRefresh, $intExpires);
return array(
'state' => 'ok',
'access_token' => $strToken,
'from_cache' => false,
);
}
function fxChronotrackApiOAuthApiGet($strPath, $arrQuery = array()) {
$arrToken = fxChronotrackApiOAuthFetchToken(false);
if ($arrToken['state'] !== 'ok') {
return $arrToken;
}
$arrQuery['client_id'] = fxChronotrackApiGetClientId();
$strUrl = fxChronotrackApiGetApiBaseUrl() . '/' . ltrim($strPath, '/');
if (!empty($arrQuery)) {
$strUrl .= '?' . http_build_query($arrQuery);
}
$arrHttp = fxChronotrackApiHttpRequest('GET', $strUrl, array(
'headers' => array('Authorization: Bearer ' . $arrToken['access_token']),
));
if ($arrHttp['state'] === 'ok') {
return array('state' => 'ok', 'json' => $arrHttp['json'], 'http' => $arrHttp);
}
return array(
'state' => 'error',
'message' => fxChronotrackApiHttpGetJsonErrorMessage($arrHttp),
'http' => $arrHttp,
);
}
function fxChronotrackApiOAuthApiPost($strPath, $mixBody, $arrQuery = array()) {
$arrToken = fxChronotrackApiOAuthFetchToken(false);
if ($arrToken['state'] !== 'ok') {
return $arrToken;
}
$arrQuery['client_id'] = fxChronotrackApiGetClientId();
$strUrl = fxChronotrackApiGetApiBaseUrl() . '/' . ltrim($strPath, '/');
if (!empty($arrQuery)) {
$strUrl .= '?' . http_build_query($arrQuery);
}
$strBody = json_encode($mixBody, JSON_UNESCAPED_UNICODE);
if (!is_string($strBody)) {
return array('state' => 'error', 'message' => 'json_encode a échoué');
}
$arrHttp = fxChronotrackApiHttpRequest('POST', $strUrl, array(
'headers' => array(
'Authorization: Bearer ' . $arrToken['access_token'],
'Content-Type: application/json',
'Accept: application/json',
),
'body' => $strBody,
));
if ($arrHttp['state'] === 'ok') {
return array('state' => 'ok', 'json' => $arrHttp['json'], 'http' => $arrHttp);
}
return array(
'state' => 'error',
'message' => fxChronotrackApiHttpGetJsonErrorMessage($arrHttp),
'http' => $arrHttp,
);
}
function fxChronotrackApiOAuthApiPut($strPath, $mixBody, $arrQuery = array()) {
$arrToken = fxChronotrackApiOAuthFetchToken(false);
if ($arrToken['state'] !== 'ok') {
return $arrToken;
}
$arrQuery['client_id'] = fxChronotrackApiGetClientId();
$strUrl = fxChronotrackApiGetApiBaseUrl() . '/' . ltrim($strPath, '/');
if (!empty($arrQuery)) {
$strUrl .= '?' . http_build_query($arrQuery);
}
$strBody = json_encode($mixBody, JSON_UNESCAPED_UNICODE);
if (!is_string($strBody)) {
return array('state' => 'error', 'message' => 'json_encode a échoué');
}
$arrHttp = fxChronotrackApiHttpRequest('PUT', $strUrl, array(
'headers' => array(
'Authorization: Bearer ' . $arrToken['access_token'],
'Content-Type: application/json',
'Accept: application/json',
),
'body' => $strBody,
));
if ($arrHttp['state'] === 'ok') {
return array('state' => 'ok', 'json' => $arrHttp['json'], 'http' => $arrHttp);
}
return array(
'state' => 'error',
'message' => fxChronotrackApiHttpGetJsonErrorMessage($arrHttp),
'http' => $arrHttp,
);
}

View File

@ -0,0 +1,51 @@
<?php
/**
* MSIN API ChronoTrack — lecture des credentials (inc_settings.php).
*/
function fxChronotrackApiSettingsConfigured() {
if (!defined('MSIN_API_CHRONOTRACK_CLIENT_ID') || !defined('MSIN_API_CHRONOTRACK_CLIENT_SECRET')) {
return false;
}
if (trim(MSIN_API_CHRONOTRACK_CLIENT_ID) === '' || trim(MSIN_API_CHRONOTRACK_CLIENT_SECRET) === '') {
return false;
}
if (!defined('MSIN_API_CHRONOTRACK_SERVICE_USER') || !defined('MSIN_API_CHRONOTRACK_SERVICE_PASS')) {
return false;
}
if (trim(MSIN_API_CHRONOTRACK_SERVICE_USER) === '' || trim(MSIN_API_CHRONOTRACK_SERVICE_PASS) === '') {
return false;
}
return true;
}
function fxChronotrackApiGetBaseUrl() {
if (defined('MSIN_API_CHRONOTRACK_BASE_URL') && trim(MSIN_API_CHRONOTRACK_BASE_URL) !== '') {
return rtrim(MSIN_API_CHRONOTRACK_BASE_URL, '/');
}
return 'https://api.chronotrack.com';
}
function fxChronotrackApiGetApiBaseUrl() {
return fxChronotrackApiGetBaseUrl() . '/api';
}
function fxChronotrackApiGetClientId() {
return defined('MSIN_API_CHRONOTRACK_CLIENT_ID') ? trim(MSIN_API_CHRONOTRACK_CLIENT_ID) : '';
}
function fxChronotrackApiGetClientSecret() {
return defined('MSIN_API_CHRONOTRACK_CLIENT_SECRET') ? MSIN_API_CHRONOTRACK_CLIENT_SECRET : '';
}
function fxChronotrackApiGetServiceUser() {
return defined('MSIN_API_CHRONOTRACK_SERVICE_USER') ? trim(MSIN_API_CHRONOTRACK_SERVICE_USER) : '';
}
function fxChronotrackApiGetServicePass() {
return defined('MSIN_API_CHRONOTRACK_SERVICE_PASS') ? MSIN_API_CHRONOTRACK_SERVICE_PASS : '';
}
function fxChronotrackApiSettingsErrorMessage() {
return 'Credentials MSIN API ChronoTrack incomplets dans php/inc_settings.php (client_id, client_secret, service_user, service_pass).';
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,17 @@
<?php
/**
* MSIN API ChronoTrack — chargement du module (parallèle au site legacy).
*/
if (!defined('MSIN_API_CHRONOTRACK_LOADED')) {
define('MSIN_API_CHRONOTRACK_LOADED', true);
require_once __DIR__ . '/fx_chronotrack_constants.php';
require_once __DIR__ . '/fx_chronotrack_settings.php';
require_once __DIR__ . '/fx_chronotrack_http.php';
require_once __DIR__ . '/fx_chronotrack_oauth.php';
require_once __DIR__ . '/fx_chronotrack_events.php';
require_once __DIR__ . '/fx_chronotrack_config.php';
require_once __DIR__ . '/fx_chronotrack_sync.php';
require_once __DIR__ . '/fx_chronotrack_admin.php';
}

466
php/doc_edition.php Normal file
View File

@ -0,0 +1,466 @@
<?php
/**
* Éditeur documentation MS1 (pop-up) — module doc_page FR/EN, toutes les pages d'un module.
*/
require_once('inc_start_time.php');
require_once('inc_fonctions.php');
require_once('inc_fx_messages.php');
if (empty($_SESSION['usa_id'])) {
fxAdminRequireSuperadmLogin();
}
if (!fxAdminCanUseTextEditTools()) {
header('HTTP/1.1 403 Forbidden');
echo 'Accès refusé.';
exit;
}
function docEdEsc($str) {
return htmlspecialchars((string)$str, ENT_QUOTES, 'UTF-8');
}
function docEdFieldAttrs($strChamp, $strRowKey, $strLang) {
$id = 'doc_' . preg_replace('/[^a-z0-9_]/i', '_', $strRowKey) . '_' . $strLang . '_' . $strChamp;
return 'id="' . docEdEsc($id) . '" name="pages[' . docEdEsc($strRowKey) . '][' . docEdEsc($strLang) . '][' . docEdEsc($strChamp) . ']"';
}
function docEdLoadModulePages($modClef, $prg) {
global $objDatabase;
$sql = "SELECT *
FROM doc_page
WHERE doc_mod_clef = '" . $objDatabase->fxEscape($modClef) . "'
AND doc_prg IN ('" . $objDatabase->fxEscape($prg) . "', 'global')
ORDER BY doc_tri, doc_page_clef, doc_langue DESC";
$rows = $objDatabase->fxGetResults($sql);
$grouped = array();
if (!is_array($rows)) {
return $grouped;
}
for ($i = 1; $i <= count($rows); $i++) {
$row = $rows[$i];
$pageClef = $row['doc_page_clef'] ?? '';
if ($pageClef === '') {
continue;
}
if (!isset($grouped[$pageClef])) {
$grouped[$pageClef] = array(
'doc_page_clef' => $pageClef,
'doc_tri' => (int)($row['doc_tri'] ?? 0),
'fr' => null,
'en' => null,
);
}
$lang = $row['doc_langue'] ?? 'fr';
if ($lang === 'fr' || $lang === 'en') {
$grouped[$pageClef][$lang] = $row;
}
}
uasort($grouped, function ($a, $b) {
if ($a['doc_tri'] === $b['doc_tri']) {
return strcmp($a['doc_page_clef'], $b['doc_page_clef']);
}
return $a['doc_tri'] <=> $b['doc_tri'];
});
return $grouped;
}
$anchor = trim($_GET['anchor'] ?? $_POST['anchor'] ?? '');
$modClef = trim($_GET['mod'] ?? $_POST['mod'] ?? '');
$prg = trim($_GET['prg'] ?? $_POST['prg'] ?? 'compte.php');
$page = trim($_GET['page'] ?? $_POST['page'] ?? '');
$bd = trim(strtolower($_GET['bd'] ?? $_POST['bd'] ?? 'client'));
$blnSaved = false;
if ($modClef === '' && $anchor !== '') {
$modClef = fxDocResolveModClef($anchor, 0);
}
if ($anchor === '' && $modClef !== '') {
$anchor = $modClef;
}
if (isset($_POST['btn_mod']) && !empty($_POST['pages']) && is_array($_POST['pages'])) {
foreach ($_POST['pages'] as $pageClef => $langs) {
if (!is_array($langs)) {
continue;
}
foreach ($langs as $lang => $tabPosted) {
if (!is_array($tabPosted) || empty($tabPosted['doc_langue'])) {
continue;
}
$tabPosted['doc_page_clef'] = $pageClef;
$tabPosted['doc_mod_clef'] = $modClef;
$tabPosted['doc_prg'] = $prg;
if (!empty($tabPosted['pk_doc_page'])) {
fxSetDocPage('mod', $tabPosted, $tabPosted['pk_doc_page']);
} else {
fxSetDocPage('add', $tabPosted);
}
}
}
if (!empty($_POST['doc_mod']) && is_array($_POST['doc_mod']) && !empty($_POST['doc_mod']['pk_doc_mod'])) {
fxSetDocMod('mod', $_POST['doc_mod'], $_POST['doc_mod']['pk_doc_mod']);
}
$blnSaved = true;
}
$sqlMod = "SELECT * FROM doc_mod
WHERE doc_mod_clef = '" . $objDatabase->fxEscape($modClef) . "'
AND doc_mod_prg IN ('" . $objDatabase->fxEscape($prg) . "', 'global')
ORDER BY FIELD(doc_mod_prg, '" . $objDatabase->fxEscape($prg) . "', 'global')
LIMIT 1";
$tabMod = $objDatabase->fxGetRow($sqlMod);
$tabPages = docEdLoadModulePages($modClef, $prg);
$tabLangMeta = array(
'fr' => 'Français',
'en' => 'English',
);
?><!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Documentation — <?php echo docEdEsc($modClef); ?></title>
<style>
*, *::before, *::after { box-sizing: border-box; }
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif;
font-size: 14px;
line-height: 1.45;
color: #212529;
background: #f1f3f5;
}
.doced-page {
max-width: 1080px;
margin: 0 auto;
padding: 20px 16px 32px;
}
.doced-card {
background: #fff;
border: 1px solid #dee2e6;
border-radius: 8px;
overflow: hidden;
}
.doced-card-header {
padding: 14px 18px;
background: #2f5fd0;
color: #fff;
}
.doced-card-header h1 {
margin: 0 0 4px;
font-size: 18px;
font-weight: 700;
}
.doced-card-header .doced-key {
font-size: 12px;
opacity: .9;
word-break: break-all;
}
.doced-meta {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 10px;
padding: 12px 18px;
background: #f8f9fa;
border-bottom: 1px solid #e9ecef;
}
.doced-meta dt {
margin: 0;
font-size: 11px;
text-transform: uppercase;
letter-spacing: .04em;
color: #6c757d;
}
.doced-meta dd {
margin: 2px 0 0;
font-weight: 600;
word-break: break-all;
}
.doced-alert {
margin: 14px 18px 0;
padding: 10px 12px;
border-radius: 6px;
background: #d1e7dd;
color: #0f5132;
border: 1px solid #badbcc;
}
.doced-mod-field {
padding: 14px 18px;
border-bottom: 1px solid #e9ecef;
}
.doced-mod-field label {
display: block;
margin-bottom: 4px;
font-size: 12px;
font-weight: 600;
}
.doced-mod-field input {
width: 100%;
max-width: 480px;
padding: 8px 10px;
border: 1px solid #ced4da;
border-radius: 4px;
font: inherit;
}
.doced-page-block {
border-bottom: 1px solid #e9ecef;
padding: 16px 18px;
}
.doced-page-block:last-of-type {
border-bottom: none;
}
.doced-page-head {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 12px;
}
.doced-page-head h2 {
margin: 0;
font-size: 15px;
font-weight: 700;
color: #2f5fd0;
}
.doced-page-head .doced-page-clef {
font-size: 12px;
color: #6c757d;
}
.doced-page-head input[type="number"] {
width: 72px;
padding: 4px 8px;
border: 1px solid #ced4da;
border-radius: 4px;
}
.doced-lang-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
@media (max-width: 900px) {
.doced-lang-grid { grid-template-columns: 1fr; }
.doced-meta { grid-template-columns: 1fr 1fr; }
}
.doced-lang-panel {
border: 1px solid #dee2e6;
border-radius: 6px;
padding: 12px;
}
.doced-lang-panel h3 {
margin: 0 0 10px;
font-size: 13px;
font-weight: 700;
}
.doced-field {
margin-bottom: 10px;
}
.doced-field:last-child {
margin-bottom: 0;
}
.doced-field label {
display: block;
margin-bottom: 4px;
font-size: 12px;
font-weight: 600;
color: #495057;
}
.doced-field .doced-hint {
font-weight: 400;
color: #868e96;
}
.doced-field input[type="text"],
.doced-field textarea {
width: 100%;
padding: 8px 10px;
border: 1px solid #ced4da;
border-radius: 4px;
font: inherit;
color: #212529;
}
.doced-field textarea.doced-html {
min-height: 200px;
font-family: Consolas, "Courier New", monospace;
font-size: 12px;
line-height: 1.45;
}
.doced-actions {
padding: 16px 18px;
}
.doced-btn-save {
padding: 10px 24px;
border: none;
border-radius: 4px;
background: #28a745;
color: #fff;
font: inherit;
font-weight: 600;
cursor: pointer;
}
.doced-btn-save:hover {
background: #218838;
}
</style>
</head>
<body>
<div class="doced-page">
<form action="<?php echo docEdEsc($vDomaine); ?>/php/doc_edition.php" method="post" id="frm_doc_edition">
<input type="hidden" name="anchor" value="<?php echo docEdEsc($anchor); ?>">
<input type="hidden" name="mod" value="<?php echo docEdEsc($modClef); ?>">
<input type="hidden" name="prg" value="<?php echo docEdEsc($prg); ?>">
<input type="hidden" name="page" value="<?php echo docEdEsc($page); ?>">
<input type="hidden" name="bd" value="<?php echo docEdEsc($bd); ?>">
<div class="doced-card">
<div class="doced-card-header">
<h1>Guide documentation</h1>
<div class="doced-key">Module <?php echo docEdEsc($modClef); ?> · ancre <?php echo docEdEsc($anchor); ?></div>
</div>
<dl class="doced-meta">
<div>
<dt>Programme</dt>
<dd><?php echo docEdEsc($prg); ?></dd>
</div>
<div>
<dt>Ancre UI</dt>
<dd><?php echo docEdEsc($anchor); ?></dd>
</div>
<div>
<dt>Pages</dt>
<dd><?php echo count($tabPages); ?></dd>
</div>
<div>
<dt>Base</dt>
<dd><?php echo docEdEsc($bd); ?></dd>
</div>
</dl>
<?php if ($blnSaved) { ?>
<div class="doced-alert">Enregistré. Fermez cette fenêtre pour recharger l'écran principal.</div>
<?php } ?>
<?php if (is_array($tabMod) && !empty($tabMod['pk_doc_mod'])) { ?>
<div class="doced-mod-field">
<label for="doc_mod_titre">Titre du module (doc_mod)</label>
<input type="hidden" name="doc_mod[pk_doc_mod]" value="<?php echo (int)$tabMod['pk_doc_mod']; ?>">
<input type="hidden" name="doc_mod[doc_mod_clef]" value="<?php echo docEdEsc($tabMod['doc_mod_clef']); ?>">
<input type="hidden" name="doc_mod[doc_mod_prg]" value="<?php echo docEdEsc($tabMod['doc_mod_prg']); ?>">
<input type="text" id="doc_mod_titre" name="doc_mod[doc_mod_titre]"
value="<?php echo docEdEsc($tabMod['doc_mod_titre'] ?? ''); ?>" maxlength="255">
</div>
<?php } ?>
<?php
$intPageNum = 0;
foreach ($tabPages as $pageClef => $tabPageGroup) {
$intPageNum++;
$tri = (int)$tabPageGroup['doc_tri'];
?>
<section class="doced-page-block">
<div class="doced-page-head">
<h2>Page <?php echo (int)$intPageNum; ?></h2>
<span class="doced-page-clef"><?php echo docEdEsc($pageClef); ?></span>
</div>
<div class="doced-lang-grid">
<?php foreach ($tabLangMeta as $strLang => $strLangLabel) {
$tabRow = $tabPageGroup[$strLang];
$isMod = is_array($tabRow) && !empty($tabRow['pk_doc_page']);
?>
<div class="doced-lang-panel">
<h3><?php echo docEdEsc($strLangLabel); ?></h3>
<input type="hidden" <?php echo docEdFieldAttrs('pk_doc_page', $pageClef, $strLang); ?>
value="<?php echo $isMod ? (int)$tabRow['pk_doc_page'] : ''; ?>">
<input type="hidden" <?php echo docEdFieldAttrs('doc_langue', $pageClef, $strLang); ?>
value="<?php echo docEdEsc($strLang); ?>">
<input type="hidden" <?php echo docEdFieldAttrs('doc_tri', $pageClef, $strLang); ?>
value="<?php echo $isMod ? (int)$tabRow['doc_tri'] : $tri; ?>">
<input type="hidden" <?php echo docEdFieldAttrs('doc_actif', $pageClef, $strLang); ?>
value="1">
<div class="doced-field">
<label>Titre overlay</label>
<input type="text" maxlength="255"
<?php echo docEdFieldAttrs('doc_titre', $pageClef, $strLang); ?>
value="<?php echo docEdEsc($isMod ? ($tabRow['doc_titre'] ?? '') : ''); ?>">
</div>
<div class="doced-field">
<label>Sous-titre</label>
<input type="text" maxlength="255"
<?php echo docEdFieldAttrs('doc_sous_titre', $pageClef, $strLang); ?>
value="<?php echo docEdEsc($isMod ? ($tabRow['doc_sous_titre'] ?? '') : ''); ?>">
</div>
<div class="doced-field">
<label>Contenu HTML <span class="doced-hint">(doc_html)</span></label>
<textarea class="doced-html" <?php echo docEdFieldAttrs('doc_html', $pageClef, $strLang); ?>><?php
echo docEdEsc($isMod ? ($tabRow['doc_html'] ?? '') : '');
?></textarea>
</div>
</div>
<?php } ?>
</div>
</section>
<?php } ?>
<?php if (empty($tabPages)) { ?>
<div class="doced-page-block">
<p>Aucune page pour ce module. Vérifiez que <code>doc_page</code> est peuplé pour
<strong><?php echo docEdEsc($modClef); ?></strong>.</p>
</div>
<?php } ?>
<div class="doced-actions">
<button type="submit" name="btn_mod" value="save" class="doced-btn-save">Enregistrer tout le guide</button>
</div>
</div>
</form>
</div>
<!--
Editeur WYSIWYG Trumbowyg — UNIQUEMENT cette page (doc_edition.php).
N'affecte pas le TinyMCE/CKEditor utilises ailleurs sur le site.
-->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/trumbowyg@2.27.3/dist/ui/trumbowyg.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/trumbowyg@2.27.3/dist/plugins/table/ui/trumbowyg.table.min.css">
<style>
.trumbowyg-box { margin: 0; border-radius: 4px; }
.trumbowyg-editor { min-height: 220px; font-family: inherit; font-size: 14px; }
</style>
<script src="https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/trumbowyg@2.27.3/dist/trumbowyg.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/trumbowyg@2.27.3/dist/plugins/table/trumbowyg.table.min.js"></script>
<script>
jQuery(function ($) {
$('textarea.doced-html').trumbowyg({
semantic: true,
autogrow: true,
resetCss: true,
removeformatPasted: true,
btns: [
['viewHTML'],
['formatting'],
['strong', 'em'],
['link'],
['unorderedList', 'orderedList'],
['table'],
['horizontalRule'],
['removeformat']
]
});
});
</script>
</body>
</html>

677
php/inc_cookie_consent.php Normal file
View File

@ -0,0 +1,677 @@
<?php
function fxCookiesPreferences(): array
{
$defaults = ['analytics' => false, 'marketing' => false, 'set' => false];
if (!empty($_COOKIE['cookiesPreferences'])) {
$prefs = json_decode(stripslashes((string) $_COOKIE['cookiesPreferences']), true);
if (is_array($prefs)) {
return [
'analytics' => !empty($prefs['analytics']),
'marketing' => !empty($prefs['marketing']),
'set' => true,
];
}
}
if (isset($_COOKIE['cookiesAccepted']) && $_COOKIE['cookiesAccepted'] === 'true') {
return ['analytics' => true, 'marketing' => true, 'set' => true];
}
if (isset($_COOKIE['cookiesRejected']) && $_COOKIE['cookiesRejected'] === 'true') {
return ['analytics' => false, 'marketing' => false, 'set' => true];
}
return $defaults;
}
function fxCookiesConsentAccepted(): bool
{
$prefs = fxCookiesPreferences();
return $prefs['set'] && ($prefs['analytics'] || $prefs['marketing']);
}
function fxCookiesConsentRejected(): bool
{
$prefs = fxCookiesPreferences();
return $prefs['set'] && !$prefs['analytics'] && !$prefs['marketing'];
}
function fxCookiesConsentPending(): bool
{
return !fxCookiesPreferences()['set'];
}
function fxCookiesAllowAnalytics(): bool
{
return fxCookiesPreferences()['analytics'];
}
function fxCookiesAllowMarketing(): bool
{
return fxCookiesPreferences()['marketing'];
}
function fxCookiesConsentResetIfRequested(): void
{
if (!isset($_GET['cookies'])) {
return;
}
$past = time() - 3600;
$names = ['cookiesAccepted', 'cookiesRejected', 'cookiesPreferences'];
foreach ($names as $name) {
setcookie($name, '', $past, '/');
unset($_COOKIE[$name]);
}
$path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) ?: '/';
$query = [];
if (!empty($_SERVER['QUERY_STRING'])) {
parse_str($_SERVER['QUERY_STRING'], $query);
}
unset($query['cookies']);
$location = $path;
if ($query) {
$location .= '?' . http_build_query($query);
}
header('Location: ' . $location);
exit;
}
function fxPageNeedsPaypal(): bool
{
global $strPage;
if (empty($strPage)) {
return false;
}
static $paypalPages = [
'panier.php',
'dons.php',
'paiement_redirect.php',
'panier',
'cart',
'don',
'donate',
];
return in_array($strPage, $paypalPages, true);
}
function fxCookieConsentDefaults(): array
{
return [
'experience_cookies' => [
'fr' => 'Nous utilisons des témoins (cookies) pour assurer le bon fonctionnement du site, sécuriser vos inscriptions et mémoriser votre panier. Avec votre consentement, nous utilisons aussi des outils de mesure d\'audience et de marketing. Vous pouvez accepter, refuser ou personnaliser vos choix à tout moment. Votre décision n\'empêche pas de vous inscrire.',
'en' => 'We use cookies to ensure the site works properly, secure your registration and remember your cart. With your consent, we also use analytics and marketing tools. You may accept, refuse or customize your choices at any time. Your decision does not prevent you from registering.',
],
'bouton_acceper_cookies' => [
'fr' => 'Tout accepter',
'en' => 'Accept all',
],
'bouton_refuser_cookies' => [
'fr' => 'Tout refuser',
'en' => 'Refuse all',
],
'bouton_personnaliser_cookies' => [
'fr' => 'Personnaliser',
'en' => 'Customize',
],
'bouton_enregistrer_cookies' => [
'fr' => 'Enregistrer mes choix',
'en' => 'Save my choices',
],
'cookie_pref_titre' => [
'fr' => 'Gérer mes préférences de témoins',
'en' => 'Manage my cookie preferences',
],
'cookie_pref_essentiels' => [
'fr' => 'Témoins essentiels — nécessaires au panier, à la session, à la connexion et à la sécurité du site. Toujours actifs.',
'en' => 'Essential cookies — required for cart, session, login and site security. Always active.',
],
'cookie_pref_analytique' => [
'fr' => 'Mesure d\'audience — statistiques de fréquentation du site via Google Analytics.',
'en' => 'Analytics — site traffic statistics via Google Analytics.',
],
'cookie_pref_marketing' => [
'fr' => 'Marketing — mesure des conversions et publicité via Meta (Facebook) et scripts ajoutés par l\'organisateur.',
'en' => 'Marketing — conversion tracking and advertising via Meta (Facebook) and scripts added by the organizer.',
],
'politique_cookie' => [
'fr' => 'Politique de témoins',
'en' => 'Cookie policy',
],
'politique_confidentialite' => [
'fr' => 'Politique de confidentialité',
'en' => 'Privacy policy',
],
];
}
function fxCookieConsentTextIsDraft(string $clef, string $text): bool
{
$text = trim($text);
if ($text === '' || $text === $clef || $text === '*' . $clef . '*') {
return true;
}
if (stripos($text, $clef) === 0) {
return true;
}
return false;
}
function fxEnsureCookieConsentTexts(): void
{
global $objDatabase;
if (!isset($objDatabase)) {
return;
}
static $done = false;
if ($done) {
return;
}
$done = true;
foreach (fxCookieConsentDefaults() as $clef => $texts) {
foreach (['fr', 'en'] as $lang) {
if (!isset($texts[$lang])) {
continue;
}
$sql = "SELECT pk_info, info_texte FROM info
WHERE info_clef = '" . $objDatabase->fxEscape($clef) . "'
AND info_langue = '" . $objDatabase->fxEscape($lang) . "'
AND info_prg = 'global'
LIMIT 1";
$row = $objDatabase->fxGetRow($sql);
if (!$row) {
if (function_exists('fxSetInfo')) {
fxSetInfo('add', [
'info_clef' => $clef,
'info_langue' => $lang,
'info_texte' => $texts[$lang],
'info_aide' => '',
'info_prg' => 'global',
'info_actif' => 1,
], $lang);
}
continue;
}
if (!fxCookieConsentTextIsDraft($clef, (string) $row['info_texte'])) {
continue;
}
$targetText = fxCookieConsentDefaults()[$clef][$lang] ?? $texts[$lang];
$objDatabase->fxQuery(
"UPDATE info SET info_texte = '" . $objDatabase->fxEscape($targetText) . "', info_maj = NOW()
WHERE pk_info = " . intval($row['pk_info'])
);
}
}
}
function fxCookieConsentText(string $clef, string $strLangue): string
{
$defaults = fxCookieConsentDefaults();
$lang = ($strLangue === 'en') ? 'en' : 'fr';
return $defaults[$clef][$lang] ?? $clef;
}
function fxCookieLegalPlaceholders(string $html): string
{
global $vClient, $vDomaine;
$org = htmlspecialchars((string) $vClient, ENT_QUOTES, 'UTF-8');
$domain = htmlspecialchars((string) $vDomaine, ENT_QUOTES, 'UTF-8');
$email = 'confidentialite@' . preg_replace('/^https?:\/\//', '', (string) parse_url($vDomaine, PHP_URL_HOST));
$date = date('Y-m-d');
$replacements = [
'[ORGANISATION]' => $org,
'[ORGANIZATION]' => $org,
'[SITE]' => $domain,
'[DATE]' => $date,
'[COURRIEL]' => $email,
'[EMAIL]' => $email,
];
return str_replace(array_keys($replacements), array_values($replacements), $html);
}
function fxCookiePolicyHtml(string $strLangue): string
{
if ($strLangue === 'en') {
return fxCookieLegalPlaceholders(<<<'HTML'
<p><em>Last updated: [DATE]</em></p>
<p>This cookie policy explains how <strong>[ORGANIZATION]</strong> uses cookies and similar technologies on the registration website <strong>[SITE]</strong>, operated on the MS1 Inscription platform (Progiweb inc.).</p>
<h3>What is a cookie?</h3>
<p>A cookie is a small file stored on your device when you visit a website. It helps the site remember information such as your language, session or cart contents.</p>
<h3>How to manage your choices</h3>
<p>When you first visit the site, a banner lets you <strong>accept all</strong>, <strong>refuse all</strong> or <strong>customize</strong> optional cookies. You can change your choices at any time by clicking the cookie icon in the footer.</p>
<h3>1. Essential cookies (always active)</h3>
<p>These cookies are necessary for the site to function. They do not require your consent.</p>
<ul>
<li><strong>Session cookie (PHPSESSID)</strong> — keeps you logged in, maintains your cart and forms. Duration: session, approximately 3 hours of inactivity.</li>
<li><strong>no_panier</strong> — identifies your registration cart. Duration: 10 hours.</li>
<li><strong>cookiesPreferences</strong> — remembers your cookie choices. Duration: 12 months.</li>
<li><strong>reCAPTCHA (Google)</strong> — protects forms against spam and abuse.</li>
</ul>
<h3>2. Analytics cookies (optional)</h3>
<p>Active only if you enable <em>Analytics</em> in the banner.</p>
<ul>
<li><strong>Google Analytics</strong> (_ga, _ga_*, _gid) — helps us understand how the site is used. Provider: Google LLC. Duration: up to 13 months.</li>
</ul>
<h3>3. Marketing cookies (optional)</h3>
<p>Active only if you enable <em>Marketing</em> in the banner and if enabled for the event.</p>
<ul>
<li><strong>Meta Pixel</strong> (_fbp, _fbc) — measures advertising effectiveness and conversions. Provider: Meta Platforms Inc. Duration: up to 90 days.</li>
<li><strong>Custom scripts</strong> — the event organizer may add additional tools for a specific event.</li>
</ul>
<h3>4. Payment cookies</h3>
<p>When you proceed to payment, <strong>PayPal</strong> may place cookies required to process your transaction securely. These cookies are used only during checkout.</p>
<h3>5. Similar technologies</h3>
<p>We may use local storage on your device to control how often certain informational messages are displayed. This data remains on your device.</p>
<h3>6. Cross-border transfers</h3>
<p>Some providers (Google, Meta, PayPal) may process data outside Quebec, including in the United States. Contractual and technical safeguards are in place.</p>
<h3>7. Your rights</h3>
<p>Under Quebec Law 25, you may withdraw your consent for optional cookies at any time. For any questions: <a href="mailto:[EMAIL]">[EMAIL]</a>.</p>
<p>See also our <a href="[SITE]/page/confidentialite/en">privacy policy</a>.</p>
HTML
);
}
return fxCookieLegalPlaceholders(<<<'HTML'
<p><em>Dernière mise à jour : [DATE]</em></p>
<p>La présente politique explique comment <strong>[ORGANISATION]</strong> utilise des témoins (cookies) et technologies similaires sur le site d'inscription <strong>[SITE]</strong>, exploité via la plateforme MS1 Inscription (Progiweb inc.).</p>
<h3>Qu'est-ce qu'un témoin?</h3>
<p>Un témoin est un petit fichier déposé sur votre appareil lors d'une visite. Il permet au site de mémoriser certaines informations, comme votre langue, votre session ou le contenu de votre panier.</p>
<h3>Comment gérer vos choix</h3>
<p>Lors de votre première visite, un bandeau vous permet d'<strong>accepter tout</strong>, de <strong>tout refuser</strong> ou de <strong>personnaliser</strong> les témoins optionnels. Vous pouvez modifier vos choix en tout temps en cliquant sur l'icône témoins dans le pied de page.</p>
<h3>1. Témoins essentiels (toujours actifs)</h3>
<p>Ces témoins sont nécessaires au fonctionnement du site. Ils ne requièrent pas votre consentement.</p>
<ul>
<li><strong>Témoin de session (PHPSESSID)</strong> — maintient votre connexion, votre panier et vos formulaires. Durée : session, environ 3 heures d'inactivité.</li>
<li><strong>no_panier</strong> — identifie votre panier d'inscription. Durée : 10 heures.</li>
<li><strong>cookiesPreferences</strong> — mémorise vos choix de témoins. Durée : 12 mois.</li>
<li><strong>reCAPTCHA (Google)</strong> — protège les formulaires contre le pourriel et les abus.</li>
</ul>
<h3>2. Témoins analytiques (optionnels)</h3>
<p>Actifs seulement si vous activez la <em>mesure d'audience</em> dans le bandeau.</p>
<ul>
<li><strong>Google Analytics</strong> (_ga, _ga_*, _gid) — nous aide à comprendre l'utilisation du site. Fournisseur : Google LLC. Durée : jusqu'à 13 mois.</li>
</ul>
<h3>3. Témoins marketing (optionnels)</h3>
<p>Actifs seulement si vous activez le <em>marketing</em> dans le bandeau et si l'organisateur les a activés pour l'événement.</p>
<ul>
<li><strong>Pixel Meta</strong> (_fbp, _fbc) — mesure l'efficacité publicitaire et les conversions. Fournisseur : Meta Platforms inc. Durée : jusqu'à 90 jours.</li>
<li><strong>Scripts personnalisés</strong> — l'organisateur de l'événement peut ajouter d'autres outils pour un événement précis.</li>
</ul>
<h3>4. Témoins de paiement</h3>
<p>Lors du paiement, <strong>PayPal</strong> peut déposer des témoins nécessaires au traitement sécurisé de la transaction. Ces témoins sont utilisés uniquement lors du passage à la caisse.</p>
<h3>5. Technologies similaires</h3>
<p>Nous pouvons utiliser le stockage local de votre navigateur pour contrôler la fréquence d'affichage de certains messages informatifs. Ces données restent sur votre appareil.</p>
<h3>6. Transferts hors Québec</h3>
<p>Certains fournisseurs (Google, Meta, PayPal) peuvent traiter des données à l'extérieur du Québec, notamment aux États-Unis. Des mesures contractuelles et techniques sont en place.</p>
<h3>7. Vos droits</h3>
<p>Conformément à la Loi 25, vous pouvez retirer votre consentement pour les témoins optionnels en tout temps. Pour toute question : <a href="mailto:[COURRIEL]">[COURRIEL]</a>.</p>
<p>Consultez aussi notre <a href="[SITE]/page/confidentialite/fr">politique de confidentialité</a>.</p>
HTML
);
}
function fxPrivacyPolicyHtml(string $strLangue): string
{
if ($strLangue === 'en') {
return fxCookieLegalPlaceholders(<<<'HTML'
<p><em>Last updated: [DATE]</em></p>
<p><strong>[ORGANIZATION]</strong> (the "Organizer") respects your privacy. This policy describes how we collect, use and protect personal information when you use our registration website <strong>[SITE]</strong>, in accordance with Quebec's Act respecting the protection of personal information in the private sector (Law 25).</p>
<p>The site is operated on the MS1 Inscription platform by Progiweb inc., which acts as a technical service provider for the Organizer.</p>
<h3>1. Person responsible for the protection of personal information</h3>
<p>For any privacy-related request:</p>
<p><strong>Email:</strong> <a href="mailto:[EMAIL]">[EMAIL]</a></p>
<h3>2. Personal information we collect</h3>
<p>Depending on your registration, we may collect:</p>
<ul>
<li><strong>Identity and contact:</strong> name, address, city, province, postal code, email, phone, date of birth, sex;</li>
<li><strong>Registration:</strong> category, race, team, bib number, answers to organizer questions;</li>
<li><strong>Account:</strong> login credentials if you create an account;</li>
<li><strong>Payment:</strong> amount, payment method, transaction ID (card numbers are processed by PayPal — we do not store them);</li>
<li><strong>Technical data:</strong> IP address, browser type, pages visited, cookies (see our <a href="[SITE]/page/cookie/en">cookie policy</a>).</li>
</ul>
<h3>3. Purposes of collection</h3>
<ul>
<li>Process and confirm your event registration;</li>
<li>Manage payments, refunds and order-related communications;</li>
<li>Send confirmations, reminders and practical event information;</li>
<li>Ensure site security and prevent fraud;</li>
<li>Produce visit statistics and measure advertising campaigns, <strong>only with your cookie consent</strong>.</li>
</ul>
<h3>4. Legal basis</h3>
<ul>
<li><strong>Contract:</strong> registration and payment processing;</li>
<li><strong>Legal obligation:</strong> retention of certain accounting records;</li>
<li><strong>Legitimate interest:</strong> site security and fraud prevention;</li>
<li><strong>Consent:</strong> non-essential cookies and marketing communications, where applicable.</li>
</ul>
<h3>5. Third parties and service providers</h3>
<p>Your information may be shared with:</p>
<ul>
<li><strong>Progiweb inc. (MS1 Inscription)</strong> — hosting and platform operation;</li>
<li><strong>PayPal</strong> — payment processing;</li>
<li><strong>Google</strong> — reCAPTCHA (security); Google Analytics (if consented);</li>
<li><strong>Meta (Facebook)</strong> — advertising pixel (if consented);</li>
<li><strong>Event organizer and partners</strong> — race management, timing, bib pickup.</li>
</ul>
<p>We do not sell your personal information.</p>
<h3>6. Cross-border transfers</h3>
<p>Some service providers may process data outside Quebec. We require appropriate contractual and security measures in such cases.</p>
<h3>7. Retention period</h3>
<ul>
<li>Confirmed registration data: retained for the period required by the Organizer and applicable laws;</li>
<li>Uncompleted cart: up to 10 hours;</li>
<li>Session: approximately 3 hours of inactivity;</li>
<li>Accounting and transaction records: minimum 6 years where required by law.</li>
</ul>
<h3>8. Security</h3>
<p>We apply reasonable measures including HTTPS encryption, access controls and backups. No method is completely risk-free.</p>
<h3>9. Your rights</h3>
<p>You may request access to, correction or deletion of your personal information, or withdraw your consent where applicable. Contact: <a href="mailto:[EMAIL]">[EMAIL]</a>. We will respond within a reasonable time, generally within 30 days.</p>
<p>You may also file a complaint with the Commission d'accès à l'information du Québec (CAI): <a href="https://www.cai.gouv.qc.ca" target="_blank" rel="noopener">www.cai.gouv.qc.ca</a>.</p>
<h3>10. Minors</h3>
<p>Some events involve minor participants. We collect only information necessary for registration and event safety. Parental consent may be required depending on the event.</p>
<h3>11. Privacy incidents</h3>
<p>In the event of an incident presenting a risk of serious harm, we will notify the CAI and affected individuals as required by law.</p>
<h3>12. Changes</h3>
<p>We may update this policy. The date above indicates the latest revision. Material changes may be communicated by email or site notice.</p>
HTML
);
}
return fxCookieLegalPlaceholders(<<<'HTML'
<p><em>Dernière mise à jour : [DATE]</em></p>
<p><strong>[ORGANISATION]</strong> (l'« Organisateur ») respecte votre vie privée. La présente politique décrit comment nous collectons, utilisons et protégeons vos renseignements personnels lorsque vous utilisez notre site d'inscription <strong>[SITE]</strong>, conformément à la <em>Loi sur la protection des renseignements personnels dans le secteur privé</em> (Loi 25).</p>
<p>Le site est exploité sur la plateforme MS1 Inscription par Progiweb inc., qui agit à titre de mandataire technique pour l'Organisateur.</p>
<h3>1. Responsable de la protection des renseignements personnels</h3>
<p>Pour toute demande relative à la confidentialité :</p>
<p><strong>Courriel :</strong> <a href="mailto:[COURRIEL]">[COURRIEL]</a></p>
<h3>2. Renseignements personnels collectés</h3>
<p>Selon votre inscription, nous pouvons collecter :</p>
<ul>
<li><strong>Identité et coordonnées :</strong> nom, adresse, ville, province, code postal, courriel, téléphone, date de naissance, sexe;</li>
<li><strong>Inscription :</strong> catégorie, épreuve, équipe, numéro de dossard, réponses aux questions de l'organisateur;</li>
<li><strong>Compte :</strong> identifiants de connexion si vous créez un compte;</li>
<li><strong>Paiement :</strong> montant, mode de paiement, identifiant de transaction (les numéros de carte sont traités par PayPal — nous ne les conservons pas);</li>
<li><strong>Données techniques :</strong> adresse IP, type de navigateur, pages visitées, témoins (voir notre <a href="[SITE]/page/cookie/fr">politique de témoins</a>).</li>
</ul>
<h3>3. Finalités de la collecte</h3>
<ul>
<li>Traiter et confirmer votre inscription à l'événement;</li>
<li>Gérer les paiements, remboursements et communications liées à votre commande;</li>
<li>Envoyer confirmations, rappels et informations pratiques sur l'événement;</li>
<li>Assurer la sécurité du site et prévenir la fraude;</li>
<li>Produire des statistiques de visite et mesurer des campagnes publicitaires, <strong>seulement avec votre consentement aux témoins</strong>.</li>
</ul>
<h3>4. Fondements</h3>
<ul>
<li><strong>Exécution du contrat :</strong> inscription et traitement du paiement;</li>
<li><strong>Obligation légale :</strong> conservation de certains documents comptables;</li>
<li><strong>Intérêt légitime :</strong> sécurité du site et prévention de la fraude;</li>
<li><strong>Consentement :</strong> témoins non essentiels et communications marketing, le cas échéant.</li>
</ul>
<h3>5. Tiers et fournisseurs de services</h3>
<p>Vos renseignements peuvent être communiqués à :</p>
<ul>
<li><strong>Progiweb inc. (MS1 Inscription)</strong> — hébergement et exploitation de la plateforme;</li>
<li><strong>PayPal</strong> — traitement des paiements;</li>
<li><strong>Google</strong> — reCAPTCHA (sécurité); Google Analytics (si consenti);</li>
<li><strong>Meta (Facebook)</strong> — pixel publicitaire (si consenti);</li>
<li><strong>Organisateur et partenaires de l'événement</strong> — gestion de la course, chronométrage, remise des dossards.</li>
</ul>
<p>Nous ne vendons pas vos renseignements personnels.</p>
<h3>6. Transferts hors Québec</h3>
<p>Certains mandataires peuvent traiter des renseignements à l'extérieur du Québec. Nous exigeons des mesures contractuelles et de sécurité appropriées.</p>
<h3>7. Durée de conservation</h3>
<ul>
<li>Données d'inscription confirmée : conservées pour la période requise par l'Organisateur et les lois applicables;</li>
<li>Panier non finalisé : jusqu'à 10 heures;</li>
<li>Session : environ 3 heures d'inactivité;</li>
<li>Documents comptables et transactions : minimum 6 ans lorsque exigé par la loi.</li>
</ul>
<h3>8. Sécurité</h3>
<p>Nous appliquons des mesures raisonnables incluant le chiffrement HTTPS, des contrôles d'accès et des sauvegardes. Aucune méthode n'est toutefois sans risque.</p>
<h3>9. Vos droits</h3>
<p>Vous pouvez demander l'accès à vos renseignements, leur rectification ou leur suppression, ou retirer votre consentement lorsque applicable. Contact : <a href="mailto:[COURRIEL]">[COURRIEL]</a>. Nous répondrons dans un délai raisonnable, généralement sous 30 jours.</p>
<p>Vous pouvez aussi déposer une plainte auprès de la Commission d'accès à l'information du Québec (CAI) : <a href="https://www.cai.gouv.qc.ca" target="_blank" rel="noopener">www.cai.gouv.qc.ca</a>.</p>
<h3>10. Mineurs</h3>
<p>Certains événements concernent des participants mineurs. Nous collectons seulement les renseignements nécessaires à l'inscription et à la sécurité. Un consentement parental peut être requis selon l'événement.</p>
<h3>11. Incidents de confidentialité</h3>
<p>En cas d'incident présentant un risque de préjudice grave, nous aviserons la CAI et les personnes concernées conformément à la loi.</p>
<h3>12. Modifications</h3>
<p>Nous pouvons mettre à jour cette politique. La date ci-dessus indique la dernière révision. Les changements importants pourront être communiqués par courriel ou avis sur le site.</p>
HTML
);
}
function fxLegalPageNeedsSeed(array $row): bool
{
if (trim((string) ($row['pag_texte_fr'] ?? '')) === '' || trim((string) ($row['pag_texte_en'] ?? '')) === '') {
return true;
}
$plainFr = trim(strip_tags((string) $row['pag_texte_fr']));
return strlen($plainFr) < 500;
}
function fxLegalLabelIsDraft(string $label): bool
{
$label = trim($label);
if ($label === '') {
return true;
}
// Un libellé qui contient un underscore ressemble à une clé technique
// (ex. "politique_cookie") plutôt qu'à un vrai libellé.
if (strpos($label, '_') !== false) {
return true;
}
return false;
}
function fxEnsureLegalPage(string $code, string $labelFr, string $labelEn, string $titleFr, string $titleEn, callable $bodyFr, callable $bodyEn, int $sort = 90): void
{
global $objDatabase;
if (!isset($objDatabase)) {
return;
}
$sql = "SELECT * FROM inscriptions_pages WHERE pag_code = '" . $objDatabase->fxEscape($code) . "' LIMIT 1";
$row = $objDatabase->fxGetRow($sql);
$textFr = $bodyFr();
$textEn = $bodyEn();
if (!$row) {
$sqlInsert = "INSERT INTO inscriptions_pages
(pag_code, pag_label_fr, pag_label_en, pag_titre_fr, pag_titre_en, pag_texte_fr, pag_texte_en, pag_tri, pag_maj, pag_menu, pag_actif)
VALUES (
'" . $objDatabase->fxEscape($code) . "',
'" . $objDatabase->fxEscape($labelFr) . "',
'" . $objDatabase->fxEscape($labelEn) . "',
'" . $objDatabase->fxEscape($titleFr) . "',
'" . $objDatabase->fxEscape($titleEn) . "',
'" . $objDatabase->fxEscape($textFr) . "',
'" . $objDatabase->fxEscape($textEn) . "',
" . intval($sort) . ",
NOW(),
1,
1
)";
$objDatabase->fxQuery($sqlInsert);
return;
}
$updates = [];
if (fxLegalPageNeedsSeed($row)) {
$updates[] = "pag_texte_fr = '" . $objDatabase->fxEscape($textFr) . "'";
$updates[] = "pag_texte_en = '" . $objDatabase->fxEscape($textEn) . "'";
}
if (fxLegalLabelIsDraft((string) $row['pag_label_fr'])) {
$updates[] = "pag_label_fr = '" . $objDatabase->fxEscape($labelFr) . "'";
}
if (fxLegalLabelIsDraft((string) $row['pag_label_en'])) {
$updates[] = "pag_label_en = '" . $objDatabase->fxEscape($labelEn) . "'";
}
if (fxLegalLabelIsDraft((string) $row['pag_titre_fr'])) {
$updates[] = "pag_titre_fr = '" . $objDatabase->fxEscape($titleFr) . "'";
}
if (fxLegalLabelIsDraft((string) $row['pag_titre_en'])) {
$updates[] = "pag_titre_en = '" . $objDatabase->fxEscape($titleEn) . "'";
}
if ((int) $row['pag_menu'] !== 1) {
$updates[] = 'pag_menu = 1';
}
if ((int) $row['pag_actif'] !== 1) {
$updates[] = 'pag_actif = 1';
}
if ($updates) {
$updates[] = 'pag_maj = NOW()';
$objDatabase->fxQuery('UPDATE inscriptions_pages SET ' . implode(', ', $updates) . " WHERE pag_id = " . intval($row['pag_id']));
}
}
function fxEnsureLegalPages(): void
{
static $done = false;
if ($done) {
return;
}
$done = true;
fxEnsureCookieConsentTexts();
fxEnsureLegalPage(
'cookie',
'Témoins',
'Cookies',
'Politique de témoins',
'Cookie policy',
static function () {
return fxCookiePolicyHtml('fr');
},
static function () {
return fxCookiePolicyHtml('en');
},
98
);
fxEnsureLegalPage(
'confidentialite',
'Confidentialité',
'Privacy',
'Politique de confidentialité',
'Privacy policy',
static function () {
return fxPrivacyPolicyHtml('fr');
},
static function () {
return fxPrivacyPolicyHtml('en');
},
99
);
}
function fxCookiePolicyPageContent(string $strLangue): string
{
return fxCookiePolicyHtml($strLangue);
}
function fxPrivacyPolicyPageContent(string $strLangue): string
{
return fxPrivacyPolicyHtml($strLangue);
}

View File

@ -13,6 +13,10 @@
//
// pixcel facebook
if (!fxCookiesAllowMarketing()) {
return;
}
// Facebook Pixel Code
$mem_facebook_pixel=0;
//echo $tabEvenement['general']['eve_facebook_pixel'];

View File

@ -5,7 +5,9 @@ define('APPLICATION_PATH', realpath(dirname(__FILE__) . '/..') . '/');
// initialiser la variable $binEnvironementDev en utiliant le nom de domaine
include_once APPLICATION_PATH . 'php/inc_fonctions_sl.php';
include_once APPLICATION_PATH . 'php/inc_fx_doc.php';
include_once APPLICATION_PATH . 'php/inc_settings.php';
include_once APPLICATION_PATH . 'php/inc_cookie_consent.php';
include_once APPLICATION_PATH . 'php/inc_mysql.php';
include_once APPLICATION_PATH . 'php/inc_dates.php';
include_once APPLICATION_PATH . 'php/inc_mail.php';
@ -22,6 +24,8 @@ $objDatabase = new clsMysql($arrConDatabaseMain);
$objDatabaseProd= new clsMysql($arrConDatabaseProd);
$objDatabasePreProd= new clsMysql($arrConDatabasePreProd);
fxEnsureLegalPages();
//MSIN-3999
if (isset($_GET['preprod'])) {
$preprod = $_GET['preprod'];
@ -155,6 +159,10 @@ if (isset($_GET['reloadlogin'])) {
function fxsessionpixelfacebook($memarray)
{
if (!fxCookiesAllowMarketing()) {
return;
}
$_SESSION['pixcel'][] = $memarray;
}
function fxcount($memarray)
@ -166,6 +174,42 @@ function fxcount($memarray)
}
/**
* MSIN-4405 — Determine, en LECTURE SEULE, pourquoi une inscription annulee (is_cancelled = 1)
* porte ce statut. Aucune ecriture : on ne touche ni is_cancelled, ni pec_actif, ni la base.
*
* Regles (cf. fxMajEpreuvesCommandees / annuler_inscription.php) :
* - vraie annulation : l'ancien pec reste actif (pec_actif = 1), ou defaut
* - transfert complete: ligne dans inscriptions_transferts (ts_id = 2) pour ce pec
*
* @param int $intPecId pec_id_original de l'inscription
* @param int|null $intPecActif valeur de pec_actif si deja connue (evite une requete)
* @return string 'annule' | 'transfert'
*/
function fxGetAnnulationCause($intPecId, $intPecActif = null) {
global $objDatabase;
$intPecId = intval($intPecId);
if ($intPecId <= 0) {
return 'annule';
}
// Une vraie annulation laisse l'inscription active (pec_actif = 1).
if ($intPecActif !== null && intval($intPecActif) === 1) {
return 'annule';
}
// Transfert complete vers une autre personne.
$varTransfert = $objDatabase->fxGetVar(
"SELECT 1 FROM inscriptions_transferts WHERE pec_id = " . $intPecId . " AND ts_id = 2 LIMIT 1"
);
if ($varTransfert) {
return 'transfert';
}
return 'annule';
}
// fonction qui retourne les infos des champs de la table (SL / JSP)
// param : nom de la table, nom de la base de données
@ -241,7 +285,7 @@ function fxListFieldsValues($tabData, $strKey, $strTable, $strBdd)
// créé : 2012-09-18
function tcheckevents($category, $action, $label)
{
if ($category == "")
if ($category == "" || !fxCookiesAllowMarketing())
return "";
else {
$param = "'_trackEvent', '" . $category . "', '" . $action . "', '" . $label . "', 1";
@ -314,6 +358,10 @@ function fxGetPage($code)
$sqlPage = "SELECT * FROM inscriptions_pages WHERE pag_actif = 1 AND pag_code = '" . $objDatabase->fxEscape($code) . "'";
$recPage = $objDatabase->fxGetRow($sqlPage);
if (!$recPage) {
return [];
}
$recPage['pag_description_fr'] = fxGetNbWords($recPage['pag_texte_fr'], 50);
$recPage['pag_description_en'] = fxGetNbWords($recPage['pag_texte_en'], 50);
@ -814,6 +862,49 @@ function fxGetPaiements($blnAdmin = false, $blnIsPromoteur = false, $strLangue =
return false;
}
/** Compte connecté au panier (com_info ou com_id en session). */
function fxPaiementSessionComId()
{
if (!empty($_SESSION['com_info']['com_id'])) {
return intval($_SESSION['com_info']['com_id']);
}
if (!empty($_SESSION['com_id'])) {
return intval($_SESSION['com_id']);
}
return 0;
}
/** Modes paiement réservés promoteur (chèque, etc.) — aligné hub v2 + legacy. */
function fxPaiementCanUseModesPromoteur($intComId, $intEveId)
{
$intComId = intval($intComId);
$intEveId = intval($intEveId);
if ($intComId <= 0 || $intEveId <= 0) {
return false;
}
if (!function_exists('fxPromoteurHubCanAccessPromoteurWeb')) {
require_once __DIR__ . '/inc_fx_promoteur_hub.php';
}
$arrLegacyFlip = array();
if (function_exists('fxPromoteurHubGetLegacyEventIds')) {
$arrLegacyFlip = array_flip(array_map('intval', fxPromoteurHubGetLegacyEventIds($intComId)));
}
return fxPromoteurHubCanAccessPromoteurWeb($intComId, $intEveId, $arrLegacyFlip);
}
/** Modes paiement réservés admin (canal superadm / usa_id). */
function fxPaiementCanUseModesAdmin()
{
return !empty($_SESSION['usa_id']);
}
// fonction qui retire/transforme le HTML présent dans les termes et conditions à accepter
// param : texteà transformer
// créé : 2013-04-26
@ -1048,6 +1139,121 @@ function fxcreer_log($strDescription, $intType = 10, $int_Email = 0, $int_eve_id
return (true);
}
/** MSIN-4379 — Table inscriptions_evt_erreur disponible ? */
function fxEvtErreurTableReady() {
static $blnReady = null;
if ($blnReady !== null) {
return $blnReady;
}
global $objDatabase;
$tabRow = $objDatabase->fxGetRow("SHOW TABLES LIKE 'inscriptions_evt_erreur'");
$blnReady = is_array($tabRow) && !empty($tabRow);
return $blnReady;
}
/**
* MSIN-4379 — Ouvre une erreur actionnable (une seule ligne ouverte par type + epr + par).
* @param string $strType ex. bib_auto_miss
* @param array<string, mixed> $tabCtx eve_id, epr_id, par_id, pec_id, no_commande, no_panier, com_id, detail
*/
function fxEvtErreurOuvrir($strType, array $tabCtx = []) {
global $objDatabase;
if (!fxEvtErreurTableReady()) {
return false;
}
$strType = trim((string)$strType);
if ($strType === '') {
return false;
}
$intEveId = (int)($tabCtx['eve_id'] ?? 0);
$intEprId = (int)($tabCtx['epr_id'] ?? 0);
$intParId = (int)($tabCtx['par_id'] ?? 0);
if ($intEveId <= 0 || $intEprId <= 0 || $intParId <= 0) {
return false;
}
$sqlExists = "
SELECT evt_err_id
FROM inscriptions_evt_erreur
WHERE evt_err_type = '" . $objDatabase->fxEscape($strType) . "'
AND epr_id = $intEprId
AND par_id = $intParId
AND evt_err_statut = 'ouvert'
LIMIT 1
";
if ((int)$objDatabase->fxGetVar($sqlExists) > 0) {
return true;
}
$intPecId = (int)($tabCtx['pec_id'] ?? 0);
$strNoCommande = trim((string)($tabCtx['no_commande'] ?? ''));
$strNoPanier = trim((string)($tabCtx['no_panier'] ?? ''));
$intComId = (int)($tabCtx['com_id'] ?? 0);
$strDetail = trim((string)($tabCtx['detail'] ?? ''));
$sqlInsert = "
INSERT INTO inscriptions_evt_erreur SET
evt_err_type = '" . $objDatabase->fxEscape($strType) . "',
evt_err_statut = 'ouvert',
eve_id = $intEveId,
epr_id = $intEprId,
par_id = $intParId,
pec_id = $intPecId,
no_commande = '" . $objDatabase->fxEscape($strNoCommande) . "',
no_panier = '" . $objDatabase->fxEscape($strNoPanier) . "',
com_id = $intComId,
evt_err_detail = " . ($strDetail !== '' ? "'" . $objDatabase->fxEscape($strDetail) . "'" : 'NULL') . ",
evt_err_created = NOW()
";
return $objDatabase->fxQuery($sqlInsert) !== false;
}
/** MSIN-4379 — Marque résolues les erreurs ouvertes pour un participant / épreuve. */
function fxEvtErreurResoudre($strType, $int_epr_id, $int_par_id, $int_resolved_by = 0) {
global $objDatabase;
if (!fxEvtErreurTableReady()) {
return false;
}
$strType = trim((string)$strType);
$int_epr_id = (int)$int_epr_id;
$int_par_id = (int)$int_par_id;
$int_resolved_by = (int)$int_resolved_by;
if ($strType === '' || $int_epr_id <= 0 || $int_par_id <= 0) {
return false;
}
if ($int_resolved_by <= 0 && isset($_SESSION['com_id'])) {
$int_resolved_by = (int)$_SESSION['com_id'];
}
$sqlUpdate = "
UPDATE inscriptions_evt_erreur
SET evt_err_statut = 'resolu',
evt_err_resolved = NOW(),
evt_err_resolved_by = $int_resolved_by
WHERE evt_err_type = '" . $objDatabase->fxEscape($strType) . "'
AND epr_id = $int_epr_id
AND par_id = $int_par_id
AND evt_err_statut = 'ouvert'
";
$objDatabase->fxQuery($sqlUpdate);
return true;
}
function fxIsRegistered($strPartId, $strChamp, $strWhere, $strTable, $strLangue)
{
global $objDatabase;
@ -1400,17 +1606,38 @@ function fxGetRoleTab($int_rol_id)
function fxIsPromoteur($intComId, $intEveId)
{
if (!fxPromoteurLegacySiteEnabled()) {
return false;
}
static $arrLegacyByCom = array();
$intComId = intval($intComId);
$intEveId = intval($intEveId);
if (!isset($arrLegacyByCom[$intComId])) {
$strCsv = '';
if (!empty($_SESSION['com_info']['com_id'])
&& intval($_SESSION['com_info']['com_id']) === $intComId
&& isset($_SESSION['com_info']['com_eve_promoteur'])) {
$strCsv = (string)$_SESSION['com_info']['com_eve_promoteur'];
} else {
global $objDatabase;
$row = $objDatabase->fxGetRow(
'SELECT com_eve_promoteur FROM inscriptions_comptes WHERE com_id = ' . $intComId
);
$strCsv = ($row !== null) ? (string)$row['com_eve_promoteur'] : '';
}
$sql = "select * from inscriptions_comptes where FIND_IN_SET('" . intval($intEveId) . "',com_eve_promoteur) AND com_id = " . intval($intComId);
$tab = $objDatabase->fxGetResults($sql);
$arrLegacyByCom[$intComId] = array();
foreach (array_map('trim', explode(',', $strCsv)) as $strId) {
if ($strId !== '') {
$arrLegacyByCom[$intComId][intval($strId)] = true;
}
}
}
if ($tab != null)
$return = true;
else
$return = false;
return $return;
return !empty($arrLegacyByCom[$intComId][$intEveId]);
}
// nouvelle fonction 2026
@ -1869,6 +2096,31 @@ function fxGetVariable($strVarNom)
return $strVarValeur;
}
/**
* Interrupteur site : promoteur legacy (com_eve_promoteur / fxIsPromoteur) actif ?
* Defaut : oui (1). Super admin : bouton dans inc_droite (variables.promoteur_legacy_actif).
*/
function fxPromoteurLegacySiteEnabled()
{
static $blnEnabled = null;
if ($blnEnabled !== null) {
return $blnEnabled;
}
global $objDatabase;
if (!isset($objDatabase)) {
$blnEnabled = true;
return $blnEnabled;
}
$strVal = fxGetVariable('promoteur_legacy_actif');
$blnEnabled = ($strVal === null || $strVal === '' || intval($strVal) === 1);
return $blnEnabled;
}
function fxGetMailHeader($blnTransaction = false, $strLangue = 'fr')
{
if ($blnTransaction) {
@ -2400,6 +2652,31 @@ function fxShowListeEvenementsContact($arrEvenements, $strLangue)
return $strHTML;
}
function fxJiraIsAutreDemande($strLibelle)
{
$strNorm = mb_strtolower(trim($strLibelle));
return ($strNorm === 'autre' || $strNorm === 'autres' || strpos($strNorm, 'autre') !== false);
}
function fxJiraMapCategorieInterne($strLibelle)
{
if (fxJiraIsAutreDemande($strLibelle)) {
return '';
}
$strNorm = mb_strtolower(trim($strLibelle));
if (strpos($strNorm, 'organisateur') !== false) {
return 'Vente';
}
if (strpos($strNorm, 'participant') !== false
&& (strpos($strNorm, 'course') !== false || strpos($strNorm, 'épreuve') !== false || strpos($strNorm, 'epreuve') !== false)) {
return 'Questions Résultats course';
}
return '';
}
function fxShowFormcontactjira($strLangue)
{ global $objDatabase;
$mem_nom="";
@ -2480,7 +2757,13 @@ function fxShowFormcontactjira($strLangue)
<?php
foreach ($tabCol as $key => $value ){
?>
<option value="<?php echo($tabCol[$key]["info_option2"]); ?>"><?php echo($tabCol[$key]["info_texte"]); ?></option>
<?php
$strCategorieInterne = trim((string)($tabCol[$key]['info_option3'] ?? ''));
if ($strCategorieInterne === '') {
$strCategorieInterne = fxJiraMapCategorieInterne($tabCol[$key]['info_texte']);
}
?>
<option value="<?php echo($tabCol[$key]["info_option2"]); ?>" data-categorie="<?php echo htmlspecialchars($strCategorieInterne, ENT_QUOTES, 'UTF-8'); ?>"><?php echo($tabCol[$key]["info_texte"]); ?></option>
<?php

View File

@ -23,17 +23,68 @@ function afficheTexte($clef, $mem_echo = 1, $mem_trduction = 1, $mem_global = 0)
$mem_page = "global";
// creation dans la bd info
$info1 = array("info_clef" => $clef, "info_langue" => "fr", "info_texte" => $clef, "info_prg" => $mem_page, "info_actif" => 1);
$info1 = array("info_clef" => $clef, "info_langue" => "fr", "info_texte" => $clef, "info_aide" => '', "info_prg" => $mem_page, "info_actif" => 1);
fxSetInfo('add', $info1, '', $mem_id);
$info1 = array("info_clef" => $clef, "info_langue" => "en", "info_texte" => $clef, "info_prg" => $mem_page, "info_actif" => 1);
$info1 = array("info_clef" => $clef, "info_langue" => "en", "info_texte" => $clef, "info_aide" => '', "info_prg" => $mem_page, "info_actif" => 1);
fxSetInfo('add', $info1, '', $mem_id);
}
if ($mem_echo == 1)
echo $mem_texte;
$strOut = $mem_texte . fxAdminTradMarker($clef, $mem_global);
return $mem_texte;
if ($mem_echo == 1) {
echo $strOut;
}
return $strOut;
}
/**
* Libellé info_texte en contexte HTML (crayon via afficheTexte en mode Textes).
*/
function afficheTextePage($clef, $mem_global = 0) {
afficheTexte($clef, 1, 0, $mem_global);
}
/**
* Texte d'aide long (pop-up Tippy sur le bouton ?) — colonne info_aide.
* info_texte = libellé / tip court ; info_aide = contenu long éditable via traduction.php.
* Créateur : stephan (refonte dossards v4)
*/
function afficheAide($clef, $mem_echo = 1, $mem_global = 0) {
global $vtexte_page, $vPage;
$mem_id = "";
if (isset($vtexte_page[$clef])) {
$mem_aide = isset($vtexte_page[$clef]['info_aide']) ? $vtexte_page[$clef]['info_aide'] : '';
} else {
$mem_aide = '';
if ($mem_global == 0) {
$mem_page = $vPage;
} else {
$mem_page = "global";
}
$info1 = array(
"info_clef" => $clef,
"info_langue" => "fr",
"info_texte" => $clef,
"info_aide" => '',
"info_prg" => $mem_page,
"info_actif" => 1
);
fxSetInfo('add', $info1, '', $mem_id);
$info1['info_langue'] = 'en';
fxSetInfo('add', $info1, '', $mem_id);
}
if ($mem_echo == 1) {
echo $mem_aide;
}
return $mem_aide;
}
/**
@ -63,20 +114,21 @@ function obtenirTextepage() {
switch ($centrale) {
case 3:
$sql_info = "SELECT info_clef,info_texte,pk_info,info_prg,'centrale' as bd FROM info where info_prg='" . $page . "' and info_langue='" . $Langue . "'";
$sql_info = "SELECT info_clef,info_texte,info_aide,pk_info,info_prg,'centrale' as bd FROM info where info_prg='" . $page . "' and info_langue='" . $Langue . "'";
$info = $GLOBALS['dbcentrale']->fxFetchArray(3, $sql_info);
break;
case 2:
$sql_info = "SELECT info_clef,info_texte,pk_info,info_prg,'client' as bd FROM info where (info_prg='" . $page . "' or info_prg='global') and info_langue='" . $Langue . "'";
/* MSIN-4379 — info_aide : texte long pop-up bib v4 (Tippy). */
$sql_info = "SELECT info_clef,info_texte,info_aide,pk_info,info_prg,'client' as bd FROM info where (info_prg='" . $page . "' or info_prg='global') and info_langue='" . $Langue . "'";
$info = $objDatabase->fxFetchArray(3, $sql_info);
break;
case 1:
$sql_info = "SELECT info_clef,info_texte,pk_info,'client' as bd FROM info where info_prg='" . $page . "' and info_langue='" . $Langue . "'";
$sql_info = "SELECT info_clef,info_texte,info_aide,pk_info,'client' as bd FROM info where info_prg='" . $page . "' and info_langue='" . $Langue . "'";
$info2 = $objDatabase->fxFetchArray(3, $sql_info);
$sql_info = "SELECT info_clef,info_texte,'centrale' as bd FROM info where info_prg='" . $page . "' and info_langue='" . $Langue . "'";
$sql_info = "SELECT info_clef,info_texte,info_aide,'centrale' as bd FROM info where info_prg='" . $page . "' and info_langue='" . $Langue . "'";
$info1 = $objDatabase->fxFetchArray(3, $sql_info);
$info = array_merge($info1, $info2);
break;
@ -97,6 +149,425 @@ function modif_texte_bt() {
echo "<button onclick=" . chr(34) . "toggle_hideme('modif_texte','text');" . chr(34) . ">changer texte</button>";
}
/**
* MSIN-4379 — Édition textes (crayons, traduction.php) : serveur dev + sous-domaine preprod seulement.
* Pas la préprod client (preprod.ms1inscription.com) — même code, autre rôle (BD inscription test).
* Superadm uniquement ; défaut = aperçu (crayons masqués).
*/
function fxAdminIsDevPreprod() {
global $vblnEnvironementDev, $vblnEnvironementPreProd, $strHost;
if (empty($vblnEnvironementDev) || empty($vblnEnvironementPreProd)) {
return false;
}
return stripos((string)$strHost, 'ms1inscriptiondev') !== false;
}
function fxAdminCanUseTextEditTools() {
return !empty($_SESSION['usa_id']) && fxAdminIsDevPreprod();
}
/** Outils admin /php/* sans usa_id → login superadm (pas « Accès refusé »). */
function fxAdminRequireSuperadmLogin() {
if (!empty($_SESSION['usa_id'])) {
return;
}
global $vDomaine;
$_SESSION['superadm_redirect_after_login'] = $_SERVER['REQUEST_URI'];
header('Location: ' . rtrim((string)$vDomaine, '/') . '/superadm/login.php');
exit;
}
/** Sync tables statiques (sync_static_db.php) — superadm, préprod dev seulement. */
function fxAdminStaticSyncAllowed() {
return !empty($_SESSION['usa_id']) && fxAdminIsDevPreprod();
}
function fxAdminTextEditModeActive() {
return fxAdminCanUseTextEditTools() && !empty($_SESSION['ms1_text_edit_mode']);
}
/** Mode inspecteur droits v2 (cadenas) — super admin connecte uniquement. */
function fxAdminCanUsePermInspectTools() {
return !empty($_SESSION['usa_id']);
}
/**
* MSIN-4401 — Créer / modifier / supprimer les définitions de kits (pas l'assignation).
* Liste : $arrEveAccesKitEditors dans php/inc_settings.php.
*/
if (!function_exists('fxAdminCanEditEveAccesKits')) {
function fxAdminCanEditEveAccesKits() {
global $arrEveAccesKitEditors;
if (empty($_SESSION['usa_id']) || empty($_SESSION['usa_info']) || !is_array($_SESSION['usa_info'])) {
return false;
}
$arrAllow = (isset($arrEveAccesKitEditors) && is_array($arrEveAccesKitEditors))
? $arrEveAccesKitEditors
: array();
$arrNorm = array();
foreach ($arrAllow as $strItem) {
$strItem = strtolower(trim((string)$strItem));
if ($strItem !== '') {
$arrNorm[$strItem] = true;
}
}
if (count($arrNorm) === 0) {
return false;
}
$arrCandidates = array(
strtolower(trim((string)($_SESSION['usa_info']['com_courriel'] ?? ''))),
strtolower(trim((string)($_SESSION['usa_info']['com_login'] ?? ''))),
);
foreach ($arrCandidates as $strCand) {
if ($strCand !== '' && isset($arrNorm[$strCand])) {
return true;
}
}
return false;
}
}
function fxAdminPermInspectModeActive() {
return fxAdminCanUsePermInspectTools() && !empty($_SESSION['ms1_perm_inspect_mode']);
}
function fxAdminPermInspectModeHandleRequest() {
if (!fxAdminCanUsePermInspectTools() || !isset($_GET['ms1_perm_inspect'])) {
return;
}
$_SESSION['ms1_perm_inspect_mode'] = ($_GET['ms1_perm_inspect'] === '1') ? 1 : 0;
$strPath = strtok($_SERVER['REQUEST_URI'], '?');
$tabParams = $_GET;
unset($tabParams['ms1_perm_inspect']);
$strQuery = http_build_query($tabParams);
header('Location: ' . $strPath . ($strQuery !== '' ? '?' . $strQuery : ''));
exit;
}
function fxAdminPermInspectSwitchHtml() {
$blnOn = fxAdminPermInspectModeActive();
return '
<span class="ms1-env-text-edit ms1-env-perm-inspect">
<label class="ms1-env-text-edit-label mb-0" for="ms1-perm-inspect-mode">
<span class="ms1-env-text-edit-opt' . ($blnOn ? '' : ' is-active') . '">'
. '<i class="fa fa-eye" aria-hidden="true"></i> Aperçu'
. '</span>
<input type="checkbox" id="ms1-perm-inspect-mode" class="ms1-env-text-edit-switch"'
. ($blnOn ? ' checked' : '')
. ' aria-label="Afficher les cadenas droits v2">
<span class="ms1-env-text-edit-opt' . ($blnOn ? ' is-active' : '') . '">'
. '<i class="fa fa-lock" aria-hidden="true"></i> Droits'
. '</span>
</label>
</span>
<script>
(function () {
var el = document.getElementById("ms1-perm-inspect-mode");
if (!el || el.dataset.ms1Bound) return;
el.dataset.ms1Bound = "1";
el.addEventListener("change", function () {
var url = new URL(window.location.href);
url.searchParams.set("ms1_perm_inspect", this.checked ? "1" : "0");
window.location.href = url.toString();
});
})();
</script>';
}
/** Échappement HTML — outils admin traduction. */
function fxAdminEsc($str) {
return htmlspecialchars((string)$str, ENT_QUOTES, 'UTF-8');
}
/** info_prg pour traduction.php — depuis $vtexte_page ou repli page / global. */
function fxAdminTradInfoPrg($clef, $strDefaultPrg = '', $mem_global = 0) {
global $vtexte_page, $vPage;
if (!empty($vtexte_page[$clef]['info_prg'])) {
return $vtexte_page[$clef]['info_prg'];
}
if ($strDefaultPrg !== '') {
return $strDefaultPrg;
}
if ($mem_global == 1) {
return 'global';
}
return !empty($vPage) ? $vPage : 'compte.php';
}
/** URL pop-up traduction.php — toute clé info (site entier). */
function fxAdminTradUrl($clef, $strInfoPrg = null, $mem_global = 0) {
global $vDomaine, $vPage;
if ($strInfoPrg === null || $strInfoPrg === '') {
$strInfoPrg = fxAdminTradInfoPrg($clef, '', $mem_global);
}
$strPage = !empty($vPage) ? $vPage : $strInfoPrg;
return $vDomaine . '/php/traduction.php?' . http_build_query(array(
'clef' => $clef,
'pk_prg' => $strInfoPrg,
'bd' => 'client',
'page' => $strPage,
));
}
/**
* Marqueur invisible (ZWSP) — sûr dans le JS ; remplacé par un lien traduction.php au chargement.
*/
function fxAdminTradMarker($clef, $mem_global = 0) {
if (!fxAdminTextEditModeActive() || trim($clef) === '') {
return '';
}
$strPayload = base64_encode(json_encode(array(
'k' => $clef,
'p' => fxAdminTradInfoPrg($clef, '', $mem_global),
'g' => (int)$mem_global,
), JSON_UNESCAPED_UNICODE));
return "\xE2\x80\x8B\xE2\x80\x8C" . $strPayload . "\xE2\x80\x8C\xE2\x80\x8B";
}
/** Classes CSS crayon traduction (sans btn-admin-trad — évite le handler bib sur data-trad-url). */
function fxAdminTradLinkClass() {
return 'btn-aide-bib btn-aide-trad ms1-trad-link';
}
/** Lien crayon traduction.php — superadm dev/préprod + switch « Textes » ON. */
function fxAdminTradButton($clef, $strInfoPrg = null, $mem_global = 0) {
if (!fxAdminTextEditModeActive() || trim($clef) === '') {
return '';
}
return '<a href="' . fxAdminEsc(fxAdminTradUrl($clef, $strInfoPrg, $mem_global)) . '"'
. ' class="' . fxAdminTradLinkClass() . '"'
. ' title="Modifier textes FR/EN"'
. ' aria-label="Modifier textes">'
. '<i class="fa fa-pencil" aria-hidden="true"></i>'
. '</a>';
}
/** Remplace les marqueurs afficheTexte() par des liens crayon (mode Textes). */
function fxAdminTradMarkerScriptHtml() {
global $vDomaine, $vPage;
static $blnRendered = false;
if ($blnRendered || !fxAdminTextEditModeActive()) {
return '';
}
$blnRendered = true;
return '
<script>
(function () {
var re = /\u200B\u200C([A-Za-z0-9+/=]+)\u200C\u200B/g;
var tradBase = ' . json_encode($vDomaine . '/php/traduction.php') . ';
var pageDefault = ' . json_encode(!empty($vPage) ? $vPage : 'compte.php') . ';
var tradLinkClass = ' . json_encode(fxAdminTradLinkClass()) . ';
function openTradPopup(url) {
var w = window.open(
url,
"ms1Trad" + Date.now(),
"width=960,height=640,scrollbars=yes,resizable=yes"
);
if (w) {
var timer = setInterval(function () {
if (w.closed) {
clearInterval(timer);
window.location.reload();
}
}, 400);
}
}
document.addEventListener("click", function (e) {
var link = e.target.closest("a.ms1-trad-link");
if (!link || !link.href) {
return;
}
e.preventDefault();
e.stopPropagation();
openTradPopup(link.href);
}, true);
function skipParent(node) {
var p = node.parentNode;
while (p) {
var tag = p.nodeName;
if (tag === "SCRIPT" || tag === "STYLE") {
return true;
}
p = p.parentNode;
}
return false;
}
function buildUrl(o) {
return tradBase + "?clef=" + encodeURIComponent(o.k)
+ "&pk_prg=" + encodeURIComponent(o.p)
+ "&bd=client"
+ "&page=" + encodeURIComponent(pageDefault);
}
function replaceMarkers(textNode) {
if (skipParent(textNode)) {
return;
}
var text = textNode.nodeValue;
if (text.indexOf("\u200C") === -1) {
return;
}
re.lastIndex = 0;
if (!re.test(text)) {
return;
}
re.lastIndex = 0;
var frag = document.createDocumentFragment();
var last = 0;
var m;
while ((m = re.exec(text)) !== null) {
if (m.index > last) {
frag.appendChild(document.createTextNode(text.slice(last, m.index)));
}
try {
var o = JSON.parse(atob(m[1]));
var a = document.createElement("a");
a.href = buildUrl(o);
a.className = tradLinkClass;
a.title = "Modifier textes FR/EN";
a.setAttribute("aria-label", "Modifier textes");
a.innerHTML = \'<i class="fa fa-pencil" aria-hidden="true"></i>\';
frag.appendChild(a);
} catch (e) {}
last = m.index + m[0].length;
}
if (last < text.length) {
frag.appendChild(document.createTextNode(text.slice(last)));
}
textNode.parentNode.replaceChild(frag, textNode);
}
function scan() {
if (!document.body) {
return;
}
var walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, null);
var nodes = [];
while (walker.nextNode()) {
nodes.push(walker.currentNode);
}
nodes.forEach(replaceMarkers);
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", scan);
} else {
scan();
}
})();
</script>';
}
function fxAdminTextEditModeHandleRequest() {
if (!fxAdminCanUseTextEditTools() || !isset($_GET['ms1_text_edit'])) {
return;
}
$_SESSION['ms1_text_edit_mode'] = ($_GET['ms1_text_edit'] === '1') ? 1 : 0;
$strPath = strtok($_SERVER['REQUEST_URI'], '?');
$tabParams = $_GET;
unset($tabParams['ms1_text_edit']);
$strQuery = http_build_query($tabParams);
header('Location: ' . $strPath . ($strQuery !== '' ? '?' . $strQuery : ''));
exit;
}
function fxAdminTextEditSwitchHtml() {
$blnEdit = fxAdminTextEditModeActive();
return '
<span class="ms1-env-text-edit">
<label class="ms1-env-text-edit-label mb-0" for="ms1-text-edit-mode">
<span class="ms1-env-text-edit-opt' . ($blnEdit ? '' : ' is-active') . '">'
. '<i class="fa fa-eye" aria-hidden="true"></i> Aperçu'
. '</span>
<input type="checkbox" id="ms1-text-edit-mode" class="ms1-env-text-edit-switch"'
. ($blnEdit ? ' checked' : '')
. ' aria-label="Basculer édition des textes">
<span class="ms1-env-text-edit-opt' . ($blnEdit ? ' is-active' : '') . '">'
. '<i class="fa fa-pencil" aria-hidden="true"></i> Textes'
. '</span>
</label>
</span>
<script>
(function () {
var el = document.getElementById("ms1-text-edit-mode");
if (!el || el.dataset.ms1Bound) return;
el.dataset.ms1Bound = "1";
el.addEventListener("change", function () {
var url = new URL(window.location.href);
url.searchParams.set("ms1_text_edit", this.checked ? "1" : "0");
window.location.href = url.toString();
});
})();
</script>';
}
function fxRenderEnvNoticeBar() {
global $vEnvNotice;
$strExtraSwitches = '';
if (function_exists('fxAdminCanUsePermInspectTools') && fxAdminCanUsePermInspectTools()) {
$strExtraSwitches .= fxAdminPermInspectSwitchHtml();
}
if (trim($vEnvNotice) === '') {
if ($strExtraSwitches !== '') {
echo '<div class="alert alert-secondary py-1 mb-0 text-center sticky-top">' . $strExtraSwitches . '</div>';
}
return;
}
if (!fxAdminCanUseTextEditTools()) {
echo $vEnvNotice;
if ($strExtraSwitches !== '') {
echo '<div class="alert alert-secondary py-1 mb-0 text-center sticky-top">' . $strExtraSwitches . '</div>';
}
return;
}
$strSwitch = fxAdminTextEditSwitchHtml() . $strExtraSwitches;
$strHtml = preg_replace('/<\/strong>/', '</strong> ' . $strSwitch, trim($vEnvNotice), 1);
if ($strHtml === null || $strHtml === trim($vEnvNotice)) {
echo $vEnvNotice;
echo $strSwitch;
echo fxAdminTradMarkerScriptHtml();
return;
}
echo $strHtml;
echo fxAdminTradMarkerScriptHtml();
}
/**
* Description : Fonction pour ajouter le value dans une formes
* Parametres : $mem_champ nom du champs ,$mem_defaut,$mem_type add ou mod ,$mem_infos tableau des info

View File

@ -0,0 +1,413 @@
<?php
/**
* MSIN-4445 — Assignation globale solo libre (sans création de séquence).
* Plage Début/Fin (ou Début/Qté) fournie par l'utilisateur ; assignation via plages virtuelles.
*/
/**
* Calcule le feedback quantité vs inscriptions à assigner.
*
* @return array{status:string, amount:int, qty:int, pending:int}
*/
function fxBibFreeQtyFeedback($intQty, $intPending) {
$intQty = (int)$intQty;
$intPending = (int)$intPending;
$intDiff = $intQty - $intPending;
if ($intDiff === 0) {
return [
'status' => 'juste',
'amount' => 0,
'qty' => $intQty,
'pending' => $intPending,
];
}
if ($intDiff > 0) {
return [
'status' => 'supplementaire',
'amount' => $intDiff,
'qty' => $intQty,
'pending' => $intPending,
];
}
return [
'status' => 'manquante',
'amount' => abs($intDiff),
'qty' => $intQty,
'pending' => $intPending,
];
}
/** Libellé Info pour le feedback quantité libre. */
function fxBibFreeQtyFeedbackMessage(array $tabFeedback) {
$strStatus = $tabFeedback['status'] ?? '';
$intAmount = (int)($tabFeedback['amount'] ?? 0);
if ($strStatus === 'juste') {
return fxBibMsg('bib_v4_global_batch_free_qty_juste');
}
if ($strStatus === 'supplementaire') {
return fxBibMsg('bib_v4_global_batch_free_qty_supplementaire', $intAmount);
}
if ($strStatus === 'manquante') {
return fxBibMsg('bib_v4_global_batch_free_qty_manquante', $intAmount);
}
return '';
}
/**
* MSIN-4445 — Nombre de dossards déjà assignés dans [start, end] pour l'événement.
* Jointure via épreuve : plus fiable que p.eve_id seul.
*/
function fxBibCountAssignedBibsInRange($int_eve_id, $intStart, $intEnd) {
global $objDatabase;
$int_eve_id = (int)$int_eve_id;
$intStart = (int)$intStart;
$intEnd = (int)$intEnd;
if ($int_eve_id <= 0 || $intStart <= 0 || $intEnd < $intStart) {
return 0;
}
$sqlBib = fxBibSqlNoBibUnsigned('p.no_bib');
return (int)$objDatabase->fxGetVar("
SELECT COUNT(DISTINCT $sqlBib)
FROM resultats_participants p
INNER JOIN inscriptions_epreuves e ON e.epr_id = p.epr_id
WHERE e.eve_id = $int_eve_id
AND IFNULL(p.is_cancelled, 0) = 0
AND p.no_bib IS NOT NULL
AND TRIM(p.no_bib) <> ''
AND $sqlBib BETWEEN $intStart AND $intEnd
");
}
/**
* MSIN-4445 — Risque de doublons pour une plage libre.
* Avertit si des dossards de la plage sont déjà portés, ou si une séquence
* existante chevauche la plage (même hors épreuves sélectionnées).
*
* @return array{has:bool, count:int, message:string}
*/
function fxBibFreeDuplicateRisk($int_eve_id, $tabPlan, $intStart = 0, $intEnd = 0) {
$int_eve_id = (int)$int_eve_id;
$intStart = (int)$intStart;
$intEnd = (int)$intEnd;
if ($intStart <= 0 || $intEnd < $intStart) {
// Repli : bornes du plan si Début/Fin absents.
foreach ((array)$tabPlan as $item) {
if (!empty($item['skip'])) {
continue;
}
$intItemStart = (int)($item['start'] ?? 0);
$intItemEnd = (int)($item['end'] ?? 0);
if ($intItemStart <= 0 || $intItemEnd < $intItemStart) {
continue;
}
if ($intStart <= 0 || $intItemStart < $intStart) {
$intStart = $intItemStart;
}
if ($intItemEnd > $intEnd) {
$intEnd = $intItemEnd;
}
}
}
if ($int_eve_id <= 0 || $intStart <= 0 || $intEnd < $intStart) {
return ['has' => false, 'count' => 0, 'message' => ''];
}
$intCount = fxBibCountAssignedBibsInRange($int_eve_id, $intStart, $intEnd);
// Séquences déjà configurées qui chevauchent la plage (autre épreuve).
$tabOverlaps = fxBibGetOverlappingRanges($int_eve_id, $intStart, $intEnd, 0, 0);
$intOverlapSeq = is_array($tabOverlaps) ? count($tabOverlaps) : 0;
if ($intCount <= 0 && $intOverlapSeq <= 0) {
return ['has' => false, 'count' => 0, 'message' => ''];
}
return [
'has' => true,
'count' => max($intCount, $intOverlapSeq),
'message' => fxBibMsg('bib_v4_global_batch_free_dupes_warn'),
];
}
/**
* Plan de plages virtuelles bornées par Début/Fin (sans écriture BD).
* Enchaîne les épreuves comme le bloc généré, mais s'arrête à $intEnd.
*
* @return array[] [{epr_id, start, end, nb_pending, skip}]
*/
function fxBibBuildFreeRangePlan($int_eve_id, $tabEprIds, $intStart, $intEnd, $strSort1, $strSort2) {
$int_eve_id = (int)$int_eve_id;
$intStart = (int)$intStart;
$intEnd = (int)$intEnd;
$tabEprIds = fxBibParseGlobalBatchGeneratedEprIds($tabEprIds, $int_eve_id);
$intCursor = $intStart;
$tabPlan = [];
foreach ($tabEprIds as $epr_id) {
$participants = fxGetParticipantsForBatchAssign($epr_id, $strSort1, $strSort2);
$intPending = count($participants);
if ($intPending <= 0) {
$tabPlan[] = [
'epr_id' => $epr_id,
'start' => 0,
'end' => 0,
'nb_pending' => 0,
'skip' => true,
];
continue;
}
$intRemaining = $intEnd - $intCursor + 1;
if ($intRemaining <= 0) {
$tabPlan[] = [
'epr_id' => $epr_id,
'start' => 0,
'end' => 0,
'nb_pending' => $intPending,
'skip' => true,
];
continue;
}
$intTake = min($intPending, $intRemaining);
$intRangeStart = $intCursor;
$intRangeEnd = $intCursor + $intTake - 1;
$tabPlan[] = [
'epr_id' => $epr_id,
'start' => $intRangeStart,
'end' => $intRangeEnd,
'nb_pending' => $intPending,
'skip' => false,
];
$intCursor = $intRangeEnd + 1;
}
return $tabPlan;
}
/**
* Valide le plan libre (plage valide + au moins une assignation possible).
* Pas de contrôle de chevauchement ni d'insertion de séquence (MSIN-4445).
*/
function fxBibValidateFreeRangePlan($int_eve_id, $tabPlan, $intStart, $intEnd, $strLangue = 'fr') {
$int_eve_id = (int)$int_eve_id;
$intStart = (int)$intStart;
$intEnd = (int)$intEnd;
if ($intStart <= 0 || $intEnd < $intStart) {
return [
'success' => false,
'message' => fxBibMsg('bib_v4_global_batch_free_plan_invalid'),
];
}
if (!is_array($tabPlan) || empty($tabPlan)) {
return [
'success' => false,
'message' => fxBibMsg('bib_v4_global_batch_free_plan_invalid'),
];
}
$blnHasAssignable = false;
foreach ($tabPlan as $item) {
if (!empty($item['skip'])) {
continue;
}
$epr_id = (int)($item['epr_id'] ?? 0);
$intItemStart = (int)($item['start'] ?? 0);
$intItemEnd = (int)($item['end'] ?? 0);
if ($epr_id <= 0 || $intItemStart <= 0 || $intItemEnd < $intItemStart) {
return [
'success' => false,
'message' => fxBibMsg('bib_v4_global_batch_free_plan_invalid'),
];
}
if (fxBibEpreuveHasAnySequence($epr_id)) {
return [
'success' => false,
'message' => fxBibMsg('bib_v4_global_batch_gen_has_seq'),
];
}
$blnHasAssignable = true;
}
if (!$blnHasAssignable) {
return [
'success' => false,
'message' => fxBibMsg('bib_v4_global_batch_gen_no_pending'),
];
}
return ['success' => true];
}
/** Stats agrégées + feedback quantité pour l'analyse libre. */
function fxBibBuildGlobalBatchFreeAnalysis($int_eve_id, $tabPlan, $intStart, $intEnd) {
$int_eve_id = (int)$int_eve_id;
$intStart = (int)$intStart;
$intEnd = (int)$intEnd;
$intQty = ($intStart > 0 && $intEnd >= $intStart) ? ($intEnd - $intStart + 1) : 0;
$tabStats = fxBibBuildGlobalBatchGeneratedAnalysisStats($tabPlan);
// MSIN-4445 — Feedback vs total à assigner (y compris épreuves non couvertes par la plage).
$intPendingAll = 0;
foreach ($tabPlan as $item) {
$intPendingAll += (int)($item['nb_pending'] ?? 0);
}
$tabFeedback = fxBibFreeQtyFeedback($intQty, $intPendingAll);
$tabDupes = fxBibFreeDuplicateRisk($int_eve_id, $tabPlan, $intStart, $intEnd);
return [
'stats' => $tabStats,
'feedback' => $tabFeedback,
'duplicates' => $tabDupes,
'info' => fxBibMsg(
'bib_v4_ajax_global_free_analysis',
$tabStats['nb_epreuves'],
$intStart,
$intEnd,
$intPendingAll,
fxBibFreeQtyFeedbackMessage($tabFeedback)
),
];
}
/**
* Simulation globale libre (plages virtuelles, aucune écriture BD).
*
* @return array{success:bool, html?:string, info?:string, message?:string}
*/
function fxBibSimulateGlobalBatchFree($int_eve_id, $tabPlan, $intStart, $intEnd, $strSort1, $strSort2, $strLangue) {
$check = fxBibValidateFreeRangePlan($int_eve_id, $tabPlan, $intStart, $intEnd, $strLangue);
if (!$check['success']) {
return $check;
}
$htmlParts = [];
$intParticipants = 0;
$intBibs = 0;
foreach ($tabPlan as $item) {
if (!empty($item['skip'])) {
continue;
}
$epr_id = (int)$item['epr_id'];
$intItemStart = (int)$item['start'];
$intItemEnd = (int)$item['end'];
$participants = fxGetParticipantsForBatchAssign($epr_id, $strSort1, $strSort2);
$tabRanges = fxBibBuildVirtualRangesForPlan($intItemStart, $intItemEnd);
$simu = fxProcessBatchAssignments($epr_id, $participants, $tabRanges, 'simulation');
$intNbPending = count($participants);
$intParticipants += $intNbPending;
$intBibs += max(0, $intItemEnd - $intItemStart + 1);
$tabEpreuve = fxGetEpreuve($epr_id);
$strEprLabel = $tabEpreuve ? fxBibEpreuveDisplayName($tabEpreuve, $strLangue) : ('#' . $epr_id);
$strRangeLabel = fxBibMsg(
'bib_v4_global_batch_gen_range_preview',
$intItemStart,
$intItemEnd,
$intNbPending
);
$htmlParts[] = '<div class="bib-global-sim-section">'
. '<h4 class="bib-global-sim-section__title">' . fxBibEsc($strEprLabel) . '</h4>'
. '<p class="bib-global-sim-section__range text-muted small">' . fxBibEsc($strRangeLabel) . '</p>'
. renderBibView(
$epr_id,
0,
$strLangue,
'simulation',
$simu,
fxBibTexte('bib_v4_ajax_title_simulation'),
fxBibMsg(
'bib_v4_ajax_sim_summary',
max(0, $intItemEnd - $intItemStart + 1),
$intNbPending,
max(0, $intNbPending - count($simu))
)
)
. '</div>';
}
$tabAnalysis = fxBibBuildGlobalBatchFreeAnalysis($int_eve_id, $tabPlan, $intStart, $intEnd);
$info = fxBibMsg(
'bib_v4_ajax_global_free_sim_summary',
$tabAnalysis['stats']['nb_epreuves'],
$intBibs,
(int)($tabAnalysis['feedback']['pending'] ?? $intParticipants),
fxBibFreeQtyFeedbackMessage($tabAnalysis['feedback'])
);
$html = fxBibWrapGlobalSimView($info, implode('', $htmlParts), 'bib-global-sim-view--free');
return [
'success' => true,
'html' => $html,
'info' => $info,
'feedback' => $tabAnalysis['feedback'],
];
}
/**
* Prépare les plans GO libres (sans INSERT séquence).
*
* @return array{success:bool, plans?:array, stats?:array, feedback?:array, message?:string}
*/
function fxBibPrepareFreeRangePlans($int_eve_id, $tabPlan, $intStart, $intEnd, $strLangue = 'fr') {
$check = fxBibValidateFreeRangePlan($int_eve_id, $tabPlan, $intStart, $intEnd, $strLangue);
if (!$check['success']) {
return $check;
}
$tabPlans = [];
foreach ($tabPlan as $item) {
if (!empty($item['skip'])) {
continue;
}
$tabPlans[] = [
'epr_id' => (int)$item['epr_id'],
'start' => (int)$item['start'],
'end' => (int)$item['end'],
];
}
$tabAnalysis = fxBibBuildGlobalBatchFreeAnalysis($int_eve_id, $tabPlan, $intStart, $intEnd);
return [
'success' => true,
'plans' => $tabPlans,
'stats' => $tabAnalysis['stats'],
'feedback' => $tabAnalysis['feedback'],
'duplicates' => $tabAnalysis['duplicates'],
];
}

View File

@ -0,0 +1,461 @@
<?php
/**
* MSIN-4443 — Assignation globale solo sans séquence existante.
* Création automatique de plages de dossards puis assignation via le moteur batch existant.
*/
/** True si l'épreuve a au moins une séquence enregistrée. */
function fxBibEpreuveHasAnySequence($epr_id) {
global $objDatabase;
$epr_id = (int)$epr_id;
if ($epr_id <= 0) {
return false;
}
$intCount = (int)$objDatabase->fxGetVar("
SELECT COUNT(*)
FROM inscriptions_epreuves_bib
WHERE epr_id = $epr_id
AND epr_bib_start IS NOT NULL
AND epr_bib_finish IS NOT NULL
");
return $intCount > 0;
}
/**
* Épreuves solo sans aucune séquence, triées par epr_id (ordre page).
*
* @return array[] lignes inscriptions_epreuves
*/
function fxBibGetSoloEpreuvesWithoutSequence($int_eve_id, $tabEpreuves = null) {
global $objDatabase;
$int_eve_id = (int)$int_eve_id;
if ($int_eve_id <= 0) {
return [];
}
if (!is_array($tabEpreuves)) {
$tabEpreuves = $objDatabase->fxGetResults(
"SELECT * FROM inscriptions_epreuves e"
. " WHERE e.eve_id = $int_eve_id AND epr_actif = 1"
. " ORDER BY epr_id"
);
}
$out = [];
foreach ($tabEpreuves as $epr) {
if ((int)($epr['epr_equipe'] ?? 0) === 1) {
continue;
}
if (fxBibEpreuveHasAnySequence((int)$epr['epr_id'])) {
continue;
}
$out[] = $epr;
}
return $out;
}
/** Bloc 1 disponible : au moins 2 épreuves solo avec séquences sélectionnables. */
function fxBibGlobalBatchHasExistingBlock($int_eve_id, $tabEpreuves) {
$int_eve_id = (int)$int_eve_id;
$intCount = 0;
foreach ($tabEpreuves as $epr) {
if ((int)($epr['epr_equipe'] ?? 0) === 1) {
continue;
}
if (empty(fxBibGetSelectableRangesForGlobalBatch((int)$epr['epr_id']))) {
continue;
}
$intCount++;
if ($intCount >= 2) {
return true;
}
}
return false;
}
/** Bloc 2 disponible : au moins 1 épreuve solo sans séquence. */
function fxBibGlobalBatchHasGeneratedBlock($int_eve_id, $tabEpreuves) {
return count(fxBibGetSoloEpreuvesWithoutSequence($int_eve_id, $tabEpreuves)) >= 1;
}
/** Tuile / coquille assignation globale visible si l'un des deux blocs est utilisable. */
function fxBibGlobalBatchToolIsAvailable($int_eve_id, $tabEpreuves) {
return fxBibGlobalBatchHasExistingBlock($int_eve_id, $tabEpreuves)
|| fxBibGlobalBatchHasGeneratedBlock($int_eve_id, $tabEpreuves);
}
/**
* Valide et ordonne les epr_id du bloc généré (solo, sans séquence, ordre epr_id).
*
* @return int[]
*/
function fxBibParseGlobalBatchGeneratedEprIds($eprIds, $int_eve_id) {
if (!is_array($eprIds)) {
return [];
}
$int_eve_id = (int)$int_eve_id;
$tabWanted = [];
foreach ($eprIds as $epr_id) {
$epr_id = (int)$epr_id;
if ($epr_id <= 0) {
continue;
}
$check = fxBibAssertSoloEpreuveForGlobalBatch($epr_id, $int_eve_id);
if (!$check['success']) {
continue;
}
if (fxBibEpreuveHasAnySequence($epr_id)) {
continue;
}
$tabWanted[$epr_id] = true;
}
if (empty($tabWanted)) {
return [];
}
$tabIds = array_keys($tabWanted);
sort($tabIds, SORT_NUMERIC);
return $tabIds;
}
/**
* Plan de plages générées : taille = a_assigner (sans marge), enchaînement par epr_id.
*
* @return array[] [{epr_id, start, end, nb_pending, skip}]
*/
function fxBibBuildGeneratedRangePlan($int_eve_id, $tabEprIds, $intStartBib, $strSort1, $strSort2) {
$int_eve_id = (int)$int_eve_id;
$intStartBib = (int)$intStartBib;
$tabEprIds = fxBibParseGlobalBatchGeneratedEprIds($tabEprIds, $int_eve_id);
$intCursor = $intStartBib;
$tabPlan = [];
foreach ($tabEprIds as $epr_id) {
$participants = fxGetParticipantsForBatchAssign($epr_id, $strSort1, $strSort2);
$intPending = count($participants);
if ($intPending <= 0) {
$tabPlan[] = [
'epr_id' => $epr_id,
'start' => 0,
'end' => 0,
'nb_pending' => 0,
'skip' => true,
];
continue;
}
$intStart = $intCursor;
$intEnd = $intCursor + $intPending - 1;
$tabPlan[] = [
'epr_id' => $epr_id,
'start' => $intStart,
'end' => $intEnd,
'nb_pending' => $intPending,
'skip' => false,
];
$intCursor = $intEnd + 1;
}
return $tabPlan;
}
/** Plages virtuelles pour simulation (même format que fxGetRangesForBatchAssign). */
function fxBibBuildVirtualRangesForPlan($intStart, $intEnd) {
$intStart = (int)$intStart;
$intEnd = (int)$intEnd;
if ($intStart <= 0 || $intEnd < $intStart) {
return [];
}
return [[
'id' => 0,
'start' => $intStart,
'end' => $intEnd,
]];
}
/**
* Valide le plan généré (chevauchements, épreuves sans séquence, au moins un à assigner).
*/
function fxBibValidateGeneratedRangePlan($int_eve_id, $tabPlan, $strLangue = 'fr') {
$int_eve_id = (int)$int_eve_id;
if (!is_array($tabPlan) || empty($tabPlan)) {
return [
'success' => false,
'message' => fxBibMsg('bib_v4_global_batch_gen_plan_invalid'),
];
}
$blnHasAssignable = false;
foreach ($tabPlan as $item) {
if (!empty($item['skip'])) {
continue;
}
$blnHasAssignable = true;
$epr_id = (int)($item['epr_id'] ?? 0);
$intStart = (int)($item['start'] ?? 0);
$intEnd = (int)($item['end'] ?? 0);
if ($epr_id <= 0 || $intStart <= 0 || $intEnd < $intStart) {
return [
'success' => false,
'message' => fxBibMsg('bib_v4_global_batch_gen_plan_invalid'),
];
}
if (fxBibEpreuveHasAnySequence($epr_id)) {
return [
'success' => false,
'message' => fxBibMsg('bib_v4_global_batch_gen_has_seq'),
];
}
$tabOverlaps = fxBibGetOverlappingRanges($int_eve_id, $intStart, $intEnd, 0, $epr_id);
if (!empty($tabOverlaps)) {
return [
'success' => false,
'message' => fxBibFormatOverlapConflictMessage($tabOverlaps, $strLangue),
];
}
}
if (!$blnHasAssignable) {
return [
'success' => false,
'message' => fxBibMsg('bib_v4_global_batch_gen_no_pending'),
];
}
return ['success' => true];
}
/**
* Crée les séquences en BD pour le plan validé.
*
* @return array{success:bool, message?:string, plans?:array}
*/
function fxBibCreateGeneratedRanges($int_eve_id, $tabPlan, $strLangue = 'fr') {
global $objDatabase;
$int_eve_id = (int)$int_eve_id;
$tabCreated = [];
foreach ($tabPlan as $item) {
if (!empty($item['skip'])) {
continue;
}
$epr_id = (int)($item['epr_id'] ?? 0);
$intStart = (int)($item['start'] ?? 0);
$intEnd = (int)($item['end'] ?? 0);
if ($epr_id <= 0 || $intStart <= 0 || $intEnd < $intStart) {
return [
'success' => false,
'message' => fxBibMsg('bib_v4_global_batch_gen_plan_invalid'),
];
}
if (fxBibEpreuveHasAnySequence($epr_id)) {
return [
'success' => false,
'message' => fxBibMsg('bib_v4_global_batch_gen_has_seq'),
];
}
$tabOverlaps = fxBibGetOverlappingRanges($int_eve_id, $intStart, $intEnd, 0, $epr_id);
if (!empty($tabOverlaps)) {
return [
'success' => false,
'message' => fxBibFormatOverlapConflictMessage($tabOverlaps, $strLangue),
];
}
// MSIN-4443 — Création séquence : vérifier INSERT + last_id (évite le message générique « Erreur »).
$sqlInsert = "
INSERT INTO inscriptions_epreuves_bib
(epr_id, epr_bib_start, epr_bib_finish, update_date, update_qui, eve_id)
VALUES
($epr_id, $intStart, $intEnd, NOW(), 'admin', $int_eve_id)
";
$qryInsert = $objDatabase->fxQuery($sqlInsert);
if ($qryInsert === false) {
return [
'success' => false,
'message' => fxBibMsg('bib_v4_global_batch_gen_insert_failed'),
];
}
$intBibId = 0;
if (method_exists($objDatabase, 'fxGetLastId')) {
$intBibId = (int)$objDatabase->fxGetLastId();
} elseif (isset($objDatabase->last_id)) {
$intBibId = (int)$objDatabase->last_id;
}
// Repli : si insert_id absent, retrouver la ligne fraîchement créée.
if ($intBibId <= 0) {
$intBibId = (int)$objDatabase->fxGetVar("
SELECT epr_bib_id
FROM inscriptions_epreuves_bib
WHERE epr_id = $epr_id
AND epr_bib_start = $intStart
AND epr_bib_finish = $intEnd
AND eve_id = $int_eve_id
ORDER BY epr_bib_id DESC
LIMIT 1
");
}
if ($intBibId <= 0) {
return [
'success' => false,
'message' => fxBibMsg('bib_v4_global_batch_gen_insert_failed'),
];
}
$tabCreated[] = [
'epr_id' => $epr_id,
'ranges' => [$intBibId],
'start' => $intStart,
'end' => $intEnd,
];
}
return [
'success' => true,
'plans' => $tabCreated,
];
}
/** Statistiques agrégées pour l'analyse du bloc généré. */
function fxBibBuildGlobalBatchGeneratedAnalysisStats($tabPlan) {
$intEpreuves = 0;
$intPending = 0;
$intBibs = 0;
foreach ($tabPlan as $item) {
if (!empty($item['skip'])) {
continue;
}
$intEpreuves++;
$intPending += (int)($item['nb_pending'] ?? 0);
$intStart = (int)($item['start'] ?? 0);
$intEnd = (int)($item['end'] ?? 0);
if ($intEnd >= $intStart && $intStart > 0) {
$intBibs += ($intEnd - $intStart + 1);
}
}
return [
'nb_epreuves' => $intEpreuves,
'nb_participants' => $intPending,
'nb_bibs' => $intBibs,
];
}
/**
* Simulation globale bloc généré (plages virtuelles, aucune écriture BD).
*
* @return array{success:bool, html?:string, info?:string, message?:string}
*/
function fxBibSimulateGlobalBatchGenerated($int_eve_id, $tabPlan, $strSort1, $strSort2, $strLangue) {
$int_eve_id = (int)$int_eve_id;
$check = fxBibValidateGeneratedRangePlan($int_eve_id, $tabPlan, $strLangue);
if (!$check['success']) {
return $check;
}
$htmlParts = [];
$intParticipants = 0;
$intBibs = 0;
foreach ($tabPlan as $item) {
if (!empty($item['skip'])) {
continue;
}
$epr_id = (int)$item['epr_id'];
$intStart = (int)$item['start'];
$intEnd = (int)$item['end'];
$participants = fxGetParticipantsForBatchAssign($epr_id, $strSort1, $strSort2);
$tabRanges = fxBibBuildVirtualRangesForPlan($intStart, $intEnd);
$simu = fxProcessBatchAssignments($epr_id, $participants, $tabRanges, 'simulation');
$intNbPending = count($participants);
$intParticipants += $intNbPending;
$intBibs += max(0, $intEnd - $intStart + 1);
$tabEpreuve = fxGetEpreuve($epr_id);
$strEprLabel = $tabEpreuve ? fxBibEpreuveDisplayName($tabEpreuve, $strLangue) : ('#' . $epr_id);
$strRangeLabel = fxBibMsg(
'bib_v4_global_batch_gen_range_preview',
$intStart,
$intEnd,
$intNbPending
);
$htmlParts[] = '<div class="bib-global-sim-section">'
. '<h4 class="bib-global-sim-section__title">' . fxBibEsc($strEprLabel) . '</h4>'
. '<p class="bib-global-sim-section__range text-muted small">' . fxBibEsc($strRangeLabel) . '</p>'
. renderBibView(
$epr_id,
0,
$strLangue,
'simulation',
$simu,
fxBibTexte('bib_v4_ajax_title_simulation'),
fxBibMsg(
'bib_v4_ajax_sim_summary',
max(0, $intEnd - $intStart + 1),
$intNbPending,
max(0, $intNbPending - count($simu))
)
)
. '</div>';
}
$info = fxBibMsg(
'bib_v4_ajax_global_gen_sim_summary',
fxBibBuildGlobalBatchGeneratedAnalysisStats($tabPlan)['nb_epreuves'],
$intBibs,
$intParticipants
);
$html = fxBibWrapGlobalSimView($info, implode('', $htmlParts), 'bib-global-sim-view--generated');
return [
'success' => true,
'html' => $html,
'info' => $info,
];
}

View File

@ -1162,9 +1162,33 @@ function fxCourrielPerduCompte($strLangue)
exit;
}
/**
* URL post-login MSIN-4339 — ignore login/mot de passe pour éviter les rebonds en boucle.
*/
function fxCompteRedirectAfterLoginConsume()
{
if (empty($_SESSION['redirect_after_login'])) {
return null;
}
$strUrl = $_SESSION['redirect_after_login'];
unset($_SESSION['redirect_after_login']);
$strPath = parse_url($strUrl, PHP_URL_PATH) ?: '';
if ($strPath === '/compte' || $strPath === '/account') {
return null;
}
if (preg_match('#/(compte|account)/(login|motdepasse|nouveau|reset)(/|$)#i', $strPath)) {
return null;
}
return $strUrl;
}
function fxLoginCompte($infoLogin, $strLangue)
{
global $objDatabase, $vDomaine;
$arrRetour = array();
$arrRetour['state'] = 'error';
@ -1198,7 +1222,8 @@ function fxLoginCompte($infoLogin, $strLangue)
fxcreer_log("ERREUR SQL UPDATE - fxLoginCompte()" . $sqlUpdate);
}
//$_SESSION['logged'] = 1;
// Login public : com_id + com_info seulement. Jamais usa_id ni canal superadm.
// com_info contient toute la ligne SQL (dont com_superadm) — ne pas s'en servir côté site public.
$_SESSION['com_id'] = $infoCompte['com_id'];
$_SESSION['vadmin_texte'] = false;
$_SESSION['com_info'] = $infoCompte;
@ -1236,18 +1261,13 @@ function fxLoginCompte($infoLogin, $strLangue)
$arrRetour['state'] = 'true';
//MSIN-4339
if (!empty($_SESSION['redirect_after_login'])) {
$url = $_SESSION['redirect_after_login'];
unset($_SESSION['redirect_after_login']);
header("Location: " . $url);
// MSIN-4339 — page protégée demandée avant login (pas login/mot de passe).
$strAfterLogin = fxCompteRedirectAfterLoginConsume();
if ($strAfterLogin !== null) {
header('Location: ' . $strAfterLogin);
exit;
}
} else {
if ($strLangue == 'fr')
$_SESSION['msg'] = 'e112';
@ -1273,8 +1293,9 @@ function fxLoginCompte($infoLogin, $strLangue)
else
$strPage = 'account';
//header("Location: " . $vDomaine . '/' . $strPage);
header("Location: " . fxGetReferer());
// Un seul atterrissage : /compte (hub v2 ou accueil selon compte.php).
$strDest = $vDomaine . '/' . $strPage;
header('Location: ' . $strDest);
}
exit;
@ -1284,32 +1305,68 @@ function fxLogout($strLangue = 'fr')
{
global $vDomaine;
$tabSuperAdmin = array();
if (!empty($_SESSION['usa_id'])) {
$tabSuperAdmin = array(
'usa_id' => $_SESSION['usa_id'],
'usa_info' => $_SESSION['usa_info'] ?? null,
'logged' => time(),
);
}
unset($_SESSION['com_id']);
unset($_SESSION['com_info']);
unset($_SESSION['com_prenom']);
//unset($_SESSION['logged']);
unset($_SESSION['vadmin_texte']);
unset($_SESSION['url_retour']);
unset($_SESSION['msg_erreur']);
unset($_SESSION['promoteur_eve_id']);
unset($_SESSION['epr_id_bib']);
unset($_SESSION['com_affilie']);
//
$_SESSION = []; // Vide toutes les variables de session
// Optionnel : détruit la session complètement (ID, fichier, etc.)
session_destroy();
$strNoPanier = $_SESSION['no_panier'] ?? null;
$_SESSION = array();
$_SESSION['date_maj'] = time();
// session_unset();
// session_destroy();
foreach ($tabSuperAdmin as $strKey => $mixValue) {
if ($mixValue !== null) {
$_SESSION[$strKey] = $mixValue;
}
}
if ($strLangue == 'fr')
if ($strLangue == 'fr') {
$strPage = 'compte';
else
} else {
$strPage = 'account';
}
fxViderPanier($_SESSION['no_panier'], 1);
if ($strNoPanier !== null && $strNoPanier !== '') {
fxViderPanier($strNoPanier, 1);
}
if (!empty($_GET['redirect']) && $_GET['redirect'] === 'mobile') {
$strTarget = $vDomaine . ($strLangue === 'en' ? '/mobile/en' : '/mobile');
header('Location: ' . $strTarget);
exit;
}
// MSIN-4401 — retour sécurisé après déconnexion (reset OU lien invite /mobile).
if (!empty($_GET['return'])) {
$strReturn = (string)$_GET['return'];
$blnReturnOk = ($strReturn !== '' && $strReturn[0] === '/'
&& strpos($strReturn, '//') === false
&& strpos($strReturn, "\n") === false
&& strpos($strReturn, "\r") === false
&& (
preg_match('#^/(compte|account)/reset(\?|$)#i', $strReturn)
|| preg_match('#^/mobile(/en)?(\?|$)#i', $strReturn)
));
if ($blnReturnOk) {
header('Location: ' . $vDomaine . $strReturn);
exit;
}
}
header('Location: ' . $vDomaine . '/' . $strPage);
exit;
@ -1324,6 +1381,59 @@ function fxShowFormReset($arrData, $strLangue)
$infoCompte = $objDatabase->fxGetRow($sqlCourriel);
if ($infoCompte != null) {
// MSIN-4401 — déjà connecté sous un autre compte : ne pas afficher le formulaire.
$intSessionComId = intval($_SESSION['com_id'] ?? 0);
$intTargetComId = intval($infoCompte['com_id']);
if ($intSessionComId > 0 && $intSessionComId !== $intTargetComId) {
$strSessionLabel = trim(
(string)($_SESSION['com_info']['com_prenom'] ?? $_SESSION['com_prenom'] ?? '')
. ' '
. (string)($_SESSION['com_info']['com_nom'] ?? '')
);
if ($strSessionLabel === '') {
$strSessionLabel = (string)($_SESSION['com_info']['com_courriel'] ?? ('#' . $intSessionComId));
}
$strTargetLabel = trim(($infoCompte['com_prenom'] ?? '') . ' ' . ($infoCompte['com_nom'] ?? ''));
if ($strTargetLabel === '') {
$strTargetLabel = (string)$infoCompte['com_courriel'];
}
$strReturnPath = ($strLangue === 'en' ? '/account' : '/compte') . '/reset?'
. http_build_query(array(
't' => $arrData['t'],
'm' => $arrData['m'],
'id' => $arrData['id'],
));
$strLogoutUrl = $vDomaine . '/logout.php?lang=' . urlencode($strLangue)
. '&return=' . rawurlencode($strReturnPath);
$strStayUrl = $vDomaine . '/' . ($strLangue === 'en' ? 'account' : 'compte');
?>
<div class="alert alert-warning" role="alert">
<?php if ($strLangue === 'en') { ?>
<p class="mb-2">You are currently signed in as
<strong><?php echo htmlspecialchars($strSessionLabel, ENT_QUOTES, 'UTF-8'); ?></strong>.</p>
<p class="mb-3">This link is for
<strong><?php echo htmlspecialchars($strTargetLabel, ENT_QUOTES, 'UTF-8'); ?></strong>
(<?php echo htmlspecialchars((string)$infoCompte['com_courriel'], ENT_QUOTES, 'UTF-8'); ?>).
Sign out first to set that account&rsquo;s password.</p>
<?php } else { ?>
<p class="mb-2">Vous êtes actuellement connecté en tant que
<strong><?php echo htmlspecialchars($strSessionLabel, ENT_QUOTES, 'UTF-8'); ?></strong>.</p>
<p class="mb-3">Ce lien concerne le compte
<strong><?php echo htmlspecialchars($strTargetLabel, ENT_QUOTES, 'UTF-8'); ?></strong>
(<?php echo htmlspecialchars((string)$infoCompte['com_courriel'], ENT_QUOTES, 'UTF-8'); ?>).
Déconnectez-vous d&rsquo;abord pour choisir le mot de passe de ce compte.</p>
<?php } ?>
<a class="btn btn-primary rounded-0 mr-2" href="<?php echo htmlspecialchars($strLogoutUrl, ENT_QUOTES, 'UTF-8'); ?>">
<?php echo ($strLangue === 'en') ? 'Sign out and continue' : 'Se déconnecter et continuer'; ?>
</a>
<a class="btn btn-outline-secondary rounded-0" href="<?php echo htmlspecialchars($strStayUrl, ENT_QUOTES, 'UTF-8'); ?>">
<?php echo ($strLangue === 'en') ? 'Stay on my current account' : 'Rester sur mon compte actuel'; ?>
</a>
</div>
<?php
return;
}
if (true) {
?>
<script type="text/javascript">
@ -1414,13 +1524,37 @@ function fxResetPassword($arrData, $strLangue)
{
global $vDomaine, $objDatabase;
$intComId = intval($arrData['id']);
$intSessionComId = intval($_SESSION['com_id'] ?? 0);
// MSIN-4401 — refuse si une autre session compte est active
if ($intSessionComId > 0 && $intSessionComId !== $intComId) {
$_SESSION['msg'] = 'e212';
$strPage = ($strLangue === 'en') ? 'account' : 'compte';
?>
<script type="text/javascript">
document.location.href = '<?php echo $vDomaine . '/' . $strPage . '/reset?'
. http_build_query(array('t' => $arrData['t'], 'm' => $arrData['m'], 'id' => $arrData['id'])); ?>';
</script>
<?php
exit;
}
$strPassword = password_hash($objDatabase->fxEscape($arrData['com_password_a']), PASSWORD_DEFAULT);
$sqlUpdate = "UPDATE inscriptions_comptes SET com_password = '" . $strPassword . "' WHERE com_reset_token = '" . $objDatabase->fxEscape($arrData['t']) . "' AND com_courriel = '" . $objDatabase->fxEscape($arrData['m']) . "' AND com_actif = 1 AND com_id = " . intval($arrData['id']);
$sqlUpdate = "UPDATE inscriptions_comptes SET com_password = '" . $strPassword . "',
com_reset_token = ''
WHERE com_reset_token = '" . $objDatabase->fxEscape($arrData['t']) . "'
AND com_courriel = '" . $objDatabase->fxEscape($arrData['m']) . "'
AND com_actif = 1 AND com_id = " . $intComId;
$qryUpdate = $objDatabase->fxQuery($sqlUpdate);
if ($qryUpdate !== false) { // succès
$_SESSION['msg'] = 'p211';
// MSIN-4401 — invitation promoteur : accès passe de invite → actif
if (!function_exists('fxEveAccesActivateInvitesForCompte')) {
require_once dirname(__FILE__) . '/inc_fx_eve_acces.php';
}
fxEveAccesActivateInvitesForCompte($intComId);
} else { // erreur
$_SESSION['msg'] = 'e212';
}
@ -1442,8 +1576,15 @@ function fxShowMenuCompte($arrCompteClient, $strLangue)
{ // affiche le menu du compte
global $objDatabase, $vDomaine;
$strPage = ($strLangue == 'fr') ? 'compte' : 'account';
// MSIN-4401 — retour hub V2 depuis l'espace client
if (!function_exists('fxShowPromoteurHubEspaceClientToolbar')) {
require_once dirname(__FILE__) . '/inc_fx_promoteur_hub.php';
}
$strRetour = fxShowPromoteurHubEspaceClientToolbar(intval($arrCompteClient['com_id'] ?? 0), $strLangue);
// modifier mon compte
$strRetour = '
$strRetour .= '
<ul class="nav flex-column ms1-menu-compte">
<li class="nav-item">
<a class="nav-list" href="' . $vDomaine . '/' . $strPage . '/modifiercompte"><i class="fa fa-user" aria-hidden="true"></i> ' . afficheTexte('compte_modifcompte_texte', 0) . '</a>
@ -1471,14 +1612,30 @@ function fxShowMenuCompte($arrCompteClient, $strLangue)
';
}
// menu promoteur
// menu promoteur v2 (hub) — comptes migres uniquement
if (!function_exists('fxPromoteurHubComUsesV2')) {
require_once dirname(__FILE__) . '/inc_fx_promoteur_hub.php';
}
$strRetour .= fxShowPromoteurHubMenuLink($arrCompteClient, $strLangue);
if (trim($arrCompteClient['com_eve_promoteur']) != '' || trim($arrCompteClient['com_rapports']) != '') { // si le compte client a des événements dont il est le promoteur
// menu promoteur legacy
if (!function_exists('fxInscrGestionUserHasLegacyPromoteurMenu')) {
require_once dirname(__FILE__) . '/inc_fx_inscriptions_gestion.php';
}
$blnLegacyPromoteurMenu = fxInscrGestionUserHasLegacyPromoteurMenu($arrCompteClient['com_id']);
$blnHasRapports = trim($arrCompteClient['com_rapports']) != '';
if ($blnLegacyPromoteurMenu || $blnHasRapports) {
if ($blnLegacyPromoteurMenu) {
$arrEvePromoteur = explode(',', $arrCompteClient['com_eve_promoteur']);
$intNbEvePromoteur = fxcount($arrEvePromoteur);
$strNbEvenements = '<small>(' . $intNbEvePromoteur . ' ';
$strNbEvenements .= ($intNbEvePromoteur > 1) ? afficheTexte('compte_evenement_pluriel', 0) : afficheTexte('compte_evenement_singulier', 0);
$strNbEvenements .= ')</small>';
} else {
$strNbEvenements = '';
}
$strRetour .= '
<li class="nav-item">
@ -1488,11 +1645,33 @@ function fxShowMenuCompte($arrCompteClient, $strLangue)
';
// MSIN-3880
if ($blnLegacyPromoteurMenu) {
$strRetour .= fxShowInfoPromoteurtitre($_SESSION['com_info'], $strLangue);
}
}
// Inscriptions mobile v2 — lien /mobile seulement si pas de menu promoteur legacy
if (!function_exists('fxInscrGestionGetPromoteurEveIds')) {
require_once dirname(__FILE__) . '/inc_fx_inscriptions_gestion.php';
}
$arrMobileEveIds = fxInscrGestionGetPromoteurEveIds($arrCompteClient['com_id']);
$blnShowMobileDirect = (count($arrMobileEveIds) > 0 && !fxInscrGestionUserHasPromoteurMenu($arrCompteClient['com_id']));
if ($blnShowMobileDirect) {
$strMobileUrl = $vDomaine . '/mobile';
if (count($arrMobileEveIds) === 1) {
$strMobileUrl .= '?promoteur_eve_id=' . urlencode(base64_encode($arrMobileEveIds[0]));
}
$strMobileLabel = fxInscrGestionPromoteurMenuLabel();
$strRetour .= '
<li class="nav-item">
<a class="nav-list" href="' . $strMobileUrl . '"><i class="fa fa-users" aria-hidden="true"></i> '
. htmlspecialchars($strMobileLabel, ENT_QUOTES, 'UTF-8') . '</a>
</li>
';
}
//
@ -2178,10 +2357,14 @@ function fxShowCommandes($arrEpreuvesCommandees, $arrCompteClient, $tep_id, $str
<td colspan="7">
<div class="h5">
<?php
$arrCapitaine = fxGetCapitaine($arrEpreuvesCommandees[$intCtrEpreuvesCommandees]['pec_id'], $arrEpreuvesCommandees[$intCtrEpreuvesCommandees]['epr_id']);
$arrCapitaine = fxGetCapitaine($arrEpreuvesCommandees[$intCtrEpreuvesCommandees]['pec_id'], $arrEpreuvesCommandees[$intCtrEpreuvesCommandees]['epr_id']);
//MSIN-4375
echo $arrEpreuvesCommandees[$intCtrEpreuvesCommandees]['par_prenom'] . ' ' . $arrEpreuvesCommandees[$intCtrEpreuvesCommandees]['par_nom'];
if ($arrCapitaine != false) {
echo $arrCapitaine['par_nom'] . ', ' . $arrCapitaine['par_prenom'];
echo 'Capitaine:'.$arrCapitaine['par_nom'] . ', ' . $arrCapitaine['par_prenom'];
}
?>
</div>
@ -2196,6 +2379,10 @@ function fxShowCommandes($arrEpreuvesCommandees, $arrCompteClient, $tep_id, $str
<?php echo fxGetTabInscription('epr_type_' . $strLangue, $arrEpreuvesCommandees[$intCtrEpreuvesCommandees]['epr_id'], 'epreuves', 'epr'); ?>
<br>
<?php
//MSIN-4376
//print_rsl($arrEpreuvesCommandees);
if($arrEpreuvesCommandees[$intCtrEpreuvesCommandees]['eve_code_qr']!=1 ){
//MSIN-4323
//if($arrEpreuvesCommandees[$intCtrEpreuvesCommandees]['eve_code_qr']==1 ){
// print_rsl($arrEpreuvesCommandees[$intCtrEpreuvesCommandees]);
@ -2216,7 +2403,7 @@ function fxShowCommandes($arrEpreuvesCommandees, $arrCompteClient, $tep_id, $str
?> <img src="<?php echo $vDomaine.'/php/qr_img.php?t='.urlencode($result['qr_public_id']); ?> " width="100" /> <?php
// }
}
?>

597
php/inc_fx_doc.php Normal file
View File

@ -0,0 +1,597 @@
<?php
/**
* Module documentation MS1 — overlay paginé HTML (distinct de info / info_aide).
*
* Clés d'appel (doc_anchor_clef) → module (doc_mod_clef) → pages (doc_page).
* Usage :
* fxDocRenderModule('bib_v4_assign_doc'); — overlay complet
* fxDocRenderTrigger('bib_v4_assign_doc'); — bouton ? seul
* afficheDocPage('bib_doc_overview'); — HTML d'une page (comme afficheTexte)
* afficheDocTitre('bib_doc_overview'); — titre d'une page
*/
/** Échappement attributs HTML doc module. */
function fxDocEsc($str) {
return htmlspecialchars((string)$str, ENT_QUOTES, 'UTF-8');
}
/** Page PHP courante pour doc_prg (miroir fxBibInfoPage / info_prg). */
function fxDocPrg($mem_global = 0) {
global $vPage;
if ($mem_global == 1) {
return 'global';
}
return !empty($vPage) ? $vPage : 'compte.php';
}
/** Langue courante doc. */
function fxDocLangue() {
global $strLangue;
return !empty($strLangue) ? $strLangue : 'fr';
}
/** Tables doc_* présentes en BD (évite erreur si SQL module pas encore exécuté). */
function fxDocSchemaReady() {
global $objDatabase;
static $blnReady = null;
if ($blnReady !== null) {
return $blnReady;
}
if (!isset($objDatabase)) {
$blnReady = false;
return false;
}
$qry = $objDatabase->fxQuery("SHOW TABLES LIKE 'doc_page'");
$blnReady = ($qry && mysqli_num_rows($qry) > 0);
return $blnReady;
}
/**
* Charge en mémoire toutes les pages doc pour une page PHP (comme obtenirTextepage).
* Remplit $vdoc_page[doc_page_clef] = row.
*/
function obtenirDocPage($page = null, $langue = null) {
global $objDatabase, $vdoc_page;
if ($page === null || $page === '') {
$page = fxDocPrg(0);
}
if ($langue === null || $langue === '') {
$langue = fxDocLangue();
}
$vdoc_page = array();
if (!isset($objDatabase) || !fxDocSchemaReady()) {
return $vdoc_page;
}
$sql = "SELECT *
FROM doc_page
WHERE doc_actif = 1
AND doc_langue = '" . $objDatabase->fxEscape($langue) . "'
AND doc_prg IN ('" . $objDatabase->fxEscape($page) . "', 'global')
ORDER BY doc_tri, pk_doc_page";
$rows = $objDatabase->fxGetResults($sql);
if (!is_array($rows)) {
return $vdoc_page;
}
for ($i = 1; $i <= count($rows); $i++) {
if (!empty($rows[$i]['doc_page_clef'])) {
$vdoc_page[$rows[$i]['doc_page_clef']] = $rows[$i];
}
}
return $vdoc_page;
}
/** Ligne doc_anchor par clé d'appel. */
function fxDocGetAnchorRow($clef, $mem_global = 0) {
global $objDatabase;
static $anchorCache = array();
$strPrg = fxDocPrg($mem_global);
$cacheKey = $clef . '|' . $strPrg;
if (array_key_exists($cacheKey, $anchorCache)) {
return $anchorCache[$cacheKey];
}
if (!isset($objDatabase) || $clef === '' || !fxDocSchemaReady()) {
$anchorCache[$cacheKey] = array();
return $anchorCache[$cacheKey];
}
$sql = "SELECT *
FROM doc_anchor
WHERE doc_actif = 1
AND doc_anchor_clef = '" . $objDatabase->fxEscape($clef) . "'
AND doc_prg IN ('" . $objDatabase->fxEscape($strPrg) . "', 'global')
ORDER BY FIELD(doc_prg, '" . $objDatabase->fxEscape($strPrg) . "', 'global')
LIMIT 1";
$row = $objDatabase->fxGetRow($sql);
$anchorCache[$cacheKey] = is_array($row) ? $row : array();
return $anchorCache[$cacheKey];
}
/** Résout doc_mod_clef depuis une clé d'ancre ou un code module direct. */
function fxDocResolveModClef($clef, $mem_global = 0) {
$anchor = fxDocGetAnchorRow($clef, $mem_global);
if (!empty($anchor['doc_mod_clef'])) {
return $anchor['doc_mod_clef'];
}
return $clef;
}
/** Pages actives d'un module, triées (pour carrousel overlay). */
function fxDocGetModulePages($modClef, $langue = null, $mem_global = 0) {
global $objDatabase, $vdoc_page;
if ($langue === null || $langue === '') {
$langue = fxDocLangue();
}
$strPrg = fxDocPrg($mem_global);
$modClef = fxDocResolveModClef($modClef, $mem_global);
if (!isset($objDatabase) || $modClef === '' || !fxDocSchemaReady()) {
return array();
}
$sql = "SELECT *
FROM doc_page
WHERE doc_actif = 1
AND doc_mod_clef = '" . $objDatabase->fxEscape($modClef) . "'
AND doc_langue = '" . $objDatabase->fxEscape($langue) . "'
AND doc_prg IN ('" . $objDatabase->fxEscape($strPrg) . "', 'global')
ORDER BY doc_tri, pk_doc_page";
$rows = $objDatabase->fxGetResults($sql);
$pages = array();
if (!is_array($rows)) {
return $pages;
}
for ($i = 1; $i <= count($rows); $i++) {
$pages[] = $rows[$i];
if (!empty($rows[$i]['doc_page_clef'])) {
if (!isset($vdoc_page) || !is_array($vdoc_page)) {
$vdoc_page = array();
}
$vdoc_page[$rows[$i]['doc_page_clef']] = $rows[$i];
}
}
return $pages;
}
/** Une page par doc_page_clef (mémoire ou BD). */
function fxDocGetPageRow($clef, $mem_global = 0) {
global $vdoc_page, $objDatabase;
if (!empty($vdoc_page[$clef])) {
return $vdoc_page[$clef];
}
if (!isset($objDatabase) || $clef === '' || !fxDocSchemaReady()) {
return array();
}
$strPrg = fxDocPrg($mem_global);
$langue = fxDocLangue();
static $pageCache = array();
$cacheKey = $clef . '|' . $langue . '|' . $strPrg;
if (array_key_exists($cacheKey, $pageCache)) {
return $pageCache[$cacheKey];
}
$sql = "SELECT *
FROM doc_page
WHERE doc_actif = 1
AND doc_page_clef = '" . $objDatabase->fxEscape($clef) . "'
AND doc_langue = '" . $objDatabase->fxEscape($langue) . "'
AND doc_prg IN ('" . $objDatabase->fxEscape($strPrg) . "', 'global')
ORDER BY FIELD(doc_prg, '" . $objDatabase->fxEscape($strPrg) . "', 'global')
LIMIT 1";
$row = $objDatabase->fxGetRow($sql);
$pageCache[$cacheKey] = is_array($row) ? $row : array();
return $pageCache[$cacheKey];
}
/** Crée une page doc vide si absente (miroir afficheTexte). */
function fxDocEnsurePage($clef, $mem_global = 0, $modClef = '') {
global $objDatabase;
$row = fxDocGetPageRow($clef, $mem_global);
if (!empty($row)) {
return $row;
}
$strPrg = fxDocPrg($mem_global);
$langue = fxDocLangue();
if ($modClef === '') {
$modClef = $clef;
}
$tabData = array(
'doc_mod_clef' => $modClef,
'doc_page_clef' => $clef,
'doc_langue' => $langue,
'doc_titre' => $clef,
'doc_sous_titre' => '',
'doc_html' => '',
'doc_prg' => $strPrg,
'doc_tri' => 0,
'doc_actif' => 1,
);
fxSetDocPage('add', $tabData);
if ($langue === 'fr') {
$tabDataEn = $tabData;
$tabDataEn['doc_langue'] = 'en';
fxSetDocPage('add', $tabDataEn);
}
return fxDocGetPageRow($clef, $mem_global);
}
/**
* Titre doc_titre — équivalent afficheTexte pour doc_page.
*/
function afficheDocTitre($clef, $mem_echo = 1, $mem_global = 0, $modClef = '') {
$row = fxDocGetPageRow($clef, $mem_global);
if (empty($row)) {
fxDocEnsurePage($clef, $mem_global, $modClef);
$row = fxDocGetPageRow($clef, $mem_global);
}
$str = trim($row['doc_titre'] ?? '');
if ($str === '' || $str === $clef) {
$str = '*' . $clef . '*';
}
if ($mem_echo == 1) {
echo fxDocEsc($str);
}
return $str;
}
/**
* Corps HTML doc_html — équivalent afficheAide (contenu long, HTML admin).
*/
function afficheDocHtml($clef, $mem_echo = 1, $mem_global = 0, $modClef = '') {
$row = fxDocGetPageRow($clef, $mem_global);
if (empty($row)) {
fxDocEnsurePage($clef, $mem_global, $modClef);
$row = fxDocGetPageRow($clef, $mem_global);
}
$str = $row['doc_html'] ?? '';
if ($mem_echo == 1) {
echo $str;
}
return $str;
}
/** Sous-titre doc_sous_titre. */
function afficheDocSousTitre($clef, $mem_echo = 1, $mem_global = 0, $modClef = '') {
$row = fxDocGetPageRow($clef, $mem_global);
if (empty($row)) {
fxDocEnsurePage($clef, $mem_global, $modClef);
$row = fxDocGetPageRow($clef, $mem_global);
}
$str = trim($row['doc_sous_titre'] ?? '');
if ($mem_echo == 1) {
echo fxDocEsc($str);
}
return $str;
}
/** URL pop-up doc_edition.php — module documentation paginé. */
function fxAdminDocUrl($anchorClef, $mem_global = 0) {
global $vDomaine, $vPage;
$strPrg = fxDocPrg($mem_global);
$modClef = fxDocResolveModClef($anchorClef, $mem_global);
return $vDomaine . '/php/doc_edition.php?' . http_build_query(array(
'anchor' => $anchorClef,
'mod' => $modClef,
'prg' => $strPrg,
'bd' => 'client',
'page' => !empty($vPage) ? $vPage : 'compte.php',
));
}
/** Lien crayon doc_edition.php — superadm dev/préprod + switch Textes ON. */
function fxAdminDocButton($anchorClef, $mem_global = 0) {
if (!fxAdminTextEditModeActive() || trim($anchorClef) === '') {
return '';
}
return '<a href="' . fxDocEsc(fxAdminDocUrl($anchorClef, $mem_global)) . '"'
. ' class="' . fxAdminTradLinkClass() . ' ms1-doc-edit-link"'
. ' title="Modifier le guide documentation"'
. ' aria-label="Modifier le guide documentation">'
. '<i class="fa fa-book" aria-hidden="true"></i>'
. '</a>';
}
/** Bouton ? pour ouvrir le module doc (clé doc_anchor_clef) — même rond que btn-aide-bib. */
function fxDocRenderTrigger($anchorClef, $mem_global = 0, $strAriaLabel = 'Documentation') {
$modClef = fxDocResolveModClef($anchorClef, $mem_global);
return '<button type="button"'
. ' class="btn-aide-bib ms1-doc-trigger btn-doc-trigger"'
. ' data-doc-anchor="' . fxDocEsc($anchorClef) . '"'
. ' data-doc-mod="' . fxDocEsc($modClef) . '"'
. ' aria-label="' . fxDocEsc($strAriaLabel) . '"'
. ' aria-haspopup="dialog">?</button>';
}
/** Panneau overlay paginé pour un module (clé doc_anchor_clef ou doc_mod_clef). */
function fxDocRenderModule($anchorClef, $mem_global = 0) {
$modClef = fxDocResolveModClef($anchorClef, $mem_global);
$pages = fxDocGetModulePages($modClef, null, $mem_global);
if (empty($pages)) {
return '';
}
$overlayId = 'ms1-doc-overlay-' . preg_replace('/[^a-z0-9_-]/i', '-', $anchorClef);
$html = '<div id="' . fxDocEsc($overlayId) . '"'
. ' class="ms1-doc-overlay"'
. ' data-doc-anchor="' . fxDocEsc($anchorClef) . '"'
. ' data-doc-mod="' . fxDocEsc($modClef) . '"'
. ' hidden'
. ' role="dialog"'
. ' aria-modal="true"'
. ' aria-labelledby="' . fxDocEsc($overlayId) . '-title">';
$html .= '<div class="ms1-doc-backdrop" data-ms1-doc-close="1"></div>';
$html .= '<div class="ms1-doc-panel">';
$html .= '<header class="ms1-doc-header">';
$html .= '<div class="ms1-doc-header-text">';
$html .= '<h2 class="ms1-doc-title" id="' . fxDocEsc($overlayId) . '-title"></h2>';
$html .= '<p class="ms1-doc-subtitle"></p>';
$html .= '</div>';
$html .= '<div class="ms1-doc-header-actions">';
$html .= fxAdminDocButton($anchorClef, $mem_global);
$html .= '<button type="button" class="ms1-doc-close" data-ms1-doc-close="1" aria-label="Fermer">&times;</button>';
$html .= '</div>';
$html .= '</header>';
$html .= '<div class="ms1-doc-body">';
foreach ($pages as $idx => $page) {
$html .= '<div class="ms1-doc-page"'
. ($idx > 0 ? ' hidden' : '')
. ' data-doc-page-index="' . (int)$idx . '"'
. ' data-doc-page-clef="' . fxDocEsc($page['doc_page_clef']) . '">';
$html .= '<div class="ms1-doc-content ms1-doc-format">' . ($page['doc_html'] ?? '') . '</div>';
$html .= '</div>';
}
$html .= '</div>';
$html .= '<footer class="ms1-doc-footer">';
$html .= '<div class="ms1-doc-nav-dots">';
foreach ($pages as $idx => $page) {
$active = $idx === 0 ? ' is-active' : '';
$label = trim($page['doc_titre'] ?? '') !== '' ? $page['doc_titre'] : (string)($idx + 1);
$html .= '<button type="button"'
. ' class="ms1-doc-dot' . $active . '"'
. ' data-doc-page-index="' . (int)$idx . '"'
. ' title="' . fxDocEsc($label) . '"'
. ' aria-label="' . fxDocEsc($label) . '">'
. (int)($idx + 1)
. '</button>';
}
$html .= '</div>';
$html .= '<div class="ms1-doc-nav-arrows">';
$html .= '<button type="button" class="ms1-doc-prev" data-ms1-doc-prev="1">Précédent</button>';
$html .= '<span class="ms1-doc-counter"><span class="ms1-doc-counter-current">1</span> / <span class="ms1-doc-counter-total">' . count($pages) . '</span></span>';
$html .= '<button type="button" class="ms1-doc-next" data-ms1-doc-next="1">Suivant</button>';
$html .= '</div>';
$html .= '</footer>';
$html .= '</div></div>';
// Métadonnées titres pour JS (init header sans parser le HTML)
$html .= '<script type="application/json" class="ms1-doc-meta" data-doc-anchor="' . fxDocEsc($anchorClef) . '">';
$meta = array();
foreach ($pages as $page) {
$meta[] = array(
'clef' => $page['doc_page_clef'],
'titre' => $page['doc_titre'] ?? '',
'sous_titre' => $page['doc_sous_titre'] ?? '',
);
}
$html .= json_encode($meta, JSON_UNESCAPED_UNICODE);
$html .= '</script>';
return $html;
}
/** Trigger + overlay (appel typique à côté d'un titre de bloc). */
function fxDocRenderBlock($anchorClef, $mem_global = 0, $strAriaLabel = 'Documentation') {
return fxDocRenderTrigger($anchorClef, $mem_global, $strAriaLabel) . fxDocRenderModule($anchorClef, $mem_global);
}
/** Clés doc — panneau remboursement promoteur (fiche gestion inscriptions). */
function fxDocRefundPromoteurAnchorClef() {
return 'inscr_gestion_refund_doc';
}
function fxDocRefundPromoteurPageClef() {
return 'inscr_gestion_refund_promoteur_info';
}
function fxDocRefundPromoteurModClef() {
return 'inscr_gestion_refund_doc';
}
/** Texte par défaut si doc_html vide (avant import SQL ou édition). */
function fxDocRefundPromoteurFallback($strLangue) {
if ($strLangue === 'en') {
return array(
'titre' => 'Refunds and MS1 fees',
'html' => '<article class="ms1-doc-article">'
. '<p>As a promoter, you may refund the customer <strong>up to 100%</strong> of the transaction amount.</p>'
. '<p>However, <strong>MS1 platform fees are not refunded</strong> to the promoter. If you refund the customer, <strong>you absorb those fees</strong> — they remain your responsibility.</p>'
. '<p>The &laquo;&nbsp;Max refundable&nbsp;&raquo; amount is the PayPal balance available for this capture, not a reimbursement of MS1 fees.</p>'
. '</article>',
);
}
return array(
'titre' => 'Remboursement et frais MS1',
'html' => '<article class="ms1-doc-article">'
. '<p>En tant que promoteur, vous pouvez rembourser le client <strong>jusqu\'à 100&nbsp;%</strong> du montant de la transaction.</p>'
. '<p>En revanche, <strong>les frais MS1 ne sont pas remboursés</strong> par la plateforme au promoteur. Si vous remboursez le client, <strong>vous assumez ces frais</strong>&nbsp;: ils restent à votre charge.</p>'
. '<p>Le montant &laquo;&nbsp;Max remboursable&nbsp;&raquo; correspond au solde PayPal disponible pour cette capture, et non au remboursement des frais MS1.</p>'
. '</article>',
);
}
/**
* Colonne droite du panneau remboursement (fiche gestion promoteur) — doc_html inline.
* Éditable via doc_edition.php (mode Textes, icône livre).
*/
function fxDocRenderRefundPromoteurAside() {
if (!fxDocSchemaReady()) {
return '';
}
$strAnchor = fxDocRefundPromoteurAnchorClef();
$strPageClef = fxDocRefundPromoteurPageClef();
$strModClef = fxDocRefundPromoteurModClef();
$strLangue = fxDocLangue();
fxDocEnsurePage($strPageClef, 0, $strModClef);
$strTitre = trim((string)afficheDocTitre($strPageClef, 0, 0, $strModClef));
$strHtml = trim((string)afficheDocHtml($strPageClef, 0, 0, $strModClef));
$tabFallback = fxDocRefundPromoteurFallback($strLangue);
if ($strTitre === '' || $strTitre === '*' . $strPageClef . '*') {
$strTitre = $tabFallback['titre'];
}
if ($strHtml === '') {
$strHtml = $tabFallback['html'];
}
$strEditBtn = function_exists('fxAdminDocButton') ? fxAdminDocButton($strAnchor, 0) : '';
$html = '<aside class="ms1-refund-doc-aside" aria-labelledby="ms1-refund-doc-title">';
$html .= '<div class="ms1-refund-doc-panel">';
$html .= '<header class="ms1-refund-doc-head">';
$html .= '<h3 class="ms1-refund-doc-title" id="ms1-refund-doc-title">' . fxDocEsc($strTitre) . '</h3>';
if ($strEditBtn !== '') {
$html .= '<div class="ms1-refund-doc-head-actions">' . $strEditBtn . '</div>';
}
$html .= '</header>';
$html .= '<div class="ms1-refund-doc-body ms1-doc-format ms1-doc-content">' . $strHtml . '</div>';
$html .= '</div></aside>';
return $html;
}
function fxSetDocMod($strAction, $tabData, $id = 0) {
global $objDatabase;
switch ($strAction) {
case 'add':
$tabFieldsValues = fxListFieldsValues($tabData, 'pk_doc_mod', 'doc_mod', $GLOBALS['arrConDatabaseMain']['db']);
$sqlValide = "SELECT COUNT(doc_mod_clef) FROM doc_mod
WHERE doc_mod_clef = '" . $objDatabase->fxEscape($tabData['doc_mod_clef']) . "'
AND doc_mod_prg = '" . $objDatabase->fxEscape($tabData['doc_mod_prg']) . "'";
if ((int)$objDatabase->fxGetVar($sqlValide) > 0) {
break;
}
$sqlInsert = "INSERT INTO doc_mod " . $tabFieldsValues['set'] . ', doc_mod_creation = NOW()';
$objDatabase->fxQuery($sqlInsert);
break;
case 'mod':
$tabFieldsValues = fxListFieldsValues($tabData, 'pk_doc_mod', 'doc_mod', $GLOBALS['arrConDatabaseMain']['db']);
$sqlUpdate = "UPDATE doc_mod " . $tabFieldsValues['set'] . ", doc_mod_maj = NOW() WHERE " . $tabFieldsValues['key'];
$objDatabase->fxQuery($sqlUpdate);
break;
}
}
function fxSetDocPage($strAction, $tabData, $id = 0) {
global $objDatabase;
switch ($strAction) {
case 'add':
$tabFieldsValues = fxListFieldsValues($tabData, 'pk_doc_page', 'doc_page', $GLOBALS['arrConDatabaseMain']['db']);
$sqlValide = "SELECT COUNT(doc_page_clef) FROM doc_page
WHERE doc_page_clef = '" . $objDatabase->fxEscape($tabData['doc_page_clef']) . "'
AND doc_langue = '" . $objDatabase->fxEscape($tabData['doc_langue']) . "'
AND doc_prg = '" . $objDatabase->fxEscape($tabData['doc_prg']) . "'";
if ((int)$objDatabase->fxGetVar($sqlValide) > 0) {
break;
}
$sqlInsert = "INSERT INTO doc_page " . $tabFieldsValues['set'] . ', doc_creation = NOW()';
$objDatabase->fxQuery($sqlInsert);
break;
case 'mod':
$tabFieldsValues = fxListFieldsValues($tabData, 'pk_doc_page', 'doc_page', $GLOBALS['arrConDatabaseMain']['db']);
$sqlUpdate = "UPDATE doc_page " . $tabFieldsValues['set'] . ", doc_maj = NOW() WHERE " . $tabFieldsValues['key'];
$objDatabase->fxQuery($sqlUpdate);
break;
}
}
function fxSetDocAnchor($strAction, $tabData, $id = 0) {
global $objDatabase;
switch ($strAction) {
case 'add':
$tabFieldsValues = fxListFieldsValues($tabData, 'pk_doc_anchor', 'doc_anchor', $GLOBALS['arrConDatabaseMain']['db']);
$sqlValide = "SELECT COUNT(doc_anchor_clef) FROM doc_anchor
WHERE doc_anchor_clef = '" . $objDatabase->fxEscape($tabData['doc_anchor_clef']) . "'
AND doc_prg = '" . $objDatabase->fxEscape($tabData['doc_prg']) . "'";
if ((int)$objDatabase->fxGetVar($sqlValide) > 0) {
break;
}
$sqlInsert = "INSERT INTO doc_anchor " . $tabFieldsValues['set'] . ', doc_creation = NOW()';
$objDatabase->fxQuery($sqlInsert);
break;
case 'mod':
$tabFieldsValues = fxListFieldsValues($tabData, 'pk_doc_anchor', 'doc_anchor', $GLOBALS['arrConDatabaseMain']['db']);
$sqlUpdate = "UPDATE doc_anchor " . $tabFieldsValues['set'] . " WHERE " . $tabFieldsValues['key'];
$objDatabase->fxQuery($sqlUpdate);
break;
}
}

2105
php/inc_fx_eve_acces.php Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,953 @@
<?php
/**
* MSIN-4401 — Gestion des accès V2 côté promoteur (UI événement).
* Style aligné sur les modules v2 (hub / gestion inscriptions / dossards).
*/
require_once __DIR__ . '/inc_fx_eve_acces.php';
function fxEveAccesPromoteurT($strClef, $strLangue = 'fr')
{
$strTexte = afficheTexte($strClef, 0, 0, 1);
if ($strTexte === $strClef || trim((string)$strTexte) === '' || strpos((string)$strTexte, '*' . $strClef . '*') !== false) {
$arrFallback = array(
'tableau_promoteur_menu_eve_acces' => array('fr' => 'Gestion des accès V2', 'en' => 'Access management V2'),
'eve_acces_promoteur_title' => array('fr' => 'Gestion des accès V2', 'en' => 'Access management V2'),
'eve_acces_promoteur_intro' => array(
'fr' => 'Assignez des kits d\'accès à d\'autres personnes pour cet événement.',
'en' => 'Assign access kits to other people for this event.',
),
'eve_acces_promoteur_list_title' => array('fr' => 'Personnes assignées', 'en' => 'Assigned people'),
'eve_acces_promoteur_invite_title' => array('fr' => 'Inviter quelqu\'un', 'en' => 'Invite someone'),
'eve_acces_promoteur_invite_soon' => array(
'fr' => 'L\'invitation par courriel arrive à l\'étape suivante. Pour l\'instant, consultez les accès déjà en place.',
'en' => 'Email invitation comes in the next step. For now, review existing access.',
),
'eve_acces_promoteur_empty' => array(
'fr' => 'Aucun accès assigné pour cet événement.',
'en' => 'No access assigned for this event.',
),
'eve_acces_promoteur_col_compte' => array('fr' => 'Compte', 'en' => 'Account'),
'eve_acces_promoteur_col_kit' => array('fr' => 'Kit', 'en' => 'Kit'),
'eve_acces_promoteur_col_statut' => array('fr' => 'Statut', 'en' => 'Status'),
'eve_acces_promoteur_statut_actif' => array('fr' => 'Actif', 'en' => 'Active'),
'eve_acces_promoteur_statut_pending' => array(
'fr' => 'Compte non activé',
'en' => 'Account not activated',
),
'eve_acces_promoteur_col_depuis' => array('fr' => 'Depuis', 'en' => 'Since'),
'eve_acces_promoteur_col_expire' => array('fr' => 'Expire', 'en' => 'Expires'),
'eve_acces_promoteur_col_action' => array('fr' => 'Action', 'en' => 'Action'),
'eve_acces_promoteur_field_courriel' => array('fr' => 'Courriel', 'en' => 'Email'),
'eve_acces_promoteur_field_prenom' => array('fr' => 'Prénom', 'en' => 'First name'),
'eve_acces_promoteur_field_nom' => array('fr' => 'Nom', 'en' => 'Last name'),
'eve_acces_promoteur_field_kit' => array('fr' => 'Kit d\'accès', 'en' => 'Access kit'),
'eve_acces_promoteur_field_jours' => array('fr' => 'Durée (jours)', 'en' => 'Duration (days)'),
'eve_acces_promoteur_jours_help' => array(
'fr' => '0 = sans expiration. Utilisé surtout pour les nouveaux accès.',
'en' => '0 = no expiration. Mainly for new access grants.',
),
'eve_acces_promoteur_invite_courriel_help' => array(
'fr' => 'Saisissez d\'abord le courriel : on vérifie s\'il existe déjà un compte MS1 avant de demander le nom.',
'en' => 'Enter the email first: we check if an MS1 account already exists before asking for a name.',
),
'eve_acces_promoteur_btn_invite' => array('fr' => 'Assigner l\'accès', 'en' => 'Grant access'),
'eve_acces_promoteur_btn_resend' => array('fr' => 'Renvoyer le mail', 'en' => 'Resend email'),
'eve_acces_promoteur_btn_revoke' => array('fr' => 'Retirer', 'en' => 'Revoke'),
'eve_acces_promoteur_confirm_revoke' => array(
'fr' => 'Retirer cet accès ?',
'en' => 'Revoke this access?',
),
'eve_acces_promoteur_no_kits' => array(
'fr' => 'Aucun kit délégable disponible. Demandez à un super admin d\'activer « délégable » sur un kit.',
'en' => 'No delegable kit available. Ask a super admin to mark a kit as delegable.',
),
'eve_acces_promoteur_ok_existing' => array(
'fr' => 'Accès assigné au compte existant. Un courriel a été envoyé.',
'en' => 'Access granted to the existing account. An email was sent.',
),
'eve_acces_promoteur_ok_created' => array(
'fr' => 'Compte créé et accès assigné. Un courriel avec le lien de mot de passe a été envoyé.',
'en' => 'Account created and access granted. An email with the password link was sent.',
),
'eve_acces_promoteur_ok_revoked' => array('fr' => 'Accès retiré.', 'en' => 'Access revoked.'),
'eve_acces_promoteur_ok_resent' => array(
'fr' => 'Courriel d\'invitation renvoyé.',
'en' => 'Invitation email resent.',
),
'eve_acces_promoteur_err_courriel' => array('fr' => 'Courriel invalide.', 'en' => 'Invalid email.'),
'eve_acces_promoteur_err_kit' => array('fr' => 'Kit non autorisé.', 'en' => 'Kit not allowed.'),
'eve_acces_promoteur_err_nom' => array(
'fr' => 'Prénom et nom requis pour créer un nouveau compte.',
'en' => 'First and last name required to create a new account.',
),
'eve_acces_promoteur_err_inactive' => array(
'fr' => 'Un compte existe avec ce courriel mais il est inactif. Contactez le support.',
'en' => 'An account exists with this email but it is inactive. Contact support.',
),
'eve_acces_promoteur_err_revoke' => array('fr' => 'Retrait impossible.', 'en' => 'Unable to revoke.'),
'eve_acces_promoteur_err_resend' => array(
'fr' => 'Renvoi du courriel impossible.',
'en' => 'Unable to resend the email.',
),
'eve_acces_promoteur_err_generic' => array('fr' => 'Une erreur est survenue.', 'en' => 'An error occurred.'),
'eve_acces_promoteur_check_exists' => array(
'fr' => 'Compte déjà existant. Un courriel de confirmation sera envoyé.',
'en' => 'Existing account. A confirmation email will be sent.',
),
'eve_acces_promoteur_check_new' => array(
'fr' => 'Aucun compte avec ce courriel. Indiquez le prénom et le nom pour créer le compte.',
'en' => 'No account with this email. Enter first and last name to create the account.',
),
'eve_acces_promoteur_check_inactive' => array(
'fr' => 'Un compte existe avec ce courriel mais il est inactif. Contactez le support.',
'en' => 'An account exists with this email but it is inactive. Contact support.',
),
'eve_acces_promoteur_check_invalid' => array(
'fr' => 'Courriel invalide.',
'en' => 'Invalid email.',
),
'eve_acces_promoteur_access_denied' => array('fr' => 'Accès refusé.', 'en' => 'Access denied.'),
'eve_acces_promoteur_btn_back' => array('fr' => 'Retour', 'en' => 'Back'),
'eve_acces_promoteur_mail_new_subject' => array(
'fr' => 'Accès promoteur — créez votre mot de passe',
'en' => 'Promoter access — set your password',
),
'eve_acces_promoteur_mail_new_body' => array(
'fr' => 'Bonjour %prenom%,<br><br>Un accès vous a été accordé pour <strong>%eve_nom%</strong> (kit : %kit%).<br><br>Un compte a été créé avec le courriel <strong>%login%</strong>.<br>Cliquez ci-dessous pour choisir votre mot de passe.<br><br>Ensuite, connectez-vous ici pour travailler sur le terrain :<br><a href="%mobile_url%">%mobile_url%</a>',
'en' => 'Hello %prenom%,<br><br>You were granted access to <strong>%eve_nom%</strong> (kit: %kit%).<br><br>An account was created with <strong>%login%</strong>.<br>Click below to set your password.<br><br>Then sign in here for field use:<br><a href="%mobile_url%">%mobile_url%</a>',
),
'eve_acces_promoteur_mail_new_cta' => array('fr' => 'Choisir mon mot de passe', 'en' => 'Set my password'),
'eve_acces_promoteur_mail_existing_subject' => array(
'fr' => 'Nouvel accès — %eve_nom%',
'en' => 'New access — %eve_nom%',
),
'eve_acces_promoteur_mail_existing_body' => array(
'fr' => 'Bonjour %prenom%,<br><br>Un accès vous a été accordé pour <strong>%eve_nom%</strong> (kit : %kit%).<br><br>Ouvrez le lien ci-dessous et connectez-vous avec le compte <strong>%login%</strong> (si un autre compte est déjà ouvert, vous serez invité à vous déconnecter).',
'en' => 'Hello %prenom%,<br><br>You were granted access to <strong>%eve_nom%</strong> (kit: %kit%).<br><br>Open the link below and sign in as <strong>%login%</strong> (if another account is already open, you will be asked to sign out).',
),
'eve_acces_promoteur_mail_existing_cta' => array(
'fr' => 'Ouvrir l\'accès (mobile)',
'en' => 'Open access (mobile)',
),
);
$strLang = ($strLangue === 'en') ? 'en' : 'fr';
if (isset($arrFallback[$strClef][$strLang])) {
return $arrFallback[$strClef][$strLang];
}
}
return $strTexte;
}
function fxEveAccesPromoteurEsc($str)
{
return htmlspecialchars((string)$str, ENT_QUOTES, 'UTF-8');
}
function fxEveAccesPromoteurFormatDt($strDt)
{
$strDt = trim((string)$strDt);
if ($strDt === '' || strpos($strDt, '0000-00-00') === 0) {
return '—';
}
return substr($strDt, 0, 16);
}
function fxEveAccesPromoteurMenuLabel($strLangue = 'fr')
{
return fxEveAccesPromoteurT('tableau_promoteur_menu_eve_acces', $strLangue);
}
function fxEveAccesPromoteurHasInviteTeamColumn()
{
global $objDatabase;
static $blnHas = null;
if ($blnHas !== null) {
return $blnHas;
}
$tab = $objDatabase->fxGetResults("SHOW COLUMNS FROM inscriptions_comptes LIKE 'com_invite_team'");
$blnHas = ($tab != null && count($tab) > 0);
return $blnHas;
}
/**
* MSIN-4401 — crée un compte MS1 minimal pour invitation promoteur.
*
* @return array{state:string,message?:string,com_id?:int,reset_token?:string}
*/
function fxEveAccesPromoteurCreateCompteInvite($strCourriel, $strPrenom, $strNom, $strLangue, $intGrantedBy)
{
global $objDatabase;
$strCourriel = trim(strtolower((string)$strCourriel));
$strPrenom = trim((string)$strPrenom);
$strNom = trim((string)$strNom);
$strLangue = ($strLangue === 'en') ? 'en' : 'fr';
if ($strCourriel === '' || !filter_var($strCourriel, FILTER_VALIDATE_EMAIL)) {
return array('state' => 'error', 'message' => 'courriel');
}
if ($strPrenom === '' || $strNom === '') {
return array('state' => 'error', 'message' => 'nom');
}
$strToken = uniqid('inv', true);
$strPasswordHash = password_hash(bin2hex(random_bytes(16)), PASSWORD_DEFAULT);
$strNow = fxGetDateTime();
$strInviteFlag = fxEveAccesPromoteurHasInviteTeamColumn()
? ', com_invite_team = 1'
: '';
$blnHasConfirme = false;
$tabConfirme = $objDatabase->fxGetResults("SHOW COLUMNS FROM inscriptions_comptes LIKE 'com_confirme'");
if ($tabConfirme != null && count($tabConfirme) > 0) {
$blnHasConfirme = true;
}
$blnHasResetDate = false;
$tabResetDate = $objDatabase->fxGetResults("SHOW COLUMNS FROM inscriptions_comptes LIKE 'com_reset_date'");
if ($tabResetDate != null && count($tabResetDate) > 0) {
$blnHasResetDate = true;
}
$blnHasNoteInterne = false;
$tabNote = $objDatabase->fxGetResults("SHOW COLUMNS FROM inscriptions_comptes LIKE 'com_note_interne'");
if ($tabNote != null && count($tabNote) > 0) {
$blnHasNoteInterne = true;
}
$blnHasResetToken = false;
$tabResetTok = $objDatabase->fxGetResults("SHOW COLUMNS FROM inscriptions_comptes LIKE 'com_reset_token'");
if ($tabResetTok != null && count($tabResetTok) > 0) {
$blnHasResetToken = true;
}
$sql = "INSERT INTO inscriptions_comptes SET
com_prenom = '" . $objDatabase->fxEscape($strPrenom) . "',
com_nom = '" . $objDatabase->fxEscape($strNom) . "',
com_courriel = '" . $objDatabase->fxEscape($strCourriel) . "',
com_login = '" . $objDatabase->fxEscape($strCourriel) . "',
com_password = '" . $objDatabase->fxEscape($strPasswordHash) . "',
com_langue = '" . $objDatabase->fxEscape($strLangue) . "',
com_actif = 1,
com_creation = '" . $objDatabase->fxEscape($strNow) . "'
$strInviteFlag";
if ($blnHasConfirme) {
$sql .= ', com_confirme = 1';
}
if ($blnHasResetToken) {
$sql .= ", com_reset_token = '" . $objDatabase->fxEscape($strToken) . "'";
}
if ($blnHasResetDate) {
$sql .= ", com_reset_date = '" . $objDatabase->fxEscape($strNow) . "'";
}
if ($blnHasNoteInterne) {
$sql .= ", com_note_interne = 'MSIN-4401 invitation promoteur (par com_id "
. intval($intGrantedBy) . ")'";
}
if ($objDatabase->fxQuery($sql) === false) {
return array('state' => 'error', 'message' => 'insert');
}
$intComId = intval($objDatabase->fxGetVar(
"SELECT com_id FROM inscriptions_comptes
WHERE com_courriel = '" . $objDatabase->fxEscape($strCourriel) . "'
ORDER BY com_id DESC LIMIT 1"
));
if ($intComId <= 0) {
return array('state' => 'error', 'message' => 'insert');
}
return array(
'state' => 'success',
'com_id' => $intComId,
'reset_token' => $strToken,
);
}
/**
* MSIN-4401 — Signature du destinataire invite (évite spoof invite_com).
*/
function fxEveAccesPromoteurInviteSig($intComId, $intEveId)
{
$strSecret = defined('QR_SECRET_KEY') ? (string)QR_SECRET_KEY : 'ms1-invite';
return substr(hash_hmac('sha256', intval($intComId) . '|' . intval($intEveId), $strSecret), 0, 20);
}
function fxEveAccesPromoteurInviteSigValid($intComId, $intEveId, $strSig)
{
$strExpect = fxEveAccesPromoteurInviteSig($intComId, $intEveId);
return is_string($strSig) && $strSig !== '' && hash_equals($strExpect, $strSig);
}
/** Ajoute invite_com + invite_sig à une URL /mobile (ou absolue). */
function fxEveAccesPromoteurAppendInviteParams($strUrl, $intComId, $intEveId)
{
$strSep = (strpos($strUrl, '?') !== false) ? '&' : '?';
return $strUrl . $strSep
. 'invite_com=' . intval($intComId)
. '&invite_sig=' . rawurlencode(fxEveAccesPromoteurInviteSig($intComId, $intEveId));
}
function fxEveAccesPromoteurSendInviteMail($arrCompte, $arrEve, $arrRole, $strLangue, $blnNewAccount, $strResetToken = '')
{
global $vDomaine, $vEmail, $vClient;
if (!function_exists('fxSendMail')) {
require_once __DIR__ . '/inc_mail.php';
}
if (!function_exists('fxGetMailFormat')) {
require_once __DIR__ . '/inc_fonctions.php';
}
$strLangue = ($strLangue === 'en') ? 'en' : 'fr';
$strEveNom = fxUnescape($arrEve['eve_nom_' . $strLangue] ?? ($arrEve['eve_nom_fr'] ?? ''));
$strKit = ($strLangue === 'en')
? ($arrRole['role_label_en'] ?? $arrRole['role_label_fr'] ?? '')
: ($arrRole['role_label_fr'] ?? '');
$strPrenom = fxUnescape($arrCompte['com_prenom'] ?? '');
$strLogin = $arrCompte['com_login'] ?? $arrCompte['com_courriel'];
$intEveId = intval($arrEve['eve_id'] ?? 0);
$intComId = intval($arrCompte['com_id'] ?? 0);
$strEveParam = urlencode(base64_encode((string)$intEveId));
$strComptePath = ($strLangue === 'en') ? '/account' : '/compte';
// MSIN-4401 — Lien terrain : /mobile + identité destinataire (invite_com/sig).
$strMobileUrl = $vDomaine . '/mobile?promoteur_eve_id=' . $strEveParam;
if ($strLangue === 'en') {
$strMobileUrl = $vDomaine . '/mobile/en?promoteur_eve_id=' . $strEveParam;
}
$strMobileUrl = fxEveAccesPromoteurAppendInviteParams($strMobileUrl, $intComId, $intEveId);
// Placeholders Info (contenu HTML géré dans table info).
$arrMailVars = array(
'%prenom%' => htmlspecialchars($strPrenom, ENT_QUOTES, 'UTF-8'),
'%eve_nom%' => htmlspecialchars($strEveNom, ENT_QUOTES, 'UTF-8'),
'%kit%' => htmlspecialchars($strKit, ENT_QUOTES, 'UTF-8'),
'%login%' => htmlspecialchars((string)$strLogin, ENT_QUOTES, 'UTF-8'),
'%mobile_url%' => htmlspecialchars($strMobileUrl, ENT_QUOTES, 'UTF-8'),
);
if ($blnNewAccount && $strResetToken !== '') {
$subject = strtr(fxEveAccesPromoteurT('eve_acces_promoteur_mail_new_subject', $strLangue), $arrMailVars);
$message = strtr(fxEveAccesPromoteurT('eve_acces_promoteur_mail_new_body', $strLangue), $arrMailVars);
$attachment_label = fxEveAccesPromoteurT('eve_acces_promoteur_mail_new_cta', $strLangue);
$attachment_url = $vDomaine . $strComptePath . '/reset?t=' . urlencode($strResetToken)
. '&m=' . urlencode($arrCompte['com_courriel'])
. '&id=' . intval($arrCompte['com_id']);
} else {
$subject = strtr(fxEveAccesPromoteurT('eve_acces_promoteur_mail_existing_subject', $strLangue), $arrMailVars);
$message = strtr(fxEveAccesPromoteurT('eve_acces_promoteur_mail_existing_body', $strLangue), $arrMailVars);
$attachment_label = fxEveAccesPromoteurT('eve_acces_promoteur_mail_existing_cta', $strLangue);
$attachment_url = $strMobileUrl;
}
$body = fxGetMailFormat(false, false, true, false, $strLangue);
$body = str_replace('%logo%', $vDomaine . '/images/logoms1inscription.png', $body);
$body = str_replace('%to_mail%', $arrCompte['com_courriel'], $body);
$body = str_replace('%subject%', $subject, $body);
$body = str_replace('%message%', $message, $body);
$body = str_replace('%attachment_url%', $attachment_url, $body);
$body = str_replace('%attachment_label%', $attachment_label, $body);
fxSendMail(
$subject,
$body,
$arrCompte['com_courriel'],
trim(($arrCompte['com_prenom'] ?? '') . ' ' . ($arrCompte['com_nom'] ?? '')),
$vEmail,
$vClient,
1,
0
);
return true;
}
/**
* MSIN-4401 — invitation / assignation par promoteur.
*
* @return array{state:string,message:string,created?:bool}
*/
function fxEveAccesPromoteurInvite($intEveId, $intGrantedBy, $arrPost, $strLangue = 'fr')
{
global $objDatabase;
$intEveId = intval($intEveId);
$intGrantedBy = intval($intGrantedBy);
$strLangue = ($strLangue === 'en') ? 'en' : 'fr';
if (!fxEveAccesCanTeamInvite($intGrantedBy, $intEveId)) {
return array('state' => 'error', 'message' => fxEveAccesPromoteurT('eve_acces_promoteur_err_generic', $strLangue));
}
$strCourriel = trim(strtolower((string)($arrPost['invite_courriel'] ?? '')));
$strPrenom = trim((string)($arrPost['invite_prenom'] ?? ''));
$strNom = trim((string)($arrPost['invite_nom'] ?? ''));
$intRoleId = intval($arrPost['invite_role_id'] ?? 0);
$intJours = intval($arrPost['invite_jours'] ?? 0);
if ($intJours < 0) {
$intJours = 0;
}
if ($intJours > 3650) {
$intJours = 3650;
}
if ($strCourriel === '' || !filter_var($strCourriel, FILTER_VALIDATE_EMAIL)) {
return array('state' => 'error', 'message' => fxEveAccesPromoteurT('eve_acces_promoteur_err_courriel', $strLangue));
}
if (!fxEveAccesRoleIsDelegable($intRoleId)) {
return array('state' => 'error', 'message' => fxEveAccesPromoteurT('eve_acces_promoteur_err_kit', $strLangue));
}
$arrEve = $objDatabase->fxGetRow(
'SELECT eve_id, eve_nom_fr, eve_nom_en FROM inscriptions_evenements WHERE eve_id = ' . $intEveId . ' LIMIT 1'
);
$arrRole = $objDatabase->fxGetRow(
'SELECT role_id, role_code, role_label_fr, role_label_en FROM inscriptions_eve_roles WHERE role_id = ' . $intRoleId . ' LIMIT 1'
);
// fxGetRow peut retourner false (pas null) selon l'environnement.
if (empty($arrEve) || empty($arrRole)) {
return array('state' => 'error', 'message' => fxEveAccesPromoteurT('eve_acces_promoteur_err_generic', $strLangue));
}
$rowExisting = $objDatabase->fxGetRow(
"SELECT com_id, com_prenom, com_nom, com_courriel, com_login, com_actif
FROM inscriptions_comptes
WHERE com_courriel = '" . $objDatabase->fxEscape($strCourriel) . "'
ORDER BY com_actif DESC, com_id DESC
LIMIT 1"
);
$blnCreated = false;
$strResetToken = '';
$arrCompte = null;
if (!empty($rowExisting) && is_array($rowExisting)) {
if (intval($rowExisting['com_actif']) !== 1) {
return array('state' => 'error', 'message' => fxEveAccesPromoteurT('eve_acces_promoteur_err_inactive', $strLangue));
}
$arrCompte = $rowExisting;
if ($strPrenom !== '' && trim((string)$arrCompte['com_prenom']) === '') {
$objDatabase->fxQuery(
"UPDATE inscriptions_comptes SET com_prenom = '" . $objDatabase->fxEscape($strPrenom) . "'
WHERE com_id = " . intval($arrCompte['com_id'])
);
$arrCompte['com_prenom'] = $strPrenom;
}
if ($strNom !== '' && trim((string)$arrCompte['com_nom']) === '') {
$objDatabase->fxQuery(
"UPDATE inscriptions_comptes SET com_nom = '" . $objDatabase->fxEscape($strNom) . "'
WHERE com_id = " . intval($arrCompte['com_id'])
);
$arrCompte['com_nom'] = $strNom;
}
} else {
if ($strPrenom === '' || $strNom === '') {
return array('state' => 'error', 'message' => fxEveAccesPromoteurT('eve_acces_promoteur_err_nom', $strLangue));
}
$arrCreate = fxEveAccesPromoteurCreateCompteInvite(
$strCourriel,
$strPrenom,
$strNom,
$strLangue,
$intGrantedBy
);
if (($arrCreate['state'] ?? '') !== 'success') {
return array('state' => 'error', 'message' => fxEveAccesPromoteurT('eve_acces_promoteur_err_generic', $strLangue));
}
$blnCreated = true;
$strResetToken = (string)($arrCreate['reset_token'] ?? '');
$arrCompte = array(
'com_id' => intval($arrCreate['com_id']),
'com_prenom' => $strPrenom,
'com_nom' => $strNom,
'com_courriel' => $strCourriel,
'com_login' => $strCourriel,
);
}
$arrGrant = fxEveAccesGrant(
intval($arrCompte['com_id']),
$intEveId,
$intRoleId,
$intJours,
$intGrantedBy,
'MSIN-4401 invitation promoteur',
$blnCreated ? 'invite' : 'actif'
);
if (($arrGrant['state'] ?? '') !== 'success') {
return array(
'state' => 'error',
'message' => $arrGrant['message'] ?? fxEveAccesPromoteurT('eve_acces_promoteur_err_generic', $strLangue),
);
}
// L'accès est déjà enregistré : un échec mail ne doit pas faire planter la page.
try {
fxEveAccesPromoteurSendInviteMail($arrCompte, $arrEve, $arrRole, $strLangue, $blnCreated, $strResetToken);
} catch (Throwable $e) {
// ignore
}
return array(
'state' => 'success',
'created' => $blnCreated,
'message' => $blnCreated
? fxEveAccesPromoteurT('eve_acces_promoteur_ok_created', $strLangue)
: fxEveAccesPromoteurT('eve_acces_promoteur_ok_existing', $strLangue),
);
}
/**
* MSIN-4401 — révocation par le promoteur (team.manage ou auteur du grant / super admin).
*/
function fxEveAccesPromoteurRevoke($intEaId, $intActorComId, $intEveId)
{
global $objDatabase;
$intEaId = intval($intEaId);
$intActorComId = intval($intActorComId);
$intEveId = intval($intEveId);
$tab = $objDatabase->fxGetRow(
'SELECT * FROM inscriptions_eve_acces WHERE ea_id = ' . $intEaId . ' LIMIT 1'
);
if ($tab === null || intval($tab['eve_id']) !== $intEveId) {
return array('state' => 'error');
}
$blnOk = fxEveAccesCanTeamManage($intActorComId, $intEveId)
|| (intval($tab['ea_granted_by'] ?? 0) === $intActorComId && fxEveAccesCanTeamInvite($intActorComId, $intEveId));
if (!$blnOk) {
return array('state' => 'error');
}
return fxEveAccesRevoke($intEaId, $intActorComId);
}
/**
* MSIN-4401 — renvoi du courriel d'invitation (lien /mobile + invite_com/sig).
* Compte non activé : nouveau jeton reset + mail « créer mot de passe ».
*/
function fxEveAccesPromoteurResendMail($intEaId, $intActorComId, $intEveId, $strLangue = 'fr')
{
global $objDatabase;
$intEaId = intval($intEaId);
$intActorComId = intval($intActorComId);
$intEveId = intval($intEveId);
$strLangue = ($strLangue === 'en') ? 'en' : 'fr';
if ($intEaId <= 0 || $intEveId <= 0 || !fxEveAccesCanTeamInvite($intActorComId, $intEveId)) {
return array('state' => 'error', 'message' => fxEveAccesPromoteurT('eve_acces_promoteur_err_resend', $strLangue));
}
$tab = $objDatabase->fxGetRow(
'SELECT * FROM inscriptions_eve_acces WHERE ea_id = ' . $intEaId . ' LIMIT 1'
);
if ($tab === null || intval($tab['eve_id']) !== $intEveId) {
return array('state' => 'error', 'message' => fxEveAccesPromoteurT('eve_acces_promoteur_err_resend', $strLangue));
}
$strStatut = (string)($tab['ea_statut'] ?? '');
if ($strStatut !== 'actif' && $strStatut !== 'invite') {
return array('state' => 'error', 'message' => fxEveAccesPromoteurT('eve_acces_promoteur_err_resend', $strLangue));
}
$blnOk = fxEveAccesCanTeamManage($intActorComId, $intEveId)
|| (intval($tab['ea_granted_by'] ?? 0) === $intActorComId);
if (!$blnOk) {
return array('state' => 'error', 'message' => fxEveAccesPromoteurT('eve_acces_promoteur_err_resend', $strLangue));
}
$intComId = intval($tab['com_id'] ?? 0);
$arrCompte = $objDatabase->fxGetRow(
'SELECT * FROM inscriptions_comptes WHERE com_id = ' . $intComId . ' LIMIT 1'
);
if ($arrCompte === null || intval($arrCompte['com_actif'] ?? 0) !== 1) {
return array('state' => 'error', 'message' => fxEveAccesPromoteurT('eve_acces_promoteur_err_resend', $strLangue));
}
$arrEve = function_exists('fxGetEvenementsId') ? fxGetEvenementsId($intEveId) : null;
if (!is_array($arrEve)) {
return array('state' => 'error', 'message' => fxEveAccesPromoteurT('eve_acces_promoteur_err_resend', $strLangue));
}
$arrRole = $objDatabase->fxGetRow(
'SELECT * FROM inscriptions_eve_roles WHERE role_id = ' . intval($tab['role_id']) . ' LIMIT 1'
);
if ($arrRole === null) {
return array('state' => 'error', 'message' => fxEveAccesPromoteurT('eve_acces_promoteur_err_resend', $strLangue));
}
$strLastVisit = trim((string)($arrCompte['com_lastvisit'] ?? ''));
$blnInviteTeam = intval($arrCompte['com_invite_team'] ?? 0) === 1;
$blnPending = ($strStatut === 'invite')
|| ($blnInviteTeam && ($strLastVisit === '' || strpos($strLastVisit, '0000-00-00') === 0));
$strResetToken = '';
if ($blnPending) {
$strResetToken = uniqid('inv', true);
$strNow = function_exists('fxGetDateTime') ? fxGetDateTime() : date('Y-m-d H:i:s');
$arrSet = array();
$tabResetTok = $objDatabase->fxGetResults("SHOW COLUMNS FROM inscriptions_comptes LIKE 'com_reset_token'");
if ($tabResetTok != null && count($tabResetTok) > 0) {
$arrSet[] = "com_reset_token = '" . $objDatabase->fxEscape($strResetToken) . "'";
}
$tabResetDate = $objDatabase->fxGetResults("SHOW COLUMNS FROM inscriptions_comptes LIKE 'com_reset_date'");
if ($tabResetDate != null && count($tabResetDate) > 0) {
$arrSet[] = "com_reset_date = '" . $objDatabase->fxEscape($strNow) . "'";
}
if (count($arrSet) > 0) {
$objDatabase->fxQuery(
'UPDATE inscriptions_comptes SET ' . implode(', ', $arrSet)
. ' WHERE com_id = ' . $intComId
);
}
}
try {
fxEveAccesPromoteurSendInviteMail($arrCompte, $arrEve, $arrRole, $strLangue, $blnPending, $strResetToken);
} catch (Throwable $e) {
return array('state' => 'error', 'message' => fxEveAccesPromoteurT('eve_acces_promoteur_err_resend', $strLangue));
}
return array(
'state' => 'success',
'message' => fxEveAccesPromoteurT('eve_acces_promoteur_ok_resent', $strLangue),
);
}
/** MSIN-4401 — rendu page promoteur (invitation + liste). */
function fxEveAccesPromoteurRenderPage($intEveId, $strLangue = 'fr', $strFlash = '', $strError = '')
{
global $vDomaine;
$intEveId = intval($intEveId);
$strLangue = ($strLangue === 'en') ? 'en' : 'fr';
$strPage = ($strLangue === 'fr') ? '/compte' : '/account';
$strBackUrl = function_exists('fxGetPromoteurBackUrl')
? fxGetPromoteurBackUrl($intEveId, $strPage)
: ($vDomaine . $strPage);
$intActor = intval($_SESSION['com_id'] ?? 0);
if (!empty($_SESSION['usa_id'])) {
$intActor = intval($_SESSION['usa_id']);
}
$arrEve = function_exists('fxGetEvenementsId') ? fxGetEvenementsId($intEveId) : null;
$strEveNom = '';
if (is_array($arrEve) && !empty($arrEve['eve_nom_' . $strLangue])) {
$strEveNom = fxUnescape($arrEve['eve_nom_' . $strLangue]);
}
$arrRows = fxEveAccesListByEveId($intEveId, true);
$arrRoles = fxEveAccesGetDelegableRoles();
$strRoleLabelKey = ($strLangue === 'en') ? 'role_label_en' : 'role_label_fr';
$blnCanManage = fxEveAccesCanTeamManage($intActor, $intEveId);
$strFormAction = $vDomaine . $strPage . '/inc_tableau_eve_acces?promoteur_eve_id='
. urlencode(base64_encode($intEveId)) . '&lng=' . $strLangue;
echo '<div class="eve-acces-promoteur-page" id="module-eve-acces-promoteur">';
echo '<a class="btn btn-primary rounded-pill" id="link_back" href="' . fxEveAccesPromoteurEsc($strBackUrl) . '">';
echo '<i class="fa fa-chevron-left mr-2" aria-hidden="true"></i>';
echo fxEveAccesPromoteurEsc(afficheTexte('promoteur_back', 0));
echo '</a>';
echo '<div class="eve-acces-promoteur-head mt-4 mb-3">';
echo '<h1 class="h4 mb-1">' . fxEveAccesPromoteurEsc(fxEveAccesPromoteurT('eve_acces_promoteur_title', $strLangue));
if (function_exists('fxEveAccesPermInspectBadge')) {
echo ' ' . fxEveAccesPermInspectBadge('team.invite', 'menu');
echo ' ' . fxEveAccesPermInspectBadge('team.manage', 'menu');
}
echo '</h1>';
if ($strEveNom !== '') {
echo '<p class="text-muted mb-1">' . fxEveAccesPromoteurEsc($strEveNom) . '</p>';
}
echo '<p class="text-muted small mb-0">' . fxEveAccesPromoteurEsc(fxEveAccesPromoteurT('eve_acces_promoteur_intro', $strLangue)) . '</p>';
echo '</div>';
if ($strFlash !== '') {
echo '<div class="alert alert-success">' . fxEveAccesPromoteurEsc($strFlash) . '</div>';
}
if ($strError !== '') {
echo '<div class="alert alert-danger">' . fxEveAccesPromoteurEsc($strError) . '</div>';
}
echo '<div class="card mb-3 eve-acces-promoteur-card">';
echo '<div class="card-header d-flex align-items-center">';
echo '<i class="fa fa-user-plus mr-2" aria-hidden="true"></i>';
echo fxEveAccesPromoteurEsc(fxEveAccesPromoteurT('eve_acces_promoteur_invite_title', $strLangue));
echo '</div>';
echo '<div class="card-body">';
if (count($arrRoles) === 0) {
echo '<p class="text-muted small mb-0">' . fxEveAccesPromoteurEsc(fxEveAccesPromoteurT('eve_acces_promoteur_no_kits', $strLangue)) . '</p>';
} else {
$strAjaxUrl = $vDomaine . '/ajax_verif_courriel.php';
echo '<form method="post" action="' . fxEveAccesPromoteurEsc($strFormAction)
. '" class="eve-acces-promoteur-invite-form" id="eve_acces_invite_form"'
. ' data-ajax-url="' . fxEveAccesPromoteurEsc($strAjaxUrl) . '"'
. ' data-msg-exists="' . fxEveAccesPromoteurEsc(fxEveAccesPromoteurT('eve_acces_promoteur_check_exists', $strLangue)) . '"'
. ' data-msg-new="' . fxEveAccesPromoteurEsc(fxEveAccesPromoteurT('eve_acces_promoteur_check_new', $strLangue)) . '">';
echo '<input type="hidden" name="eve_acces_action" value="invite">';
echo '<div class="form-row">';
echo '<div class="form-group col-md-12">';
echo '<label for="invite_courriel">' . fxEveAccesPromoteurEsc(fxEveAccesPromoteurT('eve_acces_promoteur_field_courriel', $strLangue)) . '</label>';
echo '<input class="form-control rounded-0" type="email" id="invite_courriel" name="invite_courriel" required maxlength="100" autocomplete="email">';
echo '<div id="invite_courriel_feedback" class="small mt-2 mb-0" style="display:none;" role="status" aria-live="polite"></div>';
echo '</div>';
echo '</div>';
echo '<div class="form-row" id="invite_name_row" style="display:none;">';
echo '<div class="form-group col-md-6">';
echo '<label for="invite_prenom">' . fxEveAccesPromoteurEsc(fxEveAccesPromoteurT('eve_acces_promoteur_field_prenom', $strLangue))
. ' <span class="text-danger" aria-hidden="true">*</span></label>';
echo '<input class="form-control rounded-0" type="text" id="invite_prenom" name="invite_prenom" maxlength="50">';
echo '</div>';
echo '<div class="form-group col-md-6">';
echo '<label for="invite_nom">' . fxEveAccesPromoteurEsc(fxEveAccesPromoteurT('eve_acces_promoteur_field_nom', $strLangue))
. ' <span class="text-danger" aria-hidden="true">*</span></label>';
echo '<input class="form-control rounded-0" type="text" id="invite_nom" name="invite_nom" maxlength="50">';
echo '</div>';
echo '</div>';
echo '<div class="form-row">';
echo '<div class="form-group col-md-6">';
echo '<label for="invite_role_id">' . fxEveAccesPromoteurEsc(fxEveAccesPromoteurT('eve_acces_promoteur_field_kit', $strLangue)) . '</label>';
echo '<select class="form-control rounded-0" id="invite_role_id" name="invite_role_id" required>';
foreach ($arrRoles as $arrRole) {
$strLabel = trim((string)($arrRole[$strRoleLabelKey] ?? $arrRole['role_label_fr'] ?? ''));
echo '<option value="' . intval($arrRole['role_id']) . '">' . fxEveAccesPromoteurEsc($strLabel) . '</option>';
}
echo '</select>';
echo '</div>';
echo '<div class="form-group col-md-3">';
echo '<label for="invite_jours">' . fxEveAccesPromoteurEsc(fxEveAccesPromoteurT('eve_acces_promoteur_field_jours', $strLangue)) . '</label>';
echo '<input class="form-control rounded-0" type="number" id="invite_jours" name="invite_jours" min="0" max="3650" value="0">';
echo '<small class="form-text text-muted">' . fxEveAccesPromoteurEsc(fxEveAccesPromoteurT('eve_acces_promoteur_jours_help', $strLangue)) . '</small>';
echo '</div>';
echo '<div class="form-group col-md-3 d-flex align-items-end">';
echo '<button type="submit" class="btn btn-primary rounded-0 btn-block" id="invite_submit_btn">';
echo '<i class="fa fa-paper-plane mr-1" aria-hidden="true"></i>';
echo fxEveAccesPromoteurEsc(fxEveAccesPromoteurT('eve_acces_promoteur_btn_invite', $strLangue));
echo '</button>';
echo '</div>';
echo '</div>';
echo '<p class="small text-muted mb-0">'
. fxEveAccesPromoteurEsc(fxEveAccesPromoteurT('eve_acces_promoteur_invite_courriel_help', $strLangue))
. '</p>';
echo '</form>';
fxEveAccesPromoteurRenderInviteScript();
}
echo '</div></div>';
echo '<div class="card mb-3 eve-acces-promoteur-card">';
echo '<div class="card-header d-flex justify-content-between align-items-center flex-wrap">';
echo '<span><i class="fa fa-users mr-2" aria-hidden="true"></i>';
echo fxEveAccesPromoteurEsc(fxEveAccesPromoteurT('eve_acces_promoteur_list_title', $strLangue));
echo '</span>';
echo '<span class="small text-muted">' . count($arrRows) . '</span>';
echo '</div>';
echo '<div class="card-body p-0">';
echo '<div class="table-responsive mb-0">';
echo '<table class="table table-sm table-striped table-hover mb-0">';
echo '<thead class="thead-light"><tr>';
echo '<th>' . fxEveAccesPromoteurEsc(fxEveAccesPromoteurT('eve_acces_promoteur_col_compte', $strLangue)) . '</th>';
echo '<th>' . fxEveAccesPromoteurEsc(fxEveAccesPromoteurT('eve_acces_promoteur_col_kit', $strLangue)) . '</th>';
echo '<th class="text-center">' . fxEveAccesPromoteurEsc(fxEveAccesPromoteurT('eve_acces_promoteur_col_statut', $strLangue)) . '</th>';
echo '<th>' . fxEveAccesPromoteurEsc(fxEveAccesPromoteurT('eve_acces_promoteur_col_depuis', $strLangue)) . '</th>';
echo '<th>' . fxEveAccesPromoteurEsc(fxEveAccesPromoteurT('eve_acces_promoteur_col_expire', $strLangue)) . '</th>';
echo '<th></th>';
echo '</tr></thead><tbody>';
if (count($arrRows) === 0) {
echo '<tr><td colspan="6" class="text-muted px-3 py-3">'
. fxEveAccesPromoteurEsc(fxEveAccesPromoteurT('eve_acces_promoteur_empty', $strLangue))
. '</td></tr>';
} else {
foreach ($arrRows as $arrRow) {
$strCompte = trim(($arrRow['com_prenom'] ?? '') . ' ' . ($arrRow['com_nom'] ?? ''));
$strKit = trim((string)($arrRow[$strRoleLabelKey] ?? $arrRow['role_label_fr'] ?? ''));
if ($strKit === '') {
$strKit = (string)($arrRow['role_code'] ?? '');
}
$strStatut = (string)($arrRow['ea_statut'] ?? '');
$strLastVisit = trim((string)($arrRow['com_lastvisit'] ?? ''));
$blnInviteTeam = intval($arrRow['com_invite_team'] ?? 0) === 1;
// Nouveau compte invitation : en attente tant que pas activé (mdp) / jamais connecté.
$blnPending = ($strStatut === 'invite')
|| ($blnInviteTeam && ($strLastVisit === '' || strpos($strLastVisit, '0000-00-00') === 0));
$strBadge = 'secondary';
$strStatutLabel = $strStatut;
if ($blnPending) {
$strBadge = 'warning';
$strStatutLabel = fxEveAccesPromoteurT('eve_acces_promoteur_statut_pending', $strLangue);
} elseif ($strStatut === 'actif') {
$strBadge = 'success';
$strStatutLabel = fxEveAccesPromoteurT('eve_acces_promoteur_statut_actif', $strLangue);
}
$blnCanRevoke = $blnCanManage
|| (intval($arrRow['ea_granted_by'] ?? 0) === $intActor);
echo '<tr>';
echo '<td>';
echo '<div>' . fxEveAccesPromoteurEsc($strCompte) . '</div>';
echo '<div class="small text-muted">' . fxEveAccesPromoteurEsc($arrRow['com_courriel'] ?? '') . '</div>';
echo '</td>';
echo '<td>' . fxEveAccesPromoteurEsc($strKit) . '</td>';
echo '<td class="text-center"><span class="badge badge-' . $strBadge . '">'
. fxEveAccesPromoteurEsc($strStatutLabel) . '</span></td>';
echo '<td class="text-nowrap small">'
. fxEveAccesPromoteurEsc(fxEveAccesPromoteurFormatDt($arrRow['ea_created_at'] ?? '')) . '</td>';
echo '<td class="text-nowrap small">'
. fxEveAccesPromoteurEsc(fxEveAccesPromoteurFormatDt($arrRow['ea_expires_at'] ?? '')) . '</td>';
echo '<td class="text-right text-nowrap">';
if ($blnCanRevoke && intval($arrRow['com_id']) !== $intActor) {
echo '<form method="post" action="' . fxEveAccesPromoteurEsc($strFormAction) . '" class="d-inline mr-1">';
echo '<input type="hidden" name="eve_acces_action" value="resend">';
echo '<input type="hidden" name="ea_id" value="' . intval($arrRow['ea_id']) . '">';
echo '<button type="submit" class="btn btn-sm btn-outline-primary rounded-0" title="'
. fxEveAccesPromoteurEsc(fxEveAccesPromoteurT('eve_acces_promoteur_btn_resend', $strLangue)) . '">';
echo '<i class="fa fa-envelope mr-1" aria-hidden="true"></i>';
echo fxEveAccesPromoteurEsc(fxEveAccesPromoteurT('eve_acces_promoteur_btn_resend', $strLangue));
echo '</button></form>';
echo '<form method="post" action="' . fxEveAccesPromoteurEsc($strFormAction) . '" class="d-inline"';
echo ' onsubmit="return confirm(\'' . fxEveAccesPromoteurEsc(
fxEveAccesPromoteurT('eve_acces_promoteur_confirm_revoke', $strLangue)
) . '\');">';
echo '<input type="hidden" name="eve_acces_action" value="revoke">';
echo '<input type="hidden" name="ea_id" value="' . intval($arrRow['ea_id']) . '">';
echo '<button type="submit" class="btn btn-sm btn-outline-danger rounded-0">';
echo fxEveAccesPromoteurEsc(fxEveAccesPromoteurT('eve_acces_promoteur_btn_revoke', $strLangue));
echo '</button></form>';
}
echo '</td>';
echo '</tr>';
}
}
echo '</tbody></table></div></div></div>';
echo '<a class="btn btn-primary rounded-pill mt-2" href="' . fxEveAccesPromoteurEsc($strBackUrl) . '">';
echo '<i class="fa fa-chevron-left mr-2" aria-hidden="true"></i>';
echo fxEveAccesPromoteurEsc(afficheTexte('promoteur_back', 0));
echo '</a>';
echo '</div>';
}
/** MSIN-4401 — JS vérif courriel via ajax_verif_courriel.php (existant). */
function fxEveAccesPromoteurRenderInviteScript()
{
static $blnDone = false;
if ($blnDone) {
return;
}
$blnDone = true;
?>
<script>
(function () {
var form = document.getElementById('eve_acces_invite_form');
if (!form) return;
var input = document.getElementById('invite_courriel');
var feedback = document.getElementById('invite_courriel_feedback');
var nameRow = document.getElementById('invite_name_row');
var prenom = document.getElementById('invite_prenom');
var nom = document.getElementById('invite_nom');
var submitBtn = document.getElementById('invite_submit_btn');
var ajaxUrl = form.getAttribute('data-ajax-url') || '';
var msgExists = form.getAttribute('data-msg-exists') || '';
var msgNew = form.getAttribute('data-msg-new') || '';
var timer = null;
var lastChecked = '';
function setFeedback(msg, kind) {
if (!feedback) return;
if (!msg) {
feedback.style.display = 'none';
feedback.textContent = '';
feedback.className = 'small mt-2 mb-0';
return;
}
feedback.style.display = 'block';
feedback.textContent = msg;
feedback.className = 'small mt-2 mb-0 text-' + (kind || 'muted');
}
function setNameMode(mode) {
if (!nameRow || !prenom || !nom) return;
if (mode === 'required') {
nameRow.style.display = '';
prenom.required = true;
nom.required = true;
if (submitBtn) submitBtn.disabled = false;
} else {
nameRow.style.display = 'none';
prenom.required = false;
nom.required = false;
if (submitBtn) submitBtn.disabled = false;
}
}
function isValidEmail(v) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v);
}
function checkCourriel() {
var email = (input.value || '').trim().toLowerCase();
if (email === '' || !isValidEmail(email)) {
lastChecked = '';
setFeedback('', '');
setNameMode('hidden');
return;
}
if (email === lastChecked) return;
var requestEmail = email;
// Même API que la fiche compte : true = libre, false = déjà pris (actif).
var qs = 'com_courriel=' + encodeURIComponent(email);
fetch(ajaxUrl + '?' + qs, { credentials: 'same-origin' })
.then(function (r) { return r.json(); })
.then(function (libre) {
if ((input.value || '').trim().toLowerCase() !== requestEmail) return;
lastChecked = requestEmail;
if (libre === false || libre === 'false' || libre === 0) {
setFeedback(msgExists, 'success');
setNameMode('hidden');
} else {
setFeedback(msgNew, 'info');
setNameMode('required');
}
})
.catch(function () {
if ((input.value || '').trim().toLowerCase() !== requestEmail) return;
lastChecked = '';
setFeedback('', '');
setNameMode('hidden');
});
}
input.addEventListener('blur', checkCourriel);
input.addEventListener('input', function () {
if (timer) clearTimeout(timer);
timer = setTimeout(checkCourriel, 450);
});
})();
</script>
<?php
}

666
php/inc_fx_fiche_audit.php Normal file
View File

@ -0,0 +1,666 @@
<?php
/**
* MSIN-4464 — Traçabilité fiches (config whitelist + rétention + writers).
* UI fiche (« ? ») + permission promoteur : phase suivante.
*/
/**
* Même allowlist que les kits ($arrEveAccesKitEditors dans inc_settings.php).
*/
if (!function_exists('fxAdminCanEditFicheAudit')) {
function fxAdminCanEditFicheAudit()
{
return function_exists('fxAdminCanEditEveAccesKits') && fxAdminCanEditEveAccesKits();
}
}
/** Tables MSIN-4464 présentes ? */
function fxFicheAuditTablesReady()
{
global $objDatabase;
static $blnReady = null;
if ($blnReady !== null) {
return $blnReady;
}
if (!isset($objDatabase)) {
$blnReady = false;
return $blnReady;
}
$tab = $objDatabase->fxGetResults("SHOW TABLES LIKE 'inscriptions_fiche_audit_settings'");
$blnReady = ($tab != null && count($tab) > 0);
return $blnReady;
}
/**
* Catalogue connu (ordre d'affichage admin). Les clés absentes en BD sont
* proposées à l'écran et créées au premier enregistrement.
*
* @return array[]
*/
function fxFicheAuditFieldCatalog()
{
return array(
array(
'field_key' => 'par_statut_course',
'field_label_fr' => 'Statut de course',
'field_label_en' => 'Race status',
'field_tri' => 10,
),
array(
'field_key' => 'no_bib',
'field_label_fr' => 'Numéro de dossard',
'field_label_en' => 'Bib number',
'field_tri' => 20,
),
array(
'field_key' => 'no_bib_remis',
'field_label_fr' => 'Dossard remis',
'field_label_en' => 'Bib handed out',
'field_tri' => 30,
),
array(
'field_key' => 'is_cancelled',
'field_label_fr' => 'Annulation',
'field_label_en' => 'Cancellation',
'field_tri' => 40,
),
);
}
/**
* @return array{retention_days:int,retention_max_per_field:int,track_last_touch:int}
*/
function fxFicheAuditGetSettings()
{
global $objDatabase;
$arrDefault = array(
'retention_days' => 90,
'retention_max_per_field' => 10,
'track_last_touch' => 1,
);
if (!isset($objDatabase) || !fxFicheAuditTablesReady()) {
return $arrDefault;
}
$sql = 'SELECT retention_days, retention_max_per_field, track_last_touch'
. ' FROM inscriptions_fiche_audit_settings WHERE setting_id = 1 LIMIT 1';
$row = $objDatabase->fxGetRow($sql);
if ($row === null || !is_array($row)) {
return $arrDefault;
}
return array(
'retention_days' => max(1, intval($row['retention_days'])),
'retention_max_per_field' => max(1, intval($row['retention_max_per_field'])),
'track_last_touch' => (intval($row['track_last_touch']) === 1) ? 1 : 0,
);
}
/**
* Champs whitelist fusionnés avec le catalogue (actif = 0 si absent en BD).
*
* @return array[]
*/
function fxFicheAuditGetFieldsForAdmin()
{
global $objDatabase;
$arrByKey = array();
if (isset($objDatabase) && fxFicheAuditTablesReady()) {
$sql = 'SELECT field_key, field_label_fr, field_label_en, field_actif, field_tri'
. ' FROM inscriptions_fiche_audit_fields ORDER BY field_tri, field_key';
$arrRows = $objDatabase->fxGetResults($sql);
if (is_array($arrRows)) {
foreach ($arrRows as $row) {
if (!is_array($row) || empty($row['field_key'])) {
continue;
}
$arrByKey[$row['field_key']] = array(
'field_key' => (string)$row['field_key'],
'field_label_fr' => (string)$row['field_label_fr'],
'field_label_en' => (string)$row['field_label_en'],
'field_actif' => (intval($row['field_actif']) === 1) ? 1 : 0,
'field_tri' => intval($row['field_tri']),
);
}
}
}
$arrOut = array();
foreach (fxFicheAuditFieldCatalog() as $arrCat) {
$strKey = $arrCat['field_key'];
if (isset($arrByKey[$strKey])) {
$arrOut[] = $arrByKey[$strKey];
unset($arrByKey[$strKey]);
} else {
$arrOut[] = array(
'field_key' => $strKey,
'field_label_fr' => $arrCat['field_label_fr'],
'field_label_en' => $arrCat['field_label_en'],
'field_actif' => 0,
'field_tri' => intval($arrCat['field_tri']),
);
}
}
// Champs éventuels hors catalogue (déjà en BD)
foreach ($arrByKey as $row) {
$arrOut[] = $row;
}
return $arrOut;
}
/**
* Clés actives (pour usage futur des writers).
*
* @return string[]
*/
function fxFicheAuditGetActiveFieldKeys()
{
global $objDatabase;
static $arrCache = null;
if ($arrCache !== null) {
return $arrCache;
}
$arrCache = array();
if (!isset($objDatabase) || !fxFicheAuditTablesReady()) {
return $arrCache;
}
$sql = 'SELECT field_key FROM inscriptions_fiche_audit_fields'
. ' WHERE field_actif = 1 ORDER BY field_tri, field_key';
$arrRows = $objDatabase->fxGetResults($sql);
if (is_array($arrRows)) {
foreach ($arrRows as $row) {
if (!empty($row['field_key'])) {
$arrCache[] = (string)$row['field_key'];
}
}
}
return $arrCache;
}
/** @return bool */
function fxFicheAuditIsFieldActive($strFieldKey)
{
$strFieldKey = (string)$strFieldKey;
if ($strFieldKey === '') {
return false;
}
return in_array($strFieldKey, fxFicheAuditGetActiveFieldKeys(), true);
}
/**
* Acteur depuis la session (promoteur ou superadm lié).
*
* @return array{com_id:int,label:string}
*/
function fxFicheAuditResolveActor()
{
$intComId = intval($_SESSION['com_info']['com_id'] ?? ($_SESSION['com_id'] ?? 0));
$strLabel = trim(
(string)($_SESSION['com_info']['com_prenom'] ?? '')
. ' '
. (string)($_SESSION['com_info']['com_nom'] ?? '')
);
if ($strLabel === '' && $intComId > 0) {
$strLabel = 'compte#' . $intComId;
}
if ($intComId <= 0 && !empty($_SESSION['usa_id'])) {
$intUsaId = intval($_SESSION['usa_id']);
$intComId = intval($_SESSION['usa_info']['com_id'] ?? 0);
$strLabel = trim(
(string)($_SESSION['usa_info']['com_prenom'] ?? '')
. ' '
. (string)($_SESSION['usa_info']['com_nom'] ?? '')
);
if ($strLabel === '') {
$strLabel = 'admin#' . $intUsaId;
}
}
return array(
'com_id' => $intComId,
'label' => $strLabel,
);
}
/**
* MSIN-4464 — Point unique d'écriture audit après un UPDATE réussi.
* - Si champ whitelisté et valeur changée → ligne détail
* - Si track_last_touch → ligne last_touch (même hors whitelist)
*
* @param int $intParId
* @param string $strFieldKey
* @param string|null $strOld
* @param string|null $strNew
* @param string $strSource
* @param int $intEveId
*/
function fxFicheAuditRecordChange(
$intParId,
$strFieldKey,
$strOld,
$strNew,
$strSource = '',
$intEveId = 0
) {
global $objDatabase;
$intParId = intval($intParId);
$strFieldKey = preg_replace('/[^a-z0-9_]/i', '', (string)$strFieldKey);
if ($intParId <= 0 || $strFieldKey === '' || !isset($objDatabase) || !fxFicheAuditTablesReady()) {
return;
}
$strOldNorm = ($strOld === null) ? '' : trim((string)$strOld);
$strNewNorm = ($strNew === null) ? '' : trim((string)$strNew);
if ($strOldNorm === $strNewNorm) {
return;
}
$arrSettings = fxFicheAuditGetSettings();
$blnDetail = fxFicheAuditIsFieldActive($strFieldKey);
$blnLastTouch = (intval($arrSettings['track_last_touch']) === 1);
if (!$blnDetail && !$blnLastTouch) {
return;
}
if ($intEveId <= 0) {
$intEveId = intval($objDatabase->fxGetVar(
'SELECT eve_id FROM resultats_participants WHERE par_id = ' . $intParId . ' LIMIT 1'
));
}
$arrActor = fxFicheAuditResolveActor();
if ($blnDetail) {
fxFicheAuditInsertLogRow(
$intParId,
$intEveId,
$strFieldKey,
$strOldNorm,
$strNewNorm,
$arrActor,
$strSource
);
fxFicheAuditTrimField($intParId, $strFieldKey, $arrSettings);
}
if ($blnLastTouch) {
fxFicheAuditInsertLogRow(
$intParId,
$intEveId,
'last_touch',
'',
$strFieldKey,
$arrActor,
$strSource
);
fxFicheAuditTrimField($intParId, 'last_touch', $arrSettings);
}
}
/**
* MSIN-4464 — Dernière intervention seule (édition fiche, etc.), sans old→new.
*/
function fxFicheAuditRecordLastTouchOnly($intParId, $strTriggerField, $strSource = '', $intEveId = 0)
{
global $objDatabase;
$intParId = intval($intParId);
$strTriggerField = preg_replace('/[^a-z0-9_]/i', '', (string)$strTriggerField);
if ($strTriggerField === '') {
$strTriggerField = 'fiche';
}
if ($intParId <= 0 || !isset($objDatabase) || !fxFicheAuditTablesReady()) {
return;
}
$arrSettings = fxFicheAuditGetSettings();
if (intval($arrSettings['track_last_touch']) !== 1) {
return;
}
if ($intEveId <= 0) {
$intEveId = intval($objDatabase->fxGetVar(
'SELECT eve_id FROM resultats_participants WHERE par_id = ' . $intParId . ' LIMIT 1'
));
}
$arrActor = fxFicheAuditResolveActor();
fxFicheAuditInsertLogRow(
$intParId,
$intEveId,
'last_touch',
'',
$strTriggerField,
$arrActor,
$strSource
);
fxFicheAuditTrimField($intParId, 'last_touch', $arrSettings);
}
/**
* Après annulation / rétablissement sur une commande (tous les par_id du pec).
*/
function fxFicheAuditRecordCancelRestoreForPec($intPecId, $strOld, $strNew, $strSource, $intEveId = 0)
{
global $objDatabase;
$intPecId = intval($intPecId);
if ($intPecId <= 0 || !isset($objDatabase) || !fxFicheAuditTablesReady()) {
return;
}
$sql = 'SELECT par_id, eve_id FROM resultats_participants WHERE pec_id = ' . $intPecId;
$arrRows = $objDatabase->fxGetResults($sql);
if (!is_array($arrRows)) {
return;
}
foreach ($arrRows as $row) {
if (!is_array($row) || empty($row['par_id'])) {
continue;
}
$intEveRow = intval($row['eve_id'] ?? 0);
if ($intEveRow <= 0) {
$intEveRow = intval($intEveId);
}
fxFicheAuditRecordChange(
intval($row['par_id']),
'is_cancelled',
$strOld,
$strNew,
$strSource,
$intEveRow
);
}
}
/**
* Événements pour le sélecteur admin (actifs récents + ceux déjà dans les logs).
*
* @return array[]
*/
function fxFicheAuditAdminListEvents()
{
global $objDatabase;
if (!isset($objDatabase) || !fxFicheAuditTablesReady()) {
return array();
}
// Pas de LIMIT : le select admin a une recherche (bootstrap-select).
$sql = 'SELECT e.eve_id, e.eve_nom_fr, e.eve_date_debut,'
. ' (SELECT COUNT(*) FROM inscriptions_fiche_audit_log l WHERE l.eve_id = e.eve_id) AS fal_nb'
. ' FROM inscriptions_evenements e'
. ' WHERE e.eve_actif = 1'
. ' OR EXISTS (SELECT 1 FROM inscriptions_fiche_audit_log l2 WHERE l2.eve_id = e.eve_id)'
. ' ORDER BY'
. ' CASE WHEN EXISTS (SELECT 1 FROM inscriptions_fiche_audit_log lx WHERE lx.eve_id = e.eve_id) THEN 0 ELSE 1 END,'
. ' e.eve_date_debut DESC, e.eve_id DESC';
$arrRows = $objDatabase->fxGetResults($sql);
return is_array($arrRows) ? $arrRows : array();
}
/**
* Champs (hors last_touch) qui ont au moins 1 log pour ce participant.
*
* @return array<string,true>
*/
function fxFicheAuditFieldsWithLogsForPar($intParId)
{
global $objDatabase;
$intParId = intval($intParId);
$arrOut = array();
if ($intParId <= 0 || !isset($objDatabase) || !fxFicheAuditTablesReady()) {
return $arrOut;
}
$sql = 'SELECT field_key FROM inscriptions_fiche_audit_log'
. ' WHERE par_id = ' . $intParId
. " AND field_key <> 'last_touch'"
. ' GROUP BY field_key';
$arrRows = $objDatabase->fxGetResults($sql);
if (is_array($arrRows)) {
foreach ($arrRows as $row) {
if (!empty($row['field_key'])) {
$arrOut[(string)$row['field_key']] = true;
}
}
}
return $arrOut;
}
/**
* Historique détail d'un champ (fiche / AJAX), limité par la config.
*
* @return array[]
*/
function fxFicheAuditGetRecentForField($intParId, $strFieldKey, $intLimit = 0)
{
global $objDatabase;
$intParId = intval($intParId);
$strFieldKey = preg_replace('/[^a-z0-9_]/i', '', (string)$strFieldKey);
if ($intParId <= 0 || $strFieldKey === '' || $strFieldKey === 'last_touch'
|| !isset($objDatabase) || !fxFicheAuditTablesReady()) {
return array();
}
if ($intLimit <= 0) {
$arrSettings = fxFicheAuditGetSettings();
$intLimit = max(1, intval($arrSettings['retention_max_per_field']));
}
$intLimit = min(100, max(1, intval($intLimit)));
$sql = 'SELECT fal_id, field_key, old_value, new_value, changed_by_label, source, created_at'
. ' FROM inscriptions_fiche_audit_log'
. ' WHERE par_id = ' . $intParId
. " AND field_key = '" . $objDatabase->fxEscape($strFieldKey) . "'"
. ' ORDER BY created_at DESC, fal_id DESC'
. ' LIMIT ' . $intLimit;
$arrRows = $objDatabase->fxGetResults($sql);
return is_array($arrRows) ? $arrRows : array();
}
/**
* Derniers logs d'un événement (contrôle admin).
*
* @return array[]
*/
function fxFicheAuditAdminListRecentByEve($intEveId, $intLimit = 100)
{
global $objDatabase;
$intEveId = intval($intEveId);
$intLimit = intval($intLimit);
if ($intLimit !== 200) {
$intLimit = 100;
}
if ($intEveId <= 0 || !isset($objDatabase) || !fxFicheAuditTablesReady()) {
return array();
}
$sql = 'SELECT l.fal_id, l.par_id, l.eve_id, l.field_key, l.old_value, l.new_value,'
. ' l.changed_by_com_id, l.changed_by_label, l.source, l.created_at,'
. ' p.par_prenom, p.par_nom, p.no_bib'
. ' FROM inscriptions_fiche_audit_log l'
. ' LEFT JOIN resultats_participants p ON p.par_id = l.par_id'
. ' WHERE l.eve_id = ' . $intEveId
. ' ORDER BY l.created_at DESC, l.fal_id DESC'
. ' LIMIT ' . $intLimit;
$arrRows = $objDatabase->fxGetResults($sql);
return is_array($arrRows) ? $arrRows : array();
}
/**
* @param array{com_id:int,label:string} $arrActor
*/
function fxFicheAuditInsertLogRow(
$intParId,
$intEveId,
$strFieldKey,
$strOld,
$strNew,
$arrActor,
$strSource
) {
global $objDatabase;
$strSource = preg_replace('/[^a-z0-9_\-]/i', '', (string)$strSource);
if (strlen($strSource) > 32) {
$strSource = substr($strSource, 0, 32);
}
$sql = 'INSERT INTO inscriptions_fiche_audit_log'
. ' (par_id, eve_id, field_key, old_value, new_value,'
. ' changed_by_com_id, changed_by_label, source, created_at)'
. ' VALUES ('
. intval($intParId) . ', '
. (intval($intEveId) > 0 ? intval($intEveId) : 'NULL') . ', '
. "'" . $objDatabase->fxEscape($strFieldKey) . "', "
. "'" . $objDatabase->fxEscape($strOld) . "', "
. "'" . $objDatabase->fxEscape($strNew) . "', "
. (intval($arrActor['com_id']) > 0 ? intval($arrActor['com_id']) : 'NULL') . ', '
. "'" . $objDatabase->fxEscape((string)$arrActor['label']) . "', "
. ($strSource !== '' ? "'" . $objDatabase->fxEscape($strSource) . "'" : 'NULL') . ', '
. 'NOW()'
. ')';
$objDatabase->fxQuery($sql);
}
/**
* Garde les N dernières entrées + purge par âge pour par_id × field_key.
*
* @param array{retention_days:int,retention_max_per_field:int,track_last_touch:int} $arrSettings
*/
function fxFicheAuditTrimField($intParId, $strFieldKey, $arrSettings)
{
global $objDatabase;
$intParId = intval($intParId);
$strFieldKey = (string)$strFieldKey;
if ($intParId <= 0 || $strFieldKey === '' || !isset($objDatabase)) {
return;
}
$intDays = max(1, intval($arrSettings['retention_days']));
$intMax = max(1, intval($arrSettings['retention_max_per_field']));
$strEscKey = $objDatabase->fxEscape($strFieldKey);
$sqlAge = 'DELETE FROM inscriptions_fiche_audit_log'
. ' WHERE par_id = ' . $intParId
. " AND field_key = '" . $strEscKey . "'"
. ' AND created_at < DATE_SUB(NOW(), INTERVAL ' . $intDays . ' DAY)';
$objDatabase->fxQuery($sqlAge);
// MySQL : sous-requête dérivée obligatoire pour DELETE + LIMIT même table.
$sqlMax = 'DELETE FROM inscriptions_fiche_audit_log'
. ' WHERE par_id = ' . $intParId
. " AND field_key = '" . $strEscKey . "'"
. ' AND fal_id NOT IN ('
. ' SELECT fal_id FROM ('
. ' SELECT fal_id FROM inscriptions_fiche_audit_log'
. ' WHERE par_id = ' . $intParId
. " AND field_key = '" . $strEscKey . "'"
. ' ORDER BY created_at DESC, fal_id DESC'
. ' LIMIT ' . $intMax
. ' ) AS keep_rows'
. ' )';
$objDatabase->fxQuery($sqlMax);
}
/**
* @param array $arrPost
* @return array{state:string,message:string}
*/
function fxFicheAuditSaveConfig($arrPost)
{
global $objDatabase;
if (!isset($objDatabase)) {
return array('state' => 'error', 'message' => 'Base de données indisponible.');
}
if (!fxFicheAuditTablesReady()) {
return array(
'state' => 'error',
'message' => 'Tables absentes : exécuter sql/MSIN-4464-fiche-audit-config.sql',
);
}
$intDays = isset($arrPost['retention_days']) ? intval($arrPost['retention_days']) : 90;
$intMax = isset($arrPost['retention_max_per_field']) ? intval($arrPost['retention_max_per_field']) : 10;
$intLastTouch = !empty($arrPost['track_last_touch']) ? 1 : 0;
if ($intDays < 1 || $intDays > 3650) {
return array('state' => 'error', 'message' => 'Rétention (jours) : entre 1 et 3650.');
}
if ($intMax < 1 || $intMax > 100) {
return array('state' => 'error', 'message' => 'Max par champ : entre 1 et 100.');
}
$arrActif = array();
if (!empty($arrPost['field_actif']) && is_array($arrPost['field_actif'])) {
foreach ($arrPost['field_actif'] as $strKey) {
$strKey = preg_replace('/[^a-z0-9_]/i', '', (string)$strKey);
if ($strKey !== '') {
$arrActif[$strKey] = true;
}
}
}
$sqlSettings = 'INSERT INTO inscriptions_fiche_audit_settings'
. ' (setting_id, retention_days, retention_max_per_field, track_last_touch)'
. ' VALUES (1, ' . $intDays . ', ' . $intMax . ', ' . $intLastTouch . ')'
. ' ON DUPLICATE KEY UPDATE'
. ' retention_days = VALUES(retention_days),'
. ' retention_max_per_field = VALUES(retention_max_per_field),'
. ' track_last_touch = VALUES(track_last_touch)';
if (!$objDatabase->fxQuery($sqlSettings)) {
return array('state' => 'error', 'message' => 'Erreur à l\'enregistrement des réglages.');
}
foreach (fxFicheAuditGetFieldsForAdmin() as $arrField) {
$strKey = $arrField['field_key'];
$intActif = isset($arrActif[$strKey]) ? 1 : 0;
$strLabelFr = $objDatabase->fxEscape($arrField['field_label_fr']);
$strLabelEn = $objDatabase->fxEscape($arrField['field_label_en']);
$intTri = intval($arrField['field_tri']);
$sqlField = 'INSERT INTO inscriptions_fiche_audit_fields'
. ' (field_key, field_label_fr, field_label_en, field_actif, field_tri)'
. " VALUES ('" . $objDatabase->fxEscape($strKey) . "', '" . $strLabelFr . "', '"
. $strLabelEn . "', " . $intActif . ', ' . $intTri . ')'
. ' ON DUPLICATE KEY UPDATE'
. ' field_label_fr = VALUES(field_label_fr),'
. ' field_label_en = VALUES(field_label_en),'
. ' field_actif = VALUES(field_actif),'
. ' field_tri = VALUES(field_tri)';
if (!$objDatabase->fxQuery($sqlField)) {
return array('state' => 'error', 'message' => 'Erreur à l\'enregistrement du champ ' . $strKey . '.');
}
}
return array('state' => 'ok', 'message' => 'Configuration enregistrée.');
}

File diff suppressed because it is too large Load Diff

205
php/inc_fx_kc_qr.php Normal file
View File

@ -0,0 +1,205 @@
<?php
require_once __DIR__ . '/../libs/phpqrcode-master/qrlib.php';
/**
* Chemin du logo MS1 pour le centre des QR key_chain.
*/
function fxKcQrLogoPath()
{
return __DIR__ . '/../images/ms1-kc-logo.png';
}
/**
* Charge et redimensionne le logo PNG (carré de sortie $intSize).
* Repli : dessin texte si le fichier est absent.
*/
function fxKcQrCreateLogoImage($intSize)
{
$intSize = max(24, (int) $intSize);
$strLogoPath = fxKcQrLogoPath();
if (is_file($strLogoPath)) {
$imgSrc = @imagecreatefrompng($strLogoPath);
if ($imgSrc !== false) {
$intSrcW = imagesx($imgSrc);
$intSrcH = imagesy($imgSrc);
$img = imagecreatetruecolor($intSize, $intSize);
imagealphablending($img, false);
imagesavealpha($img, true);
$colTransparent = imagecolorallocatealpha($img, 0, 0, 0, 127);
imagefilledrectangle($img, 0, 0, $intSize, $intSize, $colTransparent);
imagealphablending($img, true);
// Conserve le ratio (158×180) dans le carré cible.
$fltScale = min($intSize / max(1, $intSrcW), $intSize / max(1, $intSrcH));
$intDstW = max(1, (int) round($intSrcW * $fltScale));
$intDstH = max(1, (int) round($intSrcH * $fltScale));
$intDstX = (int) floor(($intSize - $intDstW) / 2);
$intDstY = (int) floor(($intSize - $intDstH) / 2);
imagecopyresampled(
$img,
$imgSrc,
$intDstX,
$intDstY,
0,
0,
$intDstW,
$intDstH,
$intSrcW,
$intSrcH
);
imagedestroy($imgSrc);
return $img;
}
}
// Repli : dessin texte (si PNG manquant).
$img = imagecreatetruecolor($intSize, $intSize);
imagealphablending($img, true);
imagesavealpha($img, true);
$colBlack = imagecolorallocate($img, 0, 0, 0);
$colWhite = imagecolorallocate($img, 255, 255, 255);
imagefilledrectangle($img, 0, 0, $intSize, $intSize, $colBlack);
$strFont = __DIR__ . '/../fonts/arial.ttf';
if (!is_file($strFont)) {
$strFont = 'C:/Windows/Fonts/arial.ttf';
}
if (is_file($strFont)) {
$intLeftX = (int) round($intSize * 0.12);
$intRightX = (int) round($intSize * 0.52);
$intFontSmall = max(8, (int) round($intSize * 0.16));
$intFontBig = max(12, (int) round($intSize * 0.42));
imagettftext($img, $intFontSmall, 0, $intLeftX, (int) round($intSize * 0.30), $colWhite, $strFont, 'm');
imagettftext($img, (int) round($intFontSmall * 0.55), 0, $intLeftX + (int) round($intSize * 0.04), (int) round($intSize * 0.46), $colWhite, $strFont, chr(183));
imagettftext($img, $intFontSmall, 0, $intLeftX, (int) round($intSize * 0.62), $colWhite, $strFont, 's');
imagettftext($img, $intFontBig, 0, $intRightX, (int) round($intSize * 0.68), $colWhite, $strFont, '1');
} else {
imagestring($img, 5, (int) round($intSize * 0.18), (int) round($intSize * 0.28), 'm.s', $colWhite);
imagestring($img, 5, (int) round($intSize * 0.55), (int) round($intSize * 0.40), '1', $colWhite);
}
return $img;
}
/**
* Superpose le logo au centre d'un PNG QR existant.
*/
function fxKcQrOverlayLogo($strQrPath, $fltLogoRatio = 0.22)
{
$imgQr = imagecreatefrompng($strQrPath);
if ($imgQr === false) {
return false;
}
$intQrW = imagesx($imgQr);
$intQrH = imagesy($imgQr);
$intLogoSize = (int) max(24, round(min($intQrW, $intQrH) * $fltLogoRatio));
$intPad = (int) max(2, round($intLogoSize * 0.08));
$imgLogo = fxKcQrCreateLogoImage($intLogoSize);
$imgBlock = imagecreatetruecolor($intLogoSize + ($intPad * 2), $intLogoSize + ($intPad * 2));
$colWhite = imagecolorallocate($imgBlock, 255, 255, 255);
imagefilledrectangle($imgBlock, 0, 0, $intLogoSize + ($intPad * 2), $intLogoSize + ($intPad * 2), $colWhite);
imagecopy($imgBlock, $imgLogo, $intPad, $intPad, 0, 0, $intLogoSize, $intLogoSize);
$intX = (int) floor(($intQrW - $intLogoSize - ($intPad * 2)) / 2);
$intY = (int) floor(($intQrH - $intLogoSize - ($intPad * 2)) / 2);
imagecopy($imgQr, $imgBlock, $intX, $intY, 0, 0, $intLogoSize + ($intPad * 2), $intLogoSize + ($intPad * 2));
imagepng($imgQr, $strQrPath, 0);
imagedestroy($imgQr);
imagedestroy($imgLogo);
imagedestroy($imgBlock);
return true;
}
/**
* Génère un QR key_chain prêt pour l'impression.
*/
function fxKcQrGeneratePng($intParam1, $strBaseUrl, $strOutputFile, $intModuleSize = 18, $blnWithLogo = true)
{
$strUrl = rtrim($strBaseUrl, '/') . '/kc.php?param1=' . intval($intParam1);
$strDir = dirname($strOutputFile);
if (!is_dir($strDir)) {
mkdir($strDir, 0755, true);
}
QRcode::png(
$strUrl,
$strOutputFile,
QR_ECLEVEL_H,
$intModuleSize,
2
);
if ($blnWithLogo) {
fxKcQrOverlayLogo($strOutputFile);
}
return [
'param1' => intval($intParam1),
'url' => $strUrl,
'file' => $strOutputFile,
];
}
/**
* Génère une série de QR codes key_chain.
*/
function fxKcQrGenerateBatch(array $options)
{
$intStart = intval($options['start'] ?? 500);
$intCount = max(1, intval($options['count'] ?? 100));
$strBaseUrl = rtrim($options['base_url'] ?? 'https://ms1inscription.com', '/');
$strOutputDir = rtrim($options['output_dir'] ?? (__DIR__ . '/../output/kc_qr'), DIRECTORY_SEPARATOR);
$intModuleSize = max(6, intval($options['module_size'] ?? 18));
$blnWithLogo = !empty($options['with_logo']);
if (!is_dir($strOutputDir)) {
mkdir($strOutputDir, 0755, true);
}
$tabGenerated = [];
for ($i = 0; $i < $intCount; $i++) {
$intParam1 = $intStart + $i;
$strFile = $strOutputDir . DIRECTORY_SEPARATOR . 'qr_kc_' . $intParam1 . '.png';
$tabGenerated[] = fxKcQrGeneratePng($intParam1, $strBaseUrl, $strFile, $intModuleSize, $blnWithLogo);
}
$strManifest = $strOutputDir . DIRECTORY_SEPARATOR . 'manifest.csv';
$fh = fopen($strManifest, 'w');
if ($fh !== false) {
fputcsv($fh, ['param1', 'vehicule_no', 'url', 'filename']);
foreach ($tabGenerated as $row) {
$intVehicule = $row['param1'];
if (defined('KEYCHAIN_HTTP_EDIT_SHIFT')) {
$intVehicule = $row['param1'] - KEYCHAIN_HTTP_EDIT_SHIFT;
}
fputcsv($fh, [
$row['param1'],
$intVehicule,
$row['url'],
basename($row['file']),
]);
}
fclose($fh);
}
return [
'output_dir' => $strOutputDir,
'count' => count($tabGenerated),
'start' => $intStart,
'end' => $intStart + $intCount - 1,
'manifest' => $strManifest,
'files' => array_column($tabGenerated, 'file'),
];
}

View File

@ -2,13 +2,16 @@
// fonction qui vérifie si le rabais fait déjà partie du panier et retourner le pra_id (JSP)
// param : id du rabais
// créé : 2013-06-19
function fx_liste_attente_btactiver($mem_eve_id, $mem_epreuve)
function fx_liste_attente_btactiver($mem_eve_id, $mem_epreuve, $strFrom = '')
{
global $objDatabase;
$memcode_eve_id = base64_encode(urlencode($mem_eve_id));
$memcode_epreuve = base64_encode(urlencode($mem_epreuve));
$mem_lien = "/compte/inc_tableau_liste_attente?promoteur_eve_id=" . $memcode_eve_id . "&epr_id=" . $memcode_epreuve;
if ($strFrom !== '') {
$mem_lien .= '&from=' . urlencode($strFrom);
}
if (fx_liste_attente_validelisteactive($mem_epreuve)) {
?>
<a class="btn btn-primary rounded-pill" id="link_back"

View File

@ -310,6 +310,18 @@ function fxAnnulerRetablirInscription($tabEpreuve, $int_pec_id) {
if (!$qryInscription || !$qryParticipants) {
$tabRetour['state'] = 'error';
fxcreer_log("Erreur d'annulation d'inscription : " . $_SESSION['com_info']['com_courriel']);
} else {
// MSIN-4464 — audit annulation
if (!function_exists('fxFicheAuditRecordCancelRestoreForPec')) {
require_once dirname(__FILE__) . '/inc_fx_fiche_audit.php';
}
fxFicheAuditRecordCancelRestoreForPec(
intval($int_pec_id),
'0',
'1',
'annuler',
intval($tabEpreuve['eve_id'] ?? 0)
);
}
} else {
$tabRetour['action'] = 'retablir';
@ -327,6 +339,18 @@ function fxAnnulerRetablirInscription($tabEpreuve, $int_pec_id) {
if (!$qryInscription || !$qryParticipants) {
$tabRetour['state'] = 'error';
fxcreer_log("Erreur d'annulation d'inscription : " . $_SESSION['com_info']['com_courriel']);
} else {
// MSIN-4464 — audit rétablissement
if (!function_exists('fxFicheAuditRecordCancelRestoreForPec')) {
require_once dirname(__FILE__) . '/inc_fx_fiche_audit.php';
}
fxFicheAuditRecordCancelRestoreForPec(
intval($int_pec_id),
'1',
'0',
'retablir',
intval($tabEpreuve['eve_id'] ?? 0)
);
}
// ajuster qte epreuve

View File

@ -4,6 +4,7 @@ require_once('inc_fx_options.php'); // inclure les fonctions des options
require_once('inc_fx_participants.php'); // inclure les fonctions des participants
require_once('inc_fx_code_qr.php');
require_once $_SERVER['DOCUMENT_ROOT'] . '/php/inc_fx_panier_js.php';
require_once $_SERVER['DOCUMENT_ROOT'] . '/php/inc_fx_promoteur.php';
// Retourner le numéro du panier ou en générer un (JSP)
// param : langue
@ -796,11 +797,26 @@ LEFT JOIN inscriptions_panier_epreuves ee on e.epr_id=ee.epr_id
$intDiff = ($tabAncienneEpreuve['epr_nb_participants'] - $tabNouvelleEpreuve['epr_nb_participants']);
for ($i = count($tabInscription); $i > $tabNouvelleEpreuve['epr_nb_participants']; $i--) {
$sqlUpdate = "UPDATE resultats_participants SET is_cancelled = 1, par_modification_promoteur = 1 WHERE par_id = " . intval($tabInscription[$i]['par_id']);
$intParCancel = intval($tabInscription[$i]['par_id']);
$sqlUpdate = "UPDATE resultats_participants SET is_cancelled = 1, par_modification_promoteur = 1 WHERE par_id = " . $intParCancel;
$qryUpdate = $objDatabase->fxQuery($sqlUpdate);
if (!$qryUpdate)
if (!$qryUpdate) {
fxcreer_log("Erreur lors du changemenet d'épreuve par le promoteur.");
} else {
// MSIN-4464 — participants surplus annulés au changement d'épreuve
if (!function_exists('fxFicheAuditRecordChange')) {
require_once dirname(__FILE__) . '/inc_fx_fiche_audit.php';
}
fxFicheAuditRecordChange(
$intParCancel,
'is_cancelled',
'0',
'1',
'changement_epreuve',
intval($tabInscription[$i]['eve_id'] ?? ($tabEvenement['general']['eve_id'] ?? 0))
);
}
}
$sqlUpdate = "UPDATE resultats_epreuves_commandees SET pec_equipe = " . $tabNouvelleEpreuve['epr_equipe'] . " WHERE pec_id_original = " . $intpec_id;
@ -920,6 +936,17 @@ LEFT JOIN inscriptions_panier_epreuves ee on e.epr_id=ee.epr_id
if (!$qryUpdate) {
fxcreer_log('Erreur dans la mise à jour des participants.(' . $tabInscription[1]['par_courriel'] . ')');
} elseif ($strTableSqlPart === 'resultats_participants' && $strModMarker !== '') {
// MSIN-4464 — édition fiche promoteur : dernière intervention (pas le détail de chaque champ).
if (!function_exists('fxFicheAuditRecordLastTouchOnly')) {
require_once dirname(__FILE__) . '/inc_fx_fiche_audit.php';
}
fxFicheAuditRecordLastTouchOnly(
intval($intParId),
'fiche',
'fiche_edit',
intval($tabEvenement['general']['eve_id'] ?? 0)
);
}
// catégorie
if ($tabEvenement['general']['eve_categorie_auto'] == 1) {
@ -1436,6 +1463,10 @@ function fxListPanier($strNoPanier)
// sortir la couleur de l'événement
$sqlColor = "SELECT eve_couleur FROM inscriptions_evenements WHERE eve_id = " . intval($intEvenement);
$strColor = $objDatabase->fxGetVar($sqlColor);
//MSIN-4376
// sortir la switch qrcode
$sqlqrcode = "SELECT eve_code_qr FROM inscriptions_evenements WHERE eve_id = " . intval($intEvenement);
$strqrcode = $objDatabase->fxGetVar($sqlqrcode);
// si vide, couleur par défaut
if (empty($strColor)) {
@ -1459,7 +1490,7 @@ function fxListPanier($strNoPanier)
$tabRetour[12] = $tabDons;
$tabRetour[13] = $tabDonsAuto;
$tabRetour[14] = $strColor;
$tabRetour[15] = $strqrcode;
return $tabRetour;
}
@ -1493,6 +1524,7 @@ function fxShowPanier($strNoPanier, $cart, $output, $resume, $strLangue)
$tabQuestionsEpreuve = $tabPanier[11];
$tabDons = $tabPanier[12];
$strColor = $tabPanier[14];
$strcodeqr = $tabPanier[15];
$tabTotal = fxTotalPanier($strNoPanier, $strLangue);
@ -1895,6 +1927,11 @@ function fxShowPanier($strNoPanier, $cart, $output, $resume, $strLangue)
//MSIN-4323 $tabEvenements['eve_code_qr']==1 &&
//MSIN-4376
if($strcodeqr!=1 ){
if( $resume==1){
@ -1916,7 +1953,7 @@ function fxShowPanier($strNoPanier, $cart, $output, $resume, $strLangue)
}
}
$strRetour .= '</tr>';
}
@ -3031,6 +3068,16 @@ function fxMajCommande($strNoPanier, $strLangue = 'fr', $blnResend = false)
$sqlInsert = "INSERT INTO resultats_participants(eve_id, epr_id, par_prenom, par_nom, par_naissance, par_age, par_sexe, par_adresse, par_adresse2, par_ville, pro_id, par_codepostal, pay_id, par_telephone1, par_telephone1_ext, par_telephone2, par_courriel, par_capitaine, com_id, par_nom_equipe, no_panier, par_note, par_maj, no_commande, par_id_original, par_no_equipe, par_equipe, pec_id, rol_id, no_invitation, epr_objectif_dons, epr_objectif_dons_original, par_categorie_1_long_fr, par_categorie_1_long_en,par_categorie_1_court_fr, par_categorie_1_court_en, par_categorie_2_long_fr, par_categorie_2_long_en, par_categorie_2_court_fr, par_categorie_2_court_en, par_categorie_3_long_fr, par_categorie_3_long_en, par_categorie_3_court_fr, par_categorie_3_court_en, epr_objectif_distance, epr_objectif_distance_original, epr_objectif_temps, epr_objectif_temps_original, par_contact_urgence_nom, par_contact_urgence_telephone) SELECT eve_id, epr_id, par_prenom, par_nom, par_naissance, par_age, par_sexe, par_adresse, par_adresse2, par_ville, pro_id, par_codepostal, pay_id, par_telephone1, par_telephone1_ext, par_telephone2, par_courriel, par_capitaine, com_id, par_nom_equipe, no_panier, par_note, '" . fxGetDateTime() . "', '" . $no_commande . "', par_id, par_no_equipe, par_equipe, pec_id, rol_id, no_invitation, epr_objectif_dons, epr_objectif_dons, par_categorie_1_long_fr, par_categorie_1_long_en,par_categorie_1_court_fr, par_categorie_1_court_en, par_categorie_2_long_fr, par_categorie_2_long_en, par_categorie_2_court_fr, par_categorie_2_court_en, par_categorie_3_long_fr, par_categorie_3_long_en, par_categorie_3_court_fr, par_categorie_3_court_en, epr_objectif_distance, epr_objectif_distance, epr_objectif_temps, epr_objectif_temps, par_contact_urgence_nom, par_contact_urgence_telephone FROM inscriptions_panier_participants WHERE par_id = " . intval($tabParticipants[$tabEpreuves[$intCtr]['pec_id']][$intCtr2]['par_id']);
$qryInsert = $objDatabase->fxQuery($sqlInsert);
// MSIN-4379 — Étape 7 : assignation auto du dossard après création du participant (achat).
if ($qryInsert !== false) {
$intNewParId = (int)$objDatabase->fxGetLastId();
$intEprIdAutoBib = (int)$tabParticipants[$tabEpreuves[$intCtr]['pec_id']][$intCtr2]['epr_id'];
if ($intNewParId > 0 && $intEprIdAutoBib > 0) {
fxAssignAutoBibForParticipant($intEprIdAutoBib, $intNewParId);
}
}
// insérer les questions
if (isset($tabQuestions[$tabParticipants[$tabEpreuves[$intCtr]['pec_id']][$intCtr2]['par_id']])) {
$list_file = [];
@ -4073,8 +4120,15 @@ function fxMajEpreuvesCommandees($str_ancien_no_panier, $pec_id_old, $pec_id_new
$sqlUpdate = "UPDATE resultats_participants SET is_cancelled = 1, par_modification_promoteur = 1, par_maj = '" . fxGetDateTime() . "' WHERE pec_id = " . intval($pec_id_old);
$qryUpdateParticipants = $objDatabase->fxQuery($sqlUpdate);
if (!$qryUpdateEpreuve || !$qryUpdateParticipants)
if (!$qryUpdateEpreuve || !$qryUpdateParticipants) {
fxcreer_log("Erreur de désactivation de l'ancienne épreuve lors d'un upgrade ou transfert.");
} else {
// MSIN-4464 — annulation suite upgrade / transfert
if (!function_exists('fxFicheAuditRecordCancelRestoreForPec')) {
require_once dirname(__FILE__) . '/inc_fx_fiche_audit.php';
}
fxFicheAuditRecordCancelRestoreForPec(intval($pec_id_old), '0', '1', 'transfert_upgrade', 0);
}
}
unset($_SESSION['ancien_no_panier']);

View File

@ -736,14 +736,13 @@ function fxShowBlocPaiement($tabEvenement, $fltMontant, $strLangue)
';
if (floatval($fltMontant) > 0) {
if (isset($_SESSION['com_info']) && $_SESSION['com_info'] != '') {
$blnIsPromoteur = fxIsPromoteur($_SESSION['com_info']['com_id'], $tabEvenement['general']['eve_id']);
$intComId = fxPaiementSessionComId();
if ($intComId > 0) {
$blnIsPromoteur = fxPaiementCanUseModesPromoteur($intComId, $tabEvenement['general']['eve_id']);
}
// si en mode admin, récupérer les mode de paiement réservés aux admin
if (isset($_SESSION['usa_id'])) {
$blnAdmin = true;
}
$blnAdmin = fxPaiementCanUseModesAdmin();
$tabPaiements = fxGetPaiements($blnAdmin, $blnIsPromoteur, $strLangue);
@ -916,14 +915,13 @@ function fxShowBlocPaiementpaypaladvance($tabEvenement, $fltMontant, $strLangue)
';
if (floatval($fltMontant) > 0) {
if (isset($_SESSION['com_info']) && $_SESSION['com_info'] != '') {
$blnIsPromoteur = fxIsPromoteur($_SESSION['com_info']['com_id'], $tabEvenement['general']['eve_id']);
$intComId = fxPaiementSessionComId();
if ($intComId > 0) {
$blnIsPromoteur = fxPaiementCanUseModesPromoteur($intComId, $tabEvenement['general']['eve_id']);
}
// si en mode admin, récupérer les mode de paiement réservés aux admin
if (isset($_SESSION['usa_id'])) {
$blnAdmin = true;
}
$blnAdmin = fxPaiementCanUseModesAdmin();
$tabPaiements = fxGetPaiements($blnAdmin, $blnIsPromoteur, $strLangue);

File diff suppressed because it is too large Load Diff

1811
php/inc_fx_promoteur_hub.php Normal file

File diff suppressed because it is too large Load Diff

923
php/inc_fx_static_sync.php Normal file
View File

@ -0,0 +1,923 @@
<?php
require_once __DIR__ . '/inc_fx_static_sync_config.php';
/**
* Environnements comparables — configs depuis inc_settings.php uniquement.
* Source : $arrConDatabasePreProd (dev préprod sur serveur dev).
*/
function fxStaticSyncEnvironmentDefinitions() {
global $arrConDatabasePreProd, $arrConDatabaseProd;
global $arrConDatabaseClientPreProd, $arrConDatabaseClientProd;
$tabEnvs = array(
'dev_preprod' => array(
'label' => 'Dev préprod',
'config' => $arrConDatabasePreProd,
'is_source' => true,
),
'dev_prod' => array(
'label' => 'Dev prod',
'config' => $arrConDatabaseProd,
'is_source' => false,
),
);
if (!empty($arrConDatabaseClientPreProd)) {
$tabEnvs['client_preprod'] = array(
'label' => 'Client préprod',
'config' => $arrConDatabaseClientPreProd,
'is_source' => false,
);
}
if (!empty($arrConDatabaseClientProd)) {
$tabEnvs['client_prod'] = array(
'label' => 'Client prod',
'config' => $arrConDatabaseClientProd,
'is_source' => false,
);
}
return $tabEnvs;
}
/** Connexion MySQL avec timeout court, sans die() — pour BD client additionnelles. */
class clsMysqlStaticSync extends clsMysql {
function __construct($arrDatabase = array()) {
$this->last_id = 0;
$this->affected_rows = 0;
$this->con = null;
if (empty($arrDatabase['host']) || empty($arrDatabase['db'])) {
return;
}
$mysqli = mysqli_init();
if (!$mysqli) {
return;
}
mysqli_options($mysqli, MYSQLI_OPT_CONNECT_TIMEOUT, 10);
if (!@mysqli_real_connect(
$mysqli,
$arrDatabase['host'],
$arrDatabase['user'],
$arrDatabase['pass'],
$arrDatabase['db'],
3306
)) {
if (!isset($GLOBALS['ms1_static_sync_connect_errors'])) {
$GLOBALS['ms1_static_sync_connect_errors'] = array();
}
$GLOBALS['ms1_static_sync_connect_errors'][$arrDatabase['db']] = mysqli_connect_error();
return;
}
// Pas de mysqli_set_charset ici — charset serveur / table legacy (latin1, utf8, etc.).
$this->con = $mysqli;
}
}
function fxStaticSyncConnectError($strDb) {
if (empty($GLOBALS['ms1_static_sync_connect_errors'][$strDb])) {
return '';
}
return $GLOBALS['ms1_static_sync_connect_errors'][$strDb];
}
function fxStaticSyncFormatConnectionError($tabEnv) {
$strDb = isset($tabEnv['config']['db']) ? $tabEnv['config']['db'] : '';
$strMsg = $tabEnv['label'] . ' (' . $strDb . ')';
$strErr = fxStaticSyncConnectError($strDb);
if ($strErr !== '') {
$strMsg .= ' — ' . $strErr;
}
return $strMsg;
}
function fxStaticSyncConnect($arrConfig) {
if (empty($arrConfig['host']) || empty($arrConfig['db'])) {
return null;
}
$obj = new clsMysqlStaticSync($arrConfig);
return !empty($obj->con) ? $obj : null;
}
/**
* Dev : réutilise objDatabase* de inc_fonctions.php.
* Client : $arrConDatabaseClient* de inc_settings.php (si définis pour l'environnement).
*/
function fxStaticSyncOpenConnections() {
global $objDatabasePreProd, $objDatabaseProd;
global $arrConDatabaseClientPreProd, $arrConDatabaseClientProd;
$tabConnections = array(
'dev_preprod' => !empty($objDatabasePreProd) ? $objDatabasePreProd : null,
'dev_prod' => !empty($objDatabaseProd) ? $objDatabaseProd : null,
);
if (!empty($arrConDatabaseClientPreProd)) {
$tabConnections['client_preprod'] = fxStaticSyncConnect($arrConDatabaseClientPreProd);
}
if (!empty($arrConDatabaseClientProd)) {
$tabConnections['client_prod'] = fxStaticSyncConnect($arrConDatabaseClientProd);
}
return $tabConnections;
}
function fxStaticSyncTableExists($objDb, $strTable) {
if (!$objDb) {
return false;
}
$sql = "SHOW TABLES LIKE '" . $objDb->fxEscape($strTable) . "'";
$row = $objDb->fxGetRow($sql);
return !empty($row);
}
function fxStaticSyncRowBusinessKey(array $row, array $tabKeyCols) {
$tabParts = array();
foreach ($tabKeyCols as $strCol) {
$tabParts[] = isset($row[$strCol]) ? (string)$row[$strCol] : '';
}
return implode("\0", $tabParts);
}
/**
* Colonnes communes source/cible pour compare et copy (alignés).
* missing_in_target / extra_in_target : écarts de structure (hors ignore_cols).
*/
function fxStaticSyncResolveCompareColumns($objSource, $objTarget, $strTable, array $tabIgnoreCols) {
$tabSourceCols = fxStaticSyncGetTableColumnNames($objSource, $strTable);
$tabTargetCols = fxStaticSyncGetTableColumnNames($objTarget, $strTable);
$tabCommon = array();
foreach ($tabSourceCols as $strCol) {
if (in_array($strCol, $tabTargetCols, true)) {
$tabCommon[] = $strCol;
}
}
$tabCompareCols = array();
foreach ($tabCommon as $strCol) {
if (!in_array($strCol, $tabIgnoreCols, true)) {
$tabCompareCols[] = $strCol;
}
}
$tabMissingInTarget = array();
foreach ($tabSourceCols as $strCol) {
if (!in_array($strCol, $tabTargetCols, true) && !in_array($strCol, $tabIgnoreCols, true)) {
$tabMissingInTarget[] = $strCol;
}
}
$tabExtraInTarget = array();
foreach ($tabTargetCols as $strCol) {
if (!in_array($strCol, $tabSourceCols, true) && !in_array($strCol, $tabIgnoreCols, true)) {
$tabExtraInTarget[] = $strCol;
}
}
return array(
'copy_cols' => $tabCommon,
'compare_cols' => $tabCompareCols,
'missing_in_target' => $tabMissingInTarget,
'extra_in_target' => $tabExtraInTarget,
);
}
function fxStaticSyncSchemasAligned(array $tabSchema) {
return count($tabSchema['missing_in_target']) === 0
&& count($tabSchema['extra_in_target']) === 0;
}
function fxStaticSyncFormatSchemaDiff(array $tabSchema) {
$tabParts = array();
if (!empty($tabSchema['missing_in_target'])) {
$tabParts[] = 'absentes en cible : ' . implode(', ', $tabSchema['missing_in_target']);
}
if (!empty($tabSchema['extra_in_target'])) {
$tabParts[] = 'en trop en cible : ' . implode(', ', $tabSchema['extra_in_target']);
}
return implode(' · ', $tabParts);
}
function fxStaticSyncNormalizeCellValue($val) {
if ($val === null) {
return null;
}
return (string)$val;
}
function fxStaticSyncSqlOrderByKeys(array $tabKeys) {
$tabParts = array();
foreach ($tabKeys as $strCol) {
$tabParts[] = '`' . $strCol . '`';
}
return implode(',', $tabParts);
}
function fxStaticSyncGetTableRowCount($objDb, $strTable) {
if (!$objDb || !fxStaticSyncTableExists($objDb, $strTable)) {
return 0;
}
return (int)$objDb->fxGetVar('SELECT COUNT(*) FROM `' . $strTable . '`');
}
function fxStaticSyncKeyColumnsValid($objDb, $strTable, array $tabKeys) {
if (count($tabKeys) === 0) {
return false;
}
$tabCols = fxStaticSyncGetTableColumnNames($objDb, $strTable);
foreach ($tabKeys as $strCol) {
if (!in_array($strCol, $tabCols, true)) {
return false;
}
}
return true;
}
/**
* Charge les lignes — une entrée par clé métier (dernière gagne, ordre stable si ORDER BY possible).
*/
function fxStaticSyncFetchRowsByKey($objDb, array $tabDef) {
$strTable = $tabDef['table'];
$tabKeys = $tabDef['keys'];
if (!fxStaticSyncTableExists($objDb, $strTable)) {
return array('rows' => array(), 'duplicate_count' => 0, 'physical_count' => 0, 'error' => null);
}
if (!fxStaticSyncKeyColumnsValid($objDb, $strTable, $tabKeys)) {
return array('rows' => array(), 'duplicate_count' => 0, 'physical_count' => 0, 'error' => 'bad_keys');
}
$sql = 'SELECT * FROM `' . $strTable . '` ORDER BY ' . fxStaticSyncSqlOrderByKeys($tabKeys);
$tabResults = $objDb->fxGetResults($sql);
$tabKeyed = array();
$intDupes = 0;
$intPhysical = 0;
if (!is_array($tabResults)) {
return array('rows' => array(), 'duplicate_count' => 0, 'physical_count' => 0, 'error' => 'query_failed');
}
foreach ($tabResults as $row) {
if (!is_array($row)) {
continue;
}
$intPhysical++;
$strKey = fxStaticSyncRowBusinessKey($row, $tabKeys);
if (isset($tabKeyed[$strKey])) {
$intDupes++;
}
$tabKeyed[$strKey] = $row;
}
return array(
'rows' => $tabKeyed,
'duplicate_count' => $intDupes,
'physical_count' => $intPhysical,
'error' => null,
);
}
function fxStaticSyncRowContentHash(array $row, array $tabCompareCols) {
$tabData = array();
foreach ($tabCompareCols as $strCol) {
$tabData[$strCol] = array_key_exists($strCol, $row)
? fxStaticSyncNormalizeCellValue($row[$strCol])
: null;
}
ksort($tabData);
return md5(json_encode($tabData, JSON_UNESCAPED_UNICODE));
}
function fxStaticSyncLoadKeyedRows($objDb, array $tabDef, array $tabCompareCols = null) {
$tabKeys = $tabDef['keys'];
$tabIgnore = isset($tabDef['ignore_cols']) ? $tabDef['ignore_cols'] : array();
if (!fxStaticSyncTableExists($objDb, $tabDef['table'])) {
return array('error' => 'missing_table', 'rows' => array(), 'count' => 0, 'physical_count' => 0);
}
$tabFetch = fxStaticSyncFetchRowsByKey($objDb, $tabDef);
if ($tabFetch['error'] === 'bad_keys') {
return array('error' => 'bad_keys', 'rows' => array(), 'count' => 0, 'physical_count' => 0);
}
if ($tabFetch['error'] === 'query_failed') {
return array('error' => 'query_failed', 'rows' => array(), 'count' => 0, 'physical_count' => 0);
}
$tabKeyed = array();
if ($tabCompareCols === null) {
$tabCompareCols = array();
if (count($tabFetch['rows']) > 0) {
$row = reset($tabFetch['rows']);
foreach (array_keys($row) as $strCol) {
if (!in_array($strCol, $tabIgnore, true)) {
$tabCompareCols[] = $strCol;
}
}
}
}
foreach ($tabFetch['rows'] as $strKey => $row) {
$tabKeyed[$strKey] = array(
'hash' => fxStaticSyncRowContentHash($row, $tabCompareCols),
'row' => $row,
);
}
return array(
'error' => null,
'rows' => $tabKeyed,
'count' => count($tabKeyed),
'physical_count' => $tabFetch['physical_count'],
'duplicate_count' => $tabFetch['duplicate_count'],
);
}
/** Tous les environnements cibles sont syncables (dev prod, client préprod, client prod). */
function fxStaticSyncWriteAllowed($strEnvId) {
return true;
}
function fxStaticSyncSetFlash($strMessage, $strType) {
$_SESSION['ms1_static_sync_flash'] = $strMessage;
$_SESSION['ms1_static_sync_flash_type'] = $strType;
}
function fxStaticSyncPopFlash() {
if (empty($_SESSION['ms1_static_sync_flash'])) {
return null;
}
$tabFlash = array(
'message' => $_SESSION['ms1_static_sync_flash'],
'type' => !empty($_SESSION['ms1_static_sync_flash_type']) ? $_SESSION['ms1_static_sync_flash_type'] : 'info',
);
unset($_SESSION['ms1_static_sync_flash'], $_SESSION['ms1_static_sync_flash_type']);
return $tabFlash;
}
function fxStaticSyncFinalizeCompareResult(array $tabCmp) {
if (in_array($tabCmp['status'], array('missing_table', 'schema_mismatch', 'no_connection'), true)) {
return $tabCmp;
}
$intMissingKeys = count($tabCmp['missing_keys']);
$intExtraKeys = count($tabCmp['extra_keys']);
$intDiffKeys = count($tabCmp['diff_keys']);
if ($intMissingKeys === 0 && $intExtraKeys === 0 && $intDiffKeys === 0) {
$tabCmp['status'] = 'ok';
return $tabCmp;
}
$intDelta = (int)$tabCmp['target_count'] - (int)$tabCmp['source_count'];
if ($intDelta < 0) {
$tabCmp['status'] = 'missing_rows';
} elseif ($intDelta > 0) {
$tabCmp['status'] = 'extra_rows';
} else {
$tabCmp['status'] = 'content_diff';
}
return $tabCmp;
}
/**
* @return array status: ok|content_diff|missing_rows|extra_rows|missing_table|schema_mismatch|no_connection|error
*/
function fxStaticSyncCompareTable(array $tabSourceData, array $tabTargetData, array $tabSchemaInfo = array()) {
if (!empty($tabTargetData['error']) && $tabTargetData['error'] === 'missing_table') {
return array(
'status' => 'missing_table',
'source_count' => $tabSourceData['count'],
'target_count' => 0,
'missing_keys' => array(),
'extra_keys' => array(),
'diff_keys' => array(),
);
}
$tabSourceRows = $tabSourceData['rows'];
$tabTargetRows = $tabTargetData['rows'];
$tabMissing = array();
$tabDiff = array();
foreach ($tabSourceRows as $strKey => $tabRow) {
if (!isset($tabTargetRows[$strKey])) {
$tabMissing[] = $strKey;
continue;
}
if ($tabTargetRows[$strKey]['hash'] !== $tabRow['hash']) {
$tabDiff[] = $strKey;
}
}
$tabExtra = array();
foreach ($tabTargetRows as $strKey => $tabRow) {
if (!isset($tabSourceRows[$strKey])) {
$tabExtra[] = $strKey;
}
}
return fxStaticSyncFinalizeCompareResult(array(
'status' => 'pending',
'source_count' => $tabSourceData['count'],
'target_count' => $tabTargetData['count'],
'source_physical' => isset($tabSourceData['physical_count']) ? $tabSourceData['physical_count'] : $tabSourceData['count'],
'target_physical' => isset($tabTargetData['physical_count']) ? $tabTargetData['physical_count'] : $tabTargetData['count'],
'missing_keys' => $tabMissing,
'extra_keys' => $tabExtra,
'diff_keys' => $tabDiff,
'schema_missing_cols' => isset($tabSchemaInfo['missing_in_target']) ? $tabSchemaInfo['missing_in_target'] : array(),
'schema_extra_cols' => isset($tabSchemaInfo['extra_in_target']) ? $tabSchemaInfo['extra_in_target'] : array(),
));
}
function fxStaticSyncDbLastError($objDb) {
if (!$objDb || empty($objDb->con)) {
return '';
}
return mysqli_error($objDb->con);
}
/**
* Exécute une requête d'écriture — erreur remontée dans le message flash (fxQuery n'echo pas).
*/
function fxStaticSyncExecQuery($objDb, $strSql) {
if (!$objDb) {
return array('ok' => false, 'error' => 'Connexion BD invalide.');
}
$blnOk = $objDb->fxQuery($strSql);
if ($blnOk === false) {
return array(
'ok' => false,
'error' => fxStaticSyncDbLastError($objDb) ?: 'Erreur SQL inconnue.',
);
}
return array('ok' => true, 'error' => null);
}
function fxStaticSyncGetTableColumnNames($objDb, $strTable) {
if (!$objDb) {
return array();
}
$tabCols = $objDb->fxGetResults('SHOW COLUMNS FROM `' . $strTable . '`');
$tabNames = array();
if (!is_array($tabCols)) {
return array();
}
foreach ($tabCols as $row) {
if (is_array($row) && !empty($row['Field'])) {
$tabNames[] = $row['Field'];
}
}
return $tabNames;
}
function fxStaticSyncGetTableEngine($objDb, $strTable) {
if (!$objDb) {
return '';
}
$row = $objDb->fxGetRow(
"SELECT ENGINE FROM information_schema.TABLES
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = '" . $objDb->fxEscape($strTable) . "'"
);
return !empty($row['ENGINE']) ? strtoupper($row['ENGINE']) : '';
}
function fxStaticSyncCopyTable($objSource, $objTarget, $strTable, array $tabDef = null) {
if (!$objSource || !$objTarget) {
return array('ok' => false, 'message' => 'Connexion BD invalide.');
}
if (!fxStaticSyncTableExists($objSource, $strTable)) {
return array('ok' => false, 'message' => 'Table absente dans la source.');
}
if (!fxStaticSyncTableExists($objTarget, $strTable)) {
return array('ok' => false, 'message' => 'Table absente dans la cible — exécuter le DDL d\'abord.');
}
if (!$tabDef) {
return array('ok' => false, 'message' => 'Table non configurée.');
}
$tabIgnore = ($tabDef && isset($tabDef['ignore_cols'])) ? $tabDef['ignore_cols'] : array();
$tabSchema = fxStaticSyncResolveCompareColumns($objSource, $objTarget, $strTable, $tabIgnore);
if (!fxStaticSyncSchemasAligned($tabSchema)) {
return array(
'ok' => false,
'message' => 'Structures non alignées — '
. fxStaticSyncFormatSchemaDiff($tabSchema)
. '. Aligner les structures (Navicat) avant toute sync. Aucune donnée modifiée.',
);
}
$tabCopyCols = $tabSchema['copy_cols'];
if (count($tabCopyCols) === 0) {
$tabFetchCheck = fxStaticSyncFetchRowsByKey($objSource, $tabDef);
if (count($tabFetchCheck['rows']) > 0) {
return array(
'ok' => false,
'message' => 'Aucune colonne commune entre source et cible.',
);
}
}
$tabFetch = fxStaticSyncFetchRowsByKey($objSource, $tabDef);
$tabRowsToCopy = $tabFetch['rows'];
$intDupes = $tabFetch['duplicate_count'];
// Préparer les INSERT avant toute écriture destructive (évite une table vide si échec).
$tabInsertBatches = array();
$intCopied = count($tabRowsToCopy);
if (count($tabCopyCols) > 0 && count($tabRowsToCopy) > 0) {
$strColList = '`' . implode('`,`', $tabCopyCols) . '`';
$intBatchSize = 50;
$tabBatch = array();
foreach ($tabRowsToCopy as $row) {
if (!is_array($row)) {
continue;
}
$tabVals = array();
foreach ($tabCopyCols as $strCol) {
if (!array_key_exists($strCol, $row) || $row[$strCol] === null) {
$tabVals[] = 'NULL';
} else {
$tabVals[] = "'" . $objTarget->fxEscape($row[$strCol]) . "'";
}
}
$tabBatch[] = '(' . implode(',', $tabVals) . ')';
if (count($tabBatch) >= $intBatchSize) {
$tabInsertBatches[] = 'INSERT INTO `' . $strTable . '` (' . $strColList . ') VALUES ' . implode(',', $tabBatch);
$tabBatch = array();
}
}
if (count($tabBatch) > 0) {
$tabInsertBatches[] = 'INSERT INTO `' . $strTable . '` (' . $strColList . ') VALUES ' . implode(',', $tabBatch);
}
}
$blnTransactional = (fxStaticSyncGetTableEngine($objTarget, $strTable) === 'INNODB');
$blnFkDisabled = false;
$tabFkOff = fxStaticSyncExecQuery($objTarget, 'SET FOREIGN_KEY_CHECKS=0');
if (!$tabFkOff['ok']) {
return array('ok' => false, 'message' => 'FK checks : ' . $tabFkOff['error']);
}
$blnFkDisabled = true;
if ($blnTransactional) {
$tabTx = fxStaticSyncExecQuery($objTarget, 'START TRANSACTION');
if (!$tabTx['ok']) {
fxStaticSyncExecQuery($objTarget, 'SET FOREIGN_KEY_CHECKS=1');
return array('ok' => false, 'message' => 'Transaction : ' . $tabTx['error']);
}
}
// DELETE plutôt que TRUNCATE : compatible FK (InnoDB) avec checks désactivés.
$tabDelete = fxStaticSyncExecQuery($objTarget, 'DELETE FROM `' . $strTable . '`');
if (!$tabDelete['ok']) {
if ($blnTransactional) {
fxStaticSyncExecQuery($objTarget, 'ROLLBACK');
}
if ($blnFkDisabled) {
fxStaticSyncExecQuery($objTarget, 'SET FOREIGN_KEY_CHECKS=1');
}
return array('ok' => false, 'message' => 'Vidage table : ' . $tabDelete['error']);
}
foreach ($tabInsertBatches as $strSql) {
$tabInsert = fxStaticSyncExecQuery($objTarget, $strSql);
if (!$tabInsert['ok']) {
if ($blnTransactional) {
fxStaticSyncExecQuery($objTarget, 'ROLLBACK');
} else {
return array(
'ok' => false,
'message' => 'Insertion : ' . $tabInsert['error']
. ' — la table cible peut être vide (MyISAM, pas de rollback).',
);
}
if ($blnFkDisabled) {
fxStaticSyncExecQuery($objTarget, 'SET FOREIGN_KEY_CHECKS=1');
}
return array('ok' => false, 'message' => 'Insertion : ' . $tabInsert['error'] . ' (rollback effectué).');
}
}
if ($blnTransactional) {
$tabCommit = fxStaticSyncExecQuery($objTarget, 'COMMIT');
if (!$tabCommit['ok']) {
fxStaticSyncExecQuery($objTarget, 'ROLLBACK');
if ($blnFkDisabled) {
fxStaticSyncExecQuery($objTarget, 'SET FOREIGN_KEY_CHECKS=1');
}
return array('ok' => false, 'message' => 'Commit : ' . $tabCommit['error']);
}
}
if ($blnFkDisabled) {
fxStaticSyncExecQuery($objTarget, 'SET FOREIGN_KEY_CHECKS=1');
}
$strMessage = $intCopied . ' ligne(s) copiée(s).';
if ($intDupes > 0) {
$strMessage .= ' (' . $intDupes . ' doublon(s) ignoré(s) en source — même clé métier.)';
}
return array(
'ok' => true,
'message' => $strMessage,
'rows' => $intCopied,
);
}
function fxStaticSyncStatusLabel($strStatus, array $tabCmp = null) {
switch ($strStatus) {
case 'ok':
return 'Données alignées';
case 'missing_table':
return 'Table absente';
case 'schema_mismatch':
return 'Structures non alignées';
case 'no_connection':
return 'Connexion impossible';
case 'error':
return 'Erreur lecture';
}
if (is_array($tabCmp)) {
$intDelta = (int)$tabCmp['target_count'] - (int)$tabCmp['source_count'];
if ($intDelta < 0) {
return 'Il en manque ' . abs($intDelta);
}
if ($intDelta > 0) {
return 'Il y en a ' . $intDelta . ' en trop';
}
return 'Contenu différent';
}
switch ($strStatus) {
case 'content_diff':
return 'Contenu différent';
case 'missing_rows':
return 'Il en manque';
case 'extra_rows':
return 'Il y en a en trop';
default:
return 'Erreur';
}
}
function fxStaticSyncStatusClass($strStatus) {
switch ($strStatus) {
case 'ok':
return 'ss-ok';
case 'content_diff':
case 'missing_rows':
return 'ss-warn';
case 'extra_rows':
return 'ss-bad';
case 'missing_table':
case 'no_connection':
return 'ss-muted';
case 'schema_mismatch':
return 'ss-schema';
case 'error':
return 'ss-bad';
default:
return 'ss-bad';
}
}
function fxStaticSyncFormatKeyPreview($strKey) {
$strKey = str_replace("\0", ' · ', $strKey);
if (strlen($strKey) > 120) {
return substr($strKey, 0, 117) . '…';
}
return $strKey;
}
function fxStaticSyncEnsureCsrfToken() {
if (empty($_SESSION['ms1_static_sync_csrf'])) {
$_SESSION['ms1_static_sync_csrf'] = bin2hex(random_bytes(16));
}
return $_SESSION['ms1_static_sync_csrf'];
}
function fxStaticSyncValidateCsrf($strToken) {
return !empty($_SESSION['ms1_static_sync_csrf'])
&& hash_equals($_SESSION['ms1_static_sync_csrf'], (string)$strToken);
}
function fxStaticSyncFindTableDef($strTable) {
foreach (fxStaticSyncTableDefinitions() as $tabDef) {
if ($tabDef['table'] === $strTable) {
return $tabDef;
}
}
return null;
}
function fxStaticSyncFindEnvDef($strEnvId) {
$tabEnvs = fxStaticSyncEnvironmentDefinitions();
return isset($tabEnvs[$strEnvId]) ? $tabEnvs[$strEnvId] : null;
}
function fxStaticSyncCompareEnvPair($objSource, $objTarget, array $tabDef) {
$tabIgnore = isset($tabDef['ignore_cols']) ? $tabDef['ignore_cols'] : array();
if (!fxStaticSyncTableExists($objTarget, $tabDef['table'])) {
$tabSourceData = fxStaticSyncLoadKeyedRows($objSource, $tabDef);
return array(
'status' => 'missing_table',
'source_count' => $tabSourceData['count'],
'target_count' => 0,
'missing_keys' => array(),
'extra_keys' => array(),
'diff_keys' => array(),
'schema_missing_cols' => array(),
'schema_extra_cols' => array(),
);
}
$tabSchema = fxStaticSyncResolveCompareColumns($objSource, $objTarget, $tabDef['table'], $tabIgnore);
if (!fxStaticSyncSchemasAligned($tabSchema)) {
$tabSourceData = fxStaticSyncLoadKeyedRows($objSource, $tabDef);
$tabTargetData = fxStaticSyncLoadKeyedRows($objTarget, $tabDef);
return array(
'status' => 'schema_mismatch',
'source_count' => $tabSourceData['count'],
'target_count' => $tabTargetData['count'],
'missing_keys' => array(),
'extra_keys' => array(),
'diff_keys' => array(),
'schema_missing_cols' => $tabSchema['missing_in_target'],
'schema_extra_cols' => $tabSchema['extra_in_target'],
);
}
$tabSourceData = fxStaticSyncLoadKeyedRows($objSource, $tabDef, $tabSchema['compare_cols']);
$tabTargetData = fxStaticSyncLoadKeyedRows($objTarget, $tabDef, $tabSchema['compare_cols']);
if (!empty($tabSourceData['error']) || !empty($tabTargetData['error'])) {
return array(
'status' => 'error',
'source_count' => isset($tabSourceData['count']) ? $tabSourceData['count'] : 0,
'target_count' => isset($tabTargetData['count']) ? $tabTargetData['count'] : 0,
'missing_keys' => array(),
'extra_keys' => array(),
'diff_keys' => array(),
'schema_missing_cols' => array(),
'schema_extra_cols' => array(),
);
}
return fxStaticSyncCompareTable($tabSourceData, $tabTargetData, $tabSchema);
}
function fxStaticSyncBuildReport($tabConnections) {
$tabDefs = fxStaticSyncTableDefinitions();
$tabEnvs = fxStaticSyncEnvironmentDefinitions();
$objSource = $tabConnections['dev_preprod'];
$tabReport = array();
foreach ($tabDefs as $tabDef) {
$tabSourceData = fxStaticSyncLoadKeyedRows($objSource, $tabDef);
$tabRow = array(
'def' => $tabDef,
'source_count' => $tabSourceData['count'],
'source_physical' => $tabSourceData['physical_count'],
'source_dupes' => $tabSourceData['duplicate_count'],
'envs' => array(),
);
foreach ($tabEnvs as $strEnvId => $tabEnv) {
if (!empty($tabEnv['is_source'])) {
continue;
}
$objTarget = isset($tabConnections[$strEnvId]) ? $tabConnections[$strEnvId] : null;
if (!$objTarget) {
$tabRow['envs'][$strEnvId] = array(
'status' => 'no_connection',
'source_count' => $tabSourceData['count'],
'target_count' => 0,
'missing_keys' => array(),
'extra_keys' => array(),
'diff_keys' => array(),
'schema_missing_cols' => array(),
'schema_extra_cols' => array(),
);
continue;
}
$tabRow['envs'][$strEnvId] = fxStaticSyncCompareEnvPair($objSource, $objTarget, $tabDef);
}
$tabReport[] = $tabRow;
}
return $tabReport;
}
function fxStaticSyncGetDiffDetail($strTable, $strEnvId, $tabConnections) {
$tabDef = fxStaticSyncFindTableDef($strTable);
$tabEnv = fxStaticSyncFindEnvDef($strEnvId);
if (!$tabDef || !$tabEnv || !empty($tabEnv['is_source'])) {
return null;
}
$objSource = $tabConnections['dev_preprod'];
$objTarget = $tabConnections[$strEnvId];
if (!$objSource || !$objTarget) {
return null;
}
$tabCompare = fxStaticSyncCompareEnvPair($objSource, $objTarget, $tabDef);
$tabIgnore = isset($tabDef['ignore_cols']) ? $tabDef['ignore_cols'] : array();
$tabSchema = fxStaticSyncResolveCompareColumns($objSource, $objTarget, $tabDef['table'], $tabIgnore);
$tabSourceData = fxStaticSyncLoadKeyedRows($objSource, $tabDef, $tabSchema['compare_cols']);
$tabTargetData = fxStaticSyncLoadKeyedRows($objTarget, $tabDef, $tabSchema['compare_cols']);
$tabDetail = array(
'compare' => $tabCompare,
'samples' => array(),
);
$tabKeysToShow = array_slice(
array_merge($tabCompare['missing_keys'], $tabCompare['diff_keys'], $tabCompare['extra_keys']),
0,
30
);
foreach ($tabKeysToShow as $strKey) {
$tabSample = array(
'key' => $strKey,
'type' => in_array($strKey, $tabCompare['extra_keys'], true) ? 'extra'
: (in_array($strKey, $tabCompare['missing_keys'], true) ? 'missing' : 'diff'),
'source' => isset($tabSourceData['rows'][$strKey]['row']) ? $tabSourceData['rows'][$strKey]['row'] : null,
'target' => isset($tabTargetData['rows'][$strKey]['row']) ? $tabTargetData['rows'][$strKey]['row'] : null,
);
if ($tabDef['table'] === 'info' && $tabSample['type'] === 'diff') {
$tabSample['source_texte'] = isset($tabSample['source']['info_texte']) ? $tabSample['source']['info_texte'] : '';
$tabSample['target_texte'] = isset($tabSample['target']['info_texte']) ? $tabSample['target']['info_texte'] : '';
}
$tabDetail['samples'][] = $tabSample;
}
return $tabDetail;
}

View File

@ -0,0 +1,133 @@
<?php
/**
* Liste des tables statiques pour sync_static_db.php.
* Connexions BD : inc_settings.php uniquement ($arrConDatabasePreProd, Prod, Client*).
*/
/**
* Tables statiques / programmation — source de vérité : devinscription_preprod.
* Ajouter ici toute nouvelle table de référence à synchroniser.
*
* keys : clé métier (identifiant logique de la ligne)
* ignore_cols : colonnes exclues du hash de contenu (PK auto, timestamps)
*/
function fxStaticSyncTableDefinitions() {
return array(
array(
'table' => 'info',
'label' => 'Textes UI (info)',
'keys' => array('info_clef', 'info_langue', 'info_prg'),
'ignore_cols' => array('pk_info', 'info_creation', 'info_maj'),
),
array(
'table' => 'doc_mod',
'label' => 'Documentation — modules',
'keys' => array('doc_mod_clef', 'doc_mod_prg'),
'ignore_cols' => array('pk_doc_mod', 'doc_mod_creation', 'doc_mod_maj'),
),
array(
'table' => 'doc_page',
'label' => 'Documentation — pages',
'keys' => array('doc_page_clef', 'doc_langue', 'doc_prg'),
'ignore_cols' => array('pk_doc_page', 'doc_creation', 'doc_maj'),
),
array(
'table' => 'doc_anchor',
'label' => 'Documentation — ancres',
'keys' => array('doc_anchor_clef', 'doc_prg'),
'ignore_cols' => array('pk_doc_anchor', 'doc_creation'),
),
array(
'table' => 'inscriptions_eve_permissions',
'label' => 'Accès v2 — permissions',
'keys' => array('perm_key'),
'ignore_cols' => array(),
),
array(
'table' => 'inscriptions_eve_roles',
'label' => 'Accès v2 — rôles',
'keys' => array('role_code'),
'ignore_cols' => array('role_id', 'role_created_at', 'role_updated_at'),
),
array(
'table' => 'inscriptions_eve_role_permissions',
'label' => 'Accès v2 — matrice rôles',
'keys' => array('role_id', 'perm_key'),
'ignore_cols' => array(),
),
array(
'table' => 'inscriptions_pages',
'label' => 'Pages promoteur',
'keys' => array('pag_code'),
'ignore_cols' => array('pag_id', 'pag_maj'),
),
array(
'table' => 'inscriptions_pays',
'label' => 'Pays',
'keys' => array('pay_iso'),
'ignore_cols' => array('pay_id'),
),
array(
'table' => 'inscriptions_provinces',
'label' => 'Provinces',
'keys' => array('pro_iso'),
'ignore_cols' => array('pro_id'),
),
array(
'table' => 'modes_remboursements',
'label' => 'Modes remboursement',
'keys' => array('mr_id'),
'ignore_cols' => array(),
),
array(
'table' => 'types_remboursements',
'label' => 'Types remboursement',
'keys' => array('tr_id'),
'ignore_cols' => array(),
),
array(
'table' => 'adm_menus',
'label' => 'ADM — menus',
'keys' => array('men_id'),
'ignore_cols' => array(),
),
array(
'table' => 'adm_forms',
'label' => 'ADM — formulaires',
'keys' => array('for_id'),
'ignore_cols' => array(),
),
array(
'table' => 'adm_labels',
'label' => 'ADM — labels',
'keys' => array('lab_id'),
'ignore_cols' => array(),
),
array(
'table' => 'adm_messages',
'label' => 'ADM — messages',
'keys' => array('mes_id'),
'ignore_cols' => array(),
),
array(
'table' => 'config_query',
'label' => 'Config — requêtes',
'keys' => array('query_clef'),
'ignore_cols' => array('pk_query'),
),
array(
'table' => 'config_tableau',
'label' => 'Config — tableaux',
'keys' => array('clef'),
'ignore_cols' => array('id'),
),
array(
'table' => 'regles_niveau',
'label' => 'Règles niveau',
'keys' => array('reg_id'),
'ignore_cols' => array(),
),
);
}

View File

@ -0,0 +1,61 @@
<?php
/**
* Panneau visuel V2 superadmin — zone teal + boîte blanche + en-tête badge V2.
* MSIN — aligné sur la sidebar « Espace promoteur & outils ».
*/
function fxSuperadmV2PanelOpen($strTitle, $arrOpts = array())
{
$strTitle = htmlspecialchars((string)$strTitle, ENT_QUOTES, 'UTF-8');
$strCollapseId = isset($arrOpts['collapse_id']) ? preg_replace('/[^a-zA-Z0-9_-]/', '', (string)$arrOpts['collapse_id']) : '';
$blnExpanded = !empty($arrOpts['expanded']);
$strHeaderExtra = isset($arrOpts['header_extra']) ? (string)$arrOpts['header_extra'] : '';
$strZoneClass = isset($arrOpts['zone_class']) ? (string)$arrOpts['zone_class'] : 'superadm-content-v2-zone';
$strZoneId = isset($arrOpts['zone_id']) ? preg_replace('/[^a-zA-Z0-9_-]/', '', (string)$arrOpts['zone_id']) : '';
$blnCollapsible = ($strCollapseId !== '');
$strZoneIdAttr = ($strZoneId !== '') ? ' id="' . htmlspecialchars($strZoneId, ENT_QUOTES, 'UTF-8') . '"' : '';
echo '<div class="' . htmlspecialchars($strZoneClass, ENT_QUOTES, 'UTF-8') . '"' . $strZoneIdAttr . '>';
echo '<div class="superadm-v2-panel">';
if ($blnCollapsible) {
$strAria = $blnExpanded ? 'true' : 'false';
echo '<button type="button" class="superadm-v2-panel__header superadm-v2-panel__header--toggle"'
. ' data-toggle="collapse" data-target="#' . htmlspecialchars($strCollapseId, ENT_QUOTES, 'UTF-8') . '"'
. ' aria-expanded="' . $strAria . '" aria-controls="' . htmlspecialchars($strCollapseId, ENT_QUOTES, 'UTF-8') . '">';
} else {
echo '<div class="superadm-v2-panel__header">';
}
echo '<span class="superadm-v2-panel__badge">V2</span>';
echo '<span class="superadm-v2-panel__title">' . $strTitle . '</span>';
if ($strHeaderExtra !== '') {
echo $strHeaderExtra;
}
if ($blnCollapsible) {
echo '<span class="superadm-v2-panel__toggle" aria-hidden="true">';
echo '<i class="fa fa-chevron-down superadm-v2-panel__chevron" aria-hidden="true"></i>';
echo '<span class="superadm-v2-panel__toggle-label">Afficher / Masquer</span>';
echo '</span>';
echo '</button>';
echo '<div class="collapse' . ($blnExpanded ? ' show' : '') . '" id="' . htmlspecialchars($strCollapseId, ENT_QUOTES, 'UTF-8') . '">';
} else {
// Fermer le header : sinon le body reste enfant flex du header (colonne teal vide + contraste cassé).
echo '</div>';
}
echo '<div class="superadm-v2-panel__body">';
}
function fxSuperadmV2PanelClose($arrOpts = array())
{
$strCollapseId = isset($arrOpts['collapse_id']) ? preg_replace('/[^a-zA-Z0-9_-]/', '', (string)$arrOpts['collapse_id']) : '';
echo '</div>';
if ($strCollapseId !== '') {
echo '</div>';
}
echo '</div>';
echo '</div>';
}

View File

@ -114,7 +114,10 @@ if ($tabMaintenance != null) {
</div>
</footer>
<?php echo $vGoogleAnalytics; ?>
<?php if (fxCookiesAllowAnalytics()) {
echo $vGoogleAnalytics;
} ?>
<?php if (fxCookiesAllowMarketing()) { ?>
<script>
window.fbAsyncInit = function() {
FB.init({
@ -133,6 +136,7 @@ if ($tabMaintenance != null) {
fjs.parentNode.insertBefore(js, fjs);
}(document, 'script', 'facebook-jssdk'));
</script>
<?php } ?>
<script type="text/javascript">
$(function() {
var $preloader = $('#preloader');

View File

@ -7,14 +7,106 @@
* Constantes *
*
**************/
define('_VERSION_CODE', '4.72.39');
define('_DATE_CODE', '2026-03-09');
define('_VERSION_CODE', '4.72.843');
define('_DATE_CODE', '2026-07-21');
//MSIN-4290
define('QR_SECRET_KEY', 'ms1_qr_2026_cle_secrete_longue_et_fixe');
// MSIN-4401 — OCR dossard : Google Cloud Vision (TEXT_DETECTION).
// Sans cette cle, le scan cloud est desactive (pas de Tesseract hasardeux sur mobile).
// Creer une cle API Vision (restreindre par IP / referer) et coller ici.
if (!defined('MSIN_BIB_OCR_VISION_API_KEY')) {
define('MSIN_BIB_OCR_VISION_API_KEY', 'AIzaSyCodKZxVzp8MAWjLqC5uaKM9tvZjfsxzGo');
}
// Outil temporaire ms1_kc_set.php — modifier key_chain_http (lien /kc/{pk}/)
// KEYCHAIN_HTTP_EDIT_SECRET : paramètre ?k= dans l'URL (mot de passe d'accès à la page)
// KEYCHAIN_HTTP_EDIT_SHIFT : ajouté au numéro saisi (ex. 8 → pk 108 en base)
// KEYCHAIN_HTTP_EDIT_ALLOWED_COM_IDS : com_id autorisés (virgules) pour /ms1_kc_set.php sans ?k=
define('KEYCHAIN_HTTP_EDIT_SECRET', 'ms1_kc_http_edit_2026_c7e4a91f3b2d8e0f6a5c4b3d2e1f0a9');
define('KEYCHAIN_HTTP_EDIT_SHIFT', 100);
define('KEYCHAIN_HTTP_EDIT_ALLOWED_COM_IDS', '');
// MSIN-4401 — Qui peut créer / modifier / supprimer les définitions de kits (pas l'assignation).
// MSIN-4464 — Même liste pour la config traçabilité fiche (whitelist + rétention).
// Valeurs = com_courriel ou com_login (session superadm usa_info).
$arrEveAccesKitEditors = array(
'stephan@progiweb.ca',
);
/**************************************
*
* Profils connexion BD — source unique *
*
**************************************/
$tabConnClientPreprod = array(
'host' => 'localhost',
'user' => 'ms1incription_bd',
'pass' => '3+~=,y=eV)M;',
'db' => 'ms1incription_preprod',
);
$tabConnClientProd = array(
'host' => 'localhost',
'user' => 'ms1incription_bd',
'pass' => '3+~=,y=eV)M;',
'db' => 'ms1incription_prod',
);
$tabConnDevPreprod = array(
'host' => 'localhost',
'user' => 'devinscription_bd',
'pass' => 'x$@_v0-l_I((',
'db' => 'devinscription_preprod',
);
$tabConnDevProd = array(
'host' => 'localhost',
'user' => 'devinscription_bd',
'pass' => 'x$@_v0-l_I((',
'db' => 'devinscription_prod',
);
$tabConnCrmClient = array(
'host' => 'localhost',
'user' => 'ms1crm_bd',
'pass' => 'fPDJ,][ZGPwL',
'db' => 'ms1crm_prod',
);
$tabConnCrmDev = array(
'host' => 'localhost',
'user' => 'devcrmms1_bd',
'pass' => '.vB==c$s_Yk7',
'db' => 'devcrmms1_dev',
);
/**
* Branche le site sur un profil préprod + prod (même host/user/pass).
* $strMainRole : 'preprod' ou 'prod' → $strDatabaseMain
*/
function fxSettingsUseDbProfiles($tabPreprod, $tabProd, $strMainRole = 'preprod') {
global $strDatabaseHost, $strDatabaseUser, $strDatabasePass;
global $strDatabaseMain, $strDatabasePreProd, $strDatabaseProd;
$strDatabaseHost = $tabPreprod['host'];
$strDatabaseUser = $tabPreprod['user'];
$strDatabasePass = $tabPreprod['pass'];
$strDatabasePreProd = $tabPreprod['db'];
$strDatabaseProd = $tabProd['db'];
$strDatabaseMain = ($strMainRole === 'prod') ? $tabProd['db'] : $tabPreprod['db'];
}
function fxSettingsUseCrmProfile($tabCrm) {
global $strDatabaseHostcrm, $strDatabaseUsercrm, $strDatabasePasscrm, $strDatabaseMaincrm;
$strDatabaseHostcrm = $tabCrm['host'];
$strDatabaseUsercrm = $tabCrm['user'];
$strDatabasePasscrm = $tabCrm['pass'];
$strDatabaseMaincrm = $tabCrm['db'];
}
/**********************
*
* a faire remettre avec le nom de domaine final
* Variables globales *
*
**********************/
@ -38,6 +130,42 @@ if (isset($_SESSION['usa_id'])) {
$strConAdmin = ' - Connecté en <span class="text-uppercase">admin</span> (' . $_SESSION['usa_info']['com_login'] . ')';
}
/**
* MSIN API ChronoTrack — credentials par environnement (inc_settings.php).
* Valeurs par défaut ci-dessous (utilisées par ms1inscriptiondev / préprod dev).
* Les autres blocs switch peuvent remplacer $tabChronotrackApi entièrement.
*/
function fxSettingsApplyChronotrackApi($tabCfg) {
if (defined('MSIN_API_CHRONOTRACK_BASE_URL')) {
return;
}
$strBaseUrl = (string)($tabCfg['base_url'] ?? 'https://api.chronotrack.com');
define('MSIN_API_CHRONOTRACK_BASE_URL', $strBaseUrl);
define('MSIN_API_CHRONOTRACK_CLIENT_ID', (string)($tabCfg['client_id'] ?? ''));
define('MSIN_API_CHRONOTRACK_CLIENT_SECRET', (string)($tabCfg['client_secret'] ?? ''));
define('MSIN_API_CHRONOTRACK_SERVICE_USER', (string)($tabCfg['service_user'] ?? ''));
define('MSIN_API_CHRONOTRACK_SERVICE_PASS', (string)($tabCfg['service_pass'] ?? ''));
if (!defined('MSIN_API_CHRONOTRACK_TOKEN_KEY')) {
define(
'MSIN_API_CHRONOTRACK_TOKEN_KEY',
(strpos($strBaseUrl, 'api-test') !== false) ? 'test' : 'prod'
);
}
}
// MSIN API ChronoTrack — true = api-test (QA, reset nocturne). Nécessite client_id enregistré côté CT test.
$blnChronotrackApiUseTestEndpoint = false;
$strChronotrackApiBaseUrl = $blnChronotrackApiUseTestEndpoint
? 'https://api-test.chronotrack.com'
: 'https://api.chronotrack.com';
$tabChronotrackApi = array(
'base_url' => $strChronotrackApiBaseUrl,
'client_id' => '047dcfdd',
'client_secret' => '5f14f8cb103316bfdf0558d1a6d0f20769603dde',
'service_user' => 'leith.s@ms1timing.com',
'service_pass' => 'Unimog22',
);
/**************************************
*
@ -45,6 +173,9 @@ if (isset($_SESSION['usa_id'])) {
*
**************************************/
$vblnEnvironementPreProd = false;
$vblnEnvironementDev = false;
$arrConDatabaseClientPreProd = null;
$arrConDatabaseClientProd = null;
switch ($strHost) {
@ -56,7 +187,6 @@ switch ($strHost) {
case "preprod.myinterne.com":
case "www.preprod.myinterne.com":
// variables
$vGoogleAnalytics = "";
$vblnEnvironementDev = false;
$blnShowErrors = false;
@ -70,18 +200,17 @@ switch ($strHost) {
</div>
';
$strDatabaseHost = 'localhost';
$strDatabaseUser = 'ms1incription_bd';
$strDatabasePass = '3+~=,y=eV)M;';
$strDatabaseMain = 'ms1incription_preprod';
$strDatabaseProd = 'ms1incription_prod';
$strDatabasePreProd = 'ms1incription_preprod';
fxSettingsUseDbProfiles($tabConnClientPreprod, $tabConnClientProd, 'preprod');
fxSettingsUseCrmProfile($tabConnCrmClient);
// crm
$strDatabaseHostcrm = 'localhost';
$strDatabaseUsercrm = 'ms1crm_bd';
$strDatabasePasscrm = 'fPDJ,][ZGPwL';
$strDatabaseMaincrm = 'ms1crm_prod';
// MSIN API ChronoTrack — remplir credentials par environnement
$tabChronotrackApi = array(
'base_url' => 'https://api.chronotrack.com',
'client_id' => '047dcfdd',
'client_secret' => '',
'service_user' => '',
'service_pass' => '',
);
$arrConFTP = array(
'host' => '127.0.0.1',
@ -91,20 +220,16 @@ switch ($strHost) {
'path_preprod'=> '/home/ms1incription/public_html/preprod'
);
// valide si preprod
$isPreprod = (strpos($strHost, 'preprod.') !== false);
if (!$isPreprod) {
// PROD
$favicon = 'favicon.ico';
$faviconadm = 'favicon-rouge.ico';
$strDatabaseMain = $strDatabaseProd;
fxSettingsUseDbProfiles($tabConnClientPreprod, $tabConnClientProd, 'prod');
$vblnEnvironementDev = false;
$vGoogleAnalytics = "";
// paypal prod
$vPaypaladv = true;
$vPaypaladv_SANDBOX = false;
$vPaypal_devise = 'CAD';
@ -143,14 +268,12 @@ switch ($strHost) {
} else {
// PREPROD
$favicon = 'favicon-jaune.ico';
$faviconadm = 'favicon-orange.ico';
$vblnEnvironementPreProd = true;
$vblnEnvironementDev = true;
// paypal sandbox
$vPaypaladv = true;
$vPaypaladv_SANDBOX = true;
$vPaypal_devise = 'CAD';
@ -172,126 +295,13 @@ switch ($strHost) {
break;
/****************************
* Environnement PRODUCTION avant revemp paypal *
****************************/
case "zzzms1inscription.com":
case "www.zzzms1inscription.com":
case "preprod.zzzms1inscription.com":
case "www.preprod.zzzms1inscription.com":
// astral a changer
// case "www.ms1inscription.progiweb.net":
// case "www.preprod.ms1inscription.progiweb.net":
// variables
$vGoogleAnalytics = "";
$vblnEnvironementDev = false;
$blnShowErrors = false;
$vDomaine = $vDomaine2 = $vDomaineTiming = 'https://www.preprod.ms1inscription.com';
// astral a changer
// $vDomaine = $vDomaine2 = $vDomaineTiming = 'https://www.preprod.ms1inscription.progiweb.net';
$vRepertoireFichiers = '/preprod';
$vEnvNotice = '
<div class="alert-warning py-2 text-center sticky-top">
<strong>Environnement Client <span class="text-uppercase">TEST</span>' . $strConAdmin . '</strong>
</div>
';
// bdd
//$strDatabaseHost = '158.69.235.152';
// vpn
$strDatabaseHost = 'localhost';
$strDatabaseUser = 'ms1incription_bd';
$strDatabasePass = '3+~=,y=eV)M;';
$strDatabaseMain = 'ms1incription_preprod';
$strDatabaseProd = 'ms1incription_prod';
$strDatabasePreProd = 'ms1incription_preprod';
// crm
$strDatabaseHostcrm = 'localhost';
$strDatabaseUsercrm = 'ms1crm_bd';
$strDatabasePasscrm = 'fPDJ,][ZGPwL';
$strDatabaseMaincrm = 'ms1crm_prod';
$arrConFTP = array(
'host' => '127.0.0.1',
'user' => 'ms1inscription',
'pass' => 'ms1911',
'path_prod' => '/home/ms1incription/public_html',
'path_preprod'=> '/home/ms1incription/public_html/preprod'
);
if (strpos($strHost, 'preprod.') === false /*&& strpos($currentFile, 'superadm') === false*/) { // SI PAS PRÉ-PROD => variables spécifiques à PROD
// paypal environement client
$favicon = 'favicon.ico';
$faviconadm = 'favicon-rouge.ico';
$vPaypaladv = true;
$vPaypaladv_SANDBOX = false;
//define('_Paypaladv_SANDBOX',"-SANDBOX 2024 v2");
define('_Paypaladv_SANDBOX', " v2");
$vblnEnvironementDev = false;
$vPaypal_devise = 'CAD';
$blnShowErrors = false;
$strDatabaseMain = $strDatabaseProd;
$vDomaine = $vDomaine2 = $vDomaineTiming = 'https://www.ms1inscription.com';
// astral a changer
// $vDomaine = $vDomaine2 = $vDomaineTiming = 'https://www.ms1inscription.progiweb.net';
$vRepertoireFichiers = '';
if (isset($_SESSION['usa_id'])) {
$vEnvNotice = '
<div class="alert-warning py-2 text-center sticky-top">
<strong>Environnement Client <span class="text-uppercase">PROD astral</span>' . $strConAdmin . '</strong>
</div>
';
} else {
$vEnvNotice = '';
}
$vGoogleAnalytics = "
<!-- Google tag (gtag.js) -->
<script async src=\"https://www.googletagmanager.com/gtag/js?id=G-9KF526LB6D\"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-8LEDDLNB94');
</script>
";
} else {
// paypal environement preprod
$favicon = 'favicon-jaune.ico';
$faviconadm = 'favicon-orange.ico';
$vblnEnvironementPreProd = true;
$vblnEnvironementDev = true;
$vPaypaladv = true;
$vPaypaladv_SANDBOX = true;
$vPaypal_devise = 'CAD';
define('_Paypaladv_SANDBOX', "-SANDBOX 2024 v2");
//define('_Paypaladv_SANDBOX', " v2");
}
break;
/****************************
* Environnement PRODUCTION v2 *
* Environnement PRODUCTION client *
****************************/
case "ms1inscription.com":
case "www.ms1inscription.com":
case "preprod.ms1inscription.com":
case "www.preprod.ms1inscription.com":
// variables
$vGoogleAnalytics = "";
$vblnEnvironementDev = false;
$blnShowErrors = false;
@ -305,19 +315,17 @@ switch ($strHost) {
</div>
';
fxSettingsUseDbProfiles($tabConnClientPreprod, $tabConnClientProd, 'preprod');
fxSettingsUseCrmProfile($tabConnCrmClient);
$strDatabaseHost = 'localhost';
$strDatabaseUser = 'ms1incription_bd';
$strDatabasePass = '3+~=,y=eV)M;';
$strDatabaseMain = 'ms1incription_preprod';
$strDatabaseProd = 'ms1incription_prod';
$strDatabasePreProd = 'ms1incription_preprod';
// crm
$strDatabaseHostcrm = 'localhost';
$strDatabaseUsercrm = 'ms1crm_bd';
$strDatabasePasscrm = 'fPDJ,][ZGPwL';
$strDatabaseMaincrm = 'ms1crm_prod';
// MSIN API ChronoTrack — remplir credentials par environnement
$tabChronotrackApi = array(
'base_url' => 'https://api.chronotrack.com',
'client_id' => '047dcfdd',
'client_secret' => '',
'service_user' => '',
'service_pass' => '',
);
$arrConFTP = array(
'host' => '127.0.0.1',
@ -327,24 +335,16 @@ switch ($strHost) {
'path_preprod'=> '/home/ms1incription/public_html/preprod'
);
// valide si preprod
$isPreprod = (strpos($strHost, 'preprod.') !== false);
if (!$isPreprod) {
// PROD
// afichage
$favicon = 'favicon.ico';
$faviconadm = 'favicon-rouge.ico';
$strDatabaseMain = $strDatabaseProd;
$vblnEnvironementDev = false; // attention pas vraiment developement
fxSettingsUseDbProfiles($tabConnClientPreprod, $tabConnClientProd, 'prod');
$vblnEnvironementDev = false;
// Google Analytics (exemple)
$vGoogleAnalytics = "";
// paypal environement client
$vPaypaladv = true;
$vPaypaladv_SANDBOX = false;
$vPaypal_devise = 'CAD';
@ -353,7 +353,7 @@ switch ($strHost) {
define('PAYPAL_BASE_URL', 'https://api-m.paypal.com');
define('PAYPAL_CLIENT_ID', 'AU3fhK6opJ2cBkTghAHf02fsimwj345zn3TOPL5we3z0C9HNT_sU341OkIP9l-ucgE1JdBhmt3WlHb2v');
define('PAYPAL_CLIENT_SECRET', 'EDg0Y1ZxvUmrAxY9_-NZP3EsuoMRf2lu9st1mDn8Ha7f5z4hrk7_KKsu_tM_PSri86YuFRqbaVKBADlX');
define('PAYPAL_WEBHOOK_ID', '2EW679080K7814041'); // ton Webhook réel si applicable
define('PAYPAL_WEBHOOK_ID', '2EW679080K7814041');
define('PAYPAL_SANDBOX', false);
define('PAYPAL_CURRENCY', 'CAD');
@ -367,8 +367,6 @@ switch ($strHost) {
$vEnvNotice = '';
}
$vDomaine = $vDomaine2 = $vDomaineTiming = 'https://www.ms1inscription.com';
$vRepertoireFichiers = '';
@ -385,25 +383,21 @@ switch ($strHost) {
} else {
// preprod
$favicon = 'favicon-jaune.ico';
$faviconadm = 'favicon-orange.ico';
// paypal environement preprod
$vblnEnvironementPreProd = true;
$vblnEnvironementDev = true;
// paypal environement client
$vPaypaladv = true;
$vPaypaladv_SANDBOX = true;
$vPaypal_devise = 'CAD';
define('PAYPAL_ENV', 'prod_test');
define('PAYPAL_ENV_FOOTER', 'prod_test');
define('PAYPAL_BASE_URL', 'https://api-m.sandbox.paypal.com'); //sand box
define('PAYPAL_BASE_URL', 'https://api-m.sandbox.paypal.com');
define('PAYPAL_CLIENT_ID', 'AXiOtwcCb4Vqf7QylPje-K2xp9lOIfCv_HMW8oWn1k8cRJJ0sSu_sVNjtQqaoDll9-vN8LpmfrqfI82d');
define('PAYPAL_CLIENT_SECRET', 'EEs_rbmAZlZFgm8m8e8S9lmZD47I9sf44wjbYEqu99uVpgHb0PHa3aSYG8Pe3772kr9FWZ33iDrPL9Jr');
define('PAYPAL_WEBHOOK_ID', '58G68258926315914'); // ton Webhook réel si applicable
define('PAYPAL_WEBHOOK_ID', '58G68258926315914');
define('PAYPAL_SANDBOX', true);
define('PAYPAL_CURRENCY', 'CAD');
@ -412,12 +406,11 @@ switch ($strHost) {
break;
/*************************
* Environnement INTERNE astral*
* Environnement dev (ms1inscriptiondev) *
*************************/
case "ms1inscriptiondev.progiweb.net":
case "preprod.ms1inscriptiondev.progiweb.net":
// variables
$vGoogleAnalytics = "";
$vblnEnvironementDev = true;
$blnShowErrors = true;
@ -433,21 +426,22 @@ switch ($strHost) {
</div>
';
fxSettingsUseDbProfiles($tabConnDevPreprod, $tabConnDevProd, 'preprod');
fxSettingsUseCrmProfile($tabConnCrmDev);
$strDatabaseHost = 'localhost';
$strDatabaseUser = 'devinscription_bd';
$strDatabasePass = 'x$@_v0-l_I((';
$strDatabaseMain = 'devinscription_preprod';
$strDatabaseProd = 'devinscription_prod';
$strDatabasePreProd = 'devinscription_preprod';
// MSIN API ChronoTrack — voir $blnChronotrackApiUseTestEndpoint en tête de fichier
$tabChronotrackApi = array(
'base_url' => $strChronotrackApiBaseUrl,
'client_id' => '047dcfdd',
'client_secret' => '5f14f8cb103316bfdf0558d1a6d0f20769603dde',
'service_user' => 'leith.s@ms1timing.com',
'service_pass' => 'Unimog22',
);
// crm
$strDatabaseHostcrm = 'localhost';
$strDatabaseUsercrm = 'devcrmms1_bd';
$strDatabasePasscrm = '.vB==c$s_Yk7';
$strDatabaseMaincrm = 'devcrmms1_dev';
// Cross-BD client (tunnel/VPN) — sync_static_db.php, validation avant prod
$arrConDatabaseClientPreProd = $tabConnClientPreprod;
$arrConDatabaseClientProd = $tabConnClientProd;
// ftp
$arrConFTP = array(
'host' => '127.0.0.1',
'user' => 'ms1inscription',
@ -456,10 +450,8 @@ switch ($strHost) {
'path_preprod'=> '/home/devinscription/public_html/preprod'
);
// valide si preprod
$isPreprod = (strpos($strHost, 'preprod.') !== false);
if (!$isPreprod) {
// afichage
$vEnvNotice = '
<div class="alert-danger py-2 text-center sticky-top">
<strong>Environnement de Dévelopement <span class="text-uppercase">client</span>' . $strConAdmin . '</strong>
@ -468,52 +460,41 @@ switch ($strHost) {
$favicon = 'favicon-blan-dev.ico';
$faviconadm = 'favicon-rouge-dev.ico';
$strDatabaseMain = 'devinscription_prod';
$vblnEnvironementDev = false; // attention pas vraiment developement
fxSettingsUseDbProfiles($tabConnDevPreprod, $tabConnDevProd, 'prod');
$vblnEnvironementDev = false;
// Google Analytics (exemple)
$vGoogleAnalytics = "";
// paypal environement client
$vPaypaladv = true;
$vPaypaladv_SANDBOX = true;
$vPaypal_devise = 'CAD';
define('PAYPAL_ENV', 'dev_client');
define('PAYPAL_ENV_FOOTER', 'dev_client');
define('PAYPAL_BASE_URL', 'https://api-m.sandbox.paypal.com'); //sand box
define('PAYPAL_BASE_URL', 'https://api-m.sandbox.paypal.com');
define('PAYPAL_CLIENT_ID', 'AQXvt0u9caqG-l_S5H-qFra-rEGiDifuk-eCa1DeylT0o4Vgc6RpnHQfMZt7lREyWH8nur6-_cgGtazF');
define('PAYPAL_CLIENT_SECRET', 'EOsLApT-17U2hU2bm0wXl0ZRqCg0eBaXTr9iz97LLziMHUt8nwi8ymgcDkkJ_msbBCZnOlR8IOw1KpTH');
define('PAYPAL_WEBHOOK_ID', '5AD874137H322344J'); // ton Webhook réel si applicable
define('PAYPAL_WEBHOOK_ID', '5AD874137H322344J');
define('PAYPAL_SANDBOX', true);
define('PAYPAL_CURRENCY', 'CAD');
$vDomaine = $vDomaine2 = $vDomaineTiming = 'https://ms1inscriptiondev.progiweb.net';
$vRepertoireFichiers = '';
} else {
$favicon = 'favicon-jaune-dev.ico';
$faviconadm = 'favicon-orange-dev.ico';
// paypal environement preprod
$vblnEnvironementPreProd = true;
// paypal environement client
$vPaypaladv = true;
$vPaypaladv_SANDBOX = true;
$vPaypal_devise = 'CAD';
define('PAYPAL_ENV', 'dev_test');
define('PAYPAL_ENV_FOOTER', 'dev_test');
define('PAYPAL_BASE_URL', 'https://api-m.sandbox.paypal.com'); //sand box
define('PAYPAL_BASE_URL', 'https://api-m.sandbox.paypal.com');
define('PAYPAL_CLIENT_ID', 'AXLKhIIMWSaiLlNi6EqDK7zU5c65J7hEXAZUauPm_Nnd-5A3vxO81QZej3X0T1pgiZ9Wki_FexheCPgB');
define('PAYPAL_CLIENT_SECRET', 'EEEZPzN0mTANRpj1e2KdP-U0x8UpHKjktbITVLDr2STpwnNkvKMDQ66XcdJyrK0yIsjcW4UjTY7cUr9N');
define('PAYPAL_WEBHOOK_ID', '87T78443WB181490Y'); // ton Webhook réel si applicable
define('PAYPAL_WEBHOOK_ID', '87T78443WB181490Y');
define('PAYPAL_SANDBOX', true);
define('PAYPAL_CURRENCY', 'CAD');
@ -522,6 +503,8 @@ switch ($strHost) {
break;
}
fxSettingsApplyChronotrackApi($tabChronotrackApi);
/*****************************
*
@ -562,17 +545,13 @@ $arrConDatabasecrm = array(
* Affichage des messages d'erreurs *
*
************************************/
if ($blnShowErrors) { // afficher les erreurs
// activer les erreurs
if ($blnShowErrors) {
error_reporting(E_ALL);
ini_set('display_errors', false);
ini_set('display_startup_errors', TRUE);
define('EOL', (PHP_SAPI == 'cli') ? PHP_EOL : '<br>');
}
// MSIN-3855
// info paypal
require_once $_SERVER['DOCUMENT_ROOT'] . "/paypal_advanced/config.php";

View File

@ -1,5 +1,8 @@
<?php
require_once __DIR__ . '/inc_cookie_consent.php';
fxCookiesConsentResetIfRequested();
// a faire trouver une solution
//ini_set('session.cookie_domain', '.ms1inscriptiondev.interne.ca');
//ini_set('session.cookie_domain', '.ms1inscription.com');
@ -23,7 +26,6 @@ ini_set('session.cookie_domain', $cookie_domain);
// Démarrez la session
session_start();
// Temps de la session = 60 min
// MSIN-CON-259
//ini_set('session.gc_maxlifetime', 10800);
@ -42,3 +44,7 @@ if (isset($_SESSION['date_maj']) && (time() - $_SESSION['date_maj'] > $inactive)
$_SESSION['no_panier'] = $_COOKIE['no_panier']; */
$_SESSION['date_maj'] = time();
if (!empty($_SESSION['usa_id'])) {
$_SESSION['logged'] = time();
}

183
php/inc_v2_assets.php Normal file
View File

@ -0,0 +1,183 @@
<?php
/**
* MSIN-4435 — Assets JS modules v2 (hors inc_footer_scripts.php legacy).
* Bib v4, gestion inscriptions v2 : chargés uniquement sur leurs pages.
*/
if (!function_exists('fxV2RegisterScript')) {
/** @param string $strName bib-assignments | inscr-gestion | promoteur-hub */
function fxV2RegisterScript($strName) {
global $MS1_V2_SCRIPTS;
if (!isset($MS1_V2_SCRIPTS) || !is_array($MS1_V2_SCRIPTS)) {
$MS1_V2_SCRIPTS = [];
}
if (!in_array($strName, $MS1_V2_SCRIPTS, true)) {
$MS1_V2_SCRIPTS[] = $strName;
}
}
/** Config injectée dans window.MS1_V2_PROMOTEUR_HUB avant promoteur-hub.js */
function fxV2PromoteurHubConfig($strLangue) {
global $vDomaine;
$strLangue = ($strLangue === 'en') ? 'en' : 'fr';
$strWait = afficheTexte('preloader_wait', 0, 0, 1);
if (trim((string)$strWait) === '' || $strWait === 'preloader_wait' || $strWait === '*preloader_wait*') {
$strWait = function_exists('fxPromoteurHubTexte')
? fxPromoteurHubTexte('promoteur_hub_load_dashboard', 0)
: '';
}
// MSIN-4401 — libellés Info injectés (pas de FR/EN en dur dans promoteur-hub.js).
$strAjaxError = function_exists('fxPromoteurHubTexte')
? fxPromoteurHubTexte('promoteur_hub_ajax_error', 0)
: afficheTexte('promoteur_hub_ajax_error', 0, 0, 1);
return [
'ajaxUrl' => $vDomaine . '/ajax_promoteur_hub.php',
'preloaderWait' => $strWait,
// MSIN-4399 — Ouvrir automatiquement la carte de cet événement au chargement.
'focusEveId' => function_exists('fxPromoteurHubParseFocusEveId')
? fxPromoteurHubParseFocusEveId()
: 0,
'i18n' => [
'ajaxError' => $strAjaxError,
],
];
}
/** Config injectée dans window.MS1_V2_INSCR_GESTION avant inscr-gestion.js */
function fxV2InscrGestionConfig($strLangue) {
global $vDomaine;
$strLangue = ($strLangue === 'en') ? 'en' : 'fr';
return [
'domain' => $vDomaine,
'lang' => $strLangue,
// MSIN-4401 — true = OCR dossard via serveur (Vision), pas Tesseract local.
'bibOcrCloud' => function_exists('fxInscrGestionBibOcrCloudEnabled')
? (bool)fxInscrGestionBibOcrCloudEnabled()
: false,
'preloaderWait' => afficheTexte('preloader_wait', 0, 0, 1),
'refundCancelHint' => function_exists('fxInscrGestionT')
? fxInscrGestionT('inscr_gestion_refund_cancel_hint')
: '',
'loginMessages' => [
'txt_login' => function_exists('fxInscrGestionT')
? fxInscrGestionT('inscr_gestion_login_err_user')
: '',
'txt_password' => function_exists('fxInscrGestionT')
? fxInscrGestionT('inscr_gestion_login_err_pass')
: '',
],
// MSIN-4401 — libellés Info injectés (pas de FR/EN en dur dans le JS).
'i18n' => function_exists('fxInscrGestionT') ? [
'errLoad' => fxInscrGestionT('inscr_gestion_err_load'),
'errNetwork' => fxInscrGestionT('inscr_gestion_err_network'),
'errRefund' => fxInscrGestionT('inscr_gestion_err_refund'),
'refundOk' => fxInscrGestionT('inscr_gestion_refund_ok'),
'bibOcrNotConfigured' => function_exists('fxInscrGestionScanLabel')
? fxInscrGestionScanLabel('inscr_gestion_bib_ocr_not_configured')
: fxInscrGestionT('inscr_gestion_bib_ocr_not_configured'),
'bibOcrVisionPrefix' => function_exists('fxInscrGestionScanLabel')
? fxInscrGestionScanLabel('inscr_gestion_bib_ocr_vision_prefix')
: fxInscrGestionT('inscr_gestion_bib_ocr_vision_prefix'),
'bibOcrImageInvalid' => function_exists('fxInscrGestionScanLabel')
? fxInscrGestionScanLabel('inscr_gestion_bib_ocr_image_invalid')
: fxInscrGestionT('inscr_gestion_bib_ocr_image_invalid'),
'bibOcrNetwork' => function_exists('fxInscrGestionScanLabel')
? fxInscrGestionScanLabel('inscr_gestion_bib_ocr_network')
: fxInscrGestionT('inscr_gestion_bib_ocr_network'),
'bibOcrUnreadable' => function_exists('fxInscrGestionScanLabel')
? fxInscrGestionScanLabel('inscr_gestion_bib_scan_invalid')
: fxInscrGestionT('inscr_gestion_bib_scan_invalid'),
'bibScanScanning' => function_exists('fxInscrGestionScanLabel')
? fxInscrGestionScanLabel('inscr_gestion_bib_scan_scanning')
: '',
'bibScanLoading' => function_exists('fxInscrGestionScanLabel')
? fxInscrGestionScanLabel('inscr_gestion_bib_scan_loading')
: '',
'bibScanReady' => function_exists('fxInscrGestionScanLabel')
? fxInscrGestionScanLabel('inscr_gestion_bib_scan_ready')
: '',
'bibScanFound' => function_exists('fxInscrGestionScanLabel')
? fxInscrGestionScanLabel('inscr_gestion_bib_scan_found')
: '',
'bibScanFoundSearch' => function_exists('fxInscrGestionScanLabel')
? fxInscrGestionScanLabel('inscr_gestion_bib_scan_found_search')
: '',
'bibScanInvalid' => function_exists('fxInscrGestionScanLabel')
? fxInscrGestionScanLabel('inscr_gestion_bib_scan_invalid')
: '',
'bibScanCamera' => function_exists('fxInscrGestionScanLabel')
? fxInscrGestionScanLabel('inscr_gestion_qr_camera_error')
: '',
// MSIN-4448 — Remis inline liste (confirmation retrait).
'remisConfirmTitle' => fxInscrGestionT('inscr_gestion_remis_confirm_title'),
'remisConfirmMsg' => fxInscrGestionT('inscr_gestion_remis_confirm_msg'),
'remisConfirmOk' => fxInscrGestionT('inscr_gestion_remis_confirm_ok'),
'remisConfirmCancel' => fxInscrGestionT('inscr_gestion_remis_confirm_cancel'),
// MSIN-4464 — historique fiche (!).
'auditPopupTitle' => fxInscrGestionT('inscr_gestion_audit_popup_title'),
'auditPopupEmpty' => fxInscrGestionT('inscr_gestion_audit_popup_empty'),
'auditPopupClose' => fxInscrGestionT('inscr_gestion_audit_popup_close'),
'auditColWhen' => fxInscrGestionT('inscr_gestion_audit_col_when'),
'auditColWho' => fxInscrGestionT('inscr_gestion_audit_col_who'),
'auditColFrom' => fxInscrGestionT('inscr_gestion_audit_col_from'),
'auditColTo' => fxInscrGestionT('inscr_gestion_audit_col_to'),
'auditFieldStatut' => fxInscrGestionT('inscr_gestion_audit_field_par_statut_course'),
'auditFieldBib' => fxInscrGestionT('inscr_gestion_audit_field_no_bib'),
'auditFieldRemis' => fxInscrGestionT('inscr_gestion_audit_field_no_bib_remis'),
'auditFieldCancel' => fxInscrGestionT('inscr_gestion_audit_field_is_cancelled'),
] : [],
// MSIN-4448 — Operateur pour no_bib_remis_par (chemin superadm / usa_id).
'remisOperator' => [
'par_id' => intval($_SESSION['com_info']['com_id'] ?? ($_SESSION['com_id'] ?? 0)),
'par' => trim(
(string)($_SESSION['com_info']['com_prenom'] ?? '')
. ' '
. (string)($_SESSION['com_info']['com_nom'] ?? '')
),
],
];
}
/** À appeler dans inc_footer.php après inc_footer_scripts.php */
function fxV2RenderFooterScripts() {
global $MS1_V2_SCRIPTS, $vDomaine, $strLangue;
if (empty($MS1_V2_SCRIPTS) || !is_array($MS1_V2_SCRIPTS)) {
return;
}
$mapFiles = [
'bib-assignments' => '/js/v2/bib-assignments.js',
'inscr-gestion' => '/js/v2/inscr-gestion.js',
'promoteur-hub' => '/js/v2/promoteur-hub.js',
];
if (in_array('promoteur-hub', $MS1_V2_SCRIPTS, true)) {
$cfg = fxV2PromoteurHubConfig($strLangue ?? 'fr');
echo '<script>window.MS1_V2_PROMOTEUR_HUB='
. json_encode($cfg, JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP)
. ';</script>' . "\n";
}
if (in_array('inscr-gestion', $MS1_V2_SCRIPTS, true)) {
$cfg = fxV2InscrGestionConfig($strLangue ?? 'fr');
echo '<script>window.MS1_V2_INSCR_GESTION='
. json_encode($cfg, JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_AMP)
. ';</script>' . "\n";
}
foreach ($MS1_V2_SCRIPTS as $strName) {
if (!isset($mapFiles[$strName])) {
continue;
}
$strSrc = htmlspecialchars($vDomaine . $mapFiles[$strName], ENT_QUOTES, 'UTF-8');
echo '<script src="' . $strSrc . '?v=' . htmlspecialchars(_VERSION_CODE, ENT_QUOTES, 'UTF-8') . '"></script>' . "\n";
}
}
}

View File

@ -4,7 +4,9 @@
// IMPORTANT : aucun espace avant <?php
// Aucun echo avant les headers
ini_set('display_errors', 0);
ini_set('display_startup_errors', 0);
error_reporting(E_ALL);
require_once __DIR__ . '/../libs/phpqrcode-master/qrlib.php';
// Vérification du token

294
php/sync_static_db.php Normal file
View File

@ -0,0 +1,294 @@
<?php
/**
* Tableau de bord sync tables statiques — préprod dev, superadm seulement.
* Source : devinscription_preprod
*/
require_once('inc_start_time.php');
require_once('inc_fonctions.php');
require_once('inc_fx_messages.php');
require_once('inc_fx_static_sync.php');
if (empty($_SESSION['usa_id'])) {
fxAdminRequireSuperadmLogin();
}
if (!fxAdminStaticSyncAllowed()) {
header('HTTP/1.1 403 Forbidden');
echo 'Accès refusé.';
exit;
}
set_time_limit(300);
function ssEsc($str) {
return htmlspecialchars((string)$str, ENT_QUOTES, 'UTF-8');
}
$strCsrf = fxStaticSyncEnsureCsrfToken();
$strFlash = '';
$strFlashType = 'info';
$tabPopFlash = fxStaticSyncPopFlash();
if (is_array($tabPopFlash)) {
$strFlash = $tabPopFlash['message'];
$strFlashType = $tabPopFlash['type'];
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['action']) && $_POST['action'] === 'sync') {
if (!fxStaticSyncValidateCsrf($_POST['csrf'] ?? '')) {
fxStaticSyncSetFlash('Jeton CSRF invalide.', 'bad');
} else {
$strTable = trim($_POST['table'] ?? '');
$strEnvId = trim($_POST['env'] ?? '');
$tabDef = fxStaticSyncFindTableDef($strTable);
$tabEnv = fxStaticSyncFindEnvDef($strEnvId);
if (!$tabDef || !$tabEnv || !empty($tabEnv['is_source'])) {
fxStaticSyncSetFlash('Paramètres invalides.', 'bad');
} else {
$tabConnections = fxStaticSyncOpenConnections();
$objSource = $tabConnections['dev_preprod'];
$objTarget = $tabConnections[$strEnvId];
if (!$objSource || !$objTarget) {
fxStaticSyncSetFlash(
'Connexion BD impossible — ' . fxStaticSyncFormatConnectionError($tabEnv),
'bad'
);
} else {
$tabResult = fxStaticSyncCopyTable($objSource, $objTarget, $strTable, $tabDef);
if ($tabResult['ok']) {
$tabVerify = fxStaticSyncCompareEnvPair($objSource, $objTarget, $tabDef);
$strMsg = $tabDef['label'] . ' → ' . $tabEnv['label'] . ' : ' . $tabResult['message'];
if ($tabVerify['status'] === 'ok') {
$strMsg .= ' Vérification : aligné.';
fxStaticSyncSetFlash($strMsg, 'ok');
} else {
$strMsg .= ' ATTENTION — après sync : '
. fxStaticSyncStatusLabel($tabVerify['status'], $tabVerify) . '.';
fxStaticSyncSetFlash($strMsg, 'bad');
}
} else {
fxStaticSyncSetFlash(
$tabDef['label'] . ' → ' . $tabEnv['label'] . ' — échec : ' . $tabResult['message'],
'bad'
);
}
}
}
}
header('Location: sync_static_db.php#sync-result');
exit;
}
$tabConnections = fxStaticSyncOpenConnections();
$tabEnvs = fxStaticSyncEnvironmentDefinitions();
$tabReport = fxStaticSyncBuildReport($tabConnections);
$strDetailTable = trim($_GET['detail_table'] ?? '');
$strDetailEnv = trim($_GET['detail_env'] ?? '');
$tabDetail = null;
if ($strDetailTable !== '' && $strDetailEnv !== '') {
$tabDetail = fxStaticSyncGetDiffDetail($strDetailTable, $strDetailEnv, $tabConnections);
}
?><!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Sync tables statiques</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; background: #f4f6f8; color: #222; }
h1 { font-size: 1.4rem; margin: 0 0 8px; }
.sub { color: #555; margin-bottom: 20px; font-size: 0.95rem; }
.flash { padding: 10px 14px; border-radius: 6px; margin-bottom: 16px; white-space: pre-wrap; word-break: break-word; }
.flash-ok { background: #d4edda; border: 1px solid #b7dfc3; }
.flash-bad { background: #f8d7da; border: 1px solid #f1b8be; }
.flash-info { background: #e8f0fe; border: 1px solid #c5d7fb; }
.warn-box { background: #fff3cd; border: 1px solid #ffecb5; padding: 12px 14px; border-radius: 6px; margin-bottom: 16px; font-size: 0.9rem; }
.warn-box strong { color: #856404; }
table { width: 100%; border-collapse: collapse; background: #fff; box-shadow: 0 1px 3px rgba(0,0,0,.08); }
th, td { border: 1px solid #ddd; padding: 8px 10px; vertical-align: top; font-size: 0.9rem; }
th { background: #eef2f7; text-align: left; }
.ss-ok { background: #d4edda; color: #155724; }
.ss-warn { background: #fff3cd; color: #856404; }
.ss-bad { background: #f8d7da; color: #721c24; }
.ss-muted { background: #e9ecef; color: #495057; }
.ss-schema { background: #fde2e2; color: #7f1d1d; border: 1px solid #f5c2c2; }
.badge { display: inline-block; padding: 3px 8px; border-radius: 4px; font-size: 0.82rem; font-weight: 600; max-width: 100%; line-height: 1.35; }
.count { font-size: 0.82rem; color: #444; margin-top: 4px; }
.count-hint { font-size: 0.78rem; color: #666; }
.btn { display: inline-block; margin-top: 6px; padding: 4px 10px; font-size: 0.82rem; border: 1px solid #888; background: #fff; border-radius: 4px; cursor: pointer; text-decoration: none; color: #222; }
.btn:hover { background: #f0f0f0; }
.btn-sync { border-color: #2e6da4; color: #1a4f7a; }
.detail-box { margin-top: 24px; background: #fff; padding: 16px; border: 1px solid #ccc; border-radius: 6px; }
.detail-box h2 { margin-top: 0; font-size: 1.1rem; }
.detail-list { margin: 0; padding-left: 18px; }
.detail-list li { margin-bottom: 8px; }
.legend { margin: 16px 0; font-size: 0.85rem; }
.legend span { display: inline-block; margin-right: 12px; padding: 2px 8px; border-radius: 4px; }
code { background: #f1f1f1; padding: 1px 4px; border-radius: 3px; }
</style>
</head>
<body>
<h1>Sync tables statiques</h1>
<div class="warn-box">
<strong>Attention — opération destructive.</strong>
Sync = recopie complète depuis dev préprod (efface la cible). Un clic sur Sync.
</div>
<p class="sub">
Source de vérité : <strong>dev préprod</strong> (<code><?php echo ssEsc($arrConDatabasePreProd['db'] ?? ''); ?></code>).
Comparaison vers dev prod, client préprod et client prod.
</p>
<div id="sync-result"></div>
<?php if ($strFlash !== '') { ?>
<div class="flash flash-<?php echo ssEsc($strFlashType); ?>"><?php echo ssEsc($strFlash); ?></div>
<?php } ?>
<div class="legend">
<span class="ss-ok">Données alignées</span>
<span class="ss-warn">Il en manque X</span>
<span class="ss-bad">Il y en a X en trop</span>
<span class="ss-warn">Contenu différent</span>
<span class="ss-schema">Structures non alignées</span>
<span class="ss-muted">Table absente / connexion</span>
</div>
<table>
<thead>
<tr>
<th>Table</th>
<th>Source<br><small><?php echo ssEsc($tabEnvs['dev_preprod']['config']['db'] ?? ''); ?></small></th>
<?php foreach ($tabEnvs as $strEnvId => $tabEnv) {
if (!empty($tabEnv['is_source'])) {
continue;
} ?>
<th><?php echo ssEsc($tabEnv['label']); ?><br><small><?php echo ssEsc($tabEnv['config']['db'] ?? ''); ?></small></th>
<?php } ?>
</tr>
</thead>
<tbody>
<?php foreach ($tabReport as $tabRow) {
$tabDef = $tabRow['def']; ?>
<tr>
<td>
<strong><?php echo ssEsc($tabDef['label']); ?></strong><br>
<code><?php echo ssEsc($tabDef['table']); ?></code>
</td>
<td class="ss-muted">
<span class="badge"><?php echo (int)$tabRow['source_count']; ?> ligne(s)</span>
<?php if (!empty($tabRow['source_dupes'])) { ?>
<div class="count-hint"><?php echo (int)$tabRow['source_dupes']; ?> doublon(s) clé en source</div>
<?php } ?>
</td>
<?php foreach ($tabEnvs as $strEnvId => $tabEnv) {
if (!empty($tabEnv['is_source'])) {
continue;
}
$tabCmp = isset($tabRow['envs'][$strEnvId]) ? $tabRow['envs'][$strEnvId] : null;
if (!$tabCmp) {
continue;
}
$strClass = fxStaticSyncStatusClass($tabCmp['status']);
$strLabel = fxStaticSyncStatusLabel($tabCmp['status'], $tabCmp);
?>
<td class="<?php echo ssEsc($strClass); ?>">
<span class="badge"><?php echo ssEsc($strLabel); ?></span>
<div class="count">
Cible <?php echo (int)$tabCmp['target_count']; ?> · Source <?php echo (int)$tabCmp['source_count']; ?>
</div>
<?php if ($tabCmp['status'] === 'schema_mismatch') {
$tabSchemaDiff = array(
'missing_in_target' => isset($tabCmp['schema_missing_cols']) ? $tabCmp['schema_missing_cols'] : array(),
'extra_in_target' => isset($tabCmp['schema_extra_cols']) ? $tabCmp['schema_extra_cols'] : array(),
);
$strSchemaDiff = fxStaticSyncFormatSchemaDiff($tabSchemaDiff);
if ($strSchemaDiff !== '') { ?>
<div class="count"><?php echo ssEsc($strSchemaDiff); ?></div>
<?php } } ?>
<?php if ($tabCmp['status'] !== 'ok' && $tabCmp['status'] !== 'no_connection') { ?>
<a class="btn" href="?detail_table=<?php echo urlencode($tabDef['table']); ?>&amp;detail_env=<?php echo urlencode($strEnvId); ?>">Détails</a>
<?php } ?>
<?php if ($tabCmp['status'] === 'no_connection') {
$strConnErr = fxStaticSyncConnectError($tabEnv['config']['db'] ?? '');
if ($strConnErr !== '') { ?>
<div class="count-hint"><?php echo ssEsc($strConnErr); ?></div>
<?php } } ?>
<?php if ($tabCmp['status'] !== 'no_connection' && $tabCmp['status'] !== 'schema_mismatch') { ?>
<form method="post" style="display:inline;">
<input type="hidden" name="action" value="sync">
<input type="hidden" name="csrf" value="<?php echo ssEsc($strCsrf); ?>">
<input type="hidden" name="table" value="<?php echo ssEsc($tabDef['table']); ?>">
<input type="hidden" name="env" value="<?php echo ssEsc($strEnvId); ?>">
<button type="submit" class="btn btn-sync">Sync →</button>
</form>
<?php } ?>
</td>
<?php } ?>
</tr>
<?php } ?>
</tbody>
</table>
<?php if (is_array($tabDetail) && !empty($tabDetail['compare'])) {
$tabCmp = $tabDetail['compare'];
$tabDef = fxStaticSyncFindTableDef($strDetailTable);
$tabEnv = fxStaticSyncFindEnvDef($strDetailEnv);
?>
<div class="detail-box">
<h2>Détails — <?php echo ssEsc($tabDef['label'] ?? $strDetailTable); ?> → <?php echo ssEsc($tabEnv['label'] ?? $strDetailEnv); ?></h2>
<p>
<?php
$intDelta = (int)$tabCmp['target_count'] - (int)$tabCmp['source_count'];
if ($intDelta < 0) {
echo 'Il en manque ' . abs($intDelta) . ' (cible ' . (int)$tabCmp['target_count']
. ' · source ' . (int)$tabCmp['source_count'] . ')';
} elseif ($intDelta > 0) {
echo 'Il y en a ' . $intDelta . ' en trop (cible ' . (int)$tabCmp['target_count']
. ' · source ' . (int)$tabCmp['source_count'] . ')';
} else {
echo 'Même nombre de lignes, contenu différent (cible ' . (int)$tabCmp['target_count']
. ' · source ' . (int)$tabCmp['source_count'] . ')';
}
?>
<?php if ($tabCmp['status'] === 'schema_mismatch') {
$tabSchemaDiff = array(
'missing_in_target' => isset($tabCmp['schema_missing_cols']) ? $tabCmp['schema_missing_cols'] : array(),
'extra_in_target' => isset($tabCmp['schema_extra_cols']) ? $tabCmp['schema_extra_cols'] : array(),
);
$strSchemaDiff = fxStaticSyncFormatSchemaDiff($tabSchemaDiff);
if ($strSchemaDiff !== '') { ?>
<br><?php echo ssEsc($strSchemaDiff); ?>
<?php } } ?>
<a class="btn" href="sync_static_db.php" style="margin-left:12px;">Fermer</a>
</p>
<?php if (!empty($tabDetail['samples'])) { ?>
<ul class="detail-list">
<?php foreach ($tabDetail['samples'] as $tabSample) { ?>
<li>
<strong><?php echo ssEsc(fxStaticSyncFormatKeyPreview($tabSample['key'])); ?></strong>
(<?php
echo ssEsc($tabSample['type'] === 'extra' ? 'en trop'
: ($tabSample['type'] === 'missing' ? 'manque' : 'contenu diff.'));
?>)
<?php if ($tabDef['table'] === 'info' && $tabSample['type'] === 'diff') { ?>
<br>Source : <em><?php echo ssEsc($tabSample['source_texte']); ?></em>
<br>Cible : <em><?php echo ssEsc($tabSample['target_texte']); ?></em>
<?php } ?>
</li>
<?php } ?>
</ul>
<?php } else { ?>
<p>Aucun écart à afficher.</p>
<?php } ?>
</div>
<?php } ?>
</body>
</html>

View File

@ -1,160 +1,316 @@
<head>
<meta charset="iso-8859-1">
</head>
<?php
/**
* Program test.php.
* Usager: stephan
* Date : 13-04-22
* Time : 18:07
* Éditeur traduction info (pop-up bib v4) — info_texte + info_aide FR/EN.
*/
require_once('inc_start_time.php');
require_once('inc_fonctions.php');
require_once('inc_fx_messages.php');
$clef = trim(strtolower($_GET['clef']));
$pk_info = trim(strtolower($_GET['pk_info']));
$bd = trim(strtolower($_GET['bd']));
$page = trim(strtolower($_GET['page']));
$pk_prg = trim(strtolower($_GET['pk_prg']));
if (isset($_POST['btn_mod'])) {
//unset($_POST[1]['pk_info']);
$info1=$_POST[info][1];
$mem_id=$info1['pk_info'];
// unset($info['pk_info']);
fxSetInfo('mod',$info1,'', $mem_id);
$info2=$_POST[info][2];
$mem_id=$info2['pk_info'];
// unset($info['pk_info']);
fxSetInfo('mod',$info2,'', $mem_id);
$clef = trim($info1['info_clef']);
$bd = trim(strtolower(trim($info1['bd'])));
$page = trim(strtolower(trim($info1['page'])));
$pk_prg = trim(strtolower(trim($info1['info_prg'])));
if (empty($_SESSION['usa_id'])) {
fxAdminRequireSuperadmLogin();
}
if (!fxAdminCanUseTextEditTools()) {
header('HTTP/1.1 403 Forbidden');
echo 'Accès refusé.';
exit;
}
function tradEsc($str) {
return htmlspecialchars((string)$str, ENT_QUOTES, 'UTF-8');
}
function tradFieldAttrs($strChamp, $intRow) {
return 'id="info[' . (int)$intRow . '][' . tradEsc($strChamp) . ']" name="info[' . (int)$intRow . '][' . tradEsc($strChamp) . ']"';
}
function tradFieldVal($strChamp, $strType, $tabRow, $strDefault = '') {
if ($strType === 'mod' && is_array($tabRow) && array_key_exists($strChamp, $tabRow)) {
return $tabRow[$strChamp];
}
return $strDefault;
}
function tradRowByLang($tabResults) {
$tabOut = array('fr' => null, 'en' => null);
if (!is_array($tabResults)) {
return $tabOut;
}
foreach ($tabResults as $row) {
if (!empty($row['info_langue'])) {
$tabOut[$row['info_langue']] = $row;
}
}
return $tabOut;
}
$clef = trim($_GET['clef'] ?? $_POST['clef'] ?? '');
$bd = trim(strtolower($_GET['bd'] ?? $_POST['bd'] ?? 'client'));
$page = trim(strtolower($_GET['page'] ?? $_POST['page'] ?? ''));
$pk_prg = trim($_GET['pk_prg'] ?? $_POST['pk_prg'] ?? '');
$blnSaved = false;
$sqlValide = "SELECT * from info where (info_clef='" .$clef . "' and info_prg='" . $pk_prg . "') order by info_langue desc";
if (isset($_POST['btn_mod']) && !empty($_POST['info']) && is_array($_POST['info'])) {
foreach ($_POST['info'] as $tabPosted) {
if (empty($tabPosted['info_clef']) || empty($tabPosted['info_langue'])) {
continue;
}
if (!empty($tabPosted['pk_info'])) {
fxSetInfo('mod', $tabPosted, '', $tabPosted['pk_info']);
} else {
$memId = 0;
fxSetInfo('add', $tabPosted, '', $memId);
}
$clef = trim($tabPosted['info_clef']);
$bd = trim(strtolower($tabPosted['bd'] ?? $bd));
$page = trim(strtolower($tabPosted['page'] ?? $page));
$pk_prg = trim(strtolower($tabPosted['info_prg'] ?? $pk_prg));
}
$blnSaved = true;
}
$sqlValide = "SELECT * FROM info
WHERE info_clef = '" . $objDatabase->fxEscape($clef) . "'
AND info_prg = '" . $objDatabase->fxEscape($pk_prg) . "'
ORDER BY info_langue DESC";
$recValide = $objDatabase->fxGetResults($sqlValide);
$tabLang = tradRowByLang($recValide);
$type = ($recValide != null) ? 'mod' : 'add';
$tabLangMeta = array(
'fr' => array('label' => 'Français', 'row' => 1),
'en' => array('label' => 'English', 'row' => 2),
);
if ($recValide!=null)
$type="mod";
else
$type="add";
?>
?><!DOCTYPE html>
<html lang="fr">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Untitled 1</title>
<style type="text/css">
.style1 {
background-color: #C0C0C0;
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Traduction — <?php echo tradEsc($clef); ?></title>
<style>
*, *::before, *::after { box-sizing: border-box; }
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif;
font-size: 14px;
line-height: 1.45;
color: #212529;
background: #f1f3f5;
}
.style2 {
border: 1px solid #000000;
.trad-page {
max-width: 920px;
margin: 0 auto;
padding: 20px 16px 28px;
}
.traduction {
width:85px;
height:85px;
background:#cc0000;
font:bold 13px Arial;
border-radius:50%;
border:none;
color:#fff;
.trad-card {
background: #fff;
border: 1px solid #dee2e6;
border-radius: 8px;
box-shadow: 0 1px 3px rgba(0,0,0,.06);
overflow: hidden;
}
.trad-card-header {
padding: 14px 18px;
background: #2f5fd0;
color: #fff;
}
.trad-card-header h1 {
margin: 0 0 4px;
font-size: 17px;
font-weight: 700;
}
.trad-card-header .trad-key {
font-size: 12px;
opacity: .9;
word-break: break-all;
}
.trad-meta {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
padding: 12px 18px;
background: #f8f9fa;
border-bottom: 1px solid #e9ecef;
}
.trad-meta dt {
margin: 0;
font-size: 11px;
text-transform: uppercase;
letter-spacing: .04em;
color: #6c757d;
}
.trad-meta dd {
margin: 2px 0 0;
font-weight: 600;
word-break: break-all;
}
.trad-alert {
margin: 14px 18px 0;
padding: 10px 12px;
border-radius: 6px;
background: #d1e7dd;
color: #0f5132;
border: 1px solid #badbcc;
}
.trad-lang-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
padding: 18px;
}
@media (max-width: 720px) {
.trad-lang-grid { grid-template-columns: 1fr; }
.trad-meta { grid-template-columns: 1fr; }
}
.trad-lang-panel {
border: 1px solid #dee2e6;
border-radius: 6px;
padding: 14px;
background: #fff;
}
.trad-lang-panel h2 {
margin: 0 0 12px;
font-size: 14px;
font-weight: 700;
color: #2f5fd0;
}
.trad-field {
margin-bottom: 12px;
}
.trad-field:last-child {
margin-bottom: 0;
}
.trad-field label {
display: block;
margin-bottom: 4px;
font-size: 12px;
font-weight: 600;
color: #495057;
}
.trad-field .trad-hint {
font-weight: 400;
color: #868e96;
}
.trad-field input[type="text"],
.trad-field textarea {
width: 100%;
padding: 8px 10px;
border: 1px solid #ced4da;
border-radius: 4px;
font: inherit;
color: #212529;
background: #fff;
}
.trad-field textarea {
min-height: 120px;
resize: vertical;
}
.trad-field input:focus,
.trad-field textarea:focus {
outline: none;
border-color: #2f5fd0;
box-shadow: 0 0 0 2px rgba(47,95,208,.15);
}
.trad-actions {
padding: 0 18px 18px;
}
.trad-btn-save {
padding: 9px 22px;
border: none;
border-radius: 4px;
background: #28a745;
color: #fff;
font: inherit;
font-weight: 600;
cursor: pointer;
}
.trad-btn-save:hover {
background: #218838;
}
</style>
</head>
<body>
<br>
<div class="trad-page">
<form action="<?php echo tradEsc($vDomaine); ?>/php/traduction.php" method="post" id="frm_traduction">
<input type="hidden" name="clef" value="<?php echo tradEsc($clef); ?>">
<form target=""
action="<?php echo $vDomaine; ?>/php/traduction.php"
id="frm_compte" name="frm_compte" method="post">
<div class="trad-card">
<div class="trad-card-header">
<h1>Traduction</h1>
<div class="trad-key"><?php echo tradEsc($clef); ?></div>
</div>
<dl class="trad-meta">
<div>
<dt>Programme</dt>
<dd><?php echo tradEsc($pk_prg); ?></dd>
</div>
<div>
<dt>Base</dt>
<dd><?php echo tradEsc($bd); ?></dd>
</div>
<div>
<dt>Page</dt>
<dd><?php echo tradEsc($page); ?></dd>
</div>
</dl>
<table style="width: 95%">
<tr>
<th class="style1">INFO_CLEF</th>
<th class="style1">INFO_PRG</th>
<th class="style1">BD</th>
<th class="style1">PAGE</th>
</tr>
<?php if ($blnSaved) { ?>
<div class="trad-alert">Enregistré.</div>
<?php } ?>
<tr>
<td class="style2"><?php echo ($clef)?></td>
<td class="style2"><?php echo ($pk_prg)?></td>
<td class="style2"><?php echo ($bd)?></td>
<td class="style2"><?php echo ($page)?></td>
</tr>
</table>
<br>
<table style="width: 95%">
<tr>
<th class="style1" style="width: 70px">LANGUE</th>
<th class="style1" style="width: 309px">INFO_TEXTE</th>
<th class="style1">INFO_DESCRIPTION</th>
<th class="style1">INFO_OPTION1</th>
</tr>
<div class="trad-lang-grid">
<?php foreach ($tabLangMeta as $strLang => $meta) {
$tabRow = $tabLang[$strLang];
$intRow = $meta['row'];
$strRowType = ($tabRow !== null) ? 'mod' : 'add';
?>
<section class="trad-lang-panel">
<h2><?php echo tradEsc($meta['label']); ?></h2>
<tr>
<input type="hidden" <?php echo fxFormFieldInfo('pk_info', $type, $recValide[1],"",1) ?>>
<input type="hidden" <?php echo fxFormFieldInfo('info_clef', $type, $recValide[1],"",1) ?>>
<input type="hidden" <?php echo fxFormFieldInfo('info_prg', $type, $recValide[1],"",1) ?>>
<input type="hidden" id="info[1][bd]" name="info[1][bd]" value="<?php echo ($bd)?>">
<input type="hidden" id="info[1][page]" name="info[1][page]" value="<?php echo ($page)?>">
<td class="style2" style="width: 70px"><?php echo $recValide[1]['info_langue']?></td>
<td class="style2" style="width: 309px">
<input type="text" <?php echo fxFormFieldInfo('info_texte', $type, $recValide[1],"",1) ?> size="30" maxlength="50">
</td>
<td class="style2"> <input type="text" <?php echo fxFormFieldInfo('info_description', $type, $recValide[1],"",1) ?> size="30" maxlength="50"></td>
<td class="style2"> <input type="text" <?php echo fxFormFieldInfo('info_option1', $type, $recValide[1],"",1) ?> size="30" maxlength="50"></td>
</tr>
<tr>
<input type="hidden" <?php echo fxFormFieldInfo('pk_info', $type, $recValide[2],"",2) ?>>
<input type="hidden" <?php echo fxFormFieldInfo('info_clef', $type, $recValide[2],"",2) ?>>
<input type="hidden" <?php echo fxFormFieldInfo('info_prg', $type, $recValide[2],"",2) ?>>
<td class="style2" style="width: 70px"><?php echo $recValide[2]['info_langue']?></td>
<td class="style2" style="width: 309px">
<input type="text" <?php echo fxFormFieldInfo('info_texte', $type, $recValide[2],"",2) ?> size="30" maxlength="50">
</td>
<td class="style2"> <input type="text" <?php echo fxFormFieldInfo('info_description', $type, $recValide[2],"",2) ?> size="30" maxlength="50"></td>
<td class="style2"> <input type="text" <?php echo fxFormFieldInfo('info_option1', $type, $recValide[2],"",2) ?> size="30" maxlength="50"></td>
</tr>
</table>
<button type="submit" id="btn_mod" name="btn_mod" value="save"
class="button"> Enregistrer </button>
</form>
<input type="hidden" <?php echo tradFieldAttrs('pk_info', $intRow); ?>
value="<?php echo tradEsc(tradFieldVal('pk_info', $strRowType, $tabRow)); ?>">
<input type="hidden" <?php echo tradFieldAttrs('info_clef', $intRow); ?>
value="<?php echo tradEsc($clef); ?>">
<input type="hidden" <?php echo tradFieldAttrs('info_prg', $intRow); ?>
value="<?php echo tradEsc($pk_prg); ?>">
<input type="hidden" <?php echo tradFieldAttrs('info_langue', $intRow); ?>
value="<?php echo tradEsc($strLang); ?>">
<input type="hidden" <?php echo tradFieldAttrs('bd', $intRow); ?>
value="<?php echo tradEsc($bd); ?>">
<input type="hidden" <?php echo tradFieldAttrs('page', $intRow); ?>
value="<?php echo tradEsc($page); ?>">
<div class="trad-field">
<label for="info[<?php echo $intRow; ?>][info_texte]">
Libellé <span class="trad-hint">(info_texte — vide autorisé)</span>
</label>
<input type="text"
<?php echo tradFieldAttrs('info_texte', $intRow); ?>
maxlength="50"
value="<?php echo tradEsc(tradFieldVal('info_texte', $strRowType, $tabRow)); ?>">
</div>
<div class="trad-field">
<label for="info[<?php echo $intRow; ?>][info_aide]">
Aide longue <span class="trad-hint">(info_aide — pop-up ?)</span>
</label>
<textarea <?php echo tradFieldAttrs('info_aide', $intRow); ?>><?php
echo tradEsc(tradFieldVal('info_aide', $strRowType, $tabRow));
?></textarea>
</div>
</section>
<?php } ?>
</div>
<div class="trad-actions">
<button type="submit" name="btn_mod" value="save" class="trad-btn-save">Enregistrer</button>
</div>
</div>
</form>
</div>
</body>
</html>

Some files were not shown because too many files have changed in this diff Show More